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:
199
workplans/CUST-WP-0015-agent-inbox.md
Normal file
199
workplans/CUST-WP-0015-agent-inbox.md
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
id: CUST-WP-0015
|
||||
type: workplan
|
||||
title: Agent Inbox — Inter-Agent Coordination via State-Hub
|
||||
domain: custodian
|
||||
repo: the-custodian
|
||||
status: done
|
||||
state_hub_workstream_id: 382c2e8a-28db-4db9-8c89-8bc2fea5159a
|
||||
created: 2026-03-16
|
||||
updated: 2026-03-16
|
||||
---
|
||||
|
||||
# CUST-WP-0015 — Agent Inbox
|
||||
|
||||
## Problem
|
||||
|
||||
Two Claude instances (state-hub Claude on WSL2, worker Claudes on coulombcore
|
||||
and other repos) share the state-hub as a blackboard but have no way to
|
||||
address messages directly to each other. Progress events are broadcast with
|
||||
no recipient; dispatch is pull-only. There is no lightweight protocol for a
|
||||
worker to say "I need a decision from you" or for the state-hub Claude to say
|
||||
"review this before proceeding".
|
||||
|
||||
## Goal
|
||||
|
||||
Add an agent inbox to the state-hub: a simple message-passing layer that lets
|
||||
any Claude session send a structured message to a named agent, and any
|
||||
receiving Claude to poll its inbox and reply. No real-time push required —
|
||||
agents poll on their natural iteration cadence (ralph loop, session start).
|
||||
|
||||
## Design
|
||||
|
||||
```
|
||||
Worker Claude State-hub State-hub Claude
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
send_message(
|
||||
to="state-hub",
|
||||
subject="MRKD-WP-0001 T03 done — ready for review",
|
||||
body="..."
|
||||
) ──────────────────────────► agent_messages table
|
||||
|
||||
get_messages("state-hub")
|
||||
◄──────────────────────
|
||||
reply_to_message(id,
|
||||
"Looks good, proceed")
|
||||
──────────────────────►
|
||||
|
||||
get_messages("worker-marki-docx")
|
||||
◄──────────────────────────────
|
||||
```
|
||||
|
||||
### Agent naming convention
|
||||
- `state-hub` — the custodian / state-hub Claude running locally
|
||||
- `worker-<repo-slug>` — a Claude session working inside a specific repo
|
||||
(e.g. `worker-marki-docx`, `worker-coulombcore`)
|
||||
- `broadcast` — special recipient: all agents receive it
|
||||
|
||||
### Message lifecycle
|
||||
`unread` → `read` → (optional) `archived`
|
||||
|
||||
Thread support: replies carry the original message's `id` as `thread_id`,
|
||||
grouping a conversation.
|
||||
|
||||
## Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE agent_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
from_agent VARCHAR(100) NOT NULL,
|
||||
to_agent VARCHAR(100) NOT NULL, -- or 'broadcast'
|
||||
subject VARCHAR(500) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
thread_id UUID REFERENCES agent_messages(id) ON DELETE SET NULL,
|
||||
read_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
CREATE INDEX ON agent_messages (to_agent, read_at);
|
||||
CREATE INDEX ON agent_messages (thread_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task: Migration + model
|
||||
|
||||
```task
|
||||
id: CUST-WP-0015-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "47035731-0d9b-42c0-ae87-29275046b1d8"
|
||||
```
|
||||
|
||||
- Alembic migration: create `agent_messages` table (schema above)
|
||||
- SQLAlchemy model `api/models/agent_message.py`
|
||||
- Pydantic schemas `api/schemas/agent_message.py`:
|
||||
`MessageCreate`, `MessageRead`, `MessageReply`
|
||||
- Register model in `api/models/__init__.py`
|
||||
|
||||
---
|
||||
|
||||
## Task: API router
|
||||
|
||||
```task
|
||||
id: CUST-WP-0015-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "17d61bc4-9793-48a4-929d-7c8ce0f03ba0"
|
||||
```
|
||||
|
||||
`api/routers/messages.py`, prefix `/messages`:
|
||||
|
||||
- `POST /` — send message → `MessageRead` (201)
|
||||
- `GET /?to_agent=<agent>&unread_only=false&limit=50` — inbox
|
||||
- `GET /thread/{thread_id}` — full thread ordered by created_at
|
||||
- `PATCH /{id}/read` — mark read (sets read_at=now)
|
||||
- `PATCH /{id}/archive` — soft-delete (read_at stays, add archived_at)
|
||||
|
||||
Register in `api/main.py`.
|
||||
|
||||
Acceptance: `curl -X POST /messages/ -d '{"from_agent":"worker-marki-docx","to_agent":"state-hub","subject":"test","body":"hello"}'` returns 201.
|
||||
|
||||
---
|
||||
|
||||
## Task: MCP tools
|
||||
|
||||
```task
|
||||
id: CUST-WP-0015-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e847b88b-2549-4f02-94c8-e855f5ac6dde"
|
||||
```
|
||||
|
||||
Add to `mcp_server/server.py`:
|
||||
|
||||
```python
|
||||
send_message(to_agent, subject, body, from_agent="state-hub", thread_id=None)
|
||||
get_messages(to_agent, unread_only=True, limit=20)
|
||||
mark_message_read(message_id)
|
||||
reply_to_message(message_id, body, from_agent="state-hub")
|
||||
```
|
||||
|
||||
`reply_to_message` resolves the original message's thread root, posts a new
|
||||
message with `thread_id` set, and marks the original as read in one call.
|
||||
|
||||
Update `mcp_server/TOOLS.md` with the new tools.
|
||||
|
||||
---
|
||||
|
||||
## Task: Dashboard inbox page
|
||||
|
||||
```task
|
||||
id: CUST-WP-0015-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5af2a794-923a-4aa7-a00a-a7f4862e7b2c"
|
||||
```
|
||||
|
||||
`dashboard/src/inbox.md`:
|
||||
- Unread messages table: from, to, subject, age, thread indicator
|
||||
- Mark-read button per row (POST to API)
|
||||
- Thread view: clicking subject expands the reply chain inline
|
||||
- KPI: unread count per agent (small summary bar at top)
|
||||
|
||||
Add to nav in `observablehq.config.js` after Repo Sync.
|
||||
|
||||
---
|
||||
|
||||
## Task: Session protocol update
|
||||
|
||||
```task
|
||||
id: CUST-WP-0015-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5a80fc23-fc35-4dd5-833f-9280dddf6819"
|
||||
```
|
||||
|
||||
Update CLAUDE.md files so agents know to check their inbox:
|
||||
|
||||
- `~/.claude/CLAUDE.md` (global): add `get_messages("state-hub")` to session start
|
||||
- `state-hub/CLAUDE.md`: same, inside session protocol Step 1
|
||||
- `marki-docx/CLAUDE.md`: add `get_messages("worker-marki-docx")` to session start
|
||||
- `activity-core/CLAUDE.md`: add `get_messages("worker-coulombcore")` to session start
|
||||
- Template (`state-hub/scripts/project_claude_md.template`): add inbox check
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
| # | Milestone | Tasks |
|
||||
|---|-----------|-------|
|
||||
| M1 | Messages in DB | T01 |
|
||||
| M2 | Full API live | T02 |
|
||||
| M3 | MCP tools available | T03 |
|
||||
| M4 | Dashboard inbox | T04 |
|
||||
| M5 | All agents check inbox on session start | T05 |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- State-hub v0.5 dynamic domains — live ✓
|
||||
- Repo sync / dispatch (CUST-WP-0014) — live ✓
|
||||
Reference in New Issue
Block a user