You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #68 (#39 Primary 그래프 확장) 검증 중 사용자 ↔ Primary 대화의 여러 결함이
드러났음 (#69 ~ #73). 직접 원인은 우리 구현이 모든 SendMessage 응답을
A2A Task 로 자동 wrap 한 것 — A2A 스펙은 trivial 응답을 Message 로
두는 것도 허용하는데 (공식 가이드: "Messages for Trivial Interactions,
Tasks for Stateful Interactions") 단순화로 그렇게 하지 않았음. 그 결과:
사용자의 단순 발화 ("안녕", "뭐 있어?") 마다 A2A Task 객체가 만들어짐
agent_tasks 가 chronicler-fallback 으로만 채워지는 운영 awkwardness 발생
agent_sessions 의 의미 (conversation vs turn) 모호 / agent_items 의 chain
컬럼 미사용 등 부수 결함
이 자동 Task wrap 을 풀어 trivial / stateful 분기를 해도 한 가지 더 본질적
인 점이 남는다:
사용자 ↔ 에이전트 와 에이전트 ↔ 에이전트 는 다른 영역
A2A (Agent-to-Agent) 는 이름 그대로 에이전트 간 협상 / 위임 / 자동화 협업용
사용자 인터랙션은 자체 어휘 / 자체 프로토콜로 정의하는 게 의미상 깔끔
→ 자동 Task wrap fix + 사용자-에이전트 통신을 별도 chat protocol 로 분리.
우리 도메인 Task = Assignment (이름 변경) — A2A Task 와 다른 객체. 한 Assignment 안에서 여러 A2A Task 발생 가능.
§2. 통신 layer
UG → P/A: REST POST (POST /api/chat with session_id + text → 202 ack) P/A → UG: 영속 SSE per session (GET /api/stream?session_id=X) — Pattern A
이유:
POST 와 응답 channel 분리 — semantic 깔끔
영속 SSE 라 server-initiated push 미래 확장 자연
queue 처리 자연 — POST 즉시 ack, 응답은 SSE 분리 도착
기존 SSE 인프라 (라우터 / keepalive / disconnect 폴링) 재사용
Endpoint 배치
Endpoint
위치
호출자
POST /api/chat
UG
FE
GET /api/stream?session_id=X
UG
FE
POST /chat/send (내부)
각 agent (P / A)
UG
GET /chat/stream?session_id=X (내부)
각 agent (P / A)
UG
UG 가 session 의 agent_endpoint 컬럼 보고 routing.
FE localStorage 구조
activeSessionId ← 현재 열려 있는 chat 의 session_id
sessions ← 사이드바 cache (id, title, agent_endpoint, last_chat_at)
§3. 메시지 큐 — Primary 측
P/A 의 handler 가 thread-level 동시성 관리 (#72 의 정책 그대로):
idle: 즉시 처리 시작 → SSE chunk
busy: in-process 큐 적재 → POST 응답 queued ack + SSE 로 queued 이벤트
처리 끝나면 큐 drain → batch 로 단일 user message 합쳐 graph 호출
오타 noise / 정정 / 보충 / cancel intent 의 의미 판단은 main LLM 이 persona
가이드로 처리.
§4. Schema 결정 — 8 테이블
sessions
컬럼
타입
id
uuid PK
agent_endpoint
text
initiator
text (= 'user')
counterpart
text (primary / architect)
started_at, ended_at
timestamptz
metadata
jsonb
chats
컬럼
타입
id
uuid PK
session_id
uuid FK→sessions NOT NULL
role
text (user/agent/system)
sender
text
content
jsonb (parts)
prev_chat_id
uuid FK self (nullable)
message_id
text (nullable)
created_at
timestamptz
metadata
jsonb
assignments
컬럼
타입
id
uuid PK
title
text
status
text (open/in_progress/done/cancelled)
owner_agent
text
root_session_id
uuid FK→sessions (nullable)
issue_refs
uuid[]
metadata
jsonb
created_at, updated_at
timestamptz
a2a_contexts
컬럼
타입
의미
id
uuid PK
context_id
text
A2A wire contextId
initiator_agent
text
counterpart_agent
text
parent_session_id
uuid FK→sessions (nullable)
session 발
parent_assignment_id
uuid FK→assignments (nullable)
assignment 진행 발
trace_id
text
A2A boundary 가로지르는 추적 ID. 위임자가 받은 trace_id 를 자기 호출에 forward, 부재 시 server 가 새 UUID 발급. HTTP X-A2A-Trace-Id 로 propagate. 같은 trace_id 의 모든 a2a_contexts 묶으면 한 사용자 의도의 시스템 전체 흐름 복원 가능
topic
text (nullable)
started_at, ended_at
timestamptz
metadata
jsonb
a2a_messages
컬럼
타입
id
uuid PK
message_id
text (A2A wire messageId)
a2a_context_id
uuid FK→a2a_contexts NOT NULL
a2a_task_id
uuid FK→a2a_tasks (NULLABLE) — Task.history 의 일원이면 채움
role
text
sender
text
parts
jsonb
prev_message_id
uuid FK self (nullable)
created_at
timestamptz
metadata
jsonb
a2a_tasks
컬럼
타입
id
uuid PK
task_id
text (A2A wire taskId)
a2a_context_id
uuid FK→a2a_contexts NOT NULL
state
text (SUBMITTED/WORKING/COMPLETED/INPUT_REQUIRED/FAILED)
submitted_at
timestamptz
completed_at
timestamptz (nullable)
assignment_id
uuid FK→assignments (nullable)
metadata
jsonb
a2a_task_status_updates
컬럼
타입
id
uuid PK
a2a_task_id
uuid FK→a2a_tasks NOT NULL
state
text
transitioned_at
timestamptz
reason
text (nullable)
metadata
jsonb
a2a_task_artifacts
컬럼
타입
id
uuid PK
a2a_task_id
uuid FK→a2a_tasks NOT NULL
artifact_id
text
name
text (nullable)
parts
jsonb
created_at
timestamptz
metadata
jsonb
관계 그림
sessions (UG↔P/A 대화창, server-side 영속)
├── chats (session 안의 메시지)
├── assignments (chat 중 정의된 work item)
│ └── (assignment 진행 중 발생한 a2a_contexts 가 parent_assignment_id 로 참조)
└── a2a_contexts (session 발 inter-agent 대화)
├── a2a_messages (모든 Message)
│ └── a2a_task_id NULLABLE ← Task 에 속하면 채움
└── a2a_tasks (stateful work)
├── a2a_task_status_updates
├── a2a_task_artifacts
└── (자기 history 는 a2a_messages.a2a_task_id 로 backlink)
(별 가지) standalone a2a_contexts (system trigger 발 — webhook / cron 등)
├── a2a_messages
└── a2a_tasks
├── a2a_task_status_updates
└── a2a_task_artifacts
wire id 컬럼 완전 폐기 — 원 스키마 (M3 재설계 — UG↔P/A chat tier 분리 + schema 재정의 + 어휘 정렬 #75 §4) 에는 chats.message_id / a2a_contexts.context_id / a2a_tasks.task_id 등 wire id 컬럼이 있었으나, publisher-supplied id 패턴을 모든 collection 에 일관 적용하면서 컬럼 자체 폐기. row PK (UUID) 가 wire id 역할 단일화 (migration 007)
A2A wire id 타입 UUID 화 — 원 결정 (string) 에서 UUID 로 통일. 외부 에이전트도 UUID 발급 가정 (M5+ 외부 호환 시점에 재검토)
chat session runtime in-memory 정책 — 원 결정 없음. PR 4 에서 message-aware buffer + TTL eviction + LangGraph checkpoint 와 디커플링 도입. 단일 process 가정 (다중 instance scale-out 시 Valkey Streams 외부화 — M4+ HA 시점)
MCP auto-reconnect — 원 스코프 외 인프라 보강. server restart 시 client session terminated 자동 복구
운영 fix-up — chunks ↔ message message_id 불일치 (FE 중복 표시), prev_chat_id null 컬럼 backfill (운영 데이터 정합)
배경
PR #68 (#39 Primary 그래프 확장) 검증 중 사용자 ↔ Primary 대화의 여러 결함이
드러났음 (#69 ~ #73). 직접 원인은 우리 구현이 모든 SendMessage 응답을
A2A
Task로 자동 wrap 한 것 — A2A 스펙은 trivial 응답을Message로두는 것도 허용하는데 (공식 가이드: "Messages for Trivial Interactions,
Tasks for Stateful Interactions") 단순화로 그렇게 하지 않았음. 그 결과:
컬럼 미사용 등 부수 결함
이 자동 Task wrap 을 풀어 trivial / stateful 분기를 해도 한 가지 더 본질적
인 점이 남는다:
→ 자동 Task wrap fix + 사용자-에이전트 통신을 별도 chat protocol 로 분리.
해결 방향 — 두 tier 분리
상세 설계 / schema / 어휘 / 통신 layer 결정은 본 issue 에 인라인 (다음 §1~§7).
§1. 어휘
contextId— 두 에이전트 사이 대화 namespaceMessage(parts / role / messageId). optionaltaskId보유Task(state lifecycle / artifacts / history). Message 와 응답 형식 alternative; commit 후 관련 Message 들을 자기 history 에 누적A2A 의 Message 와 Task 관계 (공식 블로그 직접 인용):
task_idfield in the Message object clearly indicates which task the user is referring to"task_idin the [message]"history: [{kind: 'message', taskId: ..., ...}]필드우리 도메인 Task = Assignment (이름 변경) — A2A Task 와 다른 객체. 한 Assignment 안에서 여러 A2A Task 발생 가능.
§2. 통신 layer
UG → P/A: REST POST (
POST /api/chatwith session_id + text → 202 ack)P/A → UG: 영속 SSE per session (
GET /api/stream?session_id=X) — Pattern A이유:
Endpoint 배치
POST /api/chatGET /api/stream?session_id=XPOST /chat/send(내부)GET /chat/stream?session_id=X(내부)UG 가 session 의
agent_endpoint컬럼 보고 routing.FE localStorage 구조
§3. 메시지 큐 — Primary 측
P/A 의 handler 가 thread-level 동시성 관리 (#72 의 정책 그대로):
queuedack + SSE 로queued이벤트오타 noise / 정정 / 보충 / cancel intent 의 의미 판단은 main LLM 이 persona
가이드로 처리.
§4. Schema 결정 — 8 테이블
sessionsprimary/architect)chatsassignmentsa2a_contextsX-A2A-Trace-Id로 propagate. 같은 trace_id 의 모든 a2a_contexts 묶으면 한 사용자 의도의 시스템 전체 흐름 복원 가능a2a_messagesa2a_tasksa2a_task_status_updatesa2a_task_artifacts관계 그림
§5. 코드 / 인프라 변경 범위
그대로 살아남음
shared/a2a, A2AClient, contextId/traceId 규약 feat(shared/a2a): 위임 시 contextId/traceId 규약 + 인프라 (옵션 B) #32)변경 / 신설 필요
shared/a2a/server/graph_handlers/Migration 정책
기존 데이터 (#39 검증 thread 등) 다 폐기. 새 schema 로 cut-over.
§6. 문서 업데이트 계획
본 PR (docs/m3-redesign-chat-tier 브랜치) 에서 다음 문서 일괄 갱신:
docs/proposal/proposal-main.mddocs/proposal/agents-roles.mddocs/proposal/architecture-agent-internals.mddocs/proposal/knowledge-model.mddocs/proposal/architecture-event-pipeline.mdshared/src/dev_team_shared/a2a/messaging.mddocs/proposal/architecture-chat-protocol.mdCLAUDE.md(필요 시)본 docs PR 은 코드 / 마이그레이션은 건드리지 않음. 후속 implementation PR 들이 코드 작업.
§7. 기존 이슈 수정 계획
본 재설계가 head 라 기존 이슈들의 scope 일부 흡수 / 재정의됨:
각 이슈 body 재작성은 본 재설계 PR 머지 후 차례로 (별 PR 또는 이슈 edit).
§8. 진행 순서
본 issue 는 docs PR 머지까지 In Progress 유지. 머지 후 implementation 진행 중인 동안 Done 으로 close 할지 In Progress 유지 할지 결정.
관련
✅ 완료 (#75 PR 1~4 머지)
본 재설계의 schema / wire / FE / 인프라 작업이 4 개 PR 로 분리 머지됨.
PR #77 (PR 1) — schema 재설계
chats(immutable + prev_chat_id chain) +assignments(chat 중 합의 work item) +a2a_contexts/a2a_tasks/a2a_messages/a2a_task_status_updates/a2a_task_artifacts신설agent_tasks폐기 (assignments / a2a_tasks 로 분리)PR #78 (PR 2) — event_bus + Chronicler
shared/event_bus(Valkey Streams 위 EventBus / 10 type / 3 layer)PR #79 (PR 3) — auto-Task wrap + agent graph 공통화
shared/a2a의 graph_handlers (send_message / send_streaming / Task lifecycle / publish)classify_response노드)shared/agent_graph(ReAct llm_call / tool_node / should_continue / serialize) — Primary / Librarian 공용PR #80 (PR 4) — chat protocol end-to-end + 정합 마무리
POST /chat/send+ 영속 SSE per sessionPOST/GET/PATCH /api/sessions+POST /api/chat+GET /api/stream+GET /api/historya2a.context.endcaller 측 발화 (Librarian reference)Message/Task/Artifact/TaskStatusUpdateEvent/TaskArtifactUpdateEvent)str→UUID타입화StreamableMCPClientauto-reconnect,shared/lifespan/shared/utils추출, in-memory chat session runtime (message-aware buffer + TTL eviction)원 스코프 대비 변경점
chats.message_id/a2a_contexts.context_id/a2a_tasks.task_id등 wire id 컬럼이 있었으나, publisher-supplied id 패턴을 모든 collection 에 일관 적용하면서 컬럼 자체 폐기. row PK (UUID) 가 wire id 역할 단일화 (migration 007)후속 이슈