ADR-0013: 선언형 Agent — 프레임워크 루프 소유·프로토콜 중립 코어·AG-UI/A2A/MCP 어댑터¶
- 상태: Accepted
- 날짜: 2026-06-24
- 선행: ADR-0009 Agentic Hexagonal Architecture
- 대체: 해당 없음
맥락 (Context)¶
ADR-0009는 @Agent를 @UseCase와 동격인 @Pod stereotype으로 모델링하고, execute()가 AgentYield stream을 반환하며, model/tool/state/signal/evidence를 building block으로 제공한다는 큰 그림을 확정했다. 그 마일스톤에서 @Agent.execute() 본문은 model stream을 직접 소비하고, tool call을 직접 invoke하며, approval/final 분기를 직접 작성하는 명령형 루프였다 (ADR-0009 "마일스톤 성공 그림"의 CodeAssistant.execute() 예시 참조).
마일스톤 #19 (DX 개선)에서 선언형 Agent로 전환하는 방향을 사용자와 길게 논의했다. 핵심 질문은 두 가지였다.
- 누가 agent 실행 루프를 소유하는가 — 개발자가
while True: async for event in model.stream(...)루프를 매번 직접 쓰는가, 아니면 프레임워크가 루프를 소유하고 개발자는 설정만 선언하는가. - AG-UI·A2A·MCP라는 세 외부 프로토콜을 코어가 어떻게 수용하는가 — 코어가 특정 프로토콜을 직접 채택하는가, 중간 변환 plugin을 두는가, 아니면 코어를 프로토콜 중립으로 두고 어댑터로 분리하는가.
ADR-0009는 이 두 결정을 명시적으로 닫지 않았다 (spakky-agent-mcp/spakky-agent-a2a를 "첫 마일스톤 범위 밖"으로만 표기). 본 ADR은 그 공백을 닫고, 후속 구현 그룹(C·D·E·F)이 따르는 단일 근거를 박제한다. 이 ADR은 ADR-0009를 대체하지 않고 그 위에 선언형 실행 모델과 프로토콜 경계를 얹는다.
비교 기준점은 pydantic-ai다. pydantic-ai는 Agent 객체가 실행 루프를 소유하고(agent.run()/run_stream()), tool은 @agent.tool 데코레이터로 선언하며, model은 생성자/실행 인자로 주입하고, 외부 프로토콜은 pydantic_ai.ui.ag_ui(AG-UI 어댑터)와 fasta2a(A2A 어댑터)로 코어 밖에서 normalize한다. 코어 자체는 어떤 UI/transport 프로토콜에도 의존하지 않는다. 본 ADR이 참조하는 범위는 개발자가 model stream 소비와 tool dispatch 배관을 직접 작성하지 않는 ownership boundary이며, pydantic-ai의 다단계 model/tool 반복 의미 전체를 구현한다는 뜻은 아니다.
결정 동인 (Decision Drivers)¶
- 개발자가 작성하는 코드의 비율을 비즈니스 의도(어떤 tool·어떤 model·어떤 정책)로 최대화하고, model stream 소비와 tool dispatch 배관 코드를 0에 수렴시킨다.
- ADR-0009가 확정한
@Agent/@UseCase동격성, 생성자 DI 의존성 주입, hexagonal 레이어 경계를 깨지 않는다. - 외부 프로토콜이 하나가 아니다 — AG-UI(클라이언트↔에이전트 UI streaming), A2A(에이전트↔에이전트 위임), MCP(에이전트↔외부 tool/server)는 서로 다른 계층의 서로 다른 계약이다. 코어가 어느 하나를 직접 채택하면 나머지 둘이 종속·왜곡된다.
- 코어는 외부 프로토콜 라이브러리에 의존하지 않는다 (ADR-0009 검증 기준 "
core/spakky-agent는 외부 protocol server에 직접 의존하지 않는다"의 연장). - HITL(Human-In-The-Loop), 멀티턴 세션, context compaction 같은 cross-cutting 실행 관심사는 개발자 루프 본문이 아니라 프레임워크 실행 루프가 일관되게 처리해야 재현 가능하고 복구 가능하다.
- v6.10.0 minor 릴리스이며 현재 외부 고객(production 소비자)이 없으므로, ADR-0009의 명령형
execute()예시 대비 하드 브레이크를 허용한다.
고려한 대안 (Considered Options)¶
프로토콜 수용 축에서 세 가지 대안을 검토했다.
대안 α: 코어가 AG-UI를 직접 채택¶
core/spakky-agent가 ag_ui 라이브러리를 직접 import하고, AgentYield 대신 AG-UI BaseEvent taxonomy를 코어 1차 이벤트 모델로 삼는다. 어댑터 레이어 없이 @Agent.execute() stream이 곧 AG-UI 이벤트 stream이다.
기각 이유:
- AG-UI는 클라이언트↔에이전트 UI streaming 프로토콜이다. A2A(에이전트 간 위임)와 MCP(외부 tool 호출)는 AG-UI 이벤트 모델로 표현되지 않는 별도 계약이다. 코어를 AG-UI에 고정하면 A2A delegation과 MCP tool 호출이 AG-UI 이벤트로 억지 매핑되어 왜곡된다.
- 코어가 외부 프로토콜 라이브러리(
ag_ui)에 의존하게 되어 ADR-0009의 "코어 비의존" 검증 기준을 위반한다. - AG-UI 스펙 버전 변경이 코어 public contract를 직접 깬다.
대안 β: 코어와 프로토콜 사이에 중간 mapper plugin¶
코어는 자체 이벤트 모델을 유지하되, spakky-agent-protocol-mapper 같은 단일 중간 plugin이 코어 이벤트를 모든 외부 프로토콜로 양방향 변환하는 단일 책임을 진다. 각 프로토콜 어댑터는 이 mapper를 경유한다.
기각 이유:
- 세 프로토콜은 변환 모양이 근본적으로 다르다 (AG-UI=outbound event stream, A2A=Task 상태 머신 + AgentCard discovery, MCP=tool descriptor 정규화). 단일 mapper는 세 변환의 공약수가 거의 없는 god-object가 되거나, 내부적으로 다시 세 갈래로 분기하여 어댑터 분리와 동형이 된다.
- mapper가 코어와 어댑터 사이의 불필요한 pass-through 레이어가 된다 (
behavioral-guidelines.md§2 "필요 없는 wrapper/resolve 레이어 제거" 위반). 어댑터가 코어 이벤트를 직접 소비하면 되는데 중간 계약을 하나 더 만든다. - pydantic-ai 선례와 어긋난다 — pydantic-ai는 중간 mapper 없이
ui.ag_ui어댑터와fasta2a어댑터가 코어 message/event를 직접 소비한다.
대안 γ: 프로토콜 중립 코어 + 프로토콜별 어댑터 분리 (채택)¶
코어는 어떤 외부 프로토콜에도 의존하지 않는 중립 이벤트 taxonomy와 실행 루프만 소유한다. 각 프로토콜은 독립 어댑터가 코어 이벤트를 자신의 프로토콜로 normalize한다.
- AG-UI 어댑터 →
ag_ui라이브러리 의존, 코어 이벤트를 AG-UIRunAgentInput/BaseEvent로 변환. - A2A 어댑터 → 공식
a2a-python라이브러리 의존, 코어 실행을 A2ATask상태 머신으로 노출. - MCP 어댑터 → MCP 라이브러리 의존, 외부 MCP server tools를 코어 tool 계약으로 정규화하되 모델에는 lazy search/call 도구만 노출.
채택 이유:
- 프로토콜이 둘 이상이라는 사실 자체가 중립 코어를 유일 정합으로 만든다. 셋 중 어느 하나를 코어에 고정하면 나머지가 종속된다.
- 코어 비의존을 유지하여 ADR-0009 검증 기준과 일관된다. 프로토콜 라이브러리 버전 변경은 해당 어댑터에 격리된다.
- pydantic-ai 선례(
ui.ag_ui어댑터 +fasta2a)와 정확히 일치한다 — 검증된 산업 패턴이다.
실행 orchestration 소유 축에서는 ADR-0009의 명령형 execute() 본문 작성 방식과, 프레임워크가 표준 model stream/tool dispatch 경로를 소유하고 개발자는 설정만 선언하는 방식을 비교했다. 후자를 채택한다 (아래 "결정" 참조). 명령형 방식은 개발자가 model.stream() 소비·tool invoke·approval 분기를 매번 재작성하게 하여 배관 코드 중복과 HITL/compaction/recovery 처리의 비일관을 낳는다.
결정 (Decision)¶
1. 프레임워크가 표준 실행 orchestration을 소유한다 (설정 선언형)¶
현재 표준 경로는 한 번의 model stream request를 열고, 그 stream에서 나온 tool-call candidate를 승인·dispatch하여 result/evidence로 방출한 뒤 terminal state와 final output으로 닫는다. 이 model stream 소비와 tool dispatch orchestration은 framework runner가 소유하며, 개발자는 @Agent spec으로 무엇을 실행할지만 선언한다 (어떤 model, 어떤 tool, 어떤 정책).
현재 runner는 실행된 tool result를 assistant/tool history가 포함된 새 ModelRequest에 재주입하거나 같은 run()/run_events() invocation에서 model을 다시 호출하지 않는다. 따라서 model → tool → model로 이어지는 single-invocation multi-step loop는 표준 경로의 일부가 아니며, 필요한 경우 custom execute()가 직접 orchestration해야 한다.
execute()인터페이스는 유지한다 —@Agent는 ADR-0009대로@UseCase와 동격인 호출 가능한 application component다. 다만 개발자가execute()본문에 model stream/tool dispatch 배관을 직접 작성하는 대신, runner가 spec과 DI graph로부터 single-pass 표준 실행 orchestration을 자동 제공한다. Tool result 재주입이나 반복 model 호출 같은 커스텀 제어가 필요할 때는execute()본문을 직접 작성한다.- 의존성은 ADR-0009대로 생성자 DI로 주입한다. model(
IAgentModel), tool(@agent_tool로 노출된 capability), outbound port는 생성자 인자다.@Agentspec은 이 의존성을 다시 선언하지 않는다. - 이 DX는 pydantic-ai의 ownership boundary를 참조한다 — 개발자는 비즈니스 의도(model·tool·정책)만 선언하고, 현재 single-pass 실행 orchestration은 프레임워크가 제공한다.
2. 프로토콜 중립 코어 + 어댑터 분리 (대안 γ)¶
core/spakky-agent는 프로토콜 중립 이벤트 taxonomy와 실행 루프만 소유하고, 외부 프로토콜은 어댑터로 분리한다.
- 어댑터가 프로토콜 라이브러리에 의존한다: AG-UI 어댑터 =
ag_ui, A2A 어댑터 = 공식a2a-python, MCP 어댑터 = MCP 라이브러리. - 코어는 어떤 프로토콜 라이브러리에도 의존하지 않는다.
3. 중립 이벤트 taxonomy¶
코어 이벤트 taxonomy는 프로토콜 중립이며 다음 종류를 포함한다. 각 이벤트는 attribution 메타데이터를 함께 운반한다.
message— 메시지 단위 출력.reasoning— model의 추론(reasoning) 출력.tool-call—start/args/end/result4단계로 tool 호출 수명주기를 표현한다.run/step— 실행(run)과 그 내부 단계(step) 경계.state— 실행 상태 변화 (ADR-0009AgentStatelifecycle과 정렬).artifact— 산출물(artifact) 출력.
모든 이벤트는 다음 attribution을 운반한다.
- agent attribution — 어느 agent가 발생시킨 이벤트인지.
- parent link — 위임(delegation) 트리에서 부모 실행으로의 연결 (ADR-0009 parent/child linkage와 정렬).
- conversation id — 멀티턴 대화 식별자.
이 taxonomy는 AG-UI BaseEvent로 어댑터에서 매핑 가능하되, A2A/MCP로도 매핑 가능하도록 어느 프로토콜에도 종속되지 않는다. ADR-0009의 public AgentYield vocabulary(Token/Progress/Tool/Evidence/Approval/Final/Error/Cancel)는 본 taxonomy로 일반화·정렬되며, 후속 구현 그룹이 정확한 매핑을 확정한다.
4. ModelCapability descriptor + IAgentModel 스트림 계약 확장¶
model backend마다 지원 능력이 다르므로(reasoning 지원 여부, context window 크기, token counting 지원 여부 등), IAgentModel에 ModelCapability descriptor를 도입한다.
ModelCapability는 reasoning 지원,context_window크기, token counting 지원 등을 선언한다.IAgentModel스트림 계약을 reasoning 이벤트 등으로 확장하되, capability를 지원하지 않는 backend에서는 graceful degrade한다 (capability 부재 시 startup fail이 아니라 해당 이벤트 종류를 생략).
5. HITL 도구 승인 — 통일된 pause → 승인요청 → resume¶
도구 승인(HITL)은 ADR-0009 INTERRUPTED(reason=APPROVAL_REQUIRED) 모델 위에서 단일 pause → 승인요청 → resume 흐름으로 통일한다.
- 실행 orchestration이 승인 필요 action에서 일시정지(pause)하고, caller에게 승인요청을 노출한 뒤, 승인 signal 수신 시 resume한다.
- AG-UI 어댑터는 이를 deferred tool /
RunAgentInput(승인 결과를 다음 run 입력으로 주입)으로 노출한다 — pydantic-ai deferred tool approval 선례. - A2A 어댑터는 이를
input-requiredTask 상태로 노출한다.
코어의 단일 pause/resume 모델을 어댑터가 각 프로토콜의 관용으로 투영하므로, HITL 의미가 프로토콜마다 갈라지지 않는다.
6. 세션 / 멀티턴 — 영속 세션 + 클라이언트 주입 이력 둘 다¶
멀티턴 대화는 두 경로를 모두 지원한다.
- 영속 세션 — 프레임워크가 대화 이력을 영속화하고
conversation_id로 이어간다. TaskStore는 spakky 영속성 계약(ADR-0009 repository contribution 모델)을 사용한다. - 클라이언트 주입 이력 — caller가 이력(message history)을 직접 주입한다 (pydantic-ai
message_history인자 선례).
둘 중 하나로 강제하지 않는다 — stateless 클라이언트 주입과 stateful 영속 세션이 둘 다 유효한 소비 형태다.
7. Context compaction — pluggable CompactionStrategy 포트 + 내장 전략¶
context 압축은 교체 가능한(pluggable) CompactionStrategy 포트로 모델링한다.
- 포트 형태는 pydantic-ai의 message history processor /
ProcessHistorycapability(compact_messages)를 참조한다. - 프레임워크는 내장(built-in) 기본 전략을 제공한다.
- compaction은
@Agentspec에 선언하고 실행 orchestration이 model request 전에 자동 적용한다 — 개발자가execute()본문에서 직접 호출하지 않는다. - ADR-0009
ContextDigest(압축 결과를 derived evidence로 append) 모델과 정렬한다 — compaction은 raw evidence를 대체하지 않는다.
8. teammate (팀 모드) — @Agent spec 선언 + A2A 위임¶
multi-agent 팀 모드(teammate)는 @Agent spec으로 선언한다.
- 로컬 teammate는 로컬
@Pod(@Agentcomponent)로 해석한다. - 원격 teammate는 A2A
AgentCard로 discovery·위임한다. - 위임은 ADR-0009 delegation building block(
DelegationPacket/DelegationResult) 위에서 A2A 어댑터를 통해 원격으로 확장된다.
9. 프레임워크 책임 경계¶
프레임워크가 소유하는 범위와 소비자(application/UI 개발자) 몫을 명확히 분리한다.
프레임워크 소유:
- building blocks (ADR-0009 model/tool/state/signal/evidence/delegation 등)
- single-pass 실행 orchestration (본 ADR 결정 1)
- 프로토콜 어댑터 (본 ADR 결정 2)
- 인터페이스/계약 (
@Agentspec, 이벤트 taxonomy, port)
소비자 몫 (프레임워크 범위 밖):
- 렌더링 / UI — AG-UI 이벤트를 받아 화면에 그리는 것은 클라이언트(소비자)의 책임이다 (ADR-0009 "별도 stream projector service를 제공하지 않는다"의 연장).
10. 릴리스¶
v6.10.0 minor 릴리스로 출시한다. 현재 production 소비자가 없으므로 ADR-0009의 명령형 execute() 예시 대비 하드 브레이크를 허용한다.
결과 (Consequences)¶
긍정적¶
- 개발자는 model stream 소비와 tool dispatch 배관 코드를 작성하지 않고
@Agentspec으로 비즈니스 의도만 선언한다. - HITL·세션·compaction이 표준 실행 orchestration에서 일관 처리되어 재현·복구 가능성이 어댑터/프로토콜 전반에서 균일해진다.
- 코어 비의존이 유지되어 AG-UI·A2A·MCP 스펙 변경이 어댑터에 격리된다. 새 프로토콜 추가는 새 어댑터 추가일 뿐 코어를 건드리지 않는다.
- ADR-0009 building block(state/signal/evidence/delegation/contribution persistence)을 그대로 재사용하며 그 위에 선언형 실행과 프로토콜 경계를 얹는다.
부정적¶
- 프레임워크가 single-pass 표준 orchestration을 소유하므로, tool result 재주입과 같은 invocation의 반복 model 호출을 포함한 multi-step 제어는 본문 직접 작성이라는 escape hatch가 필요하다.
- 중립 이벤트 taxonomy ↔ 세 프로토콜 매핑을 어댑터마다 정확히 정의해야 하며, taxonomy 누락은 특정 프로토콜에서 표현 불가로 드러난다.
- ADR-0009의 명령형
execute()예시와 하드 브레이크가 발생한다 — 마이그레이션 가이드가 필요하다 (현재 고객 없음으로 비용 수용).
중립적¶
- pydantic-ai는 DX·어댑터 분리의 참조 선례일 뿐 의존 대상이 아니다 — ADR-0009 "Pydantic AI는 첫 구현의 정본이 아니다"와 일관된다.
- 본 ADR은 결정·경계만 박제하며, 정확한 클래스/시그니처(이벤트 taxonomy 필드명,
CompactionStrategy포트 시그니처,ModelCapability필드)는 후속 구현 그룹(C·D·E·F)이 코드로 확정한다.
검증 기준¶
core/spakky-agent는ag_ui·a2a-python·MCP 라이브러리에 직접 의존하지 않는다.- AG-UI / A2A / MCP 어댑터가 각각 독립 어댑터로 분리되어 해당 프로토콜 라이브러리에만 의존한다.
@Agentspec 선언만으로ModelRequest한 번의 stream 소비, tool candidate 승인·dispatch, terminal output까지의 single-pass 실행이 동작한다.- HITL 도구 승인이 단일 pause → 승인요청 → resume으로 동작하고, AG-UI deferred tool과 A2A
input-required로 각각 투영된다. - 영속 세션과 클라이언트 주입 이력 두 경로 모두로 멀티턴이 동작한다.
CompactionStrategy포트가 교체 가능하고 내장 전략이 spec 선언으로 자동 적용된다.ModelCapability미지원 backend에서 graceful degrade한다.- 원격 teammate가 A2A
AgentCard로 discovery·위임된다.
참고 자료¶
- ADR-0009: Agentic Hexagonal Architecture
- ADR-0010: Feature Contribution Policy
- AG-UI Overview
- A2A Protocol Specification
- Model Context Protocol Tools Specification
- Pydantic AI — AG-UI Adapter (
pydantic_ai.ui.ag_ui) - Pydantic AI — Deferred Tools / Human-in-the-Loop Approval
- Pydantic AI — Message History Processors (
compact_messages)