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

> Explore OpenCode sessions' message processing pipeline. Discover how raw LLM output becomes UI-friendly ACP updates through typed chunk parsing for tools, text, files, and reasoning.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: deep-dive
- Published: 2026-02-16

---

**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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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:

```typescript
// 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_call` or `tool_call_update`
- **text** → `user_message_chunk` or `agent_message_chunk`
- **file** → `user_message_chunk` or `agent_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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/agent.ts), this loop maintains a persistent connection to the OpenCode backend:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/acp/agent.ts) | Core **processMessage** implementation, event subscription, usage updates | [View on GitHub](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/acp/agent.ts) |
| [`packages/opencode/src/session/message-v2.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/message-v2.ts) | Schema definitions for **message parts** (text, tool, file, reasoning) | [View on GitHub](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/message-v2.ts) |
| [`packages/opencode/src/acp/agent.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/acp/agent.ts) (lines 73-84) | Helper `sendUsageUpdate` for reporting token usage and cost | [View on GitHub](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/acp/agent.ts#L73-L84) |
| [`packages/opencode/src/storage/storage.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/storage/storage.ts) | Persists **session messages** to disk; the pipeline reads from here when replaying sessions | [View on GitHub](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/storage/storage.ts) |
| [`packages/opencode/src/cli/cmd/session.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/cli/cmd/session.ts) | Exposes the **session API** to the CLI, triggering the same pipeline for message retrieval | [View on GitHub](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/session.ts) |

## Summary

- The **message processing pipeline in OpenCode sessions** lives in [`packages/opencode/src/acp/agent.ts`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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.