Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ACP (Agent Client Protocol)

meka acp speaks the Agent Client Protocol over stdio so editor / web / messenger clients can drive an meka turn end to end. Where Interactive Mode and One-Shot Mode are for humans, ACP is for programs that want to host meka inside a richer UI — streamed diffs, native apply-buttons, hosted terminals, slash-command palettes.

This page describes what meka’s ACP surface looks like to a client. Editor-specific setup belongs in each editor’s own documentation; the protocol contract is the same everywhere.

Starting an ACP server

meka acp

The process speaks JSON-RPC 2.0 with newline framing on stdio. There is no human-facing prompt — the binary is meant to be spawned by a client that owns the conversation. The client sends initialize, then session/new (or session/load / session/resume), then a series of session/prompt calls.

A few flags are worth knowing:

FlagEffect
-vLogs to stderr at info (incoming client identity, session lifecycle).
-vvdebug (per-request JSON-RPC diagnostics).
RUST_LOG=meka=traceTrace level.

On startup, after the client’s initialize arrives, meka logs ACP client connected: <name> <version> so you can confirm the client identity in -v mode.

What meka advertises (agentCapabilities)

These are returned in InitializeResponse.agentCapabilities:

  • loadSession: true — the client may call session/load with any persisted session id.
  • sessionCapabilities.list — the client may call session/list to browse the persisted session catalogue (cwd-filtered, cursor-paginated; sub-agent audit sessions are hidden).
  • sessionCapabilities.resume — the client may adopt a persisted session id without replaying history.
  • sessionCapabilities.close — the client may release the active session slot.

mcpCapabilities is intentionally not advertised. meka is itself an MCP client, but the servers it consumes are configured via meka’s own config.toml rather than the mcpServers field on session/new. Advertising HTTP/SSE while silently ignoring the client’s array would have been misleading; the marker will return when client-supplied MCP server connections are actually implemented.

agentInfo carries meka’s name ("meka") and the running binary version.

What meka consumes (clientCapabilities)

The client advertises these in InitializeRequest.clientCapabilities; meka stashes them and lets the built-in tools route accordingly:

  • fs.readTextFile: trueread_file issues fs/read_text_file { sessionId, path, line?, limit? } so the client serves the in-buffer view of the file. Image and regex read_file modes have no fs/* analogue and stay local.
  • fs.writeTextFile: truewrite_file and edit_file’s apply step issue fs/write_text_file { sessionId, path, content }. meka still attaches diff metadata to the tool_call_update so clients with an apply-diff UI can render it.
  • terminal: trueexecute_command runs the four-call dance: terminal/createterminal/wait_for_exitterminal/outputterminal/release. On session/cancel or a per-call timeout, meka issues terminal/kill and still reads accumulated output. Exception: in read permission mode meka keeps the local sandboxed shell (Landlock / bwrap / sandbox-exec / Low-Integrity) rather than delegating, so the sandbox isn’t bypassed by the client’s terminal.

If the client omits a capability, the matching tool falls back to local syscalls — the user-visible behaviour is the same as meka in the REPL.

Session lifecycle

meka holds an in-memory map of sessionId → SessionEntry. Any number of sessions can coexist in one meka acp process, each with its own cwd, permission level, conversation, cancellation token, and per-session runtime mutex. Prompts on different sessions run in parallel; a second session/prompt for a session that already has one in flight is rejected with InvalidParams. The session row is also locked on disk (the same lock the REPL uses), so two meka processes can’t simultaneously write events for the same session id.

  • session/new { cwd, mcpServers } — mints a fresh persisted session, captures the cwd, takes the on-disk session lock, returns the session id and the current SessionMode state.
  • session/load { sessionId, cwd, mcpServers } — replays the persisted conversation as a stream of session/update notifications (user_message_chunk, agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update) before the response. Orphan tool calls (the persisted log stopped mid-tool) are closed out with a failed status so the client’s UI doesn’t render a stuck spinner. If the client’s cwd differs from the persisted one, meka updates the persisted row to match — the client wins.
  • session/list { cwd?, cursor? } — paginated index. Filtered to the requested cwd when present; sub-agent sessions are always hidden. nextCursor is opaque — round-trip it back to keep paging.
  • session/resume { sessionId, cwd, mcpServers } — adopts the session id without replaying. Use this when the client already has the history rendered. Same cwd-update behaviour as session/load.
  • session/close { sessionId } — cancels any in-flight prompt, releases the on-disk session lock, and removes the entry from the map.
  • session/cancel { sessionId } — interrupts the active session/prompt. The response carries stopReason: "cancelled". If a cancel arrives between turns (after one prompt completed and before the next is installed), meka latches the signal and cancels the next prompt immediately on arrival.
  • session/set_mode { sessionId, modeId } — flips the agent’s Permission cell. Modes outside [permissions].enabled from the config become JSON-RPC errors. On success, meka emits session/update: current_mode_update. The flip is atomic and applies to the next tool call within an in-flight turn — no need to wait for the turn to finish.

Prompt turn

A session/prompt carries a prompt array of ContentBlocks. meka accepts text and resource_link blocks (the ACP baseline). resource_link blocks are flattened into a <resource_link name="…" uri="…">description</resource_link> tag inside the prompt text so the model can see the reference; meka does not fetch the resource server-side. image, audio, and resource blocks are not yet supported and produce InvalidParams.

While the turn runs, meka streams session/update notifications:

  • agent_message_chunk for each piece of assistant text.
  • agent_thought_chunk for thinking blocks (Claude OAuth / extended-thinking models).
  • tool_call when a tool starts, with kind, status: "in_progress", an absolute locations array (relative paths resolved against the session cwd), and the raw input.
  • tool_call_update when a tool finishes, with the final status (completed / failed) and a content array. edit_file and write_file populate diff content blocks so clients can render the apply-diff UI.

The response carries a final stopReason:

stopReasonMeaning
end_turnThe agent finished cleanly.
max_tokensThe provider stopped because the model hit its maximum output tokens. The assistant message may be truncated.
cancelledsession/cancel interrupted the turn — including the case where the cancel caused an exception in an underlying operation. meka probes the per-session cancellation token after run_turn; any error returned while the token has fired surfaces as cancelled rather than a generic JSON-RPC error.
refusalThe model declined to comply (Claude stop_reason: "refusal" and the OpenAI equivalent). The assistant message contains the refusal text.

Permission modes

meka’s Permission levels map 1:1 to ACP SessionMode ids:

PermissionMode idDisplay nameDescription
NonenoneNoneNo tools available.
ReadreadReadFile reads and searches only. No writes, no shell.
AskaskAskEvery write or shell command requires approval.
WritewriteWriteAll tools allowed without per-call approval.

The full mode picker is advertised on every session-creation response (NewSessionResponse.modes, LoadSessionResponse.modes, ResumeSessionResponse.modes) but only the modes in [permissions].enabled from your config.toml are listed — picking a disabled mode would just error.

When the active mode is ask, write-gated tools trigger a session/request_permission round-trip. Clients render four options:

  • Allow — run this call only.
  • Always allow — run this call and skip the prompt for the same tool for the rest of the session.
  • Deny — refuse this call only.
  • Always deny — refuse this call and every subsequent call to the same tool.

Sticky decisions live in meka’s process memory; they reset on session close.

Skills as slash commands

Every installed skill (see Skills) is advertised through session/update: available_commands_update after session/new / session/load / session/resume, and refreshed at the top of every session/prompt so a skill installed mid-session shows up without a reconnect.

Each command carries a generic free-form input hint ("additional context (optional)"). When the user picks one from the palette, the client typically inserts /<skill-name> and lets the user type extra context. meka parses the prompt as follows:

  • Plain text (no leading slash) — passes through to the model unchanged.
  • /<skill-name> matching an installed skill — loads the skill body via the same path as the REPL’s /skill command (substituting ${MEKA_SKILL_DIR} and ${MEKA_SESSION_ID}) and prepends any extra context the user typed.
  • Slash with a syntactically valid but unknown skill name (/nonexistent) — JSON-RPC error.
  • Slash with content that isn’t a valid skill identifier (/etc/hosts, //comment) — passes through to the model unchanged, so pasted paths and code comments don’t get intercepted.

Sub-agents

spawn_agent and skill-based delegation produce a sub-agent that runs through PermissionForwardingFrontend. The sub-agent’s own output isn’t streamed to the client (its final report flows back through the parent’s tool_call_update), but its permission prompts, fs delegates, and terminal delegates all forward through the parent’s connection — so the editor’s apply-diff UI sees a sub-agent’s writes the same as the main agent’s.

Known limitations

  • MCP roots/list from background queries. During a tool call, roots/list reflects the calling session’s cwd via a task-local override. Outside of a tool call (e.g. server-initiated polling), the handler falls back to the process cwd, since the MCP protocol doesn’t carry session context.
  • Tool-call diff metadata isn’t persisted. A session reopened with session/load replays tool_call_updates as plain text rather than diffs. The on-disk content is unaffected.
  • read mode + terminal capability: meka runs the local sandboxed shell instead of delegating, to preserve the read-only jail. The shell appears in meka’s own output rather than the client’s terminal pane until you switch to ask or write.
  • Image and regex read_file: stay local. The fs/read_text_file request carries only text, so there’s no protocol surface to delegate either case.
  • Single content type in prompts: meka’s session/prompt accepts text only today. Image / audio / resource prompts will arrive as meka’s PromptCapabilities advertise them.