fix: live sub-agent observability — dispatch cards, steer artifacts, sidebar panel
The pi-style ack protocol (previous commit) left the client describing the
old world: artifact cards read the ack string as a result ("could not be
displayed"), the only job poller rendered on input-available parts that no
longer exist (so wake turns landed invisibly until re-navigation), steers
rendered as learner bubbles with no SVG anywhere, and server-persisted
assistant messages carried empty SDK ids (duplicate React keys).
- persistTurn (turn-service): fill empty message ids at the canonical write
— server-only messages (last turn's, wake turns') now key uniquely;
client-authored ids untouched.
- chat.tsx: ONE central /api/jobs poll (fast while jobs run, slow discovery
cadence, off for job-less sessions) drives every job surface, re-syncs
once when a job settles (the record already holds the steer + wake
response at flip time), and adopts server-side wake turns via the same
turnInFlight machinery as returning mid-turn. Steers branch to the
artifact card — never a bubble. Composer no longer gates on jobs.
- /api/jobs now reports turnInFlight alongside the job list.
- dispatch-card: the tool-call box — live activity by toolCallId while
running, quiet "delivered"/"failed" one-liner after; the ack is visible
observability, the artifact lives in the steer.
- steer-card: renders the delivered artifact by kind (research brief /
sanitized SVG / sandboxed demo iframe) from the data-subagent-artifact
part; result renderers extracted and exported from the artifact cards.
- sub-agents-panel: right-sidebar section above the plan tree listing
running sub-agents (kind, elapsed, live activity) — vanishes when idle.
- tests: dispatch/steer/panel render tests re-target the new protocol;
persistTurn id-hygiene suite. 229/229 green, build green.
Out-of-lane touches for master review: chat.tsx + components/** (cleared
this wave), app/api/jobs/route.ts, src/server/turn-service.ts, contracts
protocol doc §2. Live-verified on :3003 (full loop, SSR DOM checked).
This commit is contained in:
@@ -303,7 +303,10 @@ export interface JobRecord {
|
||||
* sub-agent loop — isolated context, modelFor(role), the
|
||||
* verify-never-one-shot doctrine (headless-tested demos, looked-at SVGs,
|
||||
* sourced briefs) — tracking a live activity line in the job record.
|
||||
* The client polls GET /api/jobs?chatId=… and renders the activity card.
|
||||
* ONE central client poll of GET /api/jobs?chatId=… (in Chat) drives every
|
||||
* job surface: the transcript dispatch cards (live activity by
|
||||
* toolCallId, quiet one-liner after delivery), the sub-agents sidebar
|
||||
* panel, and the single re-sync when a job settles.
|
||||
*
|
||||
* 3. On completion the runner builds a STEER MESSAGE (buildSubagentSteer)
|
||||
* and hands it to the wake pipeline (src/server/jobs.ts): a user-role
|
||||
|
||||
@@ -45,9 +45,24 @@ export function buildTeacherContext(graphStore: GraphStore, chatId: string): str
|
||||
* even when aborted — the partial transcript (including any pending
|
||||
* question part) is what makes resume work.
|
||||
*/
|
||||
/**
|
||||
* The SDK assigns assistant-message ids CLIENT-side (useChat); the server's
|
||||
* onFinish synthesis leaves them EMPTY. A message persisted only server-side
|
||||
* (the last turn's, or a wake turn's — nobody re-POSTs it with a client id)
|
||||
* would otherwise land in the record with id "" — and two of those in one
|
||||
* transcript break React keys (the chat map) and steer-dedup lookups. Fill any
|
||||
* missing id here, at the single canonical write: client-authored ids are
|
||||
* NEVER touched (the client's next POST still carries them verbatim).
|
||||
*/
|
||||
export function withMessageIds(messages: UIMessage[]): UIMessage[] {
|
||||
return messages.map((m) =>
|
||||
m.id ? m : { ...m, id: `msg_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}` },
|
||||
);
|
||||
}
|
||||
|
||||
export function persistTurn(chatStore: ChatStore, chatId: string, messages: UIMessage[], isAborted: boolean): void {
|
||||
try {
|
||||
chatStore.saveMessages(chatId, messages);
|
||||
chatStore.saveMessages(chatId, withMessageIds(messages));
|
||||
} catch (err) {
|
||||
console.error(`[chat ${chatId}] persist failed:`, err, { isAborted });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user