Skip to main content

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.
sessionUpdate
'agent_message_chunk'
Discriminator value.
index
number
Message index in the conversation.
content
MessageContent
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).
sessionUpdate
'agent_thought_chunk'
Discriminator value.
index
number
Message index.
content
MessageContent
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).
sessionUpdate
'user_message_chunk'
Discriminator value.
index
number
Message index.
content
MessageContent
User message content.

ToolCall

A new tool call has been initiated.
sessionUpdate
'tool_call'
Discriminator value.
toolCallId
string
Unique identifier for this tool call.
title
string
Human-readable title (e.g., “Read file: config.ts”).
status
ToolCallStatus
Initial status: "pending", "running", "requires_permission", etc.
content
ToolCallContent[]
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,
  });
}

ToolCallUpdate

A tool call’s status or content has been updated.
sessionUpdate
'tool_call_update'
Discriminator value.
toolCallId
string
The tool call being updated.
status
ToolCallStatus
New status if changed: "running", "completed", "failed", "cancelled", etc.
appendContent
ToolCallContent[]
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.
sessionUpdate
'plan'
Discriminator value.
description
string
Human-readable plan description.
steps
PlanStep[]
Array of planned steps.

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.
sessionUpdate
'current_mode_update'
Discriminator value.
currentMode
SessionMode
The new current mode.

ConfigOptionsUpdate

Session configuration options have changed.
sessionUpdate
'config_options_update'
Discriminator value.
configOptions
ConfigOption[]
Updated configuration options.

AvailableCommandsUpdate

Available slash commands have changed.
sessionUpdate
'available_commands_update'
Discriminator value.
availableCommands
AvailableCommand[]
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();
    }
  }
}

Performance Considerations

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 = "";
    }
  }
}

Virtual Scrolling

For long conversations, use virtual scrolling to render only visible messages.

See Also

Build docs developers (and LLMs) love