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:
- tool →
tool_callortool_call_update - text →
user_message_chunkoragent_message_chunk - file →
user_message_chunkoragent_message_chunk(with embedded resource) - reasoning →
agent_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:
- Pending: Emits a
tool_callupdate when the tool is first requested - In-progress: Streams
tool_call_updateevents during execution - Completed: Sends final
tool_call_updatewith output data - 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_linkfor external URLsimagefor visual content (PNG, JPEG, etc.)- Generic
resourcefor 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.tsand is orchestrated by theprocessMessagemethod. - It processes only assistant and user roles, extracting
sessionIdto scope all updates. - The pipeline decomposes messages into typed parts (tool, text, file, reasoning) and converts each into specific ACP
sessionUpdatepayloads. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →