Skip to content

Streaming

A completion arrives as a sequence of events. The client accumulates chunks into a streamingMessage and commits it to the conversation when the completion ends.

The lifecycle

completion.started        → isStreaming = true
                            streamingMessage = { role: 'assistant', message: '' }

completion_stream  *      → streamingMessage.message += chunk

function_call.begin *     → record a pending function call
function_call.end   *     → record its result
function_call_iteration   → reset for the next round (the autopilot may chain calls)

completion.ended          → push streamingMessage onto conversationMessages
                            streamingMessage = null

completion.finished       → isStreaming = false

user_input                → server is waiting for user input

* means the event can fire any number of times.

Reading streaming state

ts
client.isStreaming();          // boolean
client.getStreamingMessage();  // ConversationMessage | null (copy)

Listening

ts
client.on('completionStarted', () => console.log('thinking…'));

client.on('completionStream', ({ chunk }) => {
  process.stdout.write(chunk);
});

client.on('functionCallBegin', ({ name, arguments: args }) => {
  console.log('calling', name, args);
});

client.on('functionCallEnd', ({ id, result }) => {
  console.log('done', id, result);
});

client.on('completionEnded', () => console.log('committed'));
client.on('completionFinished', () => console.log('idle'));

Stopping mid-stream

ts
client.interruptStreaming();

Asks the server to stop. The current streamingMessage is committed as-is and completionEnded fires.

With the Pinia store

If you are using useAutopilotStore(), streamingMessage, isStreaming, and hasStreamingMessage are reactive — bind them directly in a template, no event listeners needed:

vue
<template>
  <article v-if="store.streamingMessage">
    {{ store.streamingMessage.message }}
  </article>
</template>