feat(CUST-WP-0015): implement agent inbox for inter-agent coordination

Adds a message-passing layer to state-hub so Claude instances can
coordinate across sessions without polling shared progress events.

- Migration f3a4b5c6d7e8: agent_messages table with thread support
- FastAPI router: POST/GET /messages/, thread view, mark-read, archive, reply
- 4 MCP tools: send_message, get_messages, mark_message_read, reply_to_message
- Observable dashboard: /inbox page with unread/read/archived sections + KPI
- CLAUDE.md updates: global, custodian, marki-docx, activity-core, template
- TOOLS.md: Agent Inbox tools section documented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 02:55:45 +01:00
parent 5e7a72e144
commit 4b3cb1b039
11 changed files with 554 additions and 1 deletions

View File

@@ -107,6 +107,25 @@ Domains are now first-class DB entities. Use `list_domains()` to discover availa
---
## Agent Inbox Tools
Inter-agent coordination via shared message board. Check inbox at session start;
send messages to coordinate across Claude instances.
Agent names: use the repo slug (e.g. `"marki-docx"`, `"railiance"`) or `"hub"` for the custodian agent.
Use `"broadcast"` as `to_agent` to send to all agents.
| Tool | Key Args | When to use |
|------|----------|-------------|
| `get_messages(to_agent?, from_agent?, unread_only?, limit?)` | `to_agent`: your agent name; `unread_only`: True recommended at session start | Check for pending coordination messages. |
| `send_message(from_agent, to_agent, subject, body, thread_id?)` | all except `thread_id` required | Send a coordination message to another agent (or broadcast). |
| `mark_message_read(message_id)` | `message_id`: UUID | Mark a message as read after acting on it. |
| `reply_to_message(message_id, from_agent, body)` | all required | Reply in-thread; marks original as read. |
Dashboard: `http://localhost:3000/inbox`
---
## Domain Slugs
Run `list_domains()` to get the live list. Default 6: `custodian` · `railiance` · `markitect` · `coulomb_social` · `personhood` · `foerster_capabilities`

View File

@@ -1434,6 +1434,74 @@ def get_repo_dispatch(repo_slug: str) -> str:
return json.dumps(_get(f"/repos/{repo_slug}/dispatch"), indent=2)
# ---------------------------------------------------------------------------
# Agent Inbox (inter-agent message passing)
# ---------------------------------------------------------------------------
@mcp.tool()
def send_message(from_agent: str, to_agent: str, subject: str, body: str, thread_id: str | None = None) -> str:
"""Send a message from one agent to another (or 'broadcast' for all).
Use this to coordinate with other Claude instances — e.g. a worker agent
reporting status back to the orchestrator, or the hub agent dispatching
instructions to a domain agent.
Args:
from_agent: Sender identifier (e.g. 'hub', 'marki-docx', 'railiance')
to_agent: Recipient identifier or 'broadcast' for all agents
subject: Short subject line (max 500 chars)
body: Full message body (markdown supported)
thread_id: UUID of the root message to create a thread (optional)
"""
payload: dict = {"from_agent": from_agent, "to_agent": to_agent, "subject": subject, "body": body}
if thread_id:
payload["thread_id"] = thread_id
msg = _post("/messages/", payload)
return json.dumps(msg, indent=2)
@mcp.tool()
def get_messages(to_agent: str | None = None, from_agent: str | None = None, unread_only: bool = False, limit: int = 20) -> str:
"""List messages in the agent inbox.
Call this at session start to check for pending coordination messages.
Args:
to_agent: Filter by recipient (your agent name, or omit for all)
from_agent: Filter by sender (optional)
unread_only: Return only unread messages (default: False)
limit: Maximum number of messages to return (default: 20)
"""
params: dict = {"limit": limit, "unread_only": unread_only}
if to_agent:
params["to_agent"] = to_agent
if from_agent:
params["from_agent"] = from_agent
return json.dumps(_get("/messages/", params), indent=2)
@mcp.tool()
def mark_message_read(message_id: str) -> str:
"""Mark an inbox message as read.
Args:
message_id: UUID of the message to mark as read
"""
return json.dumps(_patch(f"/messages/{message_id}/read", {}), indent=2)
@mcp.tool()
def reply_to_message(message_id: str, from_agent: str, body: str) -> str:
"""Reply to a message. Marks the original as read and creates a reply in the same thread.
Args:
message_id: UUID of the message to reply to
from_agent: Your agent identifier
body: Reply body (markdown supported)
"""
return json.dumps(_post(f"/messages/{message_id}/reply", {"from_agent": from_agent, "body": body}), indent=2)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------