Overview
During a prompt turn, the agent sends real-time updates via the sessionUpdate notification. These updates allow your client to provide live feedback to the user as the agent works.
Session Update Flow
When you call connection.prompt(), the agent processes the request and sends multiple sessionUpdate notifications before finally responding:
Client Agent
| |
|-- prompt request ------------------>|
| |
|<-- sessionUpdate (thinking) --------|
|<-- sessionUpdate (message chunk) ---| (many)
|<-- sessionUpdate (tool call) -------|
|<-- sessionUpdate (tool complete) ---|
|<-- sessionUpdate (message chunk) ---| (many)
| |
|<-- prompt response -----------------|
Clients SHOULD continue accepting tool call updates even after sending a session/cancel notification, as the agent may send final updates before responding with the cancelled stop reason.
Update Types
The SessionNotification contains an update field with a sessionUpdate discriminator:
interface SessionNotification {
sessionId: string;
update: SessionUpdate;
}
type SessionUpdate =
| AgentMessageChunk
| AgentThoughtChunk
| UserMessageChunk
| ToolCall
| ToolCallUpdate
| Plan
| CurrentModeUpdate
| ConfigOptionsUpdate
| AvailableCommandsUpdate;
AgentMessageChunk
Streaming chunks of the agent’s response message.
Message index in the conversation.
The content chunk:
{ type: "text", text: string } - Text chunk
{ type: "image", ... } - Image content
{ type: "audio", ... } - Audio content
Example
if (update.sessionUpdate === "agent_message_chunk") {
if (update.content.type === "text") {
// Stream text to UI
appendToMessageDisplay(update.index, update.content.text);
} else if (update.content.type === "image") {
// Display image
displayImage(update.content.data, update.content.mimeType);
}
}
AgentThoughtChunk
Streaming chunks of the agent’s internal reasoning (thinking process).
Thought content (typically text).
Example
if (update.sessionUpdate === "agent_thought_chunk") {
// Show in a separate "thinking" panel
appendToThinkingPanel(update.content.text);
}
UserMessageChunk
Streaming chunks of user messages (used when loading sessions).
A new tool call has been initiated.
Unique identifier for this tool call.
Human-readable title (e.g., “Read file: config.ts”).
Initial status: "pending", "running", "requires_permission", etc.
Tool call content (text, code blocks, patches, terminals, etc.).
Example
if (update.sessionUpdate === "tool_call") {
// Create a new tool call UI element
createToolCallCard({
id: update.toolCallId,
title: update.title,
status: update.status,
content: update.content,
});
}
A tool call’s status or content has been updated.
The tool call being updated.
New status if changed: "running", "completed", "failed", "cancelled", etc.
Content to append to the tool call.
Example
if (update.sessionUpdate === "tool_call_update") {
const toolCallCard = findToolCallCard(update.toolCallId);
// Update status
if (update.status) {
toolCallCard.setStatus(update.status);
}
// Append new content
if (update.appendContent) {
update.appendContent.forEach(content => {
toolCallCard.appendContent(content);
});
}
}
Plan
The agent has created an execution plan.
Human-readable plan description.
Example
if (update.sessionUpdate === "plan") {
displayPlan({
description: update.description,
steps: update.steps.map(step => ({
description: step.description,
status: step.status,
})),
});
}
CurrentModeUpdate
The session’s mode has changed.
ConfigOptionsUpdate
Session configuration options have changed.
Updated configuration options.
AvailableCommandsUpdate
Available slash commands have changed.
sessionUpdate
'available_commands_update'
Discriminator value.
Commands the agent can execute.
Implementation Example
Here’s a complete implementation of sessionUpdate:
class MyClient implements acp.Client {
async sessionUpdate(params: acp.SessionNotification): Promise<void> {
const { sessionId, update } = params;
switch (update.sessionUpdate) {
case "agent_message_chunk":
if (update.content.type === "text") {
// Stream text output
this.appendMessage(sessionId, update.index, update.content.text);
}
break;
case "agent_thought_chunk":
if (update.content.type === "text") {
// Show thinking process
this.appendThought(sessionId, update.content.text);
}
break;
case "tool_call":
// Create new tool call display
this.createToolCall(sessionId, {
id: update.toolCallId,
title: update.title,
status: update.status,
content: update.content,
});
break;
case "tool_call_update":
// Update existing tool call
const toolCall = this.getToolCall(sessionId, update.toolCallId);
if (update.status) {
toolCall.setStatus(update.status);
}
if (update.appendContent) {
toolCall.appendContent(update.appendContent);
}
break;
case "plan":
// Display execution plan
this.showPlan(sessionId, {
description: update.description,
steps: update.steps,
});
break;
case "current_mode_update":
// Update mode indicator
this.setMode(sessionId, update.currentMode);
break;
case "config_options_update":
// Update configuration UI
this.updateConfigOptions(sessionId, update.configOptions);
break;
case "available_commands_update":
// Update command palette
this.updateAvailableCommands(sessionId, update.availableCommands);
break;
default:
console.warn("Unknown update type:", update);
}
}
}
Rendering Tool Call Content
Tool calls contain various content types:
function renderToolCallContent(content: ToolCallContent) {
switch (content.type) {
case "text":
return renderText(content.text);
case "code":
return renderCodeBlock({
code: content.code,
language: content.language,
path: content.path,
});
case "patch":
return renderDiffView({
diff: content.diff,
path: content.path,
});
case "terminal":
return renderTerminal({
id: content.terminalId,
output: content.output,
exitStatus: content.exitStatus,
});
case "image":
return renderImage({
data: content.data,
mimeType: content.mimeType,
});
default:
return renderUnknownContent(content);
}
}
Handling Cancellation
When the user cancels a prompt:
class MyClient implements acp.Client {
private activeCancellations = new Set<string>();
async cancelPrompt(sessionId: string) {
// Mark as cancelled
this.activeCancellations.add(sessionId);
// Send cancellation
await this.connection.cancel({ sessionId });
}
async sessionUpdate(params: acp.SessionNotification): Promise<void> {
// Continue processing updates even after cancellation
const isCancelled = this.activeCancellations.has(params.sessionId);
if (isCancelled) {
// Show updates in a "cancelled" state
this.renderUpdateAsCancelled(params.update);
} else {
this.renderUpdate(params.update);
}
}
async handlePromptResponse(response: acp.PromptResponse) {
if (response.stopReason === "cancelled") {
// Clean up cancellation state
this.activeCancellations.delete(sessionId);
this.showCancelledMessage();
}
}
}
Debouncing UI Updates
For high-frequency updates like text chunks, consider debouncing:
class MessageBuffer {
private buffer = "";
private timeout: NodeJS.Timeout | null = null;
append(text: string) {
this.buffer += text;
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.flush();
}, 16); // ~60fps
}
flush() {
if (this.buffer) {
updateUI(this.buffer);
this.buffer = "";
}
}
}
For long conversations, use virtual scrolling to render only visible messages.
See Also