Stream Events

Consume structured text and tool events from agent.streamEvents()

agent.streamEvents() yields a typed union — text tokens, tool calls, tool results, and terminal events — in the exact order the agent produced them, so a UI can show tool activity instead of inferring it from prose.

Diagram

šŸ“ Prompt

šŸ¤– Agent

šŸ’¬ text

šŸ”§ tool_call

āœ… tool_result

šŸ finish

Quick Start

  1. Consume the event stream

    One agent, one tool, one for await — switch on event.type.

    typescript
    import { Agent } from 'praisonai';
    
    function getWeather(city: string): string {
      return `Weather in ${city}: 22°C, sunny`;
    }
    
    const agent = new Agent({
      instructions: 'You are a weather assistant',
      llm: 'gpt-4o-mini',
      stream: true,
      tools: [getWeather],
    });
    
    for await (const event of agent.streamEvents('What is the weather in Paris?')) {
      switch (event.type) {
        case 'text':        process.stdout.write(event.delta); break;
        case 'tool_call':   console.log(`\n→ ${event.name}(${JSON.stringify(event.args)})`); break;
        case 'tool_result': console.log(`  ${event.ok ? 'āœ“' : 'āœ—'} ${event.output}`); break;
        case 'finish':      console.log(`\n[done]`); break;
        case 'error':       console.error(event.error); break;
      }
    }

The Event Union

streamEvents() yields the AgentEvent discriminated union — five variants, keyed on type.

VariantFieldsEmitted when
textdelta: stringThe model emits a token.
tool_callcallId: string, name: string, args: Record<string, unknown>A tool is about to run. Announced before it runs so a UI can render a call-in-progress row.
tool_resultcallId: string, name: string, ok: boolean, output: stringA tool finished, threw, or was denied by the approval gate.
finishtext: stringThe run completed.
errorerror: ErrorThe run errored out.

Pairing Calls to Results with callId

callId is the provider’s id for the invocation and is the only key that may pair a tool_result to its tool_call.

Matching by position holds only while exactly one call is in flight, and silently attributes the wrong output the moment that stops being true.

typescript
const inFlight = new Map<string, { name: string; args: unknown }>();

for await (const event of agent.streamEvents(prompt)) {
  if (event.type === 'tool_call') {
    inFlight.set(event.callId, { name: event.name, args: event.args });
  } else if (event.type === 'tool_result') {
    const call = inFlight.get(event.callId);
    inFlight.delete(event.callId);
    renderRow(call!, event);
  }
}

Order Matters

Structured events share the same queue as the text, which keeps a tool call in its true position.

On a separate channel a call could arrive before the sentence introducing it, and a reader cannot tell a reordered transcript from a model that genuinely said things in that order.

typescript
// A transcript rendered in event order reads exactly as it happened:
//
//   Checking.                 ← text
//   → getWeather({"city":"Paris"})   ← tool_call
//     āœ“ Weather in Paris: 22°C, sunny  ← tool_result
//   It is 22°C and sunny in Paris.   ← text
//   [done]                    ← finish

stream() vs streamEvents()

Use stream() for text-only output; use streamEvents() when the UI must render tool activity.

Diagram

Yes

No

Yes

No

What do I need?

Just the
text?

agent.stream
Yields strings only

Need to SEE
tool activity?

agent.streamEvents
Yields AgentEvent union

stream() filters on type === 'text' internally, so widening the AgentEvent union does not affect it — text-only consumers are unaffected by this change.


Upgrading an Exhaustive switch

Widening a union is source-breaking for an exhaustive switch.

A consumer that handled text/finish/error and treated everything else as an error will now see tool_call/tool_result on the default branch. Add cases (or ignore the new variants explicitly) before upgrading.

typescript
for await (const event of agent.streamEvents(prompt)) {
  switch (event.type) {
    case 'text':   process.stdout.write(event.delta); break;
    case 'finish': done(event.text); break;
    case 'error':  fail(event.error); break;
    default:       /* tool_call, tool_result — ignore or handle */ break;
  }
}

Cancelling a Stream

Pass an AbortSignal through AgentStreamOptions.signal to stop generation when the user leaves the screen.

typescript
const controller = new AbortController();

// Later, when the user navigates away:
// controller.abort();

for await (const event of agent.streamEvents(prompt, { signal: controller.signal })) {
  if (event.type === 'text') process.stdout.write(event.delta);
}

Breaking out of the for await loop also aborts automatically — the iterator’s return() stops the in-flight provider request so no further tokens are generated or billed.


Best Practices

Show ok in the UI, not the output length

The whole point of ok is that output length is not a signal. Render a checkmark or an error state from event.ok, never from whether event.output is non-empty.

Render tool_call immediately

tool_call is emitted before the tool runs. Render a call-in-progress row the moment it arrives so the user knows work is happening.

Match by callId from day one

Position-matching works right up until the day two calls are in flight at once, then it silently attributes the wrong output. Key a Map on callId from the start.

Cancel long streams

Pass an AbortSignal via AgentStreamOptions.signal when the user leaves the screen, or break the for await loop. Both stop the provider request so tokens are no longer generated or billed.


Related