Skip to main content

Conversations and streaming

Conversations

const conversation = await client.createConversation({
title: 'Checkout support',
idempotencyKey: 'checkout-support-001',
});

const { conversations } = await client.listConversations();

const { messages, outputModules } = await client.getConversationMessages(
conversation.conversationRef,
);

Pass an idempotencyKey to createConversation() if the call can be retried — a retry with the same key returns the existing conversation rather than creating a duplicate.

Conversations are addressed by conversationRef throughout. See ExternalAgentConversation.

Streaming a turn

const stream = await client.streamMessage({
conversationRef: conversation.conversationRef,
content: 'Why was my card declined?',
});

for await (const event of stream) {
switch (event.type) {
case 'message:delta':
appendToBubble(event.delta);
break;
case 'message:complete':
finalize(event.content, event.usage);
break;
case 'error':
showError(event.message, event.retryable);
break;
}
}

streamMessage() returns a RuntimeEventStream — an AsyncIterable of normalized public events. Options beyond content and conversationRef include streamProfile, outputModuleTypes, providerGroundingDisplay, hostContext, fileRefs, idempotencyKey, and signal. Full type: StreamMessageInput.

Cancelling

const controller = new AbortController();
const stream = await client.streamMessage({
conversationRef,
content,
signal: controller.signal,
});

stopButton.onclick = () => controller.abort();

An aborted stream surfaces as a RuntimeSdkError with code aborted; a connection that drops mid-turn surfaces as stream_interrupted.

Event types

RuntimeStreamEvent is a discriminated union on type. Switch on it exhaustively — new event types are additive, so keep a default branch that ignores what it does not recognize.

EventCarries
message:startmessageRef, role
message:deltadelta — append it, do not replace
message:completecontent, usage, references, files, artifacts, outputModules
tool:startname, displayName, inputPreview
tool:resultoutput, references
tool:errorcode, message
interaction:requestedinteraction — the agent is waiting on the user
interaction:resolvedinteractionRef, response
approval:requestedapprovalRef, title, description
approval:resolvedapprovalRef, decision
artifact:createdartifact
runtime:statusstatus — progress projection
errorcode, message, retryable

Collecting instead of streaming

When you do not need token-level rendering, reduce the stream to one message:

import { collectAssistantMessage } from '@agentshelf/external-agents-sdk';

const stream = await client.streamMessage({ conversationRef, content });
const collected = await collectAssistantMessage(stream);

console.log(collected.message.content, collected.usage);

collectAssistantMessage returns the assembled message plus any usage, references, files, and artifacts the turn produced. It still consumes the whole stream, so cancellation via signal works the same way.

Interactions

When the agent needs input mid-turn it emits interaction:requested and waits. Render the interaction, then resolve it:

for await (const event of stream) {
if (event.type === 'interaction:requested') {
const answer = await promptUser(event.interaction);

await client.respondToInteraction({
conversationRef,
interactionRef: event.interaction.interactionRef,
response: answer,
idempotencyKey: `interaction-${event.interaction.interactionRef}`,
});
}
}

response is a JSON object of public scalar data. Resolution is confirmed by a following interaction:resolved event.

Interactions require capabilities.interactions; approvals require capabilities.approvals. Check the policy before rendering either affordance — see Policy and capabilities.

Attachments

streamMessage() accepts fileRefs, referencing files you have already uploaded via uploadFile(). There is not yet a typed turn-attachment contract beyond passing those public refs.