Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions agents/librarian/config/base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,31 @@ persona: |

## 도구 카탈로그

### DB 정보 검색 (Doc Store 5 collection, read)
### DB 정보 검색 (Doc Store, read)

도메인 산출물:
- `wiki_pages` — 위키 문서 (PRD / ADR / business_rule 등, `page_type` 필드로 분류)
- `issues` — 이슈 mirror (Epic / Story / Task 등, `type` 필드로 분류)
- `agent_tasks` — 작업 단위 (CHR 가 영속한 task 메타)
- `agent_sessions` — task 안의 대화 흐름 (contextId 별)
- `agent_items` — 개별 메시지

Chat tier (UG↔P/A 영역, #75):
- `sessions` — UG↔P/A 한 대화창
- `chats` — session 안의 한 발화 (immutable). list_by_session 으로 시간순 조회
- `assignments` — chat 중 합의된 도메인 work item (P/A 발급)

A2A tier (에이전트 간, #75):
- `a2a_contexts` — 두 에이전트 사이 대화 namespace (A2A wire `contextId` 와 1:1)
- `a2a_messages` — A2A `Message` (immutable). trivial 또는 Task.history
- `a2a_tasks` — A2A `Task` (stateful work, SUBMITTED → COMPLETED 등)
- `a2a_task_status_updates` — Task state transition 로그 (immutable)
- `a2a_task_artifacts` — Task 산출물 (immutable)

### 외부 리소스 조사 (3 트랙, M6 #62)
- context7 — 라이브러리 / 프레임워크 공식 docs
- mcp/web-fetch — 사용자 제공 URL 의 페이지 (Playwright 기반)
- Claude Web Search — 일반 web 검색 (Claude API native tool)

자세한 op 인터페이스는 도구 schema (bind_tools) 를 참조하세요. read 13 op
+ 조합 쿼리 1 (`chronicler_log_by_context`) = 14 op. write 도구는 노출하지
않습니다 (#63 분담 모델 — 각 에이전트가 자기 도메인 데이터 MCP 직접 write).
자세한 op 인터페이스는 도구 schema (bind_tools) 를 참조하세요. write 도구는
노출하지 않습니다 (#63 분담 모델 — 각 에이전트가 자기 도메인 데이터 MCP 직접 write).

## 행동 원칙

Expand All @@ -77,12 +87,12 @@ persona: |

## 현재 단계 제약 (M3)

현재 노출 도구는 Doc Store 5 collection 의 read op + 조합 쿼리 1 개.
Atlas (graph) MCP 연동, 외부 리소스 조사 3 트랙 (context7 / web-fetch /
Claude Web Search) 은 후속 마일스톤에서 추가됩니다. Diff 색인은
에이전트의 책임이 아니며 (Engineer 자체 색인 — #63 옵션 A 확정), Engineer
가 자기 변경 diff 를 직접 Atlas / Doc Store MCP 에 write 합니다. 미구현 기능
호출이 들어오면 "본 단계에선 미지원" 으로 명시합니다.
현재 노출 도구는 Doc Store collection (도메인 산출물 + chat tier + a2a tier)
의 read op. Atlas (graph) MCP 연동, 외부 리소스 조사 3 트랙 (context7 /
web-fetch / Claude Web Search) 은 후속 마일스톤에서 추가됩니다. Diff 색인은
에이전트의 책임이 아니며 (Engineer 자체 색인 — #63 옵션 A 확정),
Engineer 가 자기 변경 diff 를 직접 Atlas / Doc Store MCP 에 write 합니다.
미구현 기능 호출이 들어오면 "본 단계에선 미지원" 으로 명시합니다.

llm:
provider: anthropic
Expand Down Expand Up @@ -116,9 +126,8 @@ agent_card:
- id: librarian.doc_store_query
name: Doc Store Query
description: |
자연어 요청을 받아 Doc Store 의 5 collection (wiki_pages / issues /
agent_tasks / agent_sessions / agent_items) 의 read 도구 + 조합
쿼리 (chronicler_log_by_context 등) 를 호출하고 결과를 자연어로
정리해 응답합니다. write 는 본 에이전트의 책임이 아닙니다 — 각
에이전트가 자기 도메인 데이터를 MCP 통해 직접 write (#63 분담 모델).
자연어 요청을 받아 Doc Store collection (도메인 산출물 + chat tier +
a2a tier) 의 read 도구를 호출하고 결과를 자연어로 정리해 응답합니다.
write 는 본 에이전트의 책임이 아닙니다 — 각 에이전트가 자기 도메인
데이터를 MCP 통해 직접 write (#63 분담 모델).
tags: [librarian, doc-store, query, read, react]
12 changes: 6 additions & 6 deletions agents/librarian/scripts/verify_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
검증 시나리오 (read-only — Doc Store 데이터 변경 X):
1. AgentCard 정상 (skill = read 중심 — #64)
2. "wiki_pages.list" 자연어 요청 → wiki_pages_list tool 호출 + 자연어 응답
3. "agent_tasks 몇 개" → agent_tasks_list 호출 + count 응답
4. chronicler_log_by_context조합 쿼리 (#64 신설)
3. "assignments 몇 개" → assignments_list 호출 + count 응답 (#75 재설계)
4. a2a_contexts_find_by_context_id임의 contextId lookup
"""

from __future__ import annotations
Expand Down Expand Up @@ -105,19 +105,19 @@ async def main() -> int:
print(f" [{i}] {json.dumps(e, ensure_ascii=False)[:200]}")
return 1

print("\n[3] 'agent_tasks 몇 개?' (count via list)")
print("\n[3] 'assignments 몇 개?' (count via list)")
events = await send_streaming_message(
"agent_tasks collection 에 현재 몇 개의 task 가 있는지 list 도구로 확인하고 개수만 알려줘.",
"assignments collection 에 현재 몇 개의 assignment 가 있는지 list 도구로 확인하고 개수만 알려줘.",
)
final = _extract_final_text(events)
print(f" final text (first 300):\n {final[:300]!r}")
if not final:
print(" !! 자연어 응답 없음")
return 1

print("\n[4] chronicler_log_by_context — 임의 contextId 조회 (조합 쿼리)")
print("\n[4] a2a_contexts_find_by_context_id — 임의 contextId lookup")
events = await send_streaming_message(
"context_id 'nonexistent-ctx' 의 chronicler 로그를 chronicler_log_by_context 도구로 조회하고 결과 알려줘. (없으면 없다고)",
"context_id 'nonexistent-ctx' 를 a2a_contexts_find_by_context_id 도구로 lookup 하고 결과 알려줘. (없으면 없다고)",
)
final = _extract_final_text(events)
print(f" final text (first 300):\n {final[:300]!r}")
Expand Down
205 changes: 129 additions & 76 deletions agents/librarian/src/librarian_agent/tools.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
"""Librarian 의 LangChain tool 정의.

Doc Store 의 5 collection read 도구 + 조합 쿼리 1 개. LangChain `@tool` 로
wrapping 해 LLM 의 `bind_tools()` 에 부착 — LLM 이 자연어 → tool 매핑 결정.
Doc Store collection read 도구 + 조합 쿼리. LangChain `@tool` 로 wrapping
해 LLM 의 `bind_tools()` 에 부착 — LLM 이 자연어 → tool 매핑 결정.

도메인 wrapper (`upsert_prd` 등) 박지 않음 (root CLAUDE.md "에이전트 ↔ 외부
도구 운영 원칙"). LLM 이 매 요청 컨텍스트에 맞춰 page_type / type 필드 결정.

분담 모델 (#63 정정 — 2026-05): write 는 각 에이전트가 Doc Store / Atlas
MCP 직접. **Librarian 은 read-only 사서** — 자연어 / 교차 쿼리 매핑.
분담 모델 (#63): write 는 각 에이전트가 Doc Store / Atlas MCP 직접.
**Librarian 은 read-only 사서** — 자연어 / 교차 쿼리 매핑.

M3 노출 op (read 13 + 조합 1 = 14):
- wiki_pages: get / get_by_slug / list
- issues: get / list
- agent_tasks: get / list
- agent_sessions: get / list / list_by_task / find_by_context
- agent_items: list / list_by_session
- 조합: chronicler_log_by_context (session find + items list 결합)
#75 재설계: chat tier (sessions / chats / assignments) + A2A tier
(a2a_contexts / a2a_messages / a2a_tasks / a2a_task_status_updates /
a2a_task_artifacts) + 도메인 산출물 (issues / wiki_pages) 의 read 도구 노출.
조합 쿼리는 PR 2 (Chronicler 재작성 + chat protocol 도입) 후 새 그루핑 단위
(session, assignment) 기반으로 추가.
"""

from __future__ import annotations
Expand All @@ -24,24 +22,19 @@
from uuid import UUID

from dev_team_shared.doc_store import (
AgentItemRead,
AgentSessionRead,
AgentTaskRead,
A2AContextRead,
A2AMessageRead,
A2ATaskArtifactRead,
A2ATaskRead,
A2ATaskStatusUpdateRead,
AssignmentRead,
ChatRead,
DocStoreClient,
IssueRead,
SessionRead,
WikiPageRead,
)
from langchain_core.tools import BaseTool, tool
from pydantic import BaseModel, ConfigDict


class ChroniclerLog(BaseModel):
"""contextId 1 개의 chronicler 로그 = session + items 결합 결과."""

model_config = ConfigDict(extra="forbid")

session: AgentSessionRead | None
items: list[AgentItemRead]


def build_tools(client: DocStoreClient) -> list[BaseTool]:
Expand Down Expand Up @@ -98,111 +91,171 @@ async def issues_list(
)

# ──────────────────────────────────────────────────────────────────
# agent_tasks (read)
# assignments (도메인 work item) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def agent_tasks_get(id: str) -> AgentTaskRead | None:
"""Get agent_task by id (UUID). 미존재 시 null."""
return await client.agent_task_get(UUID(id))
async def assignments_get(id: str) -> AssignmentRead | None:
"""Get assignment by id (UUID). 미존재 시 null."""
return await client.assignment_get(UUID(id))

@tool
async def agent_tasks_list(
async def assignments_list(
where: dict[str, Any] | None = None,
limit: int = 100,
offset: int = 0,
order_by: str = "created_at DESC",
) -> list[AgentTaskRead]:
"""List agent tasks. Chronicler 가 영속한 작업 단위 메타."""
return await client.agent_task_list(
) -> list[AssignmentRead]:
"""List assignments. where 예: {"status": "open"} or {"owner_agent": "primary"}."""
return await client.assignment_list(
where=where, limit=limit, offset=offset, order_by=order_by,
)

@tool
async def assignments_list_by_session(root_session_id: str) -> list[AssignmentRead]:
"""List assignments derived from a chat session (UUID)."""
return await client.assignment_list_by_session(UUID(root_session_id))

# ──────────────────────────────────────────────────────────────────
# sessions (chat tier) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def sessions_get(id: str) -> SessionRead | None:
"""Get chat session by id (UUID). 미존재 시 null."""
return await client.session_get(UUID(id))

@tool
async def sessions_list(
where: dict[str, Any] | None = None,
limit: int = 100,
offset: int = 0,
order_by: str = "started_at DESC",
) -> list[SessionRead]:
"""List chat sessions (UG↔P/A 대화창)."""
return await client.session_list(
where=where, limit=limit, offset=offset, order_by=order_by,
)

# ──────────────────────────────────────────────────────────────────
# agent_sessions (read)
# chats (chat tier) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def chats_list_by_session(session_id: str) -> list[ChatRead]:
"""List chats (메시지) within a session (UUID), ordered by created_at."""
return await client.chat_list_by_session(UUID(session_id))

# ──────────────────────────────────────────────────────────────────
# a2a_contexts (A2A tier) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def agent_sessions_get(id: str) -> AgentSessionRead | None:
"""Get agent_session by id (UUID). 미존재 시 null."""
return await client.agent_session_get(UUID(id))
async def a2a_contexts_get(id: str) -> A2AContextRead | None:
"""Get a2a_context by id (UUID). 미존재 시 null."""
return await client.a2a_context_get(UUID(id))

@tool
async def agent_sessions_list(
async def a2a_contexts_list(
where: dict[str, Any] | None = None,
limit: int = 100,
offset: int = 0,
order_by: str = "started_at DESC",
) -> list[AgentSessionRead]:
"""List agent sessions (대화 흐름)."""
return await client.agent_session_list(
) -> list[A2AContextRead]:
"""List a2a_contexts. where 예: {"trace_id": "..."}."""
return await client.a2a_context_list(
where=where, limit=limit, offset=offset, order_by=order_by,
)

@tool
async def agent_sessions_list_by_task(task_id: str) -> list[AgentSessionRead]:
"""List sessions for a task (UUID)."""
return await client.agent_session_list_by_task(UUID(task_id))
async def a2a_contexts_find_by_context_id(context_id: str) -> A2AContextRead | None:
"""A2A wire contextId 로 가장 최근 1건 lookup. 미존재 시 null."""
return await client.a2a_context_find_by_context_id(context_id)

# ──────────────────────────────────────────────────────────────────
# a2a_messages (A2A tier) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def a2a_messages_list_by_context(a2a_context_id: str) -> list[A2AMessageRead]:
"""List messages within an a2a_context (UUID)."""
return await client.a2a_message_list_by_context(UUID(a2a_context_id))

@tool
async def agent_sessions_find_by_context(context_id: str) -> AgentSessionRead | None:
"""contextId 로 session 1개 lookup. 미존재 시 null. chronicler 로그 entry."""
return await client.agent_session_find_by_context(context_id)
async def a2a_messages_list_by_task(a2a_task_id: str) -> list[A2AMessageRead]:
"""List Task.history messages of an a2a_task (UUID)."""
return await client.a2a_message_list_by_task(UUID(a2a_task_id))

# ──────────────────────────────────────────────────────────────────
# agent_items (read)
# a2a_tasks (A2A tier) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def agent_items_list(
async def a2a_tasks_get(id: str) -> A2ATaskRead | None:
"""Get a2a_task by id (UUID). 미존재 시 null."""
return await client.a2a_task_get(UUID(id))

@tool
async def a2a_tasks_list(
where: dict[str, Any] | None = None,
limit: int = 100,
offset: int = 0,
order_by: str = "created_at ASC",
) -> list[AgentItemRead]:
"""List agent items (메시지)."""
return await client.agent_item_list(
order_by: str = "submitted_at DESC",
) -> list[A2ATaskRead]:
"""List a2a_tasks. where 예: {"state": "WORKING"}."""
return await client.a2a_task_list(
where=where, limit=limit, offset=offset, order_by=order_by,
)

@tool
async def agent_items_list_by_session(session_id: str) -> list[AgentItemRead]:
"""List items for a session (UUID). chronicler 로그 본문."""
return await client.agent_item_list_by_session(UUID(session_id))
async def a2a_tasks_find_by_task_id(task_id: str) -> A2ATaskRead | None:
"""A2A wire taskId 로 가장 최근 1건 lookup. 미존재 시 null."""
return await client.a2a_task_find_by_task_id(task_id)

# ──────────────────────────────────────────────────────────────────
# 조합 쿼리 (composite read)
# a2a_task_status_updates / a2a_task_artifacts (A2A tier) — read
# ──────────────────────────────────────────────────────────────────

@tool
async def chronicler_log_by_context(context_id: str) -> ChroniclerLog:
"""contextId 의 chronicler 로그 (session + items) 한 번에 조회.
async def a2a_task_status_updates_list_by_task(
a2a_task_id: str,
) -> list[A2ATaskStatusUpdateRead]:
"""List state transitions of an a2a_task (UUID), ordered by transitioned_at."""
return await client.a2a_task_status_update_list_by_task(UUID(a2a_task_id))

`agent_sessions_find_by_context` + `agent_items_list_by_session` 두
호출을 합친 조합 쿼리. session 미존재 시 items 는 빈 리스트.
"""
session = await client.agent_session_find_by_context(context_id)
if session is None:
return ChroniclerLog(session=None, items=[])
items = await client.agent_item_list_by_session(session.id)
return ChroniclerLog(session=session, items=items)
@tool
async def a2a_task_artifacts_list_by_task(
a2a_task_id: str,
) -> list[A2ATaskArtifactRead]:
"""List artifacts of an a2a_task (UUID), ordered by created_at."""
return await client.a2a_task_artifact_list_by_task(UUID(a2a_task_id))

return [
# 도메인 산출물
wiki_pages_get,
wiki_pages_get_by_slug,
wiki_pages_list,
issues_get,
issues_list,
agent_tasks_get,
agent_tasks_list,
agent_sessions_get,
agent_sessions_list,
agent_sessions_list_by_task,
agent_sessions_find_by_context,
agent_items_list,
agent_items_list_by_session,
chronicler_log_by_context,
# chat tier
assignments_get,
assignments_list,
assignments_list_by_session,
sessions_get,
sessions_list,
chats_list_by_session,
# a2a tier
a2a_contexts_get,
a2a_contexts_list,
a2a_contexts_find_by_context_id,
a2a_messages_list_by_context,
a2a_messages_list_by_task,
a2a_tasks_get,
a2a_tasks_list,
a2a_tasks_find_by_task_id,
a2a_task_status_updates_list_by_task,
a2a_task_artifacts_list_by_task,
]


__all__ = ["build_tools", "ChroniclerLog"]
__all__ = ["build_tools"]
Loading