# racp


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

The Agent Client Protocol (ACP) lets an editor, such as Zed, run a
coding agent as a child process and talk to it in JSON-RPC over stdin
and stdout. The editor opens a session for a folder, sends prompts,
shows the agent’s messages and tool calls as they happen, and asks the
person to allow or refuse each action the agent wants to take. This
module is the `ramabana-acp` command: it puts a Ramabana agent behind
that protocol.

## What maps onto what

Each part of the protocol already has a counterpart in Ramabana, so this
module translates between them and adds no behaviour of its own.

- `session/prompt`, the editor sending a prompt, is one turn.
- The `session/update` messages that stream the reply carry the chunks
  of
  [`Agent.stream`](https://vedicreader.github.io/ramabana/agent.html#agent.stream).
- The tool calls the editor shows are Ramabana’s
  [`Activity`](https://vedicreader.github.io/ramabana/agent.html#activity)
  rows, one per tool call.
- `session/request_permission`, the editor’s allow-or-refuse prompt, is
  the approval gate,
  [`Approvals`](https://vedicreader.github.io/ramabana/agent.html#approvals).
  The gate waits for the editor’s answer. If the editor fails to answer,
  the action is refused, so losing the editor never lets a write
  through.
- `session/load`, reopening an earlier conversation, is
  `resume_session`, so a conversation started in the terminal can
  continue in the editor.

Two requests go the other way, from the agent to the editor. A client
that offers `fs/read_text_file` can hand over the text of a file as it
stands in an open buffer, including changes not yet saved to disk. A
client that offers `terminal/*` can run a command in a terminal the
person can watch. When the editor offers these, Ramabana reads and runs
through it, so the agent works on what the person sees.

## The bridge

Ramabana runs every tool and every host method on the turn’s worker
thread, a plain thread that blocks while it waits. The ACP library runs
on an asyncio event loop. A tool that needs the editor, for example to
read a buffer, has to hand a coroutine to that loop and wait for the
result.
[`Bridge`](https://vedicreader.github.io/ramabana/acp.html#bridge) is
the one place that does this. `call` sends a request and blocks until
the editor replies or `timeout` passes, and `post` sends an update
without waiting.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/racp.py#L45"
target="_blank" style="float:right; font-size:smaller">source</a>

### Bridge

``` python
def Bridge(
    conn, # the connected ACP `Client`
    sid, # the session every call is about
    loop, # the loop serving the connection
    timeout:int=300, # seconds to wait on a person or editor
):
```

*The one crossing between the turn’s thread and the client’s event
loop.*

## The host the editor answers for

[`EditorHost`](https://vedicreader.github.io/ramabana/acp.html#editorhost)
is the ordinary
[`WorkspaceHost`](https://vedicreader.github.io/ramabana/vault.html#workspacehost)
from `ramabana.vault`, with four methods sent to the editor when the
editor says it can serve them: `read`, `text_at`, `write` and `run_cmd`.
`attach` records what the editor offered.

The four differ in what happens when the editor fails. A read falls back
to the file on disk, because a read the editor declined should not cost
the turn. A write does not fall back: if the editor refused the write,
that refusal is the answer, and writing to disk anyway would defeat the
reason for asking the editor. A command does not fall back once the
editor’s terminal has opened, because by then the command has run and a
second run on this side would run it twice.

`read` matters most. `view_file` reads through `Host.read`, so the text
the model sees, and against which it writes its edits, is the same text
those edits are applied to. If the two came from different places, the
edits would be addressed to text that is not there.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/racp.py#L70"
target="_blank" style="float:right; font-size:smaller">source</a>

### EditorHost

``` python
def EditorHost(
    *args, **kw
):
```

*A provider-configured workspace whose file text and shell may come from
the editor.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/agent.py#L4497"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_agent

``` python
def mk_agent(
    roots, model:NoneType=None, approve:str='ask', web:bool=True, vault:bool=False, pii:str='off',
    pii_ner:bool=False, timeout:int=300, **kw
):
```

*An
[`EditorHost`](https://vedicreader.github.io/ramabana/acp.html#editorhost)
over `roots` and a gated
[`Agent`](https://vedicreader.github.io/ramabana/agent.html#agent) on
it.*

``` python
import inspect, tempfile
test_eq(inspect.signature(mk_agent).parameters['timeout'].default, DFLT_TIMEOUT)
test_eq(DFLT_TIMEOUT, 300)
acp_agent, _ = mk_agent([tempfile.mkdtemp()], web=False)
test_eq(acp_agent.approvals.timeout, 300)
```

## The prompt

An ACP prompt is a list of content blocks.
[`blocks`](https://vedicreader.github.io/ramabana/acp.html#blocks) turns
that list into the text and media that
[`Agent`](https://vedicreader.github.io/ramabana/agent.html#agent)
takes. Text blocks join into one string, separated by blank lines. An
embedded file becomes a `<file>` element holding its text, and a link to
a resource becomes `@uri`. Image and audio blocks are decoded to bytes
and passed to the model when its capabilities accept that kind of media,
or when its capabilities are unknown. Otherwise the block is dropped and
a line saying so is added to the text. The examples under Tests show a
text prompt and an image.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/racp.py#L155"
target="_blank" style="float:right; font-size:smaller">source</a>

### blocks

``` python
def blocks(
    prompt, spec:NoneType=None
):
```

*ACP content blocks as `(text, media)`, keeping the media `spec`
accepts.*

## The session

[`Session`](https://vedicreader.github.io/ramabana/acp.html#session)
holds one editor session: one agent, its host, and the bridge to the
editor. Everything the editor shows comes from hooks the agent already
has. `on_activity` reports each tool call, `on_plan` reports the agent’s
checklist, and
[`Approvals.listen`](https://vedicreader.github.io/ramabana/agent.html#approvals.listen)
is told about each approval request.
[`Session`](https://vedicreader.github.io/ramabana/acp.html#session)
turns each of these into the matching ACP update. For an edit it also
works out the file’s text before and after, without writing anything, so
the editor can show a diff in its permission prompt.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/racp.py#L169"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session

``` python
def Session(
    roots, conn, loop, sid:NoneType=None, model:NoneType=None, mk:NoneType=None, timeout:int=300, **kw
):
```

*One ACP session: an agent, and the editor it reports to.*

The editor’s permission prompt offers three choices. The second reads
“Allow every write this session”, so choosing it answers the request
with `scope='edits'`: file and notebook edits stop asking for the rest
of the session, while shell commands and `add_root` still ask. Only the
terminal’s own approve-all key switches the gate to `auto`. The example
below stands in an editor that always picks that option, and checks what
the gate decides afterwards.

``` python
class _Editor:
    "A bridge whose editor always picks `allow_always`."
    def call(self, coro): coro.close(); return True, 'allow_always in the editor', True
    def post(self, coro): coro.close()
s = Session.__new__(Session)
s.agent, s.host = mk_agent([tempfile.mkdtemp()], web=False)
s.conn, s.br, s.gated, s.seen = None, _Editor(), {}, set()
ap = s.agent.approvals
s.unhook = ap.listen(s._ask)
test_eq(bool(ap.request('edit_file', {'path': 'a.py'}, force=True)), True)
test_eq(ap.mode, 'edits')
test_eq(ap.decide('run_shell', {'command': 'ls'}, force=True), None)
test_eq(ap.decide('add_root', {'path': tempfile.mkdtemp()}, force=True), None)
test_eq(ap.decide('create_file', {'path': 'b.py', 'text': ''}, force=True).answer, True)
s.unhook()
```

## The agent

[`AcpAgent`](https://vedicreader.github.io/ramabana/acp.html#acpagent)
is the object the ACP library drives. It answers `initialize`, opens a
[`Session`](https://vedicreader.github.io/ramabana/acp.html#session) for
each `session/new`, resumes one for `session/load` and replays its turns
to the editor, runs `session/prompt`, and handles cancel and close. A
prompt that starts with `/` runs as a Ramabana slash command instead of
a turn.

`initialize` records the capabilities the editor declares, and
[`EditorHost.attach`](https://vedicreader.github.io/ramabana/acp.html#editorhost.attach)
turns on only those. A client that declares none gets a host that reads,
writes and runs on this machine, as a plain
[`WorkspaceHost`](https://vedicreader.github.io/ramabana/vault.html#workspacehost)
would.

[`serve`](https://vedicreader.github.io/ramabana/acp.html#serve) speaks
the protocol on stdin and stdout until the editor closes the connection,
and [`main`](https://vedicreader.github.io/ramabana/cli.html#main) is
the `ramabana-acp` command around it. Its options are listed on the
[index page](index.ipynb#run-it-inside-an-editor).

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/racp.py#L290"
target="_blank" style="float:right; font-size:smaller">source</a>

### AcpAgent

``` python
def AcpAgent(
    model:NoneType=None, roots:tuple=('.',), mk:NoneType=None, timeout:int=300, **kw
):
```

*Ramabana behind the Agent Client Protocol, one agent per session.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/cli.py#L2411"
target="_blank" style="float:right; font-size:smaller">source</a>

### main

``` python
def main(
    root:str='.', # comma-separated folders if the editor names none
    model:str=None, # turn model; omit for the routing default
    web:bool=True, # let web tools reach the network via fossick
    vault:bool=False, # keep reads in a vishalakshi vault
    pii:str='off', # off | redact | refuse PII from the vault
    pii_ner:bool=False, # --pii also gates titled names, not only patterns
    cfg:str=None, # config dir, for skills, extensions and history
):
```

*Serve Ramabana over the Agent Client Protocol for an editor.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/racp.py#L366"
target="_blank" style="float:right; font-size:smaller">source</a>

### serve

``` python
async def serve(
    agent:NoneType=None
):
```

*Speak ACP on stdio until the editor closes it.*

## Tests

`tests/test_acp.py` tests the wire protocol: it starts `ramabana-acp` as
a subprocess and speaks JSON-RPC to it. The cells below check the
translation and the host. The first two run
[`blocks`](https://vedicreader.github.io/ramabana/acp.html#blocks) on a
text prompt and on a prompt with an image.

``` python
text, media = blocks([acp.text_block('why does'), acp.text_block('it fail?')])
test_eq(text, 'why does\n\nit fail?')
test_eq(media, [])
```

``` python
png = b'\x89PNG\r\n\x1a\n'
text, media = blocks([acp.text_block('what is this'), acp.image_block(base64.b64encode(png).decode(), 'image/png')])
test_eq(media, [png])
test_eq(text, 'what is this')
```

``` python
import typing
from acp.schema import ToolCallStart
known = set(typing.get_args(typing.get_args(ToolCallStart.model_fields['kind'].annotation)[0]))
test_eq(set(KIND.values()) - known, set())
test_eq(set(TOOL.values()) - known, set())
test_eq(set(PLAN.values()), {'pending', 'in_progress', 'completed'})
```

`KIND`, `TOOL` and `PLAN` must use only values the protocol defines,
which the cell above checks against the ACP schema. An
[`EditorHost`](https://vedicreader.github.io/ramabana/acp.html#editorhost)
with no editor attached behaves as a plain
[`WorkspaceHost`](https://vedicreader.github.io/ramabana/vault.html#workspacehost).
The empty command that
[`tools_for`](https://vedicreader.github.io/ramabana/tools.html#tools_for)
sends to test whether a host can run commands must never reach the
editor:

``` python
h = EditorHost(['.'])
test_eq((h.can_read, h.can_write, h.can_run), (False, False, False))
test_eq(h.run_cmd(''), (0, ''))
```
