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.