|
| 1 | +# Agents: Resident Lead Inbox + Dynamic Members (Workflow-Scoped) |
| 2 | + |
| 3 | +Status: implemented |
| 4 | + |
| 5 | +Related: |
| 6 | +- Issue 21: "Agents: make lead a persistent inbox actor (auto-consume messages)" |
| 7 | +- `docs/features/agents.md` |
| 8 | + |
| 9 | +This document extends the Agents design with two capabilities: |
| 10 | + |
| 11 | +1) A runner-owned **resident lead inbox broker** that continuously consumes the lead's mailbox and maintains a stable "latest agents context" artifact. |
| 12 | +2) **Dynamic team membership** during a workflow run, driven by a file-backed desired-state config and reconciled by the runner. |
| 13 | + |
| 14 | +This design intentionally keeps these behaviors **workflow-scoped** (only while a workflow is executing). |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## 1. Problem |
| 19 | + |
| 20 | +Agents teammates can be long-lived workers that continuously send findings to the lead. Today, the lead (the workflow itself) is not a resident actor, so receiving/triaging messages requires embedding explicit polling into step prompts (brittle, easy to forget). |
| 21 | + |
| 22 | +Additionally, some workflows need to **change the team** based on runtime evidence (e.g. spawn a k8s-focused investigator when cluster-level symptoms appear). We want to allow the lead to do this directly, even if it is operationally risky. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## 2. Goals / Non-goals |
| 27 | + |
| 28 | +Goals: |
| 29 | +- With Agents enabled, lead inbox messages are processed even when no lead step is currently executing. |
| 30 | +- Provide a stable artifact under `context_dir` for "latest agents context". |
| 31 | +- Allow the lead to modify team membership during workflow execution via `roboppi agents members ...`. |
| 32 | +- Deterministic shutdown: broker + dynamically spawned teammates stop cleanly when the workflow ends. |
| 33 | + |
| 34 | +Non-goals (v1 of this doc): |
| 35 | +- LLM-based triage/decision making for inbox messages (no automatic management hook trigger). |
| 36 | +- Cross-workflow persistence (daemon-style always-on lead/teammates). |
| 37 | +- Distributed agents across multiple hosts. |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## 3. Architecture Overview |
| 42 | + |
| 43 | +We introduce two runner-owned components and one store/CLI extension. |
| 44 | + |
| 45 | +### 3.1 LeadInboxBroker (runner-owned) |
| 46 | + |
| 47 | +New component: |
| 48 | +- `LeadInboxBroker` (suggested path: `src/agents/lead-inbox-broker.ts`) |
| 49 | + |
| 50 | +Responsibilities: |
| 51 | +- Poll/consume the lead's mailbox using Agent Store primitives (`recvMessages` + claim tokens + ack). |
| 52 | +- Update a bounded, secret-safe summary artifact. |
| 53 | +- Optionally emit lightweight `ExecEventSink` warnings/notifications when new actionable messages arrive. |
| 54 | + |
| 55 | +Key property: |
| 56 | +- It is runner-owned and runs independently of step execution, so the lead behaves like a resident actor for inbox processing. |
| 57 | + |
| 58 | +### 3.2 Dynamic membership reconcile loop (runner-owned) |
| 59 | + |
| 60 | +Extend `AgentCoordinator` (or add a `AgentsTeamController`) to reconcile "desired members" from disk: |
| 61 | + |
| 62 | +- The lead updates desired membership via CLI (see 3.3). |
| 63 | +- The coordinator periodically reads desired state and applies diffs: |
| 64 | + - spawn missing teammates |
| 65 | + - request shutdown for removed teammates |
| 66 | + |
| 67 | +This keeps process management centralized and ensures deterministic workflow shutdown. |
| 68 | + |
| 69 | +### 3.3 CLI + store extensions for membership mutation |
| 70 | + |
| 71 | +Extend the Agents CLI (`src/agents/cli.ts`) with membership mutation commands that update the desired-state file atomically. |
| 72 | + |
| 73 | +The lead can call these commands directly (danger accepted) to reshape the team at runtime. |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +## 4. Disk Artifacts |
| 78 | + |
| 79 | +### 4.1 Lead inbox summary artifact |
| 80 | + |
| 81 | +New stable artifact: |
| 82 | + |
| 83 | +- `context/_agents/inbox-summary.json` |
| 84 | + |
| 85 | +Design constraints: |
| 86 | +- Bounded size (both message count and per-entry size). |
| 87 | +- Secret-safe by default: do not copy full message bodies. |
| 88 | +- Provide pointers so a lead step can open the original message file if needed. |
| 89 | + |
| 90 | +Suggested schema: |
| 91 | + |
| 92 | +```json |
| 93 | +{ |
| 94 | + "version": "1", |
| 95 | + "team_id": "uuid", |
| 96 | + "lead_member_id": "lead", |
| 97 | + "updated_at": 1710000000000, |
| 98 | + "unread_count": 3, |
| 99 | + "entries": [ |
| 100 | + { |
| 101 | + "message_id": "uuid", |
| 102 | + "ts": 1710000000000, |
| 103 | + "from": "researcher", |
| 104 | + "topic": "findings", |
| 105 | + "kind": "text", |
| 106 | + "body_preview": "First 200 bytes...", |
| 107 | + "mailbox_path": "_agents/mailbox/inbox/lead/cur/<ts>-<id>.json" |
| 108 | + } |
| 109 | + ] |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +Notes: |
| 114 | +- `mailbox_path` is workspace-relative (context-relative) so it can be opened via READ. |
| 115 | +- `body_preview` is optional; if present, it MUST be short and safe. |
| 116 | + |
| 117 | +### 4.2 Desired membership state |
| 118 | + |
| 119 | +We need a runner-readable desired-state file that the lead can update. |
| 120 | + |
| 121 | +Option (preferred): extend `_agents/members.json` entries with an optional `agent` field. |
| 122 | + |
| 123 | +- Existing type: `src/agents/types.ts` `MemberEntry` |
| 124 | +- Proposed addition: `agent?: string` (agent catalog id) |
| 125 | + |
| 126 | +This keeps one canonical membership file and avoids adding another config file. |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## 5. Runtime Flows |
| 131 | + |
| 132 | +### 5.1 Workflow start |
| 133 | + |
| 134 | +When `agents.enabled: true`: |
| 135 | +- `WorkflowExecutor` initializes agents context (`initAgentsContext`) and starts `AgentCoordinator`. |
| 136 | +- `WorkflowExecutor` starts `LeadInboxBroker` with the resolved `contextDir`, `teamId`, and `leadMemberId`. |
| 137 | + |
| 138 | +### 5.2 Inbox consumption loop |
| 139 | + |
| 140 | +The broker loops until workflow abort/shutdown: |
| 141 | + |
| 142 | +1) `recvMessages({ memberId: leadMemberId, claim: true, max: N })` |
| 143 | +2) For each claimed message: |
| 144 | + - append/update `inbox-summary.json` |
| 145 | + - `ackMessageByClaimToken(...)` |
| 146 | +3) Sleep (poll interval) and repeat |
| 147 | + |
| 148 | +Backpressure defaults: |
| 149 | +- `N` small (e.g. 10) |
| 150 | +- poll interval small but bounded (e.g. 1-2s) |
| 151 | +- summary bounded (e.g. keep last 100 entries; truncate previews) |
| 152 | + |
| 153 | +Crash/interrupt safety: |
| 154 | +- Claimed-but-not-acked messages remain in `processing/` and will be requeued by existing housekeeping. |
| 155 | + |
| 156 | +### 5.3 Dynamic membership mutation |
| 157 | + |
| 158 | +The lead updates desired membership by writing to the Agent Store via CLI. |
| 159 | + |
| 160 | +Example (conceptual): |
| 161 | +- `roboppi agents members upsert --member k8s --agent k8s-investigator` |
| 162 | + |
| 163 | +Coordinator reconciliation loop: |
| 164 | +- On an interval (e.g. 2s), read desired membership from disk. |
| 165 | +- Compute diff against currently running teammate handles. |
| 166 | +- Apply: |
| 167 | + - spawn: create `_agent:<memberId>` long-lived worker task |
| 168 | + - remove: send `shutdown_request` message and then abort after a wait |
| 169 | + |
| 170 | +Determinism: |
| 171 | +- Reconcile is idempotent. |
| 172 | +- Member ordering is deterministic (sort by `member_id`). |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## 6. CLI Surface (Additions) |
| 177 | + |
| 178 | +Add new subcommands under `roboppi agents members`: |
| 179 | + |
| 180 | +- `roboppi agents members set --context <dir> --json-stdin` |
| 181 | + - Replaces the full desired member list (atomic). |
| 182 | + |
| 183 | +- `roboppi agents members upsert --context <dir> --member <id> --agent <agentId> [--name <n>] [--role <r>]` |
| 184 | + - Adds or updates a member entry. |
| 185 | + |
| 186 | +- `roboppi agents members remove --context <dir> --member <id>` |
| 187 | + - Removes a member entry. |
| 188 | + |
| 189 | +Behavior constraints: |
| 190 | +- All stdout remains JSON-only. |
| 191 | +- Member mutation is restricted to the lead identity by default: |
| 192 | + - compare `team.json.lead_member_id` and `ROBOPPI_AGENTS_MEMBER_ID` |
| 193 | + - if mismatch: error |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## 7. Safety and Guardrails (Explicitly Risky Feature) |
| 198 | + |
| 199 | +Dynamic membership is operationally risky (can spawn many long-lived workers with `RUN_COMMANDS`). We still add a few guardrails to prevent accidental runaway: |
| 200 | + |
| 201 | +- Explicit opt-in (recommended): `agents.dynamic_members.enabled: true` (DSL extension) OR env flag `ROBOPPI_AGENTS_DYNAMIC_MEMBERS=1`. |
| 202 | +- Hard caps: |
| 203 | + - `max_teammates` (default small) |
| 204 | + - spawn rate limit (e.g. N per minute) |
| 205 | +- Deterministic shutdown always wins: |
| 206 | + - on workflow end, coordinator stops all teammates regardless of desired state |
| 207 | + |
| 208 | +--- |
| 209 | + |
| 210 | +## 8. Acceptance Criteria |
| 211 | + |
| 212 | +- With `agents.enabled: true`, teammate messages to the lead are consumed without embedding `roboppi agents message recv ...` into step prompts. |
| 213 | +- `context/_agents/inbox-summary.json` exists and is updated as messages arrive. |
| 214 | +- Lead can modify team membership during workflow execution via `roboppi agents members ...`. |
| 215 | +- Coordinator spawns/removes teammates to match desired state within a bounded time. |
| 216 | +- Workflow shutdown stops broker + teammates deterministically. |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## 9. Testing Strategy |
| 221 | + |
| 222 | +Unit: |
| 223 | +- LeadInboxBroker summarization bounds (max entries, preview truncation). |
| 224 | +- Broker claim/ack behavior (ack by claim token). |
| 225 | +- Store writes for membership mutation are atomic. |
| 226 | + |
| 227 | +Integration: |
| 228 | +- Spawn agents with 1 teammate, send messages, verify summary artifact updates without lead polling. |
| 229 | +- During run, mutate membership to add/remove a teammate; verify coordinator spawns/stops. |
| 230 | +- Shutdown behavior: workflow end stops broker and all teammates; no lingering timers. |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## 10. Implementation Notes (Where Changes Landed) |
| 235 | + |
| 236 | +Touch points: |
| 237 | +- `src/agents/lead-inbox-broker.ts`: new `LeadInboxBroker` class — runner-owned, polls lead inbox via claim+ack, writes bounded `inbox-summary.json`. |
| 238 | +- `src/workflow/executor.ts`: starts/stops `LeadInboxBroker` alongside `AgentCoordinator`; writes `MemberEntry.agent` from `WorkflowDefinition.agents.members`. |
| 239 | +- `src/agents/coordinator.ts`: reconcile loop periodically reads desired membership from `members.json`, spawns missing non-lead members, shuts down removed members; runaway guards (max teammates cap + spawn rate limit). |
| 240 | +- `src/agents/store.ts`: atomic membership mutation helpers — `writeMembersConfig()`, `upsertMember()`, `removeMember()`. |
| 241 | +- `src/agents/cli.ts`: new `members set|upsert|remove` subcommands with lead identity restriction. |
| 242 | +- `src/agents/types.ts`: `MemberEntry.agent?: string` for desired-state membership. |
| 243 | +- `src/agents/paths.ts`: `inboxSummaryPath()` for `_agents/inbox-summary.json`. |
| 244 | +- `src/agents/constants.ts`: broker and reconcile defaults (poll intervals, summary bounds, max teammates, spawn rate limit). |
| 245 | + |
| 246 | +Core (`src/core/*`) remains unchanged: Agents is runner-owned policy orchestration built on mechanism-grade file operations. |
0 commit comments