Goose — 패러다임과 약점
Rust 진영의 가장 큰 오픈소스 agent. 그들이 무슨 가정 위에 서 있는가, 어디서 균열이 보이는가, 그 균열을 깨려는 흐름들은 무엇인가. 코드 디테일이 아니라 패러다임과 인사이트.
Goose가 서있는 가정들
Goose의 모든 결정은 다음 가정 위에 있다. 이 가정을 알면 그들의 trait 두께·계층 구조·MCP 일원화가 왜 그런 모양인지 바로 보인다.
| 가정 | 표현 |
|---|---|
| 모델은 서비스(provider)다 | LLM 호출은 외부 vendor를 부르는 행위. 추상화 한 겹이 있어야 함 |
| Tool은 표준이어야 한다 | MCP를 처음부터 유일한 tool 정의로 채택. 자체 정의 없음 |
| Agent는 single-loop에서 시작한다 | turn 단위 while-loop. plan/multi-agent는 그 위에 얹는 것 |
| Stream-first가 옳다 | 토큰 단위 UX가 기본. complete는 stream의 collect |
| Provider는 호출 추상화다 | session·history·permission은 호출의 부속 정보 |
| 통합 product는 trait가 두꺼워질 수밖에 없다 | embedding·oauth·session naming까지 한 trait에 모음 |
여기서 마지막 가정이 가장 중요하다. Goose는 통합 product를 만들고 있고, 그래서 trait가 시간이 지나며 두꺼워진다. v0.1은 그 가정을 안 따른다.
추상적 작동 구조
핵심: 모든 것이 Provider 호출과 Tool dispatch 두 동작으로 환원된다. 위 layer에 있는 모든 것(session·permission·oauth)은 그 호출에 끼어드는 부속 정보로 모델링된다.
이 패러다임의 강점
- MCP 일원화로 plugin ecosystem 무료 — 70+ extension을 자체 코드 없이
- Provider 추상화로 vendor 자유 — 15+ provider, 사용자 구독 모델까지 (ACP)
- Cargo workspace 분리로 깔끔한 의존 방향 — core ← cli/server/extensions
- Rust 단일 binary, native 배포 — Electron + Rust 조합으로 desktop도 커버
- 재단 거버넌스 — block에서 AAIF로 이전. 단일 회사 의존 제거
약점 — 어디서 균열이 보이는가
1. trait이 시간이 지나며 두꺼워진다
Provider trait가 호출 추상화로 시작했지만, 통합 product의 책임이 누적되며 oauth·embedding·session naming·mode·permission까지 한 trait에 모인다. 결과:
- 새 provider 추가 비용이 점점 커짐 (default impl로 가려지지만 본질 미해결)
- mock 만들기가 힘들고, 부분 차용이 어려움
- 어떤 메서드가 진짜 핵심인지 흐려짐
이 균열이 통합 product의 운명인가, 추상화 설계 실패인가는 열린 질문.
2. single-loop 가정이 후행 패턴을 끼워넣게 만든다
agent loop가 직선적이라 plan/reflect/multi-agent는 위에 얹는 형태로만 들어온다. 결과:
- ReAct 변형은 자연스럽지만, plan-then-act, tree-of-thought, graph-based agent는 부자연스러움
- multi-agent를 단일 loop의 sub-routine으로 표현해야 함 → 책임 경계가 흐려짐
3. Provider라는 개념의 균열
Goose는 Claude Code/Gemini CLI 같은 자기 context를 가진 wrapper까지 Provider로 다루기 위해 manages_own_context 같은 escape hatch를 추가했다. 이건 Provider 추상화의 근본 균열을 보여주는 신호:
- LLM API 호출과 wrapper CLI 호출은 같은 개념인가? — 아니다
- 하지만 Provider trait로 묶으려면 escape hatch가 필요해진다
- 균열이 보이지만 paradigm을 바꾸진 않음
4. observability를 위한 인자 침투
session_id 같은 orthogonal한 정보가 모든 호출에 인자로 들어옴. 이건 trait 시그니처가 호출의 본질이 아니라 runtime 환경까지 책임지게 만드는 패턴.
- 좋은 점: stateless 위반 없이 telemetry 가능
- 나쁜 점: trait가 자라면 인자가 더 늘어날 수밖에 없음 (request_id, trace_id, user_id, ...)
5. MCP 일원화의 비용
모든 tool을 MCP로 통일한 결과:
- in-process 가벼운 tool도 MCP protocol overhead를 거침
- protocol stability에 발이 묶임 (MCP가 변하면 따라가야 함)
- fast path가 없음 — agent가 1ms로 끝낼 수 있는 tool도 MCP 직렬화 거침
6. statelessness vs UX 책임의 충돌
Provider trait에 generate_session_name 같은 UX 헬퍼가 들어와 있음. provider가 stateless 호출 추상화여야 한다면 이건 layering violation. 통합 product를 빨리 만들려는 압력의 흔적.
이 약점들을 깨려는 흐름들
A. trait 두께 → composable trait 흐름
- 학술/오픈소스 진영에서 Provider 추상화를 여러 trait의 조합으로 분리하려는 흐름
Embedder,OAuthProvider,ModelCatalog,CompletionProvider를 따로 두고 implementor가 골라 구현- 이건 v0.1이 직접 채택할 방향
B. single-loop 가정 → state-machine / graph 기반 agent
- LangGraph (TypeScript/Python) — agent를 노드와 엣지로 표현
- DSPy / BAML — agent action을 선언적으로 분리
- 이 흐름은 "loop는 한 종류의 patten일 뿐, 일반적 agent는 graph"라고 주장
- v0.1이 agent loop를 SDK에 안 넣은 것은 이 흐름과 정렬
C. Provider 균열 → ACP / agent-as-MCP-server
- Agent Client Protocol(ACP) — agent와 client를 protocol로 분리. provider 추상화 아래 한 층 더 둠
- agent-as-MCP-server — agent 자체를 tool로 노출. provider/tool 구분이 흐려짐
- Goose도 이미 ACP/MCP server 양쪽 채택했지만, Provider라는 개념 자체의 재정의는 아직 안 됨
D. observability 침투 → context propagation 표준
- OpenTelemetry context propagation을 함수 인자가 아니라 task-local storage로 받는 패턴
- Tokio의 task_local, Rust의 thread_local 활용
- v0.1이 채택하면 trait 시그니처에서 session_id를 뺄 수 있음
E. MCP overhead → fast path / capability layer
- 가벼운 tool은 in-process trait로 호출하고, 외부 tool만 MCP를 거치는 2-tier 흐름
- 아직 표준은 없지만, Claude Code의 hooks(zero-cost lifecycle)와 skills(low-cost) 차등이 같은 통찰
- v0.1이 trait 설계에서 미리 고려할 만함
F. UX 헬퍼 침투 → presentation layer 분리
- session naming 같은 건 LLM 호출의 한 종류일 뿐 → 별도 utility로 빼는 것이 옳음
- 이건 trait 설계 원칙 문제이지 새 패러다임은 아님
v0.1이 흡수할 인사이트
(코드가 아니라 철학 단위로)
- 호출 추상화와 product 책임을 섞지 않는다 — 이게 두꺼운 trait의 근본 원인
- single-loop를 SDK에 박지 않는다 — 다른 패턴 실험을 위한 substrate가 v0.1의 가치
- observability는 인자가 아니라 context propagation으로 — 가능하면 task-local로
- Tool을 MCP에 일원화는 하되, fast path는 열어둔다 — 지금 결정할 필요 없지만 trait 설계가 그걸 막지는 말 것
- provider라는 개념은 모델 호출 추상화에 한정 — wrapper CLI는 다른 추상화 (escape hatch는 v0.1에서 거부)
v0.1이 다르게 갈 가설
- trait 분할 — Provider는 호출만. embedding·oauth·permission·session naming은 모두 별도 trait 또는 utility
- agent loop 외부화 — v0.2에서 별도 crate. v0.1 사용자는 loop 없이도 SDK 사용 가능
- session_id 같은 인자 최소화 — observability는 옵션 또는 task-local
- wrapper CLI escape hatch 거부 — provider 의미를 좁게 유지
이 가설들은 통합 product가 아닌 라이브러리를 지향하기 때문에 가능. Goose는 product, v0.1은 library. 둘은 다른 게 옳다.
관련
-
- stateless-definition — v0.1 경계
- research/agent-harness/core-engine — Tool=MCP 원칙의 산업 근거
- evolution-roadmap — 패러다임 변화 흐름의 시간축 (예정)