Skip to content

M3 재설계 — UG↔P/A chat tier 분리 + schema 재정의 + 어휘 정렬 #75

@hagyutae

Description

@hagyutae

배경

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 로 분리.

해결 방향 — 두 tier 분리

[Chat tier — 논의]                          [A2A tier — 위임]
사용자 ──┐                                                          ┌── Engineer
         ├── Primary (논의 → Assignment 정의) ──── A2A ─────────────┤
사용자 ──┤                                                          └── QA
         └── Architect (M4+, 직접 논의 / 위임 받기)

상세 설계 / schema / 어휘 / 통신 layer 결정은 본 issue 에 인라인 (다음 §1~§7).


§1. 어휘

차원 객체 정의
Chat tier Session 한 대화창 단위 (UG↔P/A). server-side 영속 (Doc Store). FE 는 active session_id reference 만 localStorage 에 보관
Chat Session 안 발화
Assignment Chat 중 합의된 도메인 work item — P/A 가 발급, 다른 agent 에 A2A 위임
A2A tier Context A2A contextId — 두 에이전트 사이 대화 namespace
Message A2A Message (parts / role / messageId). optional taskId 보유
A2A Task A2A Task (state lifecycle / artifacts / history). Message 와 응답 형식 alternative; commit 후 관련 Message 들을 자기 history 에 누적

A2A 의 Message 와 Task 관계 (공식 블로그 직접 인용):

  • "Messages for Trivial Interactions" / "Tasks for Stateful Interactions"
  • "Notice how the task_id field in the Message object clearly indicates which task the user is referring to"
  • "Next time, the user is sending a message specific to that task to the A2A server, they can attach this relevant task_id in the [message]"
  • Task 객체에 history: [{kind: 'message', taskId: ..., ...}] 필드

우리 도메인 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

§5. 코드 / 인프라 변경 범위

그대로 살아남음

변경 / 신설 필요

영역 변화
UG ↔ P/A 인터페이스 A2A 제거 → REST POST + 영속 SSE per session 신설. FE Chat.tsx 재작성 (영속 SSE client + POST submit + 사이드바 multi-chat UI)
shared/a2a/server/graph_handlers/ UG 진입점 제거 (chat 핸들러는 별 layer). graph_handlers 는 inter-agent 전용
send_message / send_streaming 의 자동 Task wrap trivial 응답은 Message 만, stateful 작업은 Task 만 결정 분기 추가
Schema 새 8 테이블 (§4) 로 재정의. 기존 데이터 폐기 + drop & recreate
Chronicler chat event processor (UG↔P/A) vs A2A event processor (inter-agent) 분리
Event bus events chat lifecycle / assignment lifecycle / A2A lifecycle 9~10 종류
proposal docs tier 분리 / 어휘 / schema 정렬 (§6)

Migration 정책

기존 데이터 (#39 검증 thread 등) 다 폐기. 새 schema 로 cut-over.


§6. 문서 업데이트 계획

본 PR (docs/m3-redesign-chat-tier 브랜치) 에서 다음 문서 일괄 갱신:

문서 변경 내용
docs/proposal/proposal-main.md 두 tier (chat / A2A) 분리 추가. 어휘 표 갱신 (Session/Chat/Assignment/A2A *)
docs/proposal/agents-roles.md 에이전트 간 통신 boundary 명시 — UG↔P/A 는 chat, inter-agent 는 A2A
docs/proposal/architecture-agent-internals.md 각 agent 의 chat endpoint + A2A endpoint 분리 표시
docs/proposal/knowledge-model.md Episodic Layer 의 schema 전면 재정의 — agent_tasks/sessions/items 표 삭제, 새 8 테이블 표 추가, 관계 그림 갱신
docs/proposal/architecture-event-pipeline.md Chronicler 가 처리하는 event type 재정의 (chat / assignment / a2a 세 그룹), task/session 어휘 정정
shared/src/dev_team_shared/a2a/messaging.md A2A 가 inter-agent 한정임 명시. UG↔P/A 는 별 (chat) protocol 임을 cross-reference. Task vs Message 의 정확한 관계 (응답 alternative + Task.history 누적) 보강
신규: docs/proposal/architecture-chat-protocol.md UG↔P/A chat protocol spec — REST POST + 영속 SSE per session, endpoint 배치, FE localStorage, 메시지 큐 + batch merge 정책, 어휘 정의
루트 CLAUDE.md (필요 시) "통신 프로토콜 우선순위" 섹션 갱신 — chat tier 등장

본 docs PR 은 코드 / 마이그레이션은 건드리지 않음. 후속 implementation PR 들이 코드 작업.


§7. 기존 이슈 수정 계획

본 재설계가 head 라 기존 이슈들의 scope 일부 흡수 / 재정의됨:

Issue 변경
#69 (Chronicler 적재 누락) 새 schema (8 테이블) 기반 적재 path 재구현. processor split (chat / assignment / a2a). chronicler-fallback 폐기. body 재작성
#70 (UG/FE thread 영속화 + multi-chat UI) Chat tier protocol 위에서 구현. session source of truth = server. FE localStorage 는 active session reference + 사이드바 cache. body 재작성
#71 (UG/FE 인터페이스 결함) 새 protocol (REST POST + 영속 SSE) 위에서 streaming flag stuck / Stop 버튼 / queued 이벤트 표시 재정의. dead AbortController 활성화는 동일. body 재작성
#72 (Primary thread queue + batch merge) scope 변동 작음 — handler layer 만 chat protocol handler 로 옮김. persona 가이드 그대로
#73 (graph entry partial state sanity) scope 변동 없음 — A2A tier (inter-agent) 에 그대로 적용. PR #74 재개 시 그대로 진행

각 이슈 body 재작성은 본 재설계 PR 머지 후 차례로 (별 PR 또는 이슈 edit).


§8. 진행 순서

  1. ✅ 본 issue 생성 + In Progress
  2. 🔄 docs PR 작성 → 사용자 승인 → 머지
  3. 후속: 본 재설계 implementation 작업 (schema migration + UG protocol + handler split + Chronicler split)
  4. 후속: shared/a2a: graph entry partial state sanity — cancel 후 dangling user / tool 안전망 #73 / PR feat(shared/a2a): graph entry partial state sanity (#73) #74 재개
  5. 후속: Chronicler 적재 누락 / 버그 — agent_tasks/sessions/items 데이터 모델 의도와 어긋남 #69 ~ P / A thread-level queue + batch merge — 응답 중 추가 발화 의미 판단 #72 body 재작성 + 각 implementation

본 issue 는 docs PR 머지까지 In Progress 유지. 머지 후 implementation 진행 중인 동안 Done 으로 close 할지 In Progress 유지 할지 결정.


관련


✅ 완료 (#75 PR 1~4 머지)

본 재설계의 schema / wire / FE / 인프라 작업이 4 개 PR 로 분리 머지됨.

PR #77 (PR 1) — schema 재설계

  • migration 004: 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 로 분리)
  • Pydantic schema + Doc Store MCP 도구 / repository

PR #78 (PR 2) — event_bus + Chronicler

  • shared/event_bus (Valkey Streams 위 EventBus / 10 type / 3 layer)
  • Chronicler: schema-별 processor 등록 (OCP), publisher-supplied id dedup, double-end guard
  • chat / assignment / A2A 이벤트 정의

PR #79 (PR 3) — auto-Task wrap + agent graph 공통화

  • shared/a2a 의 graph_handlers (send_message / send_streaming / Task lifecycle / publish)
  • A2A 응답 shape 결정 = callee LLM 추론 (classify_response 노드)
  • shared/agent_graph (ReAct llm_call / tool_node / should_continue / serialize) — Primary / Librarian 공용
  • session 종료 개념 폐기 (D1), trace 시작점 3원 (D2)

PR #80 (PR 4) — chat protocol end-to-end + 정합 마무리

  • Chat protocol wire (UG↔P) — POST /chat/send + 영속 SSE per session
  • UG endpoints: POST/GET/PATCH /api/sessions + POST /api/chat + GET /api/stream + GET /api/history
  • FE multi-chat UI + 영속 SSE + history hydrate 재연결
  • Assignment 도구 + persona (D13)
  • a2a.context.end caller 측 발화 (Librarian reference)
  • publisher-supplied id 통일 — chats / a2a_* 의 wire id 컬럼 폐기, row PK UUID 단일화 (migration 007)
  • A2A wire id 필드들 (Message / Task / Artifact / TaskStatusUpdateEvent / TaskArtifactUpdateEvent) strUUID 타입화
  • 인프라: event_bus init fail-fast, StreamableMCPClient auto-reconnect, shared/lifespan / shared/utils 추출, in-memory chat session runtime (message-aware buffer + TTL eviction)
  • chats.prev_chat_id chain 의 FE→UG→Primary→CHR 전파

원 스코프 대비 변경점

  • 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 (운영 데이터 정합)

후속 이슈

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions