Skip to content

Event Streaming

The personaforge/streaming module provides a LangGraph-style event stream protocol. Nodes, tools, and LLM adapters emit typed events; consumers choose which event types they want.

ts
import {
  StreamEventBus, StreamContext, createStreamableRun,
  type StreamEvent, type StreamMode,
} from 'personaforge/streaming';

Stream modes

ModeWhat it delivers
valuesFull state snapshot after each node finishes
updatesPer-node delta (node name + output)
messagesToken-level chunks from the LLM
debugTool call details, timing, internal telemetry
customUser-emitted events via ctx.emit()

Subscribe to one or more modes when you create a bus or a streamable run:

ts
const bus = new StreamEventBus(['messages', 'updates']);

Quick start with createStreamableRun

ts
const { events, result } = createStreamableRun(async (ctx) => {
  ctx.token('Hello');
  ctx.token(' world');
  ctx.emit('milestone', { step: 1 });
  return { answer: 'Hello world' };
}, { streamMode: ['messages', 'custom'] });

for await (const event of events) {
  if (event.type === 'token') process.stdout.write(event.data);
  if (event.type === 'custom') console.log('Custom:', event.name, event.data);
}

const output = await result;

Using StreamContext inside a node or tool

Every node receives a StreamContext bound to the event bus:

ts
function myNode(input: unknown, ctx: StreamContext) {
  ctx.token('Thinking...');
  ctx.emit('search_started', { query: 'x' });
  ctx.toolCall('websearch', { q: 'x' }, { results: ['...'] });
  ctx.debug({ latencyMs: 42 });
  ctx.update({ partial: true });
  ctx.value({ full: 'state' });
}

Only events matching the bus's modes are forwarded to consumers.


Using StreamEventBus directly

For low-level integration:

ts
const bus = new StreamEventBus(['messages', 'debug']);

// Consumer
const iter = bus.events();
const consume = (async () => {
  for await (const event of iter) {
    console.log(event.type, event);
  }
})();

// Producer
bus.emit({ type: 'token', data: 'hi', timestamp: Date.now() });
bus.close();
await consume;

The iterator terminates cleanly when close() is called and the buffer is drained.


Event types

ts
interface TokenEvent   { type: 'token';     data: string;   node?: string }
interface UpdateEvent  { type: 'update';    data: unknown;  node: string  }
interface ValueEvent   { type: 'value';     data: Record<string, unknown>; node: string }
interface ToolCallEvent{ type: 'tool_call'; data: { name; arguments; result? } }
interface DebugEvent   { type: 'debug';     data: Record<string, unknown> }
interface CustomEvent  { type: 'custom';    name: string;  data: unknown }

All events carry a timestamp (epoch ms).


Released under the MIT License.