Message Processing Pipeline in OpenCode Sessions: A Deep Dive into ACP Agent Architecture

The message processing pipeline in OpenCode sessions translates raw LLM output into UI-friendly ACP session updates by parsing message parts into typed chunks for tools, text, files, and reasoning.

The message processing pipeline in OpenCode sessions is the core mechanism that transforms raw LLM responses into structured, real-time updates within the anomalyco/opencode repository. Located primarily in packages/opencode/src/acp/agent.ts, this pipeline ensures that every assistant or user message is decomposed into discrete parts and streamed to the UI as ACP (Agent Client Protocol) sessionUpdate events.

Core Architecture of the OpenCode Message Pipeline

Entry Point: The processMessage Method

The pipeline entry point is the processMessage method in packages/opencode/src/acp/agent.ts (starting at line 796). This private method is invoked for every incoming SessionMessageResponse and performs initial validation before dispatching content:

// packages/opencode/src/acp/agent.ts
private async processMessage(message: SessionMessageResponse) {
  log.debug("process message", message)
  if (message.info.role !== "assistant" && message.info.role !== "user") return
  const sessionId = message.info.sessionID

}

The method filters for assistant or user roles only, extracting the sessionId to scope all subsequent updates to the correct conversation context.

Message Part Dispatch Loop

After validation, processMessage iterates over the message.parts array. Each part is a typed object describing a segment of the LLM output. The loop dispatches to specialized handlers based on part.type, converting each segment into the appropriate ACP sessionUpdate payload.

The supported part types and their corresponding ACP update types are:

  • tooltool_call or tool_call_update
  • textuser_message_chunk or agent_message_chunk
  • fileuser_message_chunk or agent_message_chunk (with embedded resource)
  • reasoningagent_thought_chunk
  • other → Ignored (unsupported)

Processing Specific Message Types in OpenCode

Tool Call Lifecycle Management

Tool parts trigger the most complex handling logic in the pipeline (lines 800-880 and 910-960 in agent.ts). The handler manages the full lifecycle of a tool invocation:

  1. Pending: Emits a tool_call update when the tool is first requested
  2. In-progress: Streams tool_call_update events during execution
  3. Completed: Sends final tool_call_update with output data
  4. Error: Emits error state if execution fails

After a tool completes, the pipeline invokes sendUsageUpdate (defined at lines 73-84) to report token consumption and cost back to the ACP client.

Text and File Content Streaming

Text parts (lines 948-962) are converted directly into message chunks. The pipeline distinguishes between user and assistant roles, wrapping content in user_message_chunk or agent_message_chunk updates.

File attachments (lines 967-1042) undergo type detection based on MIME type. The handler reconstructs files as:

  • resource_link for external URLs
  • image for visual content (PNG, JPEG, etc.)
  • Generic resource for other binary data

These are embedded within the same message chunk wrappers as text content.

Reasoning and Thought Chunks

Reasoning parts (lines 1046-1062) capture the model's internal "thinking" or chain-of-thought. The pipeline extracts the reasoning text and emits agent_thought_chunk updates, allowing the UI to display the model's deliberation process separately from final output.

Event Subscription and Usage Tracking

Global Event Subscription Loop

The processMessage method is invoked from a continuous event subscription loop established in the ACP agent constructor. Located at lines 150-169 in agent.ts, this loop maintains a persistent connection to the OpenCode backend:

while (true) {
  const events = await this.sdk.global.event(...)
  for await (const event of events.stream) {
    await this.handleEvent(event.payload as Event)
  }
}

The handleEvent method routes session-specific messages to processMessage, ensuring real-time processing of all LLM responses.

Token Usage Reporting

After tool execution completes, the pipeline triggers usage tracking via the sendUsageUpdate helper (lines 73-84). This function calculates token consumption and associated costs from the session state, then transmits a usage update to the ACP client. This enables the UI to display real-time cost and token metrics alongside the conversation.

Practical Code Examples

Simulating a Complete Assistant Message

This example demonstrates how the pipeline processes a complex message containing text, a tool call, and metadata:

import { MessageV2 } from "@/session/message-v2"
import type { SessionMessageResponse } from "@opencode-ai/sdk/v2"

// Build a mock assistant message with a text part and a tool call
const mockMessage: SessionMessageResponse = {
  info: {
    role: "assistant",
    sessionID: "sess-123",
    providerID: "openai",
    modelID: "gpt-4o",
    tokens: { input: 150, output: 200 },
    cost: 0.001,
  },
  parts: [
    {
      type: "text",
      text: "Here is the result:",
    },
    {
      type: "tool",
      tool: "edit",
      callID: "tool-1",
      state: {
        status: "completed",
        title: "Edit file",
        input: { filePath: "src/main.ts", oldString: "foo", newString: "bar" },
        output: "Edited successfully",
      },
    },
  ],
}

// The ACP agent will turn this into:
// - `agent_message_chunk` with the text
// - `tool_call` (pending) → `tool_call_update` (completed) → usage update
await agent.processMessage(mockMessage)

Handling File Attachments

When the LLM returns file content, the pipeline reconstructs it as a resource:

const fileMsg: SessionMessageResponse = {
  info: { role: "assistant", sessionID: "sess-456", providerID: "openai", modelID: "gpt-4o", tokens: { input: 0, output: 0 }, cost: 0 },
  parts: [
    {
      type: "file",
      mime: "image/png",
      filename: "screenshot.png",
      url: "data:image/png;base64,iVBORw0KGgoAAAANS...",
    },
  ],
}

// `processMessage` will generate an `image` block inside an `agent_message_chunk`.
await agent.processMessage(fileMsg)

Streaming Reasoning Content

To display the model's internal thought process:

const reasoningMsg: SessionMessageResponse = {
  info: { role: "assistant", sessionID: "sess-789", providerID: "anthropic", modelID: "claude-3", tokens: { input: 0, output: 0 }, cost: 0 },
  parts: [
    {
      type: "reasoning",
      text: "I need to fetch the repository to locate the function.",
    },
  ],
}

// Results in an `agent_thought_chunk` that appears as "thinking" in the UI.
await agent.processMessage(reasoningMsg)

Key Source Files in the Pipeline

File Role in the Pipeline Link
packages/opencode/src/acp/agent.ts Core processMessage implementation, event subscription, usage updates View on GitHub
packages/opencode/src/session/message-v2.ts Schema definitions for message parts (text, tool, file, reasoning) View on GitHub
packages/opencode/src/acp/agent.ts (lines 73-84) Helper sendUsageUpdate for reporting token usage and cost View on GitHub
packages/opencode/src/storage/storage.ts Persists session messages to disk; the pipeline reads from here when replaying sessions View on GitHub
packages/opencode/src/cli/cmd/session.ts Exposes the session API to the CLI, triggering the same pipeline for message retrieval View on GitHub

Summary

  • The message processing pipeline in OpenCode sessions lives in packages/opencode/src/acp/agent.ts and is orchestrated by the processMessage method.
  • It processes only assistant and user roles, extracting sessionId to scope all updates.
  • The pipeline decomposes messages into typed parts (tool, text, file, reasoning) and converts each into specific ACP sessionUpdate payloads.
  • Tool calls undergo a full lifecycle (pending → in-progress → completed/error) with accompanying usage updates via sendUsageUpdate.
  • A global event subscription loop continuously streams session events, ensuring real-time message processing.

Frequently Asked Questions

How does OpenCode handle different message types in the processing pipeline?

The pipeline in packages/opencode/src/acp/agent.ts uses a type-based dispatch system. When processMessage iterates over message.parts, it checks the part.type field and routes to specialized handlers: tool parts trigger lifecycle state management, text parts become message chunks, file parts are reconstructed as resources or images, and reasoning parts are streamed as thought chunks. Unsupported types are silently ignored.

What happens when a tool call completes in an OpenCode session?

When a tool part reaches the completed status, the handler in packages/opencode/src/acp/agent.ts (lines 800-880) emits a final tool_call_update with the output data. Immediately after, it invokes sendUsageUpdate (lines 73-84) to calculate and transmit token consumption and cost metrics back to the ACP client. This ensures the UI displays both the tool result and accurate usage statistics in real-time.

Where does the message processing pipeline receive its input from?

The pipeline receives input from a global event subscription loop established in the ACP agent constructor (lines 150-169 in packages/opencode/src/acp/agent.ts). This loop continuously calls this.sdk.global.event(), streaming SessionMessageResponse objects from the OpenCode backend. Each event is routed through handleEvent to processMessage, ensuring that LLM responses are processed as they arrive without polling delays.

How are file attachments processed differently from text in OpenCode?

While text parts are wrapped directly in agent_message_chunk or user_message_chunk updates, file parts undergo MIME type detection and reconstruction (lines 967-1042 in packages/opencode/src/acp/agent.ts). The pipeline inspects the mime property to categorize files as image (for visual content), resource_link (for external URLs), or generic resource objects. These are then embedded within the same message chunk wrappers, allowing the UI to render attachments inline with conversational text.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →