Files
boocode/apps/coder/src/services/agent-status-publish.ts
indifferentketchup 59cf082e06 feat: normalized external-agent status (#10 scoped) (v2.7.6)
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>
2026-06-01 14:04:04 +00:00

56 lines
2.2 KiB
TypeScript

/**
* agent-status-publish (#10) — builds + publishes the `agent_status_updated`
* WS frame on the per-session channel (the same channel CoderPane subscribes to).
*
* Kept separate from normalize-agent-status.ts so that module stays a pure,
* broker-free helper (trivially unit-testable; reused by the config-injection
* follow-on). The frame contract is pinned in apps/server/src/types/ws-frames.ts
* (`AgentStatusUpdatedFrame`) and mirrored byte-identical in apps/web.
*/
import type { Broker } from '@boocode/server/broker';
import type { WsFrame } from '@boocode/server/ws-frames';
import type { AgentStatus } from './normalize-agent-status.js';
// The exact slice of Broker we need — accepting just the bound method keeps call
// sites flexible (pass `broker.publishFrame.bind(broker)` or, since the broker's
// publishFrame doesn't read `this`, `broker.publishFrame` directly).
type PublishFrame = Broker['publishFrame'];
/**
* Best-effort publish of a normalized agent status. The broker's publishFrame
* already fail-closes (validates + logs + drops on bad input, never throws), but
* we additionally swallow any unexpected error so a publish can NEVER break the
* turn it's reporting on.
*
* @param publishFrame the session channel publisher (broker.publishFrame)
* @param sessionId WS subscription channel (CoderPane subscribes per-session)
* @param chatId the (chat) half of the (chat, agent) status key
* @param agent the (agent) half of the key
* @param status normalized lifecycle status
* @param reason free-form discriminator (turn_start / turn_complete / …)
* @param at ISO timestamp; defaults to now
*/
export function publishAgentStatus(
publishFrame: PublishFrame,
sessionId: string,
chatId: string,
agent: string,
status: AgentStatus,
reason?: string,
at: string = new Date().toISOString(),
): void {
try {
const frame: WsFrame = {
type: 'agent_status_updated',
chat_id: chatId,
agent,
status,
...(reason ? { reason } : {}),
at,
};
publishFrame(sessionId, frame);
} catch {
// never let a status publish break the turn — best-effort only.
}
}