Skip to content

LOU-V2: typed, versioned event stream (agent.stream()) - #64

Merged
LinuxDevil merged 5 commits into
mainfrom
lou-v2-event-stream
Oct 1, 2026
Merged

LinuxDevil merged 5 commits into
mainfrom
lou-v2-event-stream

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

agent.stream(input, { signal }) (on createAgent) and AgentExecutor.stream(options) return an AgentRun: an AsyncIterable<AgentEvent> that also has result: Promise<ExecutionResult>.

const run = agent.stream('Weather in Paris?', { signal });
for await (const event of run) {
  if (event.type === 'text.delta') process.stdout.write(event.text);
}
const result = await run.result; // same ExecutionResult send() returns
  • AgentEvent is a discriminated union. Every event has { type, runId, seq (0, 1, 2, ...), timestamp (ISO), v: 1 }. The types are run.start, step.start, text.delta, text.done, tool.start, tool.done, tool.error, approval.requested, step.done, error and run.done. run.done is always last and exactly once.
  • New exports: AGENT_EVENT_SCHEMA_VERSION, every event type, AgentEventOf<T>, AgentEventType, AgentRun, and the guards isAgentEvent (for parsing events from SSE/WebSocket), isToolEvent, isTextEvent and isStepEvent.
  • Real token streaming. If the provider has stream() (and supportsStreaming(model) is not false), each step is streamed: text.delta per chunk, tool calls from tool-call chunks or else from the toolCalls promise, which is where the 'ai'-SDK adapters put them. The 'ai' SDK spelling 'tool-calls' is normalised. Without stream(), the step calls generate() and emits one text.delta plus text.done.
  • Docs: new docs/streaming.md with the full event table, ordering guarantees, versioning policy, a terminal example and an SSE example. README gets a feature bullet, a streaming snippet under the quick example and a docs link. llms.txt and llms-full.txt are regenerated.

Why

LOU-V2: the audit found no streaming through the executor. This event schema is the base for the UI hooks ticket and for SSE/WebSocket transports.

How

The stream uses the same loop as execute(); it is not a fork. AgentExecutor.stream() calls execute() with three changes:

  • the onEvent callback is composed with a translator. The existing start, text-complete, tool-call, tool-result and error events map to run.start, text.done, tool.start, tool.done/tool.error and error.
  • a combined signal: the caller's signal plus an internal controller that an early break aborts.
  • an internal sink under a symbol key on the options. The loop calls it in three places: runStepOrAbort (step.start/step.done), pauseForApproval (approval.requested) and generateInSpan, where it swaps how one model step is obtained.

So hooks, validation, approvals, the parallel tool batch, checkpoints, abort, the chat span and failure compaction all run unchanged. Without the sink (send()/execute()) the code path is identical to before.

Decisions:

  • Backpressure: none. The run never waits for the consumer, and events are buffered without loss.
  • Start: the run starts immediately. Awaiting result alone drives it to completion.
  • Errors: result rejects like send() would, and is pre-marked as handled so leaving it unawaited never causes an unhandled rejection. The iterator never throws on a run failure; you get error then run.done { finishReason: 'error' }.
  • One consumer: a run can be iterated once.
  • Invalid options throw synchronously, with messages naming AgentExecutor.stream.
  • tool.done.result is JSON-normalised (undefined becomes null). args is the parsed JSON ({} if invalid). Errors are { name, message }.

How verified

Ran on the result of merging origin/main at 995f649:

  • npx tsc --noEmit: clean
  • npm run lint: ✖ 433 problems (0 errors, 433 warnings) (no new warnings)
  • npm run build && npm run build --workspace=packages/create-loushy-agent: OK
  • npm run test:coverage: Test Files 119 passed (119), Tests 1669 passed | 4 skipped (1673), thresholds met
    • Three earlier runs each failed on a different timing/Windows-EPERM test unrelated to this change: http.test timeout, guardrails slow-command, sandbox-wiring timeout. Each passes alone. The machine was shared with other agents' test runs.
  • npm run fallow: ✓ 0 above threshold · 2818 analyzed
  • npm run test:types: Tests 19 passed, Type Errors no errors
  • npm run docs:verify-snippets: all 65 snippet(s) type-check against src/; 6 also run cleanly
  • npm run docs:llms:check: clean
  • agent-forge typecheck and typecheck:server: clean (no agent-forge files touched; the SimpleAgent interface gained stream)

New tests:

  • src/execution/agentStream.test.ts, using mockModel. It covers event order for a text-only run, a tool run (whose result deep-equals send()'s), parallel calls (starts in call order, done in completion order), a tool error, an approval pause, abort mid-stream, an already-aborted signal, an early break (the tool sees the abort), result without iterating (events still readable afterwards), no stream(), supportsStreaming returning false, a provider failure, a failure outside any step, an error chunk and single iteration. Every test checks that seq has no gaps, run.done is last exactly once, steps are paired, and every event survives JSON.parse(JSON.stringify(e)) (toStrictEqual).
  • agentStream.aiSdk.test.ts: streams through OpenAIProvider with a mocked streamText.
  • agentEvents.test-d.ts: type tests for narrowing.

Notes for reviewer

  • Shared files: AgentExecutor.ts has small additions: stream(), the three sink calls, and a caller parameter on validateExecuteOptions. generateStep.ts has a 5-line swap in generateInSpan. createAgent.ts merged with LOU-W4 sessions; send, stream and session share one options builder. Nothing touches resume.ts, agentRunState.ts, ApprovalGate.ts or toolBatch.ts.
  • aiSdkProvider.ts is unchanged. Its fullStream does not yield tool-call chunks, so the executor reads them from toolCalls.
  • Follow-ups:
    • resumeAfterApproval() has no streaming variant yet.
    • With usage/cost accounting landing on ExecutionResult, AgentEventUsage deliberately copies only the three token fields. Extend it if cost should be streamed.

🤖 Generated with Claude Code

LinuxDevil and others added 5 commits October 1, 2026 10:17
WIP before merging main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nerate llms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	README.md
#	llms-full.txt
#	src/createAgent.ts
@LinuxDevil
LinuxDevil merged commit d5948a0 into main Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant