Scoped half of boocode_code_review_v2 §1 #10 — publish the agent status BooCoder already observes (the config-injection notify-hook is the documented follow-on, clean-room from superset ELv2). - agent_status_updated WS frame (working|blocked|idle|error), server+web parity. - Published from the dispatcher's turn boundaries (warm-acp/opencode/sdk/pty: working at start, idle/error at end) + the permission flow (blocked/working). Best-effort, never breaks a turn. - Clean-room normalizeAgentEvent helper (superset's vendor-event -> Start/blocked /Stop collapse, event names as facts) + 25 tests — reused by the follow-on. - AgentComposerBar status dot (distinct from the WS-liveness dot), tracked per (chat,agent) by a useAgentStatus map in CoderPane. Built by 2 parallel agents vs a pinned frame contract. Server 545 + coder 294 tests passing (25 new); web tsc + builds clean; ws-frames parity green. Clears the actionable review backlog (#1/#3/#4/#6-#12). Builds on v2.7.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
4.0 KiB
Markdown
62 lines
4.0 KiB
Markdown
# Normalized external-agent status (#10, scoped)
|
|
|
|
**Status:** in progress (started 2026-06-01)
|
|
**Source:** `boocode_code_review_v2.md` §1 #10, §5j (superset, Elastic License 2.0 — PATTERN-ONLY,
|
|
clean-room; `/opt/forks/superset/.../map-event-type.ts`, `notify-hook.template.sh`, `agent-setup/*`).
|
|
**Decision (Sam, 2026-06-01):** scoped status-publish now; config-injection notify-hook as a follow-on.
|
|
|
|
## Why (corrected premise)
|
|
BooCoder already *observes* agent lifecycle (warm-acp/opencode/SDK backends know active/idle/crashed;
|
|
the permission-waiter knows blocked) but never **publishes a normalized per-`(chat,agent)` status** to the
|
|
UI — so blocked-on-permission is invisible and crash/idle aren't pushed proactively. The `AgentComposerBar`
|
|
dot only shows WS liveness. This batch publishes the status BooCoder already knows; the heavier
|
|
config-injection notify-hook (for out-of-band signals) is the documented follow-on.
|
|
|
|
## State model (clean-room from superset's `mapEventType`)
|
|
Superset collapses ~30 vendor event names → 3 signals: **Start** (working), **PermissionRequest**
|
|
(blocked), **Stop** (done). BooCoder adds idle (after done) + error (crash/fail). Normalized status:
|
|
`working | blocked | idle | error`.
|
|
|
|
## Pinned frame contract (server + web, byte-identical, parity-tested)
|
|
```ts
|
|
{ type: 'agent_status_updated', chat_id: Uuid, agent: string,
|
|
status: 'working' | 'blocked' | 'idle' | 'error', reason?: string, at: IsoTimestamp }
|
|
```
|
|
Added to `apps/server/src/types/ws-frames.ts` AND `apps/web/src/api/ws-frames.ts` (the `ws-frames` parity
|
|
test), plus the web `WsFrame` union in `apps/web/src/api/types.ts`. Published via the coder's
|
|
`broker.publishFrame` (validated against the server `WsFrameSchema`).
|
|
|
|
## Clean-room normalize helper (built now, reused by the injection follow-on)
|
|
`apps/coder/src/services/normalize-agent-status.ts`:
|
|
`normalizeAgentEvent(raw: string): 'working' | 'blocked' | 'done' | null` — a clean-room reimplementation
|
|
of the vendor-event-name → bucket mapping (the event names are facts about each agent's hooks:
|
|
`SessionStart`/`UserPromptSubmit`/`PostToolUse`→working; `PreToolUse`/`Notification`/`PermissionRequest`/
|
|
`exec_approval_request`→blocked; `Stop`/`session_end`/`task_complete`→done). The scoped publish points use
|
|
BooCoder's own already-normalized turn boundaries; this helper exists so the config-injection follow-on
|
|
(which receives raw vendor event names POSTed from agent hooks) reuses it. Unit-tested.
|
|
|
|
## Publish points (BooCoder's existing observation — no per-backend change)
|
|
- Dispatcher (`dispatcher.ts`) turn boundaries, for every external-agent path (warm-acp/opencode/sdk/pty):
|
|
`working` at turn start, `idle` on clean completion, `error` on failure.
|
|
- Permission-waiter (`permission-waiter.ts` / the `setPermissionHooks` publish in `index.ts`): `blocked`
|
|
when a permission is requested, back to `working` when resolved.
|
|
A small `publishAgentStatus(broker, chatId, agent, status, reason?)` helper centralizes the frame.
|
|
|
|
## Frontend
|
|
- `CoderPane.tsx` tracks the latest `agent_status_updated` per `(chat, agent)` (a small live map; reset on
|
|
chat switch).
|
|
- `AgentComposerBar.tsx` renders a normalized status dot beside the existing session chip (reuse the
|
|
`StatusDot` visual language: working=spinner/green, blocked=amber, idle=gray, error=red), distinct from
|
|
the WS-liveness `connected` dot.
|
|
|
|
## Follow-on (documented, not built): config-injection notify-hook
|
|
Clean-room re-derive superset's `agent-setup`: inject a notify hook into each agent's native config
|
|
(claude `~/.claude/settings.json`, opencode plugin, codex/gemini templates) that POSTs
|
|
`{agent, chat_id, eventType}` to a new `POST /api/coder/agent-status` endpoint, which runs
|
|
`normalizeAgentEvent` → publishes the SAME `agent_status_updated` frame. Reuses everything this batch
|
|
builds. Catches out-of-band signals BooCoder's dispatch can't see.
|
|
|
|
## Verify
|
|
- `pnpm -C apps/coder test` (+ normalize-agent-status tests) + `pnpm -C apps/server test` (ws-frames parity)
|
|
- `pnpm -C apps/server build && pnpm -C apps/coder build`; `npx tsc -p apps/web/tsconfig.app.json --noEmit`
|