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.
| Event | Carries |
|---|---|
message:start | messageRef, role |
message:delta | delta — append it, do not replace |
message:complete | content, usage, references, files, artifacts, outputModules |
tool:start | name, displayName, inputPreview |
tool:result | output, references |
tool:error | code, message |
interaction:requested | interaction — the agent is waiting on the user |
interaction:resolved | interactionRef, response |
approval:requested | approvalRef, title, description |
approval:resolved | approvalRef, decision |
artifact:created | artifact |
runtime:status | status — progress projection |
error | code, 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.