Skip to content

3.2 Agentao as an ACP Server

Drive Agentao as a black-box ACP server — the integration path for hosts in any language (Node, Go, Rust, Kotlin, Swift, C#…).

Launch command

bash
agentao --acp --stdio
  • --acp enables ACP mode
  • --stdio declares the transport (v1 supports stdio only, but the flag is required)
  • The process runs until stdin closes or SIGTERM is received

Environment variables: same as CLI / SDK (OPENAI_API_KEY, etc.). The ACP layer does not transport credentials.

Logs: Agentao writes to <session-cwd>/agentao.log (the cwd from the handshake). Hosts can tail / inspect it for debugging.

Resume a session on startup

bash
agentao --acp --resume                 # resume the latest saved session
agentao --acp --resume <SESSION_ID>    # resume a specific session (UUID / prefix / timestamp)

ACP is client-driven — the server can't open a session on its own — so --resume arms a one-shot directive consumed by the first session/new. That request hydrates the persisted history, replays it as session/update notifications (exactly like session/load), and returns the persisted sessionId instead of a fresh one. Every later session/new on the connection starts blank.

The store is keyed by the client-supplied cwd, so the lookup runs at request time against <cwd>/.agentao/sessions. Any recoverable miss — empty store, unknown id, corrupt file, or an id already live in the registry — degrades to a fresh session (logged at WARNING) rather than failing the client's first session/new.

The resumed sessionId is only returned in the session/new response, so the replayed session/update notifications arrive before the client learns that id. Clients that strictly validate session/update.sessionId against sessions they opened may drop those early updates; the conversation still continues correctly from the next prompt. See docs/guides/acp.md.

Full method catalog

Host → Agent (5 request methods)

MethodPurposeKey params
initializeHandshake + capability negotiationprotocolVersion:int, clientCapabilities:obj, clientInfo?:obj
session/newStart a new sessioncwd:string, mcpServers?:array
session/promptSend one user turnsessionId:string, prompt:array<PromptChunk>
session/cancelCancel an in-flight promptsessionId:string
session/loadRestore from historysessionId:string, history:array

Agent → Host (1 request, 1 notification, 1 extension)

MethodKindPurpose
session/updateNotificationStreamed events: text chunks, thinking, tool-call status
session/request_permissionRequest (needs client response)Ask to approve a risky tool (file write, shell, etc.)
_agentao.cn/ask_userRequest (extension)Ask the user a free-form question

Handshake initialize

Request:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": 1,
    "clientCapabilities": {},
    "clientInfo": {
      "name": "my-ide",
      "version": "1.0.0"
    }
  }
}

Agentao response:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": 1,
    "agentCapabilities": {
      "loadSession": true,
      "promptCapabilities": {
        "image": false,
        "audio": false,
        "embeddedContext": false
      },
      "mcpCapabilities": {
        "http": false,
        "sse": true
      }
    },
    "authMethods": [],
    "agentInfo": {
      "name": "agentao",
      "title": "Agentao",
      "version": "0.2.14"
    },
    "_meta": {
      "_agentao.cn/extensions": [
        {
          "method": "_agentao.cn/ask_user",
          "description": "Request free-form text input from the user."
        }
      ]
    }
  }
}

Rules

  • protocolVersion must be an integer ("1" as string or True are rejected)
  • Version negotiation: if Agentao supports your version, it echoes it back; otherwise it returns its highest supported version. Never errors — the client decides whether to continue
  • clientCapabilities is required as an object (can be {})

Session creation session/new

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "session/new",
  "params": {
    "cwd": "/path/to/user/project",
    "mcpServers": [
      {
        "name": "github",
        "type": "stdio",
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-github"],
        "env": [{"name":"GITHUB_TOKEN","value":"<secret>"}]
      }
    ]
  }
}

Response:

json
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"sess-a1b2c3"}}

Notes

  • cwd sets this session's working directory — file tools, AGENTAO.md, .agentao/ all resolve against it. Keep it unique per concurrent session
  • mcpServers only accepts "type":"stdio" or "type":"sse" (because mcpCapabilities.http=false)
  • Internally these map to the extra_mcp_servers constructor param

Sending a prompt session/prompt

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "session/prompt",
  "params": {
    "sessionId": "sess-a1b2c3",
    "prompt": [
      {"type": "text", "text": "Find the 3 largest .py files"}
    ]
  }
}

In v1, prompt array entries may only be {"type":"text", "text": ...}.

Final response (returned after all streaming updates complete):

json
{
  "jsonrpc":"2.0",
  "id":3,
  "result":{
    "stopReason":"end_turn"
  }
}

stopReason values: end_turn (normal), max_tokens, cancelled, refusal, error, etc.

Streaming updates session/update (notification)

Before session/prompt returns, Agentao emits many notifications like:

json
{
  "jsonrpc":"2.0",
  "method":"session/update",
  "params":{
    "sessionId":"sess-a1b2c3",
    "update":{
      "sessionUpdate":"agent_message_chunk",
      "content":{"type":"text","text":"Let me help"}
    }
  }
}

Key sessionUpdate values:

ValueMeaning
agent_message_chunkStreamed text chunk
agent_thought_chunkThinking/reasoning (when enabled)
tool_callTool invocation started
tool_call_updateTool progress/output

Hosts must not respond to notifications (JSON-RPC 2.0: notifications have no id).

Tool confirmation session/request_permission (request)

When Agentao attempts a requires_confirmation=True tool:

json
{
  "jsonrpc":"2.0",
  "id":42,
  "method":"session/request_permission",
  "params":{
    "sessionId":"sess-a1b2c3",
    "toolCall":{
      "toolCallId":"call-x",
      "status":"pending",
      "title":"Run: rm -rf build/",
      ...
    },
    "options":[
      {"optionId":"allow_once","name":"Allow once","kind":"allow_once"},
      {"optionId":"reject_once","name":"Reject","kind":"reject_once"}
    ]
  }
}

The host must respond (otherwise the agent blocks until timeout):

json
{
  "jsonrpc":"2.0",
  "id":42,
  "result":{
    "outcome":{"outcome":"selected","optionId":"allow_once"}
  }
}

Recommended host UI flow:

  1. On request → show a modal with title and toolCall details
  2. On user choice → respond immediately with result
  3. On timeout → respond with {"outcome":"cancelled"} and optionally send session/cancel

Cancellation session/cancel

json
{"jsonrpc":"2.0","id":99,"method":"session/cancel","params":{"sessionId":"sess-a1b2c3"}}

Effect: the in-flight session/prompt turn finishes with stopReason:"cancelled". Idempotent — repeat calls don't error.

Idempotency — sending session/cancel again after the prompt already finished is a no-op (no error, no extra notifications).

Restoring sessions session/load

For persistent session scenarios: store sessionId + history in your DB, restore after a process restart. Agentao advertises loadSession:true in the handshake.

json
{
  "jsonrpc":"2.0",
  "id":5,
  "method":"session/load",
  "params":{
    "sessionId":"sess-restored",
    "cwd":"/path/to/project",
    "history":[
      {"role":"user","content":[{"type":"text","text":"previous question"}]},
      {"role":"assistant","content":[{"type":"text","text":"previous answer"}]}
    ]
  }
}

⚠️ Common pitfalls

ACP wire-level mistakes that will burn you

  1. Framing: NDJSON always — no raw newlines inside a JSON object. Use your JSON library's compact mode.
  2. Stdout pollution: Agentao routes all logs to agentao.log + stderr, never stdout. Your client reads pure JSON from stdout.
  3. Stdin backpressure: if the client ignores stdout after sending a request, the server's stdout buffer will fill. Use async I/O or a dedicated reader thread.
  4. Version field typing: protocolVersion must be int, not a date string.
  5. Don't reply to session/update: JSON-RPC 2.0 prohibits responses to notifications.

End-to-end minimal client (Python, for demo)

⚡ Runnable end-to-end (≈ 5 minutes)

Outcome — drives agentao --acp --stdio from a 50-line Python client; you see streaming session/update notifications + the final stopReason. Stackpip install 'agentao[cli]>=0.4.0' + 3 env vars; the Python below is the only host code you need. Run — paste into acp_demo.py, then python acp_demo.py. Even if your host isn't Python — read this to understand the wire flow, then translate to your language using 3.3 Host as ACP Client.

Even if your host isn't Python, this snippet clarifies the wire flow:

python
"""Minimal ACP client (Python) driving agentao --acp --stdio."""
import json, subprocess, threading, queue, uuid

class AcpClient:
    def __init__(self):
        self.proc = subprocess.Popen(
            ["agentao", "--acp", "--stdio"],
            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            bufsize=0, text=True,
        )
        self._pending: dict = {}
        self._notifications: queue.Queue = queue.Queue()
        threading.Thread(target=self._reader, daemon=True).start()

    def _reader(self):
        for line in self.proc.stdout:
            msg = json.loads(line)
            if "id" in msg and "method" not in msg:        # response
                fut = self._pending.pop(msg["id"], None)
                if fut: fut.put(msg)
            elif "method" in msg and "id" not in msg:       # notification
                self._notifications.put(msg)
            elif "method" in msg and "id" in msg:           # server → client request
                self._notifications.put(msg)

    def call(self, method, params):
        id_ = str(uuid.uuid4())
        fut: queue.Queue = queue.Queue(maxsize=1)
        self._pending[id_] = fut
        msg = {"jsonrpc":"2.0","id":id_,"method":method,"params":params}
        self.proc.stdin.write(json.dumps(msg) + "\n"); self.proc.stdin.flush()
        return fut.get()

    def respond(self, id_, result):
        msg = {"jsonrpc":"2.0","id":id_,"result":result}
        self.proc.stdin.write(json.dumps(msg) + "\n"); self.proc.stdin.flush()

# Usage
cli = AcpClient()
print(cli.call("initialize", {"protocolVersion":1,"clientCapabilities":{}}))
r = cli.call("session/new", {"cwd":"/tmp"})
sid = r["result"]["sessionId"]
# Listen to notifications asynchronously while sending prompts
cli.call("session/prompt", {
    "sessionId": sid,
    "prompt":[{"type":"text","text":"List 3 largest files"}],
})

Production-grade patterns (error handling, UI bridging, timeouts) live in 3.3 Host Client Architecture.

Key source locations

TopicFile
Launch entrypointagentao/cli/entrypoints.py:254-389
Protocol constantsagentao/acp/protocol.py:18, 47-58
Handshakeagentao/acp/initialize.py
Capability blockagentao/acp/initialize.py:53-76
Session creationagentao/acp/session_new.py
Prompt handlingagentao/acp/session_prompt.py

Part 4 · Event Layer & UI Integration