# hagent-acp

An [Agent Client Protocol](https://agentclientprotocol.com) (ACP) agent over
stdio that exposes Hadrian's server-side-executing `/v1/responses` API to any
ACP client (Zed, JetBrains, …). It is `hagent`'s agent with an ACP frontend
instead of the Ratatui TUI: it reuses [`hagent-core`](../hagent-core) unchanged
(HTTP/SSE client, the `Conversation` state machine, MCP OAuth, the file-editing
skill bootstrap, config, conversation logging) and only translates between the
`StreamEvent` wire protocol and ACP `SessionUpdate`s.

Execution stays **server-side** — the agent renders tool calls (shell, web
search, file search, MCP, file edits) as ACP tool-call cards but never runs them
on the editor's machine. A client that advertises zero `fs`/`terminal`
capability still gets a fully working agent.

## Build & run

```bash
cargo build -p hagent-acp
# stdio JSON-RPC agent; talks to a Hadrian at <base-url> (note the /api mount):
hagent-acp --base-url http://localhost:8080/api --model <model>
```

**stdout is the JSON-RPC channel** — all logging goes to stderr (`HAGENT_LOG=debug`
to raise the level). Config is resolved exactly like `hagent`: CLI flags / env
(`HAGENT_BASE_URL`, `HAGENT_API_KEY`, `HAGENT_MODEL`, …) → `~/.config/hagent/config.toml`
→ defaults, plus any `./.mcp.json` servers. `--no-bootstrap` suppresses the
file-editing skill upload on `session/new`. `--flat-model-list` (or
`HAGENT_FLAT_MODEL_LIST=1`) presents the model picker as a flat list instead of
grouping by provider — use it for clients that don't render grouped select
options (JetBrains today; Zed renders groups fine).

## Use from Zed

Register it as an external agent in Zed `settings.json`:

```json
{
  "agent_servers": {
    "hagent": {
      "command": "/path/to/hagent-acp",
      "args": ["--base-url", "http://localhost:8080/api", "--model", "<model>"],
      "env": { "HAGENT_API_KEY": "gw_…" }
    }
  }
}
```

## What's implemented

| ACP method | Behaviour |
| --- | --- |
| `initialize` | Advertises `loadSession` + MCP (http/sse); reasoning-effort session modes. |
| `session/new` | Builds a `Conversation` + logged client; bootstraps the file-edit skill (shell + no cached skill); merges ACP-provided HTTP/SSE MCP servers with config. |
| `session/prompt` | Runs the turn off the dispatch loop; streams text→`AgentMessageChunk`, reasoning→`AgentThoughtChunk`, tools→`ToolCall`/`ToolCallUpdate`; returns a `StopReason`. |
| `session/load` | Restores the on-disk snapshot and replays the transcript. |
| `session/set_mode` | Maps the mode id to a reasoning-effort level. |
| `session/set_config_option` | **Model picker** (Select, category `Model`, from `/v1/models`, grouped by provider — pass `--flat-model-list` for clients that don't render grouped selects, e.g. JetBrains) + **tool on/off toggles** (boolean options). Returns the updated option set. |
| Slash commands | Advertises `/model`, `/effort`, `/status` via `AvailableCommandsUpdate`; handled locally (no upstream turn). |
| `session/cancel` | Cancels the in-flight stream (`StopReason::Cancelled`). |
| MCP approvals | `mcp_approval_request` → `session/request_permission` → continuation turn (remembers `*_always`). |
| MCP OAuth | Per-turn token gate; surfaces the authorization URL via a session update and opens the browser. |

The translation seam (`StreamEvent`/`KnownItem` → `SessionUpdate`) lives in
[`src/translate.rs`](src/translate.rs) as pure, unit-tested functions — including
the Hadrian quirk that a `shell_call`'s output arrives in a *separate* item after
the call reports completed (the tool call is held `InProgress` until the output
lands, joined by `call_id`).

## Targeting other endpoints

`Settings.{base_url, api_key}` parameterise *any* server-side-executing OpenAI
Responses endpoint, so this agent is provider-agnostic by construction.
