refactor(events): remove the agent/stream-chunk mirror of assistant/chunk

The loop recorded every model token delta as a durable `assistant/chunk`
session event AND emitted an identical live `agent/stream-chunk` Cordis event
one line later. Same StreamChunk, same turn/step; the emit added only the live
Agent handle, which the sole consumer discarded. This is the boundary-mirror
duplication the event-domain work removed for turn/step boundaries, applied to
the token stream — a follow-up the boundary RFC explicitly deferred.

The premise is settled: chunk persistence is authoritative (the proposal to
stop persisting chunks was rejected — replay/snapshots depend on it), so
`assistant/chunk` on `session/event` is the load-bearing token stream and
`agent/stream-chunk` is pure redundancy.

- Remove the `agent/stream-chunk` declaration + emit; drop the now-unused
  StreamChunk import from dsh-agent's types.
- Migrate `dsh-ui-stdio` (the only live consumer; ACP already reads
  assistant/chunk off session/event) to render assistant/chunk in its existing
  session/event listener. Consolidating to one listener also makes the
  inReasoning dim-SGR flag deterministic across chunk/boundary events (they no
  longer race across two listeners).
- Repoint the agent-loop tests (cancel/loop) and ui-stdio tests to the
  session/event assistant/chunk feed.
- New RFC (implemented/simplification/2026-07-02-remove-stream-chunk-mirror);
  amend the boundary RFC's retained-list entry to cross-link; update
  architecture, cookbook, event-domain-semantics, the ACP proposal, and the
  regenerated cordis catalog.

Snapshot goldens unchanged (ACP never used the mirror), confirming no
editor-facing transcript change.
This commit is contained in:
Tianyi Cui
2026-07-02 23:42:16 +08:00
parent 2a66b7c4e0
commit b84d4828a8
15 files changed
+116 -89

No files matched your search

+23 -20
View File
@@ -1,8 +1,10 @@
/**
* Minimal stdio UI plugin: reads lines from stdin → `agent.send()`/`steer()`,
* and renders the agent's stream chunks and tool activity to stdout. A UI is
* "just a plugin" — it only consumes the `agent/*` event taxonomy and the
* `agents` service, so the same plugin drives any example or product surface.
* and renders the durable transcript to stdout. A UI is "just a plugin" — it
* consumes the `session/event` feed (the assistant token stream, turn/step
* boundaries, tool activity, todos) plus a few `agent/*` control events
* (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service,
* so the same plugin drives any example or product surface.
*
* Consolidates what were two near-identical copies under `examples/echo-agent`
* and `examples/coding-agent` (the latter a superset). This package IS that
@@ -91,25 +93,26 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
ctx.on('agent/created', (agent) => { labelBySession.set(agent.session.header.id, agent.id) })
ctx.on('agent/disposed', (agent) => { labelBySession.delete(agent.session.header.id) })
// Transcript rendering off the durable `session/event` feed — the assistant
// token stream, turn/step boundaries, tool activity, and todos all come from
// the one canonical stream (no agent/* mirrors). A single listener over the
// append order keeps `inReasoning` transitions deterministic across chunk and
// boundary events.
let inReasoning = false
ctx.on('agent/stream-chunk', (_agent, _turn, _step, chunk) => {
if (chunk.type === 'reasoning-delta') {
// Dim the chain-of-thought so the final answer stands out.
if (!inReasoning) output.write('\x1B[2m')
inReasoning = true
output.write(chunk.text)
} else if (chunk.type === 'text-delta') {
if (inReasoning) output.write('\x1B[0m\n')
inReasoning = false
output.write(chunk.text)
}
})
// Transcript rendering off the durable `session/event` feed — turn/step
// boundaries, tool activity, and todos all come from the one canonical stream
// (no agent/* boundary mirrors).
ctx.on('session/event', (session, event) => {
if (event.type === 'turn/start') {
if (event.type === 'assistant/chunk') {
const { chunk } = event.data
if (chunk.type === 'reasoning-delta') {
// Dim the chain-of-thought so the final answer stands out.
if (!inReasoning) output.write('\x1B[2m')
inReasoning = true
output.write(chunk.text)
} else if (chunk.type === 'text-delta') {
if (inReasoning) output.write('\x1B[0m\n')
inReasoning = false
output.write(chunk.text)
}
} else if (event.type === 'turn/start') {
const label = labelBySession.get(session.header.id) ?? session.header.id
output.write(`\n[${label} turn ${event.data.turn}] `)
} else if (event.type === 'turn/end') {