Skip to content

Latest commit

 

History

History
306 lines (224 loc) · 12.6 KB

acp-integration.md

File metadata and controls

306 lines (224 loc) · 12.6 KB

Agent Client Protocol (ACP) Integration Guide

This guide covers building IDE integrations for tinycode using the Agent Client Protocol (ACP).

What is ACP?

The Agent Client Protocol is a standardized JSON-RPC 2.0 protocol for communication between IDEs and AI coding agents. Originally developed by Zed Industries, ACP defines a bidirectional stdio-based protocol for:

  • Session management (create, list, load, fork, close)
  • Prompt turns with streamed session/update notifications
  • Tool execution with session/request_permission requests
  • User questions via elicitation/create (form mode) when the client advertises elicitation
  • Multi-turn conversations with context preservation

ACP enables tinycode to integrate with any editor that supports spawning a child process and communicating over stdin/stdout.

Quick Start

Run tinycode in ACP mode:

tinycode acp --cwd /path/to/project

The process will:

  1. Boot an embedded tinycode server (ephemeral localhost port) with providers, tools, permissions, agents, plugins, and MCP
  2. Listen on stdin for JSON-RPC requests using official ACP wire methods (session/new, session/prompt, …)
  3. Stream LLM responses and tool events as session/update notifications on stdout
  4. Ask the IDE for tool permissions via session/request_permission requests
  5. Ask the user questions via elicitation/create when the client supports form elicitation

Flags:

Flag Description
--cwd <dir> Working directory for new sessions (also accepts -cwd)
--model / -m Default model (provider/model); required if discovery finds none
--title Default title for sessions created via session/new
--safe-mode Skip plugins, MCP, and user agents
Prompt knobs --append-system-prompt, --max-tokens, etc.

--continue / --resume are TUI/run flags; ACP clients use session/load or session/resume.

Architecture

┌─────────────────┐
│  IDE Extension  │
│   (VS Code,     │
│   Zed, etc.)    │
└────────┬────────┘
         │ stdio (NDJSON)
         │ JSON-RPC 2.0
         │
┌────────▼──────────────────────────────┐
│  tinycode acp                         │
│  ┌────────────┐  ┌─────────────────┐  │
│  │ ACP Service│──│ SessionManager  │  │
│  │ + EventRelay│  │ (in-process)    │  │
│  └────────────┘  └────────┬────────┘  │
│                           │           │
│              embedded HTTP server     │
│              (port 0 / ephemeral)     │
└───────────────────────────────────────┘

Go tinycode acp boots the same core stack as TUI/serve (providers, tools, permissions, SessionManager, plugins, MCP) and speaks ACP over stdio. Session operations prefer in-process SessionManager calls; the ephemeral HTTP listener exists so the full server lifecycle (hooks, discovery, etc.) runs even when the IDE never opens an HTTP client.

Note: The legacy TypeScript packages/tinycode ACP path started HTTP + SDK separately. The Go binary is the supported ACP entrypoint.

Building an IDE Extension

Installation

npm install @agentclientprotocol/sdk

Basic Example

import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk'
import { spawn } from 'child_process'

const proc = spawn('tinycode', ['acp', '--cwd', workspaceRoot], {
  stdio: ['pipe', 'pipe', 'pipe']
})

const stream = ndJsonStream(proc.stdout, proc.stdin)

const conn = new ClientSideConnection((agent) => ({
  async sessionUpdate(params) {
    // Handle streamed chunks / tool calls
    const update = params.update
    if (update.sessionUpdate === 'agent_message_chunk') {
      console.log(update.content?.text)
    }
  },
  async requestPermission(params) {
    // Present options to the user; return selected optionId
    return {
      outcome: { outcome: 'selected', optionId: 'allow_once' }
    }
  }
}), stream)

const initResult = await conn.initialize({
  protocolVersion: 1,
  clientCapabilities: {
    // Advertise form elicitation so the question tool can prompt the user.
    elicitation: { form: {} },
  },
  clientInfo: { name: 'my-editor', version: '1.0.0' }
})

console.log('Connected to:', initResult.agentInfo)

const session = await conn.newSession({ cwd: workspaceRoot })
console.log('Session ID:', session.sessionId)

const result = await conn.prompt({
  sessionId: session.sessionId,
  prompt: [{ type: 'text', text: 'Explain this codebase' }]
})
console.log('stopReason:', result.stopReason) // "end_turn" | "cancelled" | "refusal"

Listening for Session Updates

Official ACP notifications use method session/update:

{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "ses_…",
    "update": {
      "sessionUpdate": "agent_message_chunk",
      "content": { "type": "text", "text": "Hello…" }
    }
  }
}

Common sessionUpdate values:

Value Meaning
agent_message_chunk Assistant text (including streamed deltas)
agent_thought_chunk Reasoning/thought text (including streamed deltas)
user_message_chunk Replayed user text (loadSession)
tool_call Tool invocation started (pending)
tool_call_update Tool status: in_progress after args stream; completed/failed after execution with rawOutput when available. File-modifying tools (edit/write/apply_patch) include locations and diff/summary content.

Permission Handling

When a tool needs approval, the agent sends a request (not a notification):

  • Method: session/request_permission
  • Includes toolCallId when the ask is tied to a tool call (IDE sheets can correlate to the tool card)
  • Client must reply with { outcome: { outcome: "selected", optionId: "allow_once" | "allow_always" | "reject_once" } }
  • On timeout (120s) or disconnect, tinycode auto-denies
  • session/cancel cancels in-flight permission and elicitation RPCs (clears orphaned IDE modals)

Question Elicitation

When the agent uses the question tool:

  • Method: elicitation/create with mode: "form" and a flat requestedSchema (answer string, optionally enum options)
  • Requires clientCapabilities.elicitation.form during initialize
  • Client replies with { action: "accept", content: { answer: "…" } } or decline / cancel
  • Without form elicitation support, the question tool fails immediately (no hang)

Supported Operations

Wire method CamelCase alias Status
initialize — Supported
authenticate — Supported (no-op)
session/new newSession Supported
session/load loadSession Supported (replays text + tool history)
session/list listSessions Supported
session/resume resumeSession Supported (no history replay)
session/close closeSession Supported
session/fork forkSession Supported (copies messages)
session/prompt prompt Supported — waits until idle, returns stopReason
session/cancel cancel Supported — aborts active run
session/set_mode setSessionMode Supported — maps to agent name
session/set_model setSessionModel Supported — provider/model
session/set_config_option setSessionConfigOption Supported for model; unknown IDs error

Capabilities (honest)

During initialize, tinycode advertises:

  • loadSession: true
  • sessionCapabilities: close, fork, list, resume
  • promptCapabilities.image: false (not yet supported)
  • promptCapabilities.embeddedContext: false (not yet supported)

End-of-Turn Handling

session/prompt blocks until the SessionManager reports idle (or cancel), then returns:

{ "stopReason": "end_turn" }

or { "stopReason": "cancelled" } if the client sent session/cancel, or { "stopReason": "refusal" } if the turn failed with a session.error (the error text is also streamed as an agent_message_chunk).

Prompt params accept either official prompt: ContentBlock[] or legacy content: ContentBlock[].

Session Management

Creating Sessions

const session = await conn.newSession({
  cwd: '/path/to/project'
})

New sessions use agent build and the default model from config/discovery (same heuristics as TUI). session/new returns modes (primary agents) and a model configOptions select. Creation fails closed with an actionable error if no model is available after a short discovery wait — run tinycode doctor or pass --model provider/model.

Listing / Loading / Forking

const { sessions } = await conn.listSessions({ cwd: workspaceRoot })
await conn.loadSession({ sessionId, cwd: workspaceRoot }) // replays history via session/update
const forked = await conn.unstable_forkSession({ sessionId })

Resume vs load

session/resume (resumeSession) re-attaches the agent to an existing session id and returns setup metadata (modes / model config). It does not replay conversation history to the client as session/update notifications.

Use session/load (loadSession) when the IDE needs prior messages, tool calls, and tool results streamed back for UI restoration. After resume alone, the client only sees new turns.

Error Handling

All ACP methods return promises that reject on error. Handle JSON-RPC error codes (-32602 invalid params, -32603 internal, -32601 method not found).

Editor Status

Editor Status
VS Code Spawn tinycode acp --cwd … from the editor. There is no VS Code extension in this repository.
Zed Use official ACP agent spawn pointing at tinycode acp --cwd …
JetBrains Not packaged yet; same stdio protocol applies

Dogfood matrix (elicitation + permission)

Manual checklist when verifying ACP clients. Not automated.

Client Spawn Permission UI Form elicitation Notes
Zed Official ACP agent → tinycode acp --cwd <proj> session/request_permission shows Allow once / Always / Reject; toolCallId correlates with tool card Advertise clientCapabilities.elicitation.form; answer elicitation/create Confirm cancel dismisses modal without 120s hang
VS Code Spawn tinycode acp --cwd … from the editor (no in-repo extension) Same three options; Always scope is tool-side Same capability + handler Confirm session/cancel clears pending permission modal
JetBrains Same stdio protocol; not packaged here yet Same Same Track client variance; reject without form elicitation must not hang the turn

Smoke steps (each client):

  1. Tool that needs permission → modal appears with non-empty toolCallId when the call has an id
  2. Allow once → tool continues; Always → matching asks auto-approve this turn
  3. session/cancel while permission modal open → modal closes promptly (no AskTimeout orphan)
  4. question tool with form elicitation → prompt appears; decline/cancel rejects without hanging
  5. Client without form elicitation → question tool fails immediately
  6. session/load replays history; session/resume does not

Troubleshooting

Connection Failed

  • Verify which tinycode (Go binary from make build → dist/tinycode)
  • Ensure --cwd points to a valid directory
  • Check stderr for bootstrap/discovery errors

No Response to Prompts

  • Confirm a model is available (tinycode models) or pass --model provider/model
  • Read process stderr — missing providers surface there
  • session/prompt should eventually return a stopReason; if it hangs, check permission UI

Permission Requests Hang

  • Client must implement requestPermission and always respond (deny on cancel)
  • Auto-deny kicks in after 120s

Questions Hang or Fail Immediately

  • Advertise clientCapabilities.elicitation.form and implement elicitation/create
  • Without that capability, the question tool rejects immediately so the turn can continue

Go implementation

ACP is implemented in Go under internal/acp/ (service.go, transport.go, adapter.go, event.go) — not a stub. Entry point: tinycode acp.

Specification

The full ACP specification is available at agentclientprotocol.com.

Next Steps