feat: #75 PR 3 — auto-Task wrap 분기 + session/a2a_context 종료 모델 정정 (docs first)#79
Merged
Merged
Conversation
PR 1/2 후 재검토 결과 옛 모델의 두 가정이 잘못이었음을 정정. session (사용자 대화창) — 종료 개념 없음: - Slack DM / ChatGPT 류로 사용자가 언제든 재개 - session.end 이벤트 / sessions.ended_at 컬럼 / SessionEndProcessor 모두 두지 않는다 — archive 가 필요해지면 별도 컬럼 (archived_at) 으로 a2a_context (에이전트 간 대화) — 종료는 agent 가 결정: - agent 가 "이 inter-agent 대화 마무리" 라 판단한 시점에 a2a.context.end publish. RPC 라이프사이클이 아님 (한 contextId 위 다중 RPC 누적 가능) - 발화 위치 / 결정 로직은 agent 통합 PR 에서 폐기: - session.end → a2a_contexts cascade close 논의 (session 안 끝나니 cascade 자체 없음) - "trace 는 사용자 의도에서만 시작" 가정 (autonomous / system trigger 도 trace 시작점) 영향 문서: - architecture-chat-protocol.md: chat lifecycle 표 + tier 비교 표 - architecture-event-pipeline.md: chat layer / a2a layer 이벤트 표 - architecture-user-gateway.md: UG publish 항목 - knowledge-model.md: sessions / a2a_contexts schema 설명
PR 3) trace 정의 보완: - 시작점 셋 — 사용자 / agent autonomous / 외부 system trigger - 사용자 의도에서만 시작한다는 옛 가정 폐기. agent 가 자력으로 시작한 일도 trace 가 붙어 boundary 가로지르는 추적 가능 Context lifecycle (start / end) section 신설: - start: 새 contextId 첫 RPC 시 (CHR idempotent dedup) - end: agent 가 "대화 마무리" 판단 시. RPC 단위 아님 - session 과 다르게 a2a_context 는 종료 개념 있음 (agent 가 관리하는 대화) Task wrap vs Message-only 분기 신설 (#75 PR 3): - spec 가이드 "Messages for Trivial, Tasks for Stateful" 따라 method-level 분기 — SendMessage = Message, SendStreamingMessage = Task wrap - server / graph 안 LLM 분류 도입하지 않음 (호출자가 method 선택으로 명시) - 같은 contextId 위에 두 method 가 섞여도 같은 a2a_context 에 누적 (contextId 가 grouping key, method 무관) publish 패턴은 별도 publish.py 섹션에서 구현 시 매핑.
이전 commit 의 §3.4 가 두 axis 를 합쳐버림 — method (transport) 와 response shape (trivial / stateful) 는 spec 상 직교. 정정: - method 는 transport (sync vs streaming) 만 결정 - response shape (Message only / Task wrap) 는 agent (graph) 결정 - 4가지 조합 모두 가능 — 예: SendStreamingMessage 도 trivial 이면 Message stream 결정 메커니즘: - graph state 에 hint 명시 (`requires_task: bool` 또는 `task_state`) - handler 는 hint 만 보고 wrap 분기 — 분류 로직 0 - graph 가 hint 미구현이면 default 는 Message only (Task 는 명시적 opt-in) publish 패턴도 method 가 아닌 결정 결과 (Message only / Task wrap) 별로 정리.
룰베이스 / 휴리스틱 옵션 제거. callee graph 안 LLM 이 자기 응답이 trivial 인지 stateful 인지 추론해 결정. "그게 진짜 에이전트" — 룰 따르지 않고 추론. 구현: - ReAct 응답 generation LLM 의 structured output 에 `requires_task` 필드 포함 (schema 강제) - LLM 이 자기 응답 의도 (조회 vs 위임 / fact 확인 vs long-running) 를 보고 직접 채움 - handler 는 hint 보고 wrap 분기 — 분류 / 분석 로직 0 - persona text 에 결정 가이드 단편 추가 default (hint 누락 시) 는 Message only — Task 는 LLM 명시 시에만.
callee agent 의 graph 안 LLM 이 응답이 trivial / stateful 인지 추론한 결과를 담는 Pydantic schema. graph 측은 `llm.with_structured_output(A2AResponseDecision)` 으로 schema 강제. handler 측은 graph state 의 hint 만 읽어 wrap 분기. Fields: - requires_task: True 면 Task wrap (a2a.task.create + status_update 발화) - reason: LLM 의 결정 근거 (관찰 / 디버깅용) shared/a2a/__init__ 에 export 추가.
Primary graph 의 `classify_response` 노드가 LLM structured output 으로 `requires_task` 결정 → handler 가 hint 만 보고 Message / Task 분기. agents/primary: - graph.State 에 `requires_task: NotRequired[bool]` 추가 - `_make_classify_response_node` — `llm.with_structured_output(A2AResponseDecision)` 로 LLM 강제 · 대화를 user message text 로 직렬화 (Anthropic "must end with user msg" 제약 회피) · 실패 시 보수적 default (Message only) - ReAct edge: `llm_call` 종료 → `classify_response` → END - persona text 에 결정 가이드 추가 (위임 / long-running = true, 조회 / fact = false) shared/a2a/server/graph_handlers: - send_message.py: graph.ainvoke 결과의 `requires_task` 보고 분기 · True → 기존 Task wrap (publish task.create + WORKING + agent reply + COMPLETED) · False (default) → Message only (publish message.append × 2, task.* 0) - factories.py: trivial 응답용 helper 신설 (`make_agent_reply_message` / `make_agent_error_message` — task_id 비움) - stream.py: classify_response 노드의 token 은 SSE stream 에서 제외 (사용자 미노출) streaming 경로 (`SendStreamingMessage`) 는 SSE 형식이 Task 중심이라 항상 Task wrap 유지. classify_response 는 실행되어 hint 가 graph state 에 기록은 되지만 (관찰용) handler 가 사용 X. messaging.md §3.4 에 명시. 검증: - shared 57/57 / chronicler 12/12 / doc-store 24/24 unit pass - e2e (SendMessage 직접 호출): · 정보 조회 → LLM `requires_task=False` → a2a_messages=2, a2a_tasks=0 ✅ · 위임 미구현 인식해 정보 응답 → 같은 결과 ✅ - e2e (UG /api/chat = SendStreamingMessage): · 단순 인사 → LLM `requires_task=False` 기록만, 실제론 Task wrap (streaming 제약) ✅
…Event / Processor 폐기 #75 PR 3: session 은 사용자 대화창 metaphor 상 종료 개념 없음 (Slack DM / ChatGPT 류) — 사용자가 언제든 재개. archive 가 필요해지면 별 컬럼 (`archived_at`) 으로. shared/event_bus: - `SessionEndEvent` 클래스 삭제 + EventType / __all__ 정리 - 본문 주석에 폐기 사유 명시 shared/doc_store/schemas/session: - SessionUpdate 의 `ended_at` 필드 제거 (metadata 만 남음) - SessionRead 의 `ended_at` 필드 제거 mcp/doc-store/migrations: - 005_drop_session_ended_at — `sessions.ended_at` 컬럼 drop chronicler: - `SessionEndProcessor` 파일 삭제 + ALL_PROCESSORS / __all__ 정리 - test_handler.py: SessionEnd 관련 import / 테스트 / count assertion 삭제 - _session_read helper 의 ended_at 인자 제거 shared/tests/test_event_bus.py: - chat layer publish 테스트의 SessionEnd 발화 제거 (총 3 → 2 events) `A2AContextEndEvent` / `A2AContextEndProcessor` / `a2a_contexts.ended_at` 은 유지 — agent 가 결정해 발화 (PR 5 예정).
messaging.md §3.4 에 현재 구현 상 SendStreamingMessage 는 항상 Task wrap 임을 명시. 이유: SSE 형식이 Task 중심 (initial Task → TaskArtifactUpdateEvent × N → TaskStatusUpdateEvent). Message-only streaming 은 별도 SSE 흐름이 필요해 향후 확장. classify_response 노드는 streaming 경로에서도 실행되며 hint 가 graph state 에 기록되지만 (관찰용) handler 가 사용 X. SendMessage (sync) 만 hint 따라 실제 분기.
graph.py 안에 박혀있던 `_CLASSIFY_PROMPT` 상수가 프로젝트의 prompt 자료 처리 패턴 위반 — root CLAUDE.md "AI 에이전트 런타임 자산" 원칙은 자연어 prompt 자료는 코드 밖 (config / resources) 으로 분리. 본 결정은 agent 정체성이 아닌 A2A 프로토콜 차원 — 모든 agent (Primary / Architect / Engineer / QA) 가 동일 기준으로 답해야 함. 따라서 agent 별 resources 가 아닌 shared 로: - shared/src/dev_team_shared/a2a/decision.py 에 `DEFAULT_RESPONSE_DECISION_PROMPT` 상수 추가 + __init__ export - agents/primary/.../graph.py 는 import 해 사용 — 코드 안 자연어 0 - shared/.../a2a/messaging.md §3.4 에 prompt 위치 / 원칙 명시 (재발 방지) 검증: shared 57 / chronicler 12 unit pass + e2e 동작 동일 (LLM requires_task=False 결정 로그 확인).
#75 PR 3) Primary 와 Librarian 의 graph.py 가 99% 동일 (persona / tools 만 다름) 한 상태에서 agent 추가마다 복붙 강요되던 구조를 정리. shared 가 building blocks 만 제공하고 각 agent 는 자기 graph 를 명시적으로 조립 (옵션 D — topology 자유 보존 + DRY). 신설: - `shared/src/dev_team_shared/agent_graph/react.py` — ReAct 패턴 building blocks · `make_llm_call_node(persona, llm_with_tools)` — persona + messages → LLM · `make_tool_node(tools)` — tool_calls 실행 → ToolMessage · `should_continue_react(state, *, when_done)` — 분기 helper · `serialize_tool_result(value)` — tool 결과 → JSON 확장: - `shared/src/dev_team_shared/a2a/decision.py` — `make_classify_response_node` factory 추가 (graph.py 안에 박혀 있던 것 이동) + `format_conversation_for_classifier` helper. A2A 프로토콜 차원 결정이라 모든 agent 공유. 분류 원칙: - ReAct 일반 building blocks → `agent_graph/` - A2A 프로토콜 결정 → `a2a/decision.py` - agent 정체성 / 도메인 워크플로 → 각 agent 의 config / resources
…으로 graph.py 슬림화 (#75 PR 3) 각 agent 의 graph.py 가 ReAct building blocks 와 classify_response factory 를 shared 에서 import. 코드 ~80% 감소 + protocol-level 일관 동작 (모든 agent 가 classify_response 자동 포함). agent 별 차별화 지점은 graph.py 의 build_graph() 안에서 명시적 조립 (어떤 노드 / edge 로 구성할지) — topology 자유 보존. Primary: - ReAct + classify_response (#75 PR 3 의 분기) - tools 4 채널 (Doc Store / IssueTracker / Wiki / Librarian A2A) Librarian: - 동일 구조 (Primary 와 같은 패턴) + classify_response 자동 포함 - tools = Doc Store read 단일 채널 agent 정체성은 config/base.yaml (persona) / resources/*.md (도메인 가이드) 에 그대로 — graph.py 는 조립만. 검증: shared 57/57 unit pass + e2e 양쪽 정상: - SendMessage 직접 (Primary) → LLM `requires_task=False` → a2a_tasks=0 ✅ - UG /api/chat streaming → 같은 LLM 결정, streaming 제약으로 Task wrap ✅
…ns 폐기 + 옛 구조 정정 본 PR 3 의 graph 리팩터링 (shared/agent_graph/ 신설 + workflow.extensions 패턴 채택 안 함) 을 반영해 docs 정정. 추가로 옛 구조 (langgraph_base / adapters / broker / mcp-servers) 가 현 구현과 어긋난 부분도 정합. shared/CLAUDE.md §3 카탈로그: - `agent_graph/` 추가 (Pattern B — LangGraph building blocks) - `a2a/decision.py` 의 역할 명시 (A2A 응답 shape 결정) docs/proposal/architecture-role-config.md: - `workflow` 필드 — "필수" → "현재 미사용". graph 토폴로지는 graph.py 가 shared/agent_graph 조립으로 표현 (#75 PR 3 옵션 D 채택) - 역할별 yaml 예시들 위에 정정 노트 추가 — `workflow.base + extensions` 블록은 옛 디자인 의도 기록으로만 남김 docs/proposal/architecture-agent-internals.md: - "공통 베이스 그래프 위에 sub-graph 모듈을 얹는 구조" 설명 정정 → building blocks 조립 패턴 - agent 별 능력 (user_chat / prd_authoring / three_stage_design 등) 은 "추후 노드로 분화될 책임 영역" 으로 재정의 — 현재는 persona 가이드 + ReAct 루프로 흡수 docs/proposal/project-structure.md (전면 재작성): - 옛 구조 (langgraph_base / extensions/ / adapters / broker / mcp-servers) → 현 구조 (agent_graph / shared 8 서브패키지 / mcp/ / chronicler 등) - agent 별 차별화 지점 표 추가 (정체성 / 워크플로 / 도구 / 토폴로지 / 연결) - Pattern A/B 분류 / Role Config 로딩 / Chronicler 정체성 등 현 사실 반영
직전 commit 의 "공통 베이스 그래프 위에 sub-graph 모듈을 얹는 방식은 채택하지 않음" 표현이 LangGraph subgraph feature 자체 거부로 읽힐 수 있어 정정. 실제로 거부한 건 두 개념 중 하나만: 거부: config-driven extension dispatch (workflow.base + workflow.extensions 로 config 가 dispatch → 코드가 dynamic compose) 허용: LangGraph 자체의 subgraph (StateGraph 안 StateGraph) composition. graph.py 의 build_graph() 안에서 코드가 명시적으로 끼우는 형태. 예: Architect 의 three-stage design 같이 자연스러운 subgraph 단위는 자유롭게 사용. 차이는 "누가 dispatch 하는가" — config 가 아닌 코드. 옵션 D 정신 그대로.
This was referenced May 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#75 의 PR 3. 세 가지 작업 묶음:
requires_task결정 → handler 가 hint 만 보고 Message / Task 분기ended_at/SessionEndEvent/SessionEndProcessor폐기부수: 옛 디자인 (workflow.base + extensions / langgraph_base / extensions/ 디렉터리) 와 어긋난 docs 일괄 정정.
(1) auto-Task wrap 분기 — LLM 추론 기반
결정 메커니즘
callee graph 안
classify_response노드가llm.with_structured_output(A2AResponseDecision)으로 LLM 강제 — 자기 응답 의도 (조회 / 위임 / long-running) 보고requires_task채움. handler 는 graph state 에서 hint 만 읽어 분기 (분류 / 분석 로직 0).분기 결과
requires_task=Falsea2a.context.start+a2a.message.append × 2requires_task=Truea2a.task.create+a2a.task.status_update × 2(WORKING/COMPLETED) + agent reply with task_idStreaming 한계
SendStreamingMessage는 SSE 형식 자체가 Task 중심 (initial Task → TaskArtifactUpdateEvent × N → TaskStatusUpdateEvent) 이라 항상 Task wrap. classify_response 는 실행되며 hint 는 graph state 에 기록되지만 (관찰용) handler 가 사용하지 않음. Message-only streaming 은 향후 확장.Persona 가이드
Primary persona text 에 결정 가이드 단편 추가 (위임 / long-running = true, 조회 / fact = false).
Prompt 위치 — shared
A2A protocol-level 결정이라 prompt 텍스트는
shared/a2a/decision.py:DEFAULT_RESPONSE_DECISION_PROMPT상수. 모든 agent 가 import. 코드 안 자연어 자료 박지 않는다는 root CLAUDE.md "AI 에이전트 런타임 자산" 원칙 준수.(2) session 종료 모델 정정
session 은 사용자 대화창 metaphor 상 종료 개념 없음 (Slack DM / ChatGPT 류 — 사용자가 언제든 재개).
폐기
SessionEndEvent클래스 + wiresession.endSessionEndProcessor(chronicler)sessions.ended_at컬럼 (migration 005)SessionUpdate.ended_at/SessionRead.ended_at필드publish_session_end호출유지
A2AContextEndEvent/A2AContextEndProcessor/a2a_contexts.ended_at— agent 가 결정해 발화 (PR 5 예정)(3) agent graph building blocks 추출 (옵션 D)
Primary / Librarian 의 graph.py 가 99% 동일 (persona / tools 만 다름) 인 상태에서 새 agent 추가마다 복붙 강요되던 구조 정리. shared 가 building blocks 만 제공하고 각 agent 가 자기 graph 를 명시적으로 조립 — topology 자유 보존 + DRY.
신설 —
shared/src/dev_team_shared/agent_graph/ReAct 패턴 building blocks (Pattern B — in-process library):
make_llm_call_node(persona, llm_with_tools)— persona + messages → LLMmake_tool_node(tools)— tool_calls 실행 → ToolMessageshould_continue_react(state, *, when_done)— tool_calls 분기 helperserialize_tool_result(value)— tool 결과 → JSON확장 —
shared/src/dev_team_shared/a2a/decision.pyA2A 프로토콜 차원 결정 (모든 agent 공유):
A2AResponseDecisionPydantic schemaDEFAULT_RESPONSE_DECISION_PROMPT(system prompt)make_classify_response_node(llm, *, system_prompt)— LangGraph 노드 factoryformat_conversation_for_classifier(messages)— Anthropic "must end with user msg" 회피용 helper분류 원칙
agent_graph/a2a/decision.pyconfig/(persona) +resources/*.md결과
각 agent graph.py 는 building blocks 조립만:
LangGraph subgraph feature 는 사용 가능
위 결정은 config 가 dispatch 하는 패턴 (
workflow.base + extensions) 거부일 뿐, LangGraph 자체의 subgraph (StateGraph안StateGraph) composition 은 자유롭게 사용 가능. 향후 Architect 의 three-stage design 같이 자연스러운 subgraph 단위는build_graph()안에서 명시적으로 끼움 — config 가 아닌 코드가 결정.변경 파일
코드 — A2A response decision
shared/src/dev_team_shared/a2a/decision.py(신설) — schema + prompt + factory + helpershared/src/dev_team_shared/a2a/__init__.py— exportagents/primary/config/base.yaml— persona 결정 가이드코드 — handler 분기
shared/src/dev_team_shared/a2a/server/graph_handlers/send_message.py— hint 보고 Message / Task 분기shared/src/dev_team_shared/a2a/server/graph_handlers/factories.py— trivial Message helper 신설shared/src/dev_team_shared/a2a/server/graph_handlers/stream.py— classify_response token 필터코드 — agent graph building blocks
shared/src/dev_team_shared/agent_graph/__init__.py(신설)shared/src/dev_team_shared/agent_graph/react.py(신설) — ReAct building blocksagents/primary/src/primary_agent/graph.py— shared blocks 조립으로 슬림화agents/librarian/src/librarian_agent/graph.py— 동일 패턴 + classify_response 자동 포함코드 — session 종료 cleanup
shared/src/dev_team_shared/event_bus/events.py—SessionEndEvent폐기shared/src/dev_team_shared/event_bus/__init__.py— exports 정리shared/src/dev_team_shared/doc_store/schemas/session.py—ended_at필드 제거chronicler/src/chronicler/processors/__init__.py—SessionEndProcessor등록 제거chronicler/src/chronicler/processors/session_end.py— 삭제mcp/doc-store/migrations/005_drop_session_ended_at.sql(+ rollback) — 컬럼 drop문서
shared/CLAUDE.md— §3 카탈로그에agent_graph/추가,a2a/decision.py역할 명시shared/src/dev_team_shared/a2a/messaging.md— trace 시작점 셋, context lifecycle, Task/Message 분기 (LLM 추론 + streaming 한계 + prompt 위치)docs/proposal/architecture-chat-protocol.md— chat lifecycle 표 + tier 비교 표 (session.end 제거)docs/proposal/architecture-event-pipeline.md— chat / a2a layer 이벤트 표 (session 종료 개념 없음 명시 + a2a.context.end agent 결정)docs/proposal/architecture-user-gateway.md— UG publish 항목docs/proposal/architecture-role-config.md—workflow필드 = 현재 미사용 명시docs/proposal/architecture-agent-internals.md— building blocks 조립 패턴 + LangGraph subgraph 사용 가능성 명시docs/proposal/knowledge-model.md— sessions / a2a_contexts schema 설명docs/proposal/project-structure.md— 옛 구조 → 현 구조 전면 재작성테스트
chronicler/tests/test_handler.py— SessionEnd 관련 제거 (13 → 12)shared/tests/test_event_bus.py— chat layer 검증 갱신 (3 → 2 events)검증
requires_task=False→ a2a_messages=2, a2a_tasks=0 ✅/api/chat= SendStreamingMessage):requires_task=False기록 + 실제론 Task wrap (streaming 제약) ✅sessions.ended_at컬럼 drop 확인후속
chat.append(role=agent) publishA2AContextEndEvent발화 위치 결정 (agent 가 inter-agent 대화 마무리 판단)memory/pr4_chat_protocol_followups.md🤖 Generated with Claude Code