diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000..0ffbf7d9
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,47 @@
+name: Documentation
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'docs/**'
+ - 'src/acp/**'
+ - 'examples/**'
+ - 'README.md'
+ - 'mkdocs.yml'
+ - 'Makefile'
+ - 'pyproject.toml'
+ - 'uv.lock'
+ - '.github/actions/setup-python-env/**'
+ - '.github/workflows/docs.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ publish:
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pages: write
+ steps:
+ - name: Check out
+ uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
+ - name: Set up the environment
+ uses: ./.github/actions/setup-python-env
+ - name: Publish documentation to gh-pages
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git fetch origin gh-pages:gh-pages --depth=1
+ uv run --frozen mkdocs gh-deploy --strict --remote-branch gh-pages
+ - name: Request a Pages build
+ run: gh api --method POST "repos/$GITHUB_REPOSITORY/pages/builds"
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4e40a810..44d0e0f3 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,6 +7,9 @@ on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
+permissions:
+ contents: read
+
jobs:
quality:
runs-on: ubuntu-latest
@@ -67,8 +70,9 @@ jobs:
- name: Check out
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
- - name: Link Checker
+ - name: Check local documentation links
id: lychee
uses: lycheeverse/lychee-action@1ef33e2493308e49729a7789ddd73e7f8bed8f45
with:
- fail: false
+ args: --offline --include-fragments --no-progress README.md 'docs/**/*.md'
+ fail: true
diff --git a/.github/workflows/on-release-main.yml b/.github/workflows/on-release-main.yml
index 620606a2..88175d2e 100644
--- a/.github/workflows/on-release-main.yml
+++ b/.github/workflows/on-release-main.yml
@@ -6,12 +6,6 @@ on:
permissions:
contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: "pages"
- cancel-in-progress: true
jobs:
set-version:
@@ -21,7 +15,7 @@ jobs:
- name: Export tag
id: vars
- run: echo tag=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT
+ run: echo "tag=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT"
if: ${{ github.event_name == 'release' }}
- name: Update project version
@@ -59,32 +53,3 @@ jobs:
run: uv publish
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
-
- build-docs:
- needs: publish
- runs-on: ubuntu-latest
- steps:
- - name: Check out
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
-
- - name: Set up the environment
- uses: ./.github/actions/setup-python-env
-
- - name: Build site
- run: uv run mkdocs build -f mkdocs.yml --clean
-
- - name: Upload Pages artifact
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
- with:
- path: site
-
- deploy-docs:
- needs: build-docs
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
diff --git a/.gitignore b/.gitignore
index 88ea146b..5cf7e99d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -214,5 +214,5 @@ __marimo__/
.zed/
# others
-reference/
+/reference/
.DS_Store
diff --git a/README.md b/README.md
index b8aaac8a..2f46a131 100644
--- a/README.md
+++ b/README.md
@@ -2,74 +2,66 @@
-# Agent Client Protocol (Python)
+# Agent Client Protocol SDK for Python
-Build ACP-compliant agents and clients in Python with generated schema models, asyncio transports, and runnable demos.
+Implement ACP agent and client integrations in Python. The SDK provides typed protocol models, asynchronous handler interfaces, and connection helpers. Your application supplies the agent runtime, client callbacks, and session state.
-> Releases track the upstream ACP schema; contributions that tighten coverage or tooling are always welcome.
+Requires Python 3.10 through 3.14.
## Install
```bash
pip install agent-client-protocol
-# or
-uv add agent-client-protocol
```
-## At a glance
+For a project managed with uv:
+
+```bash
+uv add agent-client-protocol
+```
-- **Spec parity:** Generated Pydantic models in `acp.schema` track every ACP release so payloads stay valid.
-- **Direct construction:** Generated discriminator fields have defaults, so models can be instantiated directly.
-- **Runtime ergonomics:** Async base classes, stdio JSON-RPC plumbing, and lifecycle helpers keep custom agents tiny.
-- **Examples ready:** Streaming, permissions, Gemini bridge, and duet demos live under `examples/`.
-- **Contrib utilities:** Session accumulators, tool call trackers, and permission brokers share patterns from real deployments.
+Import the package as `acp`:
-## Who benefits
+```python
+from acp.schema import TextContentBlock
-- Agent authors who need typed models and event-stream ergonomics for ACP-compatible assistants.
-- Client integrators embedding ACP parties inside Python applications or wrapping existing CLIs via stdio.
-- Tooling teams experimenting with permission flows, streaming UX, or Gemini bridges without re-implementing transports.
-See real adopters like kimi-cli in the [Use Cases list](https://agentclientprotocol.github.io/python-sdk/use-cases/).
+message = TextContentBlock(text="Hello ACP")
+```
-## How to get started
+## Run an agent and client
-- Follow the [Quickstart guide](https://agentclientprotocol.github.io/python-sdk/quickstart/) for installation, echo-agent validation, editor wiring (e.g. Zed), and programmatic launch recipes.
-- Browse the [example gallery](https://github.com/agentclientprotocol/python-sdk/tree/main/examples) to see progressively richer integrations you can copy or extend.
-- Skim the [docs hub](https://agentclientprotocol.github.io/python-sdk/) for focused references on contrib helpers, releasing, and transport details.
+The example client starts the echo agent, sends your message, and prints the streamed reply. Install Git and uv, then run:
-## Quick links
+```bash
+git clone https://github.com/agentclientprotocol/python-sdk.git
+cd python-sdk
+uv sync --frozen
+uv run --frozen python examples/client.py python examples/echo_agent.py
+```
-| Need | Link |
-|--------------------|------------------------------------------------------------------------|
-| Docs hub | |
-| Quickstart | |
-| Use cases | |
-| Contrib helpers | |
-| Releasing workflow | |
-| Examples | |
-| Tests | |
-| PyPI | |
+Type `Hello ACP` at the prompt. The client starts the agent and prints `| Agent: Hello ACP`. See the [quickstart](https://agentclientprotocol.github.io/python-sdk/tutorials/quickstart/) for the initialization and session calls behind this exchange.
-## Project layout
+This runs the checked-out SDK and examples. Installing the package alone does not create an `examples/` directory. The documentation follows `main`, which can include features beyond the latest stable PyPI release.
-- `src/acp/`: runtime package (agents, clients, transports, schema bindings, contrib utilities)
-- `schema/`: upstream JSON schema sources (regenerate via `make gen-all`)
-- `docs/`: MkDocs content backing the published documentation
-- `examples/`: runnable scripts covering stdio orchestration patterns
-- `tests/`: pytest suite with golden fixtures and optional Gemini coverage
+## Build your integration
-## Developer commands
+| Task | Guide |
+| --- | --- |
+| Implement an agent | [Build an agent](https://agentclientprotocol.github.io/python-sdk/tutorials/agent/) |
+| Implement client callbacks and manage an agent process | [Build a client](https://agentclientprotocol.github.io/python-sdk/tutorials/client/) |
+| Handle tool calls and permissions | [Permission guide](https://agentclientprotocol.github.io/python-sdk/how-to/permissions/) |
+| Look up signatures and models | [Core API reference](https://agentclientprotocol.github.io/python-sdk/reference/api/) |
+| Study adapters built with this SDK | [Integration patterns](https://agentclientprotocol.github.io/python-sdk/integrations/) |
-- `make install` provisions the `uv` virtualenv and installs pre-commit hooks.
-- `make check` runs Ruff formatting/linting, type analysis, dependency hygiene, and lock verification.
-- `make test` executes `pytest` (with doctests) inside the managed environment.
-- `ACP_SCHEMA_VERSION=[ make gen-all` refreshes protocol artifacts when the schema advances.
+Protocol v2, remote transports, and `acp.contrib` are experimental. Check their documentation before depending on their APIs.
-Keep docs and examples current whenever you ship public API or transport changes, and prefer Conventional Commits (`feat:`, `fix:`, etc.) when submitting patches.
+## Contribute
-## Community & support
+```bash
+make install
+make check
+make test
+make docs-test
+```
-- File issues or feature requests at .
-- Discuss ideas or get help via GitHub Discussions: .
-- Join the broader ACP conversations at , the Zed community channels, or the community Zulip: .
-- Shared learnings, integrations, or third-party transports are welcome additions to the documentation—open a PR!
+Report bugs in [GitHub Issues](https://github.com/agentclientprotocol/python-sdk/issues). Include the SDK version, the agent or client you used, and a minimal reproduction. See the [release guide](https://agentclientprotocol.github.io/python-sdk/maintainers/releasing/) for schema regeneration and package publishing.
diff --git a/docs/contrib.md b/docs/contrib.md
deleted file mode 100644
index b2e78f14..00000000
--- a/docs/contrib.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Experimental Contrib Modules
-
-The helpers under `acp.contrib` package recurring patterns we saw in integrations such as Toad, kimi-cli, and Gemini. Keep in mind they are experimental—APIs can still shift as we learn. Use this page as a menu:
-
-- **`session_state.SessionAccumulator`** — build a canonical, immutable snapshot of every session update so UIs can render tool calls and plans without re-implementing state machines.
-- **`tool_calls.ToolCallTracker` + `permissions.PermissionBroker`** — coordinate streamed tool call updates and permission prompts from one place.
-
-## SessionAccumulator (`acp.contrib.session_state`)
-
-**Use it when:** you need a live, merged view of `SessionNotification` events (e.g. tool calls, plan entries, user/agent messages) to drive UI widgets.
-
-**Key capabilities**
-
-- `SessionAccumulator.apply(notification)` reconciles `tool_call` and `tool_call_update` payloads, even if the start event arrives late.
-- `snapshot()` returns an immutable `SessionSnapshot` Pydantic model containing the plan, current mode, commands, and ordered message history.
-- `subscribe(callback)` lets you push snapshots into stores or UI components whenever state changes.
-- Automatic reset on session-change (toggle with `auto_reset_on_session_change`).
-
-> Tip: Create one accumulator per UI controller. Feed every raw `SessionNotification` into it and render from `snapshot.tool_calls` / `snapshot.user_messages` instead of mutating state manually.
-
-## ToolCallTracker & PermissionBroker (`acp.contrib.tool_calls`, `acp.contrib.permissions`)
-
-**Use them when:** your agent runtime synthesises tool call IDs, streams arguments, and prompts the user for approval. The helpers centralise the bookkeeping so you don’t juggle raw Pydantic models.
-
-- `ToolCallTracker.start()/progress()/append_stream_text()` emits canonical `ToolCallStart` / `ToolCallProgress` updates and keeps an in-memory view via `view()` or `tool_call_model()`.
-- `PermissionBroker.request_for()` wraps `requestPermission` RPCs. It reuses tracker state (or a provided `ToolCall`), lets you append extra content, and defaults to Approve / Approve for session / Reject options.
-- Omit `options` or pass `None` to use broker defaults. An explicit empty option list raises `MissingPermissionOptionsError` before the permission request is sent; the same applies to empty `default_options` when no per-request options are supplied.
-- `default_permission_options()` exposes that canonical option triple if you need to customise it.
-
-> Tip: Keep one tracker near the agent event loop. Emit notifications through it and share the tracker with `PermissionBroker` so permission prompts always match the latest tool call state.
-
-## Design Guardrails
-
-To stay aligned with the ACP schema, the contrib layer follows a few rules:
-
-- Schema data classes continue to live in `acp.schema`. Contrib helpers clone them with `.model_copy(deep=True)` before mutation.
-- The core runtime never imports contrib modules implicitly—you opt in when they help.
-- Helpers focus on painful bookkeeping (tool call aggregation, permission UX) and leave product-specific policy to your application.
-
-Try the contrib modules, then open an issue or PR with feedback so we know which APIs should graduate into the stable surface.
diff --git a/docs/explanation/lifecycle.md b/docs/explanation/lifecycle.md
new file mode 100644
index 00000000..4d1d0f44
--- /dev/null
+++ b/docs/explanation/lifecycle.md
@@ -0,0 +1,47 @@
+# Roles and session lifecycle
+
+An ACP client starts or connects to an agent, creates sessions, sends prompts, and handles callbacks. The agent owns the work performed for each prompt. An editor is one kind of client; a Python application can be another.
+
+`Agent` and `Client` are typed Protocol interfaces describing handler methods. You may subclass them or provide an object with the corresponding methods. The SDK's connections translate JSON-RPC requests into these handler calls and validate protocol models.
+
+## A v1 exchange
+
+```text
+Client Agent
+ |--- initialize ------------------------->|
+ |<-- protocol version and capabilities ---|
+ |--- session/new ------------------------>|
+ |<-- session ID --------------------------|
+ |--- session/prompt ---------------------->|
+ |<-- session/update (text, tools, ...) ----|
+ |<-- more session/update notifications ----|
+ |<-- prompt response with stop_reason -----|
+```
+
+The client sends its latest supported major protocol version. The agent returns that version if supported, or its latest supported version. A client that cannot use the selected version closes the connection. Omitted optional capabilities mean unsupported. These are application obligations in the v1 runtime; the connection helpers do not choose capabilities or reject a negotiated version for you. See the protocol's [initialization contract](https://agentclientprotocol.com/protocol/v1/initialization).
+
+The client may need to authenticate before creating a session. A session has an ID and an absolute working directory; prompts and updates refer to that ID. Agents support text and resource links as baseline prompt content. Images, audio, and embedded resources depend on the agent's advertised prompt capabilities.
+
+In v1, `prompt()` waits for the turn's response. Text and tool activity arrive separately through `session_update()`. Register callbacks before opening a session, and use the update's type to decide how to display it. The final `PromptResponse` reports why the turn stopped; it does not contain the streamed answer text.
+
+A session can contain several prompt turns. Keeping a connection open does not automatically provide conversation storage or support for loading a previous session. Implement those behaviors in the agent and advertise the corresponding capabilities.
+
+Updates can also arrive outside a prompt turn, for example when the agent advertises commands or replays a loaded session. Install update handlers before opening sessions and allow the first update for a session ID to create its state entry. See [prompt turns](https://agentclientprotocol.com/protocol/v1/prompt-turn) and [session setup](https://agentclientprotocol.com/protocol/v1/session-setup).
+
+## Requests can travel in both directions
+
+While processing a prompt, an agent can ask the client to read a file, run a terminal command, or request user approval. The client advertises the filesystem and terminal services it implements. Tool call notifications describe activity; permission requests ask for a decision. Those are separate messages.
+
+The client and agent each need handlers for the methods their peer will call. Implement every capability your application advertises. The [API reference](../reference/api.md) gives the signatures, and the [permission guide](../how-to/permissions.md) shows how to interpret a decision.
+
+## Cancel a turn
+
+`await connection.cancel(session_id=session_id)` sends `session/cancel`, a notification with no response. Your agent's `cancel` handler stops the session's active work. The original `prompt` request still completes with `stop_reason="cancelled"`, after cleanup and any final updates. The client continues accepting updates until that response and resolves pending permission requests with a cancellation outcome. This differs from [cancelling an individual JSON-RPC request](../reference/protocol.md#request-cancellation).
+
+## Process and transport lifetime
+
+For stdio, the client normally starts an agent process and connects its stdin/stdout pipes. Agent diagnostics go to stderr. `spawn_agent_process` manages the connection and child-process shutdown inside an async context manager.
+
+HTTP and WebSocket move protocol messages across a network transport while retaining the v1 connection API. Their [transport architecture](transports.md) explains how routing differs from stdio.
+
+The [experimental v2 runtime](../reference/experimental/protocol-v2.md) has a different prompt contract: a response acknowledges the user message rather than waiting for processing to finish. Use its own models and handlers; the v1 flow above does not describe v2 completion.
diff --git a/docs/explanation/transports.md b/docs/explanation/transports.md
new file mode 100644
index 00000000..13ceb3d9
--- /dev/null
+++ b/docs/explanation/transports.md
@@ -0,0 +1,49 @@
+# Transport architecture
+
+Stdio uses byte streams between a client and an agent process. The experimental
+HTTP and WebSocket adapters expose message transports to the same ACP connection
+layer. See the [web transport guide](../reference/experimental/web-transports.md) for setup and APIs.
+
+## HTTP and WebSocket
+
+Start reading at `acp/http/asgi.py`. It creates Starlette routes, passes parsed
+HTTP requests to `AcpServer`, and binds WebSockets in `acp/ws/server.py`. Both use
+`AgentSideConnection` and its message-level `Transport` interface:
+
+```text
+HTTP POST → _HttpTransport incoming queue → AgentSideConnection → agent
+HTTP GET ← StreamingResponse ← SSE buffer ← _HttpTransport.send() ← agent output
+
+Starlette WebSocket ↔ _WebSocketTransport ↔ AgentSideConnection ↔ agent
+```
+
+For HTTP, `AcpServer` owns a dictionary of active connections. Each connection
+has one incoming queue and one SSE buffer per stream. The incoming queue lets
+POST return `202` while the agent handles the request. Output goes directly to
+the relevant SSE buffer.
+
+HTTP output follows these routing rules:
+
+| Message | Destination | Why |
+| --- | --- | --- |
+| `initialize` response | POST body, via one Future | Establishes the connection before GET streams open |
+| Response containing a new `sessionId` | Connection SSE stream | The client needs the ID before it can open the session stream |
+| `session/load` replay and response, `session/resume` response | Connection SSE stream | Replay precedes the response; the client gets the session ID from the original request |
+| Other messages | Session SSE stream when known, otherwise connection stream | Responses use their request's recorded session; requests/notifications carry `sessionId` |
+
+`OutboundStream` retains a bounded buffer, backpressure, and close handling.
+Idle SSE streams emit keepalives. These support slow readers, streams that open
+after messages arrive, and orderly teardown. `DELETE` and server shutdown close
+the HTTP connections and cancel their agent work.
+
+The transport retains connection and stream state, not conversation history.
+Reopening a transport creates a new connection; loading or resuming a session
+depends on the agent's storage and advertised capabilities. A dropped
+connection-scoped SSE stream ends the client's receive loop and rejects pending
+requests. A session stream ending does not close the whole connection. The SDK
+does not retry either stream automatically.
+
+WebSocket already provides one bidirectional stream. Its transport adapts
+Starlette's socket to JSON-RPC messages; it needs no HTTP connection registry,
+session routing, SSE buffers, or multiplex mode. The ASGI handler owns the agent
+connection and closes it on socket disconnect or handler cancellation.
diff --git a/docs/how-to/permissions.md b/docs/how-to/permissions.md
new file mode 100644
index 00000000..c703c4be
--- /dev/null
+++ b/docs/how-to/permissions.md
@@ -0,0 +1,85 @@
+# Handle permissions
+
+An agent requests permission for a tool call through `request_permission`. The client displays the supplied options and returns the option the user selected, or a cancellation outcome.
+
+## Return the user's choice
+
+Implement this method on your `Client`. `choose_option` represents your application's UI and returns an option ID or `None`:
+
+```python
+from typing import Any
+
+from acp.schema import (
+ AllowedOutcome,
+ DeniedOutcome,
+ PermissionOption,
+ RequestPermissionResponse,
+ ToolCallUpdate,
+)
+
+
+async def request_permission(
+ self,
+ session_id: str,
+ tool_call: ToolCallUpdate,
+ options: list[PermissionOption],
+ **kwargs: Any,
+) -> RequestPermissionResponse:
+ option_id = await choose_option(tool_call, options)
+ if option_id is None:
+ return RequestPermissionResponse(outcome=DeniedOutcome())
+ if option_id not in {option.option_id for option in options}:
+ raise ValueError("Choose an option from the permission request")
+ return RequestPermissionResponse(outcome=AllowedOutcome(option_id=option_id))
+```
+
+Despite its Python name, `AllowedOutcome` means the user selected an option. That option can be a rejection. Read the option's `kind` to distinguish `allow_once` and `allow_always` from `reject_once` and `reject_always`. `DeniedOutcome` represents cancellation.
+
+When the client cancels a prompt turn with `session/cancel`, resolve every pending permission request for that turn with `DeniedOutcome()`. A dismissed dialog also returns that outcome. The agent must not execute the operation after a cancellation or a rejection. See the protocol's [permission outcomes](https://agentclientprotocol.com/protocol/v1/tool-calls#requesting-permission).
+
+## Request approval from the agent
+
+The experimental contrib helpers associate a permission request with a tracked tool call. Pass your session's tracker to this function. It publishes the pending call and returns whether the user approved it; the caller performs the operation only if it returns `True`:
+
+```python
+from acp import Client
+from acp.contrib.permissions import PermissionBroker, default_permission_options
+from acp.contrib.tool_calls import ToolCallTracker
+from acp.schema import AllowedOutcome, RequestPermissionRequest, RequestPermissionResponse
+
+
+async def request_tool_permission(
+ client: Client, session_id: str, tracker: ToolCallTracker
+) -> bool:
+ start = tracker.start("write-file", title="Write a file", kind="edit", status="pending")
+ await client.session_update(session_id=session_id, update=start)
+
+ async def request_permission(
+ request: RequestPermissionRequest,
+ ) -> RequestPermissionResponse:
+ return await client.request_permission(
+ session_id=request.session_id,
+ tool_call=request.tool_call,
+ options=request.options,
+ )
+
+ options = default_permission_options()
+ broker = PermissionBroker(session_id, request_permission, tracker=tracker)
+ response = await broker.request_for("write-file", options=options)
+ selected = response.outcome
+ option = next(
+ (item for item in options if isinstance(selected, AllowedOutcome)
+ and item.option_id == selected.option_id),
+ None,
+ )
+ approved = option is not None and option.kind in {"allow_once", "allow_always"}
+ progress = tracker.progress(
+ "write-file", status="in_progress" if approved else "failed"
+ )
+ await client.session_update(session_id=session_id, update=progress)
+ return approved
+```
+
+After an approved operation finishes, send a tool call update with its final status and result. Keep the tracker with your session if you need to update the same tool call later. Your application defines what a persistent approval covers; `PermissionBroker` does not enforce an approval policy or execute tools.
+
+The broker supplies its defaults when you omit `options` or pass `None`. An empty list raises `MissingPermissionOptionsError` before the request is sent. See [contrib reference](../reference/experimental/contrib.md) for the helper contracts and [the Gemini client](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/gemini.py) for a terminal UI that displays the supplied options.
diff --git a/docs/how-to/session-state.md b/docs/how-to/session-state.md
new file mode 100644
index 00000000..e7174e13
--- /dev/null
+++ b/docs/how-to/session-state.md
@@ -0,0 +1,36 @@
+# Accumulate session state
+
+Use the experimental `SessionAccumulator` to merge tool call updates and keep message chunks for a UI. Create one accumulator per session so interleaved events from different sessions stay separate.
+
+```python
+from typing import Any
+
+from acp import Client
+from acp.contrib.session_state import SessionAccumulator
+from acp.schema import SessionNotification
+
+
+class StatefulClient(Client):
+ def __init__(self) -> None:
+ self.sessions: dict[str, SessionAccumulator] = {}
+
+ async def session_update(
+ self, session_id: str, update: Any, **kwargs: Any
+ ) -> None:
+ if session_id not in self.sessions:
+ self.sessions[session_id] = SessionAccumulator(
+ auto_reset_on_session_change=False
+ )
+ snapshot = self.sessions[session_id].apply(
+ SessionNotification(session_id=session_id, update=update)
+ )
+ print(snapshot.session_id, len(snapshot.agent_messages))
+```
+
+The accumulator tracks tool call starts and progress, full `AgentPlanUpdate` entries, the current mode, available commands, and user/agent/thought message chunks. A progress event without a start creates a provisional tool call entry. A later start can replace its fields; the helper does not reconstruct event chronology.
+
+It does not retain every update type or field. Handle session metadata, usage, config options, notices, compaction, incremental plan events, and tool names separately if your UI uses them. Message collections contain chunks; group by `message_id` when present and concatenate text chunks when rendering a message.
+
+`apply()` returns a snapshot; `snapshot()` reads the current one after at least one notification. The helper copies model data and freezes the outer snapshot model, but its dictionaries and nested objects remain mutable. Treat snapshots as read-only in your application.
+
+`subscribe(callback)` invokes a synchronous callback after each update and returns a function that unsubscribes it. Remove the session's accumulator when your application no longer needs it. See the [contrib reference](../reference/experimental/contrib.md) for reset behavior and related tool call helpers.
diff --git a/docs/index.md b/docs/index.md
index ba1c710b..ad99eacf 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,64 +1,41 @@
-
- ]
-
+# ACP Python SDK
-# Agent Client Protocol SDK (Python)
+Use this SDK to implement an ACP agent or client in Python, or to add ACP to an existing application. The SDK handles protocol models, JSON-RPC communication, and connection lifetime. Your code supplies the agent runtime, client callbacks, and session storage.
-Ship ACP-compatible agents and clients in Python without rebuilding JSON-RPC transports or schema models. This SDK mirrors each ACP release so your integrations stay interoperable with editors, CLIs, and hosted clients.
+These docs follow `main`. For a released SDK, use the examples and source at its [release tag](https://github.com/agentclientprotocol/python-sdk/releases). Features on `main` can be newer than the default PyPI installation.
-## Install & verify
+## Start with a working exchange
+
+The [quickstart](tutorials/quickstart.md) runs a local echo agent and client, then shows the Python calls that initialize the connection, create a session, and send a prompt. It requires Python, Git, and uv. You can complete it without an editor or a model account.
+
+To add the SDK to your own project:
```bash
pip install agent-client-protocol
-# or
+# or:
uv add agent-client-protocol
```
-Next steps live in the [Quickstart](quickstart.md): launch the echo agent, wire it to Zed (or another ACP client), and exercise the programmatic spawn helpers.
-
-## ACP at a glance
-
-- ACP is the stdio protocol that lets “clients” (editors, shells, CLIs) orchestrate AI “agents.”
-- Sessions exchange structured payloads (`session/update`, permission prompts, tool calls) defined in the upstream schema.
-- Matching the schema version keeps your Python integrations compatible with tools such as Zed, Gemini CLI, or kimi-cli.
-
-## SDK building blocks
-
-- `acp.schema`: generated Pydantic models with defaults for protocol discriminator fields.
-- `acp.agent` / `acp.client`: async base classes, JSON-RPC supervision, and lifecycle orchestration.
-- `acp.contrib`: experimental utilities (session accumulators, permission brokers, tool call trackers) harvested from production deployments.
-- `examples/`: runnable agents, clients, duet demos, and the Gemini CLI bridge.
-
-## Quick links
+## Choose your next task
-| Need | Link |
+| You want to | Read |
| --- | --- |
-| Quickstart walkthrough | [quickstart.md](quickstart.md) |
-| Real-world adopters | [use-cases.md](use-cases.md) |
-| Contrib helpers | [contrib.md](contrib.md) |
-| Releasing workflow | [releasing.md](releasing.md) |
-| Upgrade to 0.11 | [migration-guide-0.11.md](migration-guide-0.11.md) |
-| Example scripts | [github.com/agentclientprotocol/python-sdk/tree/main/examples](https://github.com/agentclientprotocol/python-sdk/tree/main/examples) |
+| Expose your agent through ACP | [Build an agent](tutorials/agent.md) |
+| Implement client callbacks and manage a connection | [Build a client](tutorials/client.md) |
+| Request and handle approval for a tool call | [Handle permissions](how-to/permissions.md) |
+| Merge updates into a UI's session state | [Accumulate session state](how-to/session-state.md) |
+| Map an existing runtime to ACP handlers | [Integration patterns](integrations/index.md) |
-## Choose a path
+## Understand the API
-- **Just exploring?** Skim [use-cases.md](use-cases.md) to see how kimi-cli, agent-client-kernel, and others use the SDK.
-- **Building agents?** Copy `examples/echo_agent.py` or `examples/agent.py`, then use the generated models directly. Use `acp.contrib` for permission workflows and other stateful patterns.
-- **Embedding clients?** Start with `examples/client.py` or the `spawn_agent_process` / `spawn_client_process` helpers in the [Quickstart](quickstart.md#programmatic-launch).
+Start with [roles and session lifecycle](explanation/lifecycle.md) to understand the flow of requests, updates, and prompt results. The [core API reference](reference/api.md) lists handler signatures, connection helpers, and commonly used models. [Protocol features and cancellation](reference/protocol.md) covers optional methods and capability checks.
-## Reference material
+## Experimental features
-- [Quickstart](quickstart.md) — installation, editor wiring, and programmatic launch walkthroughs.
-- [Use Cases](use-cases.md) — real adopters with succinct descriptions of what they build.
-- [Experimental Contrib](contrib.md) — deep dives on the `acp.contrib` utilities.
-- [Releasing](releasing.md) — schema upgrade process, versioning policy, and publishing checklist.
-- [0.11 Migration Guide](migration-guide-0.11.md) — interface signature updates, elicitation, and schema notes for 0.10 users.
+The default API uses protocol v1. [Protocol v2](reference/experimental/protocol-v2.md) has a separate runtime and different prompt semantics. [HTTP and WebSocket](reference/experimental/web-transports.md) are optional remote transports. [Contrib helpers](reference/experimental/contrib.md) provide tool call tracking and session state utilities. These APIs may change.
-Need API-level details? Browse the source in `src/acp/` or generate docs with `mkdocstrings`.
+## Maintain or upgrade an integration
-## Feedback & support
+The migration guides cover [0.11](migration/0.11.md), [0.8](migration/0.8.md), and [0.7](migration/0.7.md). Maintainers can find [release steps](maintainers/releasing.md), [documentation deployment](maintainers/documentation.md), and [protocol route maintenance](maintainers/protocol-routes.md) in the navigation.
-- Open issues or discussions on GitHub for bugs, feature requests, or integration help.
-- Join [GitHub Discussions](https://github.com/agentclientprotocol/python-sdk/discussions) to swap ideas.
-- Chat with the community on [agentclientprotocol.zulipchat.com](https://agentclientprotocol.zulipchat.com/).
-- Follow ACP roadmap updates at [agentclientprotocol.com](https://agentclientprotocol.com/).
+The [integration patterns](integrations/index.md) page links SDK calls to the application responsibilities in existing adapters. Report bugs in [GitHub Issues](https://github.com/agentclientprotocol/python-sdk/issues) or discuss integration questions in [GitHub Discussions](https://github.com/agentclientprotocol/python-sdk/discussions).
diff --git a/docs/integrations/index.md b/docs/integrations/index.md
new file mode 100644
index 00000000..9576b9d4
--- /dev/null
+++ b/docs/integrations/index.md
@@ -0,0 +1,30 @@
+# Integration patterns
+
+An integration maps an application's events, history, and user decisions to ACP handlers. The projects below use this SDK at that boundary. For a local stdio exchange, start with the [quickstart](../tutorials/quickstart.md) or [agent tutorial](../tutorials/agent.md).
+
+## Adapt an existing agent runtime: Bub
+
+The [Bub ACP adapter](https://github.com/bubbuild/bub-contrib/tree/5257e58fef85c735001995a6da0186c1b66403dc/packages/bub-acp-server) exposes Bub's runtime over ACP. Its [package metadata](https://github.com/bubbuild/bub-contrib/blob/5257e58fef85c735001995a6da0186c1b66403dc/packages/bub-acp-server/pyproject.toml) pins SDK `1.0.0rc2` and requires Python 3.12 or newer.
+
+| Integration responsibility | Implementation to read |
+| --- | --- |
+| Map initialization, sessions, prompts, and cancellation to the runtime | `BubACPAgent` in [agent.py](https://github.com/bubbuild/bub-contrib/blob/5257e58fef85c735001995a6da0186c1b66403dc/packages/bub-acp-server/src/bub_acp_server/agent.py) |
+| Convert model text and tool activity into `AgentMessageChunk`, `ToolCallStart`, and `ToolCallProgress` notifications | `ACPStreamRouter` in the same file |
+| Select client-backed file and terminal tools from advertised capabilities | `ACPClientToolRuntime` and `build_client_tools` in [client_tools.py](https://github.com/bubbuild/bub-contrib/blob/5257e58fef85c735001995a6da0186c1b66403dc/packages/bub-acp-server/src/bub_acp_server/client_tools.py) |
+| Retain session metadata and replay application history for `session/load` | Session methods in `BubACPAgent` |
+
+The adapter calls SDK `run_agent` to serve stdio. Bub owns execution, tapes, and configuration; the SDK carries protocol messages and dispatches client callbacks. The adapter's [end-to-end test](https://github.com/bubbuild/bub-contrib/blob/5257e58fef85c735001995a6da0186c1b66403dc/packages/bub-acp-server/tests/test_e2e.py) shows client-backed file and terminal operations.
+
+## Adapt a framework agent: PydanticAI
+
+PydanticAI's [experimental ACP adapter](https://pydantic.dev/docs/ai/harness/acp/) wraps a PydanticAI agent. [`PydanticAIACPAgent`](https://github.com/pydantic/pydantic-ai/blob/0a28fc079668fa03040f0a3024f3bf1f1b63e819/src/pydantic_ai_harness/pydantic_ai_harness/experimental/acp/_adapter.py) implements the SDK's `Agent` interface, maps run events to session updates, and uses `request_permission` for deferred tool approvals. Its [server entry point](https://github.com/pydantic/pydantic-ai/blob/0a28fc079668fa03040f0a3024f3bf1f1b63e819/src/pydantic_ai_harness/pydantic_ai_harness/experimental/acp/_server.py) passes that adapter to `acp.run_agent`.
+
+Session history and approval behavior stay with the adapter; protocol framing and callback dispatch come from the SDK. The [stdio test agent](https://github.com/pydantic/pydantic-ai/blob/0a28fc079668fa03040f0a3024f3bf1f1b63e819/tests/harness/experimental/acp/_demo_agent.py) uses PydanticAI's `TestModel` without a model service.
+
+The adapter is experimental. Its [ACP extra](https://github.com/pydantic/pydantic-ai/blob/0a28fc079668fa03040f0a3024f3bf1f1b63e819/src/pydantic_ai_harness/pyproject.toml) requires SDK `>=0.11,<0.12`; use that dependency range when running this version of the adapter.
+
+## Connect protocol events to notebook output
+
+[`agent-client-kernel`](https://github.com/wiki3-ai/agent-client-kernel/blob/fc10043bf78d3e16af0117417aaf783472a194ef/agent_client_kernel/kernel.py) is an SDK-based client. Its `ACPClientImpl` handles session updates and permission requests. The kernel starts an agent subprocess, passes its pipes to `connect_to_agent`, initializes the connection, and sends cell input as a prompt. Client callbacks translate agent updates into notebook output.
+
+The kernel owns the subprocess and notebook UI; the SDK dispatches updates and permission requests to its callbacks. Its [package metadata](https://github.com/wiki3-ai/agent-client-kernel/blob/fc10043bf78d3e16af0117417aaf783472a194ef/pyproject.toml) marks the project alpha and depends directly on the SDK Git repository. The [client tutorial](../tutorials/client.md) covers the same connection and callback structure in a standalone Python application.
diff --git a/docs/maintainers/documentation.md b/docs/maintainers/documentation.md
new file mode 100644
index 00000000..35374088
--- /dev/null
+++ b/docs/maintainers/documentation.md
@@ -0,0 +1,31 @@
+# Deploy documentation
+
+The site documents `main`. Documentation deployment uses `.github/workflows/docs.yml` and runs independently of package releases.
+
+## Build and preview locally
+
+```bash
+uv sync --frozen
+make docs-test
+make docs
+```
+
+`make docs-test` builds with MkDocs strict mode. `make docs` starts a local server at `http://127.0.0.1:8000/python-sdk/`. MkDocs renders the Markdown pages and API references from `src/acp/`. The `redirect_maps` setting in `mkdocs.yml` maps older page URLs to their current destinations and preserves URL fragments.
+
+## GitHub Pages publishing
+
+The repository publishes from the root of `gh-pages`. Settings → Pages uses **Deploy from a branch**, with `gh-pages` and `/ (root)` selected. The Documentation workflow builds with MkDocs strict mode and commits the generated site to that branch using `mkdocs gh-deploy`.
+
+The workflow explicitly requests a Pages build after pushing. GitHub notes that pushes made with `GITHUB_TOKEN` do not trigger a Pages build automatically; see [publishing-source troubleshooting](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#troubleshooting-publishing-from-a-branch). The build request uses the [Pages API](https://docs.github.com/en/rest/pages/pages#request-a-github-pages-build).
+
+The `github-pages` environment must allow the `gh-pages` branch used by GitHub's Pages deployment.
+
+## Publish and recover
+
+The Main workflow checks documentation on pull requests. The Documentation workflow runs when relevant files change on `main`, including documentation, source, examples, build configuration, and dependencies. It fetches the existing `gh-pages` history, builds strictly, and pushes a new site commit without a force push.
+
+Only the main-only publication job receives `contents: write` to update `gh-pages` and `pages: write` to request its build. PR checks use read-only permissions. The Pages concurrency group serializes publication runs without cancelling an in-progress run.
+
+To rebuild without another source commit, select **Documentation** in Actions and run it with `main` selected. Runs on other refs skip publication. This operation does not publish a Python package and does not require `PYPI_TOKEN` or a separate deployment token.
+
+The build request queues publication asynchronously. Check both the Documentation run and GitHub's Pages build/deployment run to see when the [site](https://agentclientprotocol.github.io/python-sdk/) is deployed.
diff --git a/docs/maintainers/protocol-routes.md b/docs/maintainers/protocol-routes.md
new file mode 100644
index 00000000..21cb3b2f
--- /dev/null
+++ b/docs/maintainers/protocol-routes.md
@@ -0,0 +1,43 @@
+# Maintain protocol routes
+
+The `Agent` and `Client` protocols in `src/acp/interfaces.py` are the source of
+truth for incoming routes. Declare routing metadata alongside each method's
+parameter model:
+
+```python
+from typing import Any
+
+from acp.meta import AGENT_METHODS
+from acp.schema import DeleteSessionRequest, DeleteSessionResponse
+from acp.utils import normalize_result, param_model
+
+
+class SessionHandlers:
+ @param_model(
+ DeleteSessionRequest,
+ method=AGENT_METHODS["session_delete"],
+ adapt_result=normalize_result,
+ )
+ async def delete_session(
+ self, session_id: str, **kwargs: Any
+ ) -> DeleteSessionResponse: ...
+```
+
+`build_agent_router` and `build_client_router` read these declarations through
+`MessageRouter.from_protocol`. New protocol declarations automatically become
+routes; implementation-only methods are not exposed. Use `kind="notification"`
+for notifications and `unstable=True` for methods requiring explicit opt-in.
+Optional handlers can declare `optional=True` and `default_result`.
+
+`param_model` takes exactly one type expression: a model, a union such as
+`ModelA | ModelB`, or `Annotated[ModelA | ModelB, Field(discriminator="type")]`.
+The router validates the original type with Pydantic's `TypeAdapter`, preserving
+`Annotated` validation metadata. Union handlers receive the fields common to all
+branches by default.
+
+`validate_params` and `adapt_params` provide custom validation and conversion
+when the wire representation differs from the Python signature, as with config
+options and elicitation. Legacy handlers still receive the validated request
+model. Signature generation expands single-model fields and preserves handwritten
+union signatures. Connection methods retain model-only decorators for signature generation
+and legacy calls; they do not repeat the routing options.
diff --git a/docs/maintainers/releasing.md b/docs/maintainers/releasing.md
new file mode 100644
index 00000000..a74d198f
--- /dev/null
+++ b/docs/maintainers/releasing.md
@@ -0,0 +1,49 @@
+# Release the SDK
+
+The package release workflow and the documentation workflow run independently. Publishing a GitHub Release builds and uploads a package to PyPI. Documentation changes on `main` build and deploy the site without a package release.
+
+## Prepare the release
+
+Choose the upstream schema ref for each protocol version you are updating. The pinned refs live in `schema/VERSION` for v1 and `schema/v2/VERSION` for experimental v2.
+
+For example, regenerate v1 bindings with a selected schema tag:
+
+```bash
+ACP_SCHEMA_VERSION=schema-v1.23.0 make gen-all
+```
+
+For v2, select the protocol explicitly:
+
+```bash
+uv run scripts/gen_all.py --protocol-version 2 --version schema-v2.0.0-alpha.5
+```
+
+These commands update the selected schema, generated models, method metadata, and handler signatures. Review their diff alongside the implementation and tests. To check generated schema and metadata against the cached inputs without rewriting files:
+
+```bash
+make gen-check
+```
+
+Choose a PEP 440 SDK version, update `pyproject.toml`, and run `uv lock` to keep the lockfile consistent. The SDK version and the upstream schema tag are separate identifiers.
+
+Run the release checks:
+
+```bash
+make check
+make test
+make docs-test
+```
+
+Update documentation and examples for public API changes. When the CLI and credentials are available, run the opt-in Gemini smoke test with `ACP_ENABLE_GEMINI_TESTS=1 uv run python -m pytest tests/test_gemini_example.py`. Set `ACP_GEMINI_BIN` to select its executable and `ACP_GEMINI_TEST_ARGS` for additional CLI arguments.
+
+## Review the changes
+
+Use a Conventional Commit prefix such as `feat:`, `fix:`, or `docs:`. Call out schema regenerations in the PR description, including the selected refs, changed behaviors, and validation results. Link migration instructions for breaking API changes.
+
+## Publish a GitHub Release
+
+Once the changes land on `main`, create a tag and GitHub Release using the exact package version, such as `1.0.0rc3`. Use prerelease status for an alpha, beta, or release candidate.
+
+Publishing the release triggers `.github/workflows/on-release-main.yml`. It checks out the release ref, substitutes the tag's version into `pyproject.toml`, builds the package with `uv build`, and uploads it with `uv publish` using the repository's `PYPI_TOKEN` secret.
+
+Inspect the `set-version` and `publish` jobs and confirm the package appears on PyPI. A package publication does not deploy the documentation site. For site setup, deployment, and recovery, see [Deploy documentation](documentation.md).
diff --git a/docs/migration-guide-0.11.md b/docs/migration/0.11.md
similarity index 93%
rename from docs/migration-guide-0.11.md
rename to docs/migration/0.11.md
index a34ab5b4..f6604512 100644
--- a/docs/migration-guide-0.11.md
+++ b/docs/migration/0.11.md
@@ -62,12 +62,14 @@ The generated `SetSessionModelRequest` and `SetSessionModelResponse` types are n
0.11 adds schema and connection support for the unstable `elicitation/create` request and `elicitation/complete` notification.
-Clients that advertise elicitation support should implement:
+Clients that advertise elicitation support implement the callbacks below. This
+handler returns a cancellation outcome. An accepting form handler returns
+content satisfying the request's `requested_schema`, including required fields:
```python
from typing import Any
-from acp import AcceptElicitationResponse, Client, CreateElicitationResponse, ElicitationMode
+from acp import CancelElicitationResponse, Client, CreateElicitationResponse, ElicitationMode
class MyClient(Client):
@@ -77,7 +79,7 @@ class MyClient(Client):
mode: ElicitationMode,
**kwargs: Any,
) -> CreateElicitationResponse:
- return AcceptElicitationResponse(action="accept", content={})
+ return CancelElicitationResponse(action="cancel")
async def complete_elicitation(self, elicitation_id: str, **kwargs: Any) -> None:
...
diff --git a/docs/migration-guide-0.7.md b/docs/migration/0.7.md
similarity index 77%
rename from docs/migration-guide-0.7.md
rename to docs/migration/0.7.md
index 63aa9063..63f6b42f 100644
--- a/docs/migration-guide-0.7.md
+++ b/docs/migration/0.7.md
@@ -17,8 +17,8 @@ PromptResponse(stop_reason="end_turn")
params.session_id
```
-- If you relied on `model_dump()` to emit camelCase keys automatically, switch to `model_dump(by_alias=True)` (or use helpers such as `text_block`, `start_tool_call`, etc.) so responses continue to match the protocol.
-- `field_meta` stays available for extension data. Any extra keys that were nested under `_meta` should now be provided via keyword arguments when constructing the schema models (see section 3).
+- For wire field names, use `model_dump(by_alias=True)`. The connection helpers serialize generated models for you.
+- Set `field_meta={...}` when constructing a schema model. Additional keyword arguments on connection methods become request `_meta`; handlers receive those metadata entries through `**kwargs` (see section 3).
## 2. `acp.run_agent` and `acp.connect_to_agent` replace manual connection wiring
@@ -55,8 +55,8 @@ conn = ClientSideConnection(lambda conn: MyClient(), proc.stdin, proc.stdout)
conn = connect_to_agent(MyClient(), proc.stdin, proc.stdout)
```
-- `spawn_agent_process` / `spawn_client_process` now accept concrete `Agent`/`Client` instances instead of factories that received the connection. Instantiate your implementation first and pass it in.
-- Importing the legacy connection classes via `acp.AgentSideConnection` / `acp.ClientSideConnection` issues a `DeprecationWarning` (see `src/acp/__init__.py:82-96`). Update your imports to `run_agent` and `connect_to_agent` to silence the warning.
+- In 0.7, `spawn_agent_process` / `spawn_client_process` accept concrete `Client`/`Agent` instances as well as connection factories. To use an instance, instantiate your implementation first and pass it in.
+- Importing the legacy connection classes via `acp.AgentSideConnection` / `acp.ClientSideConnection` issues a `DeprecationWarning` in 0.7. Update your imports to `run_agent` and `connect_to_agent` to silence the warning.
## 3. `Agent` and `Client` interface methods take explicit parameters
@@ -105,5 +105,4 @@ class RecordingClient(Client):
### Backward compatibility
-- The change should be 100% backward compatible as long as you update your method names and signatures. The `compatible_class` wrapper ensures that existing callers passing full request models continue to work. The old style API will remain functional before the next major release(1.0).
-- Because camelCase wrappers remain for now, you can migrate file-by-file while still running against ACP 0.7. Just watch for the new deprecation warnings in your logs/tests.
+- In 0.7, `compatible_class` retains camelCase wrappers and calls that pass a full request model, with deprecation warnings. Migrate those calls to the expanded snake-case signatures when upgrading.
diff --git a/docs/migration-guide-0.8.md b/docs/migration/0.8.md
similarity index 88%
rename from docs/migration-guide-0.8.md
rename to docs/migration/0.8.md
index efb1d611..16636959 100644
--- a/docs/migration-guide-0.8.md
+++ b/docs/migration/0.8.md
@@ -31,7 +31,3 @@ The default stdio reader limit is now 50MB to support multimodal payloads. If yo
```python
await run_agent(agent, stdio_buffer_limit_bytes=2 * 1024 * 1024)
```
-
-## 4. Documentation and quickstart updates
-
-Docs and settings examples have been refreshed for ACP 0.10.8. If you maintain internal onboarding material, sync it with the latest docs in `docs/`.
diff --git a/docs/quickstart.md b/docs/quickstart.md
deleted file mode 100644
index 14c43d9a..00000000
--- a/docs/quickstart.md
+++ /dev/null
@@ -1,305 +0,0 @@
-# Quickstart
-
-Spin up a working ACP agent/client loop in minutes. Keep this page beside the terminal and check off each section as you go. Want inspiration? Hop to the [Use Cases](use-cases.md) list to see how teams like kimi-cli or Zed apply the SDK in production.
-
-## Quick checklist
-
-| Goal | Command / Link |
-| ----------------------------------- | --------------------------------------------------------------------- |
-| Install the SDK | `pip install agent-client-protocol` or `uv add agent-client-protocol` |
-| Run the echo agent | `python examples/echo_agent.py` |
-| Point Zed (or another client) at it | Update `settings.json` as shown below |
-| Programmatically drive an agent | Copy the `spawn_agent_process` example |
-| Run tests before hacking further | `make check && make test` |
-
-## Before you begin
-
-- Python 3.10–3.14 with `pip` or `uv`
-- An ACP-capable client such as Zed (recommended for validation)
-- Optional: the Gemini CLI (`gemini --acp`; use `--experimental-acp` for older versions) for the bridge example
-
-## Step 1 — Install the SDK
-
-_Install the library from PyPI or add it to your uv workspace._
-
-```bash
-pip install agent-client-protocol
-# or
-uv add agent-client-protocol
-```
-
-## Step 2 — Launch the Echo agent
-
-_Run the provided streaming agent so clients have something to talk to._
-
-Start the ready-made echo example; it streams text blocks back to any ACP client. Leave it running in a terminal:
-
-```bash
-python examples/echo_agent.py
-```
-
-## Step 3 — Connect from an ACP-aware client
-
-_Point a client at the script and confirm you can exchange streamed updates._
-
-### Zed
-
-Add an Agent Server entry in `settings.json` (Zed → Settings → Agents panel):
-
-```json
-{
- "agent_servers": {
- "Echo Agent (Python)": {
- "type": "custom",
- "command": "/abs/path/to/python",
- "args": [
- "/abs/path/to/agentclientprotocol/python-sdk/examples/echo_agent.py"
- ]
- }
- }
-}
-```
-
-Or, if using `uv`:
-
-```json
-{
- "agent_servers": {
- "Echo Agent (Python)": {
- "type": "custom",
- "command": "uv",
- "args": [
- "run",
- "/abs/path/to/agentclientprotocol/python-sdk/examples/echo_agent.py"
- ]
- }
- }
-}
-```
-
-Open the Agents panel and start the session. Each message you send should be echoed back via streamed `session/update` notifications.
-
-### Other clients
-
-Any ACP client that communicates over stdio can spawn the same script; no additional transport configuration is required.
-
-### Programmatic launch
-
-Prefer to drive agents directly from Python? The `spawn_agent_process` helper wires stdio and lifecycle management for you:
-
-```python
-import asyncio
-import sys
-from pathlib import Path
-from typing import Any
-
-from acp import PROTOCOL_VERSION, spawn_agent_process
-from acp.interfaces import Client
-from acp.schema import TextContentBlock
-
-
-class SimpleClient(Client):
- async def request_permission(
- self, session_id, tool_call, options, **kwargs: Any
- ):
- return {"outcome": {"outcome": "cancelled"}}
-
- async def session_update(self, session_id, update, **kwargs):
- print("update:", session_id, update)
-
-
-async def main() -> None:
- script = Path("examples/echo_agent.py")
- async with spawn_agent_process(SimpleClient(), sys.executable, str(script)) as (conn, _proc):
- await conn.initialize(protocol_version=PROTOCOL_VERSION)
- session = await conn.new_session(cwd=str(script.parent), mcp_servers=[])
- await conn.prompt(
- session_id=session.session_id,
- prompt=[TextContentBlock(text="Hello from spawn!")],
- )
-
-asyncio.run(main())
-```
-
-`spawn_agent_process` manages the child process, wires its stdio into ACP framing, and closes everything when the block exits. The mirror helper `spawn_client_process` lets you drive an ACP client from Python as well.
-
-## Step 4 — Extend the agent
-
-_Swap the echo demo for your own `Agent` subclass._
-
-Create your own agent by subclassing `acp.Agent`. The pattern mirrors the echo example:
-
-```python
-from acp import Agent, PromptResponse
-
-
-class MyAgent(Agent):
- async def prompt(self, session_id, prompt, **kwargs) -> PromptResponse:
- # inspect prompt, stream updates, then finish the turn
- return PromptResponse(stop_reason="end_turn")
-```
-
-Run it with `run_agent()` inside an async entrypoint and wire it to your client. Refer to:
-
-- [`examples/echo_agent.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/echo_agent.py) for the smallest streaming agent
-- [`examples/agent.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/agent.py) for an implementation that negotiates capabilities and streams richer updates
-- [`examples/duet.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/duet.py) to see `spawn_agent_process` in action alongside the interactive client
-- [`examples/gemini.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/gemini.py) to drive the Gemini CLI (`--acp`; use `--experimental-acp` for older versions) directly from Python
-
-Generated models provide defaults for discriminator fields such as `type` and
-`sessionUpdate`, so they can be constructed directly:
-
-```python
-from acp.schema import AgentMessageChunk, TextContentBlock
-
-update = AgentMessageChunk(
- content=TextContentBlock(text="File opened."),
-)
-```
-
-## Session notices (unstable)
-
-The v1 bindings use `schema-v1.23.0`. Clients that can display live advisory
-notices advertise `ClientCapabilities(session=ClientSessionCapabilities(notices=NoticeCapabilities()))`
-in `initialize`; these models are available from `acp.schema`.
-
-After checking that capability, agents can send a notice:
-
-```python
-from acp.schema import SessionUpdateNotice
-
-await client.session_update(
- session_id=session_id,
- update=SessionUpdateNotice(
- severity="warning",
- title="Context is nearly full",
- description="Start a new session soon.",
- ),
-)
-```
-
-Notices are live events, outside session history. The title must be non-empty;
-severity accepts `info`, `warning`, `error`, and custom or future strings.
-Agents must only send notices to clients that advertise support.
-
-Tool calls now expose the stable `name` field. Terminal authentication methods
-are available as `TerminalAuthMethod`, replacing the incorrect `EnvVarAuthMethod`
-name. Accepted elicitation content validates scalar values and string lists;
-nested objects are not valid form values.
-
-## Additional v1 protocol methods
-
-The SDK exposes all agent and client methods in the bundled `schema-v1.23.0`
-method tables. `session/delete` and `logout` are stable and do not require
-`use_unstable_protocol=True`. The other methods in the table below are unstable;
-enable that flag on the receiving connection (or `run_agent`) to route them.
-Check the peer's advertised capabilities before calling these methods.
-
-| Wire method | Python method | Receiver |
-| --- | --- | --- |
-| `session/delete` | `delete_session(session_id=...)` | Agent |
-| `providers/list`, `providers/set`, `providers/disable` | `list_providers`, `set_provider`, `disable_provider` | Agent |
-| `logout` | `logout()` | Agent |
-| `mcp/connect`, `mcp/disconnect` | `connect_mcp`, `disconnect_mcp` | Client |
-| `mcp/message` (request) | `mcp_message` | Either peer |
-| `mcp/message` (notification) | `notify_mcp` | Either peer |
-| `nes/start`, `nes/suggest`, `nes/close` | `start_nes`, `suggest_nes`, `close_nes` | Agent |
-| `nes/accept`, `nes/reject` | `accept_nes`, `reject_nes` | Agent |
-| `document/didOpen`, `document/didChange`, `document/didClose`, `document/didSave`, `document/didFocus` | `did_open`, `did_change`, `did_close`, `did_save`, `did_focus` | Agent |
-
-For example, an agent that advertises session deletion implements:
-
-```python
-from acp import DeleteSessionResponse
-
-async def delete_session(self, session_id: str, **kwargs) -> DeleteSessionResponse:
- await self.session_store.delete(session_id)
- return DeleteSessionResponse()
-```
-
-A client then calls `await connection.delete_session(session_id=session_id)`.
-Deletion removes stored session data; `close_session` only closes the active
-session. The SDK dispatches these calls to your implementation; it does not
-provide session storage or provider management itself. Missing request handlers
-return the JSON-RPC method-not-found error.
-
-MCP requests return the inner JSON result unchanged, including `null`. Use
-`notify_mcp` for one-way MCP messages. Both APIs accept `connection_id`, `method`,
-and optional `params`. These methods share the same connections and routers
-across stdio, HTTP, and WebSocket transports.
-
-## Request cancellation
-
-Connections handle the protocol's `$/cancel_request` notification. When the
-peer cancels one of its requests, the SDK cancels the task running your handler.
-The handler may catch `asyncio.CancelledError` and return a (partial) result;
-otherwise the peer receives a `-32800` "Request cancelled" error. Cancellations
-for unknown or already finished requests are ignored; a cancelled request still
-gets exactly one response unless the connection is closed first.
-
-Cancelling the task that awaits an outgoing request (for example through
-`asyncio.wait_for`) still raises `CancelledError` locally without waiting for the
-peer, and additionally sends a best-effort `$/cancel_request` for that request.
-Cancellation support is optional for peers. Use `session/cancel` to stop a
-prompt turn.
-
-## Maintaining protocol routes
-
-The `Agent` and `Client` protocols in `src/acp/interfaces.py` are the source of
-truth for incoming routes. Declare routing metadata alongside each method's
-parameter model:
-
-```python
-@param_model(
- DeleteSessionRequest,
- method=AGENT_METHODS["session_delete"],
- adapt_result=normalize_result,
-)
-async def delete_session(self, session_id: str, **kwargs: Any) -> DeleteSessionResponse: ...
-```
-
-`build_agent_router` and `build_client_router` read these declarations through
-`MessageRouter.from_protocol`. New protocol declarations automatically become
-routes; implementation-only methods are not exposed. Use `kind="notification"`
-for notifications and `unstable=True` for methods requiring explicit opt-in.
-Optional handlers can declare `optional=True` and `default_result`.
-
-`param_model` takes exactly one type expression: a model, a union such as
-`ModelA | ModelB`, or `Annotated[ModelA | ModelB, Field(discriminator="type")]`.
-The router validates the original type with Pydantic's `TypeAdapter`, preserving
-`Annotated` validation metadata. Union handlers receive the fields common to all
-branches by default. The former `param_models(A, B, ...)` form is replaced by
-`param_model(A | B, ...)`.
-
-`validate_params` and `adapt_params` provide custom validation and conversion
-when the wire representation differs from the Python signature, as with config
-options and elicitation. Legacy handlers still receive the validated request
-model. Signature generation expands single-model fields and preserves handwritten
-union signatures. Connection methods retain model-only decorators for signature generation
-and legacy calls; they do not repeat the routing options.
-
-## Optional — Talk to the Gemini CLI
-
-_Have the Gemini CLI installed? Run the bridge to exercise permission flows._
-
-If you have the Gemini CLI installed and authenticated:
-
-```bash
-python examples/gemini.py --skip-trust --yolo # auto-approve permission prompts
-python examples/gemini.py --skip-trust --sandbox --model gemini-1.5-pro
-```
-
-Environment helpers:
-
-- `ACP_GEMINI_BIN` — override the CLI path (defaults to `PATH` lookup)
-- `ACP_GEMINI_TEST_ARGS` — extra flags forwarded during the smoke test
-- `ACP_ENABLE_GEMINI_TESTS=1` — opt-in toggle for `tests/test_gemini_example.py`
-
-Authentication hiccups (e.g. missing `GOOGLE_CLOUD_PROJECT`) are surfaced but treated as skips during testing so the suite stays green on machines without credentials.
-
-## Next steps
-
-- Compare what you built with the real integrations listed on the [Use Cases](use-cases.md) page.
-- Explore `docs/contrib.md` for higher-level utilities like session accumulators and permission brokers.
-- Run `make check` / `make test` before committing changes, and regenerate schema artifacts with `make gen-all` when ACP versions advance.
-- Need help? Start a thread in [GitHub Discussions](https://github.com/agentclientprotocol/python-sdk/discussions) or chat with other ACP developers at [agentclientprotocol.zulipchat.com](https://agentclientprotocol.zulipchat.com/).
diff --git a/docs/reference/api.md b/docs/reference/api.md
new file mode 100644
index 00000000..f8531872
--- /dev/null
+++ b/docs/reference/api.md
@@ -0,0 +1,70 @@
+# Core API
+
+Import the main entry points from `acp` and generated payload models from `acp.schema`. This reference covers the default protocol v1 runtime. For draft protocol v2, use [its separate API](experimental/protocol-v2.md).
+
+## Connection helpers
+
+Use `run_agent` to serve an agent over stdio. Use `spawn_agent_process` when a client owns the agent subprocess, or `connect_to_agent` when it already has byte streams or a message transport.
+
+For `run_agent`, `input_stream` is the writer for agent messages (stdout by default), and `output_stream` is the reader for client messages (stdin by default). Omit both for the standard stdio setup.
+
+::: acp.core.run_agent
+ options:
+ show_docstring_parameters: false
+
+::: acp.core.connect_to_agent
+
+::: acp.stdio.spawn_agent_process
+
+::: acp.stdio.spawn_client_process
+
+## Agent handlers
+
+`Agent` defines the callbacks implemented by your agent. The methods below cover the [agent tutorial](../tutorials/agent.md); additional capabilities require their corresponding handlers in `acp.interfaces.Agent`.
+
+::: acp.interfaces.Agent
+ options:
+ members: [on_connect, initialize, new_session, prompt, cancel]
+ show_if_no_docstring: true
+
+## Client handlers
+
+`Client` defines callbacks the agent can send to your application. Permission responses select a supplied option or cancel the request; updates are notifications. Implement filesystem and terminal methods before advertising those capabilities.
+
+::: acp.interfaces.Client
+ options:
+ members: [on_connect, session_update, request_permission]
+ show_if_no_docstring: true
+
+## Calls on the returned connection
+
+`spawn_agent_process` yields a connection and its process. `connect_to_agent` returns the same connection type. Use the helpers to create it; this section documents the methods you call after connecting.
+
+::: acp.client.connection.ClientSideConnection
+ options:
+ members: [initialize, new_session, prompt, cancel, close]
+ show_if_no_docstring: true
+
+## Common payload models
+
+Construct models with snake-case Python field names, such as `session_id` and `stop_reason`. Generated discriminator fields have defaults, so `TextContentBlock(text="Hello")` is enough to create a text block. Connection helpers handle wire serialization. If you serialize a model yourself, use `model_dump(by_alias=True)` for protocol field names.
+
+::: acp.schema.TextContentBlock
+ options:
+ members: [type, text]
+ inherited_members: [text]
+
+::: acp.schema.AgentMessageChunk
+ options:
+ members: [session_update, content]
+ inherited_members: [content]
+
+::: acp.schema.PromptResponse
+ options:
+ members: [stop_reason]
+
+::: acp.schema.RequestPermissionResponse
+ options:
+ members: [outcome]
+
+See [protocol features and cancellation](protocol.md) for optional methods, and [contrib reference](experimental/contrib.md) for session state and tool call utilities.
diff --git a/docs/reference/experimental/contrib.md b/docs/reference/experimental/contrib.md
new file mode 100644
index 00000000..cb922269
--- /dev/null
+++ b/docs/reference/experimental/contrib.md
@@ -0,0 +1,40 @@
+# Experimental contrib helpers
+
+`acp.contrib` provides optional helpers for session state, tool call tracking, and permission requests. These APIs are experimental and may change. For worked examples, see [Handle permissions](../../how-to/permissions.md) and [Accumulate session state](../../how-to/session-state.md).
+
+- `session_state.SessionAccumulator` merges supported updates into a session snapshot.
+- `tool_calls.ToolCallTracker` and `permissions.PermissionBroker` associate tool call updates with permission requests.
+
+## SessionAccumulator (`acp.contrib.session_state`)
+
+Use it when your UI needs to merge tool call updates and retain message chunks from `SessionNotification` events.
+
+**Key capabilities**
+
+- `SessionAccumulator.apply(notification)` applies `tool_call` and `tool_call_update` payloads. Progress without a start creates a provisional entry; a later start can replace its fields.
+- `snapshot()` returns a `SessionSnapshot` containing full plan updates, the current mode, commands, and message chunks. The outer model is frozen; nested values remain mutable.
+- `subscribe(callback)` lets you push snapshots into stores or UI components whenever state changes.
+- Automatic reset on session-change (toggle with `auto_reset_on_session_change`).
+
+Create one accumulator per session. It tracks tool call starts/progress, full `AgentPlanUpdate` events, current mode, available commands, and user/agent/thought chunks. Handle session metadata, config options, usage, notices, compaction, incremental plan events, and tool names separately if your UI needs them.
+
+## ToolCallTracker & PermissionBroker (`acp.contrib.tool_calls`, `acp.contrib.permissions`)
+
+Use them when your agent needs to associate streamed tool call updates and permission requests with the same ID.
+
+- `ToolCallTracker.start()/progress()/append_stream_text()` emits canonical `ToolCallStart` / `ToolCallProgress` updates and keeps an in-memory view via `view()` or `tool_call_model()`.
+- `PermissionBroker.request_for()` wraps `requestPermission` RPCs. It reuses tracker state (or a provided `ToolCall`), lets you append extra content, and defaults to Approve / Approve for session / Reject options.
+- Omit `options` or pass `None` to use broker defaults. An explicit empty option list raises `MissingPermissionOptionsError` before the permission request is sent; the same applies to empty `default_options` when no per-request options are supplied.
+- `default_permission_options()` exposes that canonical option triple if you need to customise it.
+
+Keep a tracker with the session. Its methods return models; your agent sends those models through `session_update`. Share the tracker with `PermissionBroker` to include the latest tool call state in a request.
+
+## API boundaries
+
+The contrib layer uses the generated models in `acp.schema`:
+
+- Schema data classes continue to live in `acp.schema`. Contrib helpers clone them with `.model_copy(deep=True)` before mutation.
+- The core runtime does not import contrib modules implicitly; your application opts in.
+- Your application enforces permission policy and performs tool operations. The broker requests a decision; it does not execute a tool.
+
+Report helper bugs or API feedback in [GitHub Issues](https://github.com/agentclientprotocol/python-sdk/issues).
diff --git a/docs/experimental-v2.md b/docs/reference/experimental/protocol-v2.md
similarity index 53%
rename from docs/experimental-v2.md
rename to docs/reference/experimental/protocol-v2.md
index e62ecb6d..a121ab56 100644
--- a/docs/experimental-v2.md
+++ b/docs/reference/experimental/protocol-v2.md
@@ -3,7 +3,7 @@
> **Experimental.** Protocol v2 is a draft. Import it from `acp.experimental` and
> expect its API and generated models to change with the upstream schema.
-The bindings use `schema-v2.0.0-alpha.5`.
+The bindings use [`schema-v2.0.0-alpha.5`](https://github.com/agentclientprotocol/agent-client-protocol/blob/schema-v2.0.0-alpha.5/schema/v2/schema.unstable.json). The official [v2 documentation](https://agentclientprotocol.com/protocol/v2/overview) describes the draft protocol; this page covers the bundled schema and Python runtime.
The v2 runtime is separate from the stable v1 API. Like v1, connection methods
and agent/client handlers accept expanded, snake-case parameters. Responses and
@@ -11,58 +11,68 @@ nested values (content blocks, updates, capabilities) use `v2.schema` models.
Extra keyword arguments carry request `_meta`.
Install update handlers before opening a session because updates are independent
-connection traffic:
+connection traffic. This function opens a session on a caller-provided message
+transport and returns the connection for further use. The caller closes it
+during teardown. `cwd` is an absolute path on the agent's host:
```python
from typing import Any
+
from acp.experimental import v2
+
class MyClient(v2.Client):
async def session_update(
self, session_id: str, update: Any, **kwargs: Any,
) -> None:
- handle_update(session_id, update)
-
-
-connection = v2.connect_to_agent(MyClient(), transport)
-initialized = await connection.initialize(
- protocol_version=v2.PROTOCOL_VERSION,
- info=v2.schema.Implementation(name="my-client", version="1.0.0"),
-)
-session = await connection.new_session(cwd="/workspace")
-accepted = await connection.prompt(
- session_id=session.session_id,
- prompt=[v2.schema.TextContentBlock(text="Hello")],
-)
-```
+ print(session_id, update)
-Implement an agent with the same expanded handler style:
-```python
-class MyAgent(v2.Agent):
- async def initialize(
- self,
- protocol_version: int,
- info: v2.schema.Implementation,
- capabilities: v2.schema.ClientCapabilities | None = None,
- **kwargs: Any,
- ) -> v2.schema.InitializeResponse:
- return v2.schema.InitializeResponse(
+async def open_session(
+ transport: Any, cwd: str,
+) -> tuple[v2.ClientSideConnection, str]:
+ connection = v2.connect_to_agent(MyClient(), transport)
+ try:
+ initialized = await connection.initialize(
protocol_version=v2.PROTOCOL_VERSION,
- info=v2.schema.Implementation(name="my-agent", version="1.0.0"),
+ info=v2.schema.Implementation(name="my-client", version="1.0.0"),
)
+ if (
+ initialized.capabilities is None
+ or initialized.capabilities.session is None
+ ):
+ raise RuntimeError("The agent does not support sessions")
+ session = await connection.new_session(cwd=cwd)
+ except BaseException:
+ await connection.close()
+ raise
+ return connection, session.session_id
+```
- async def new_session(
- self,
- cwd: str,
- additional_directories: list[str] | None = None,
- mcp_servers: list[Any] | None = None,
- **kwargs: Any,
- ) -> v2.schema.NewSessionResponse:
- return v2.schema.NewSessionResponse(session_id="session-1")
+The connection validates the returned protocol version and closes on
+initialization failure. Agents advertise session support with
+`AgentCapabilities(session=SessionCapabilities())`; an omitted or null `session`
+means unsupported. Agents supporting sessions implement the baseline methods,
+including list, resume, close, prompt, cancellation, and updates.
+Agent handlers use expanded parameters too. This initialization handler belongs
+to an agent that implements those session methods:
-await v2.run_agent(MyAgent())
+```python
+async def initialize(
+ self,
+ protocol_version: int,
+ info: v2.schema.Implementation,
+ capabilities: v2.schema.ClientCapabilities | None = None,
+ **kwargs: Any,
+) -> v2.schema.InitializeResponse:
+ return v2.schema.InitializeResponse(
+ protocol_version=v2.PROTOCOL_VERSION,
+ info=v2.schema.Implementation(name="my-agent", version="1.0.0"),
+ capabilities=v2.schema.AgentCapabilities(
+ session=v2.schema.SessionCapabilities()
+ ),
+ )
```
`v2.Agent` and `v2.Client` describe the v2 handler signatures; subclassing is
@@ -70,23 +80,24 @@ optional. Implement only the methods you support. Unimplemented requests return
method-not-found, and unimplemented notifications are ignored. The v2 protocols
are separate from v1 because initialization, prompt responses, permissions, and
session updates have different contracts. Both versions use `param_model`
-metadata to derive their routes. V2 retains strict request/response validation
+metadata to derive their routes. V2 validates request and response models
and requires successful initialization before other traffic.
-Previously, v2 methods accepted a whole request model. Replace
-`connection.new_session(v2.schema.NewSessionRequest(cwd="/workspace"))` with
-`connection.new_session(cwd="/workspace")`, and expand handler parameters likewise.
+## Configuration and elicitation
-Union requests also use expanded parameters:
+Union requests use expanded parameters. These fragments assume an initialized
+client-side `connection`, an existing `session_id`, and an agent-side
+`agent_connection`. Set only options the agent exposes; request elicitation
+only for modes the client advertises:
```python
-await connection.set_config_option(config_id="thinking", session_id=session.session_id, value=True)
+await connection.set_config_option(config_id="thinking", session_id=session_id, value=True)
# type defaults to "boolean" for bool values and "id" otherwise.
await connection.set_config_option(
- config_id="vendor/limit", session_id=session.session_id, value=10, type="vendor/number",
+ config_id="vendor/limit", session_id=session_id, value=10, type="_vendor/number",
)
await agent_connection.create_elicitation(
- message="Sign in", mode="url", session_id=session.session_id,
+ message="Sign in", mode="url", session_id=session_id,
elicitation_id="sign-in-1", url="https://example.com/login",
)
```
@@ -96,6 +107,17 @@ selects request scope (including `None`). Pass `requested_schema` for form mode,
or `elicitation_id` and `url` for URL mode. Handlers receive the validated
branch's fields, including `type` for config options and `mode` for elicitation.
+## Prompt acceptance and completion
+
+With the connection and session ID from `open_session`, submit a prompt:
+
+```python
+accepted = await connection.prompt(
+ session_id=session_id,
+ prompt=[v2.schema.TextContentBlock(text="Hello")],
+)
+```
+
`session/prompt` returns after the agent inserts the user message into the ACP
conversation, without waiting for processing to finish. The response requires a
non-null `message_id`. Agents return `v2.schema.PromptResponse(message_id=...)`
@@ -104,12 +126,21 @@ the same ID. That update may arrive before or after the response; use
`accepted.message_id` to match it. Other session updates are independent traffic
and do not carry a prompt identifier.
+Foreground work is reported through `state_update`: `running`,
+`requires_action`, or `idle`. An `IdleSessionStateUpdate` indicates that the
+agent is ready for another prompt and may include `stop_reason`. Background
+activity may still emit updates while idle. Track these updates in the client;
+the SDK does not turn them into a completion awaitable. See the protocol's
+[prompt lifecycle](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle).
+
+## Notices, patches, and replay
+
Agents can send `v2.schema.SessionNotice(severity="warning", title="Context is nearly full")`
with `await agent_connection.session_update(session_id=session_id, update=notice)`. V2 notices require no client capability and
are live advisory events, outside retained session history. Clients may ignore
them. Titles must be non-empty, and severity also accepts custom or future strings.
-For patch fields in session updates, omit a field to leave its current
+For patch fields on tool calls and terminals, omit a field to leave its current
value unchanged, or explicitly pass `None` to clear it. For example,
`v2.schema.SessionToolCallUpdate(tool_call_id="tool-1", name=None)` clears the
tool name, while omitting `name` leaves it unchanged. This also applies to
@@ -121,7 +152,11 @@ requests all retained conversation history; agents need not retain every message
Accepted elicitation content validates scalar values and string lists; nested
objects are not valid form values.
-Agents that serve both versions use `AgentProtocolRouter`:
+## Version selection and extensions
+
+Agents that serve both versions use `AgentProtocolRouter`. In this fragment,
+`V1Agent` and `V2Agent` are your application's implementations, each accepting
+the selected connection in its constructor:
```python
from acp.experimental import AgentProtocolRouter
@@ -144,10 +179,10 @@ result = await connection.send_extension_request("_vendor/method", {"value": 1})
await connection.send_extension_notification("_vendor/event", {"value": 1})
```
-The selected runtime remains strict after initialization: v1 messages are not
-accepted by a v2 connection, and v2 messages are not translated into v1 calls.
-Only the initial v2 request is reduced to the common v1 initialization fields
-when an agent selects v1.
+The router selects a runtime from the first initialization request and retains
+it for the connection. When it selects v1 for a v2 initialization request, it
+maps the initialization fields to v1. Subsequent calls use the selected runtime's
+schema and handler contracts; the router does not translate between versions.
Client-side fallback is application controlled and may require opening a new
transport. Protocol-level request cancellation (`$/cancel_request`) is handled
diff --git a/docs/reference/experimental/web-transports.md b/docs/reference/experimental/web-transports.md
new file mode 100644
index 00000000..fbdcd185
--- /dev/null
+++ b/docs/reference/experimental/web-transports.md
@@ -0,0 +1,228 @@
+# Web Transport (Streamable HTTP & WebSocket)
+
+> **Experimental.** The remote web transports are experimental and may change.
+> They ship as an optional extra and are import-guarded.
+
+The SDK can run ACP over two remote connectivity profiles in addition to stdio:
+
+- **Streamable HTTP** — `POST` for client→server messages, long-lived `GET` SSE
+ streams for server→client messages (one connection-scoped stream plus one per
+ session), and `DELETE` to terminate. `initialize` returns `200 OK` with a JSON
+ body; accepted post-initialization messages return `202 Accepted`. Invalid
+ HTTP requests return an error status. The transport profile requires HTTP/2.
+- **WebSocket** — a `GET` upgrade on the same endpoint carrying full-duplex
+ JSON-RPC text frames, one message per frame. Binary frames are ignored, as
+ the transport RFD specifies; the server logs a warning on the first one.
+
+Both carry ACP JSON-RPC messages. The examples below use the v1 lifecycle
+(`initialize` → session methods → close). The
+[transport RFD](https://agentclientprotocol.com/rfds/streamable-http-websocket-transport)
+defines these profiles; the SDK's connection and routing behavior is described below.
+
+## Installation
+
+```bash
+pip install "agent-client-protocol[http]"
+```
+
+This pulls in `httpx2[http2]` (HTTP/2 + SSE consumption), `websockets`, and
+`starlette` (the server application). The core SDK and stdio transport do not
+require these optional dependencies.
+
+## Client
+
+Both transports produce a message transport for `connect_to_agent`. Complete
+the [quickstart](../../tutorials/quickstart.md#make-the-calls-from-python) first
+to create `hello_client.py`. Save this as `web_client.py` beside it:
+
+```python
+import asyncio
+import sys
+from pathlib import Path
+
+from acp import PROTOCOL_VERSION, connect_to_agent
+from acp.http import create_http_stream
+from acp.schema import TextContentBlock
+from acp.ws import create_websocket_stream
+
+from hello_client import EchoClient
+
+
+async def main(url: str, cwd: str) -> None:
+ if url.startswith(("ws://", "wss://")):
+ transport = await create_websocket_stream(url)
+ else:
+ transport = create_http_stream(url)
+ conn = connect_to_agent(EchoClient(), transport)
+ try:
+ initialized = await conn.initialize(protocol_version=PROTOCOL_VERSION)
+ if initialized.protocol_version != PROTOCOL_VERSION:
+ raise RuntimeError("The agent selected an unsupported protocol version")
+ session = await conn.new_session(cwd=cwd, mcp_servers=[])
+ response = await conn.prompt(
+ session_id=session.session_id,
+ prompt=[TextContentBlock(text="Hello ACP")],
+ )
+ print(f"Turn finished: {response.stop_reason}")
+ finally:
+ await conn.close()
+ await transport.close()
+
+
+if __name__ == "__main__":
+ asyncio.run(main(sys.argv[1], str(Path.cwd())))
+```
+
+Run it against the local server below with
+`uv run --frozen --extra http python web_client.py ws://localhost:8000/acp`.
+It prints `You said: Hello ACP` and `Turn finished: end_turn`. For a remote
+agent, pass its absolute workspace path as `cwd` instead of a local path.
+
+For Streamable HTTP, the client sends `initialize` first, reads the `Acp-Connection-Id` response
+header, then opens the connection-scoped SSE stream. When a new `sessionId`
+appears it opens that session-scoped stream too. A single SSE attempt is made per
+stream; reconnect/retry is the caller's responsibility. Losing the connection
+stream ends the connection's receive loop and rejects outstanding requests.
+
+### Loading an existing session
+
+Both HTTP and WebSocket support `load_session()` when the agent advertises
+`loadSession` and implements session persistence:
+
+Within the `try` block above, replace `new_session` with a load call after
+initialization. `saved_session_id` must identify a session retained by the
+agent, and `cwd` must match that session's working directory:
+
+```python
+if (
+ initialized.agent_capabilities is None
+ or not initialized.agent_capabilities.load_session
+):
+ raise RuntimeError("The agent does not support loading sessions")
+await conn.load_session(session_id=saved_session_id, cwd=cwd, mcp_servers=[])
+response = await conn.prompt(
+ session_id=saved_session_id,
+ prompt=[TextContentBlock(text="Continue")],
+)
+```
+
+For HTTP, history replay and the load response use the connection SSE stream.
+The client correlates the response with the session ID from the load request,
+then opens the session SSE stream for further prompts and agent callbacks. This
+also works with an empty history: a load response need not contain `sessionId`.
+Replay is consumed as it arrives, so histories larger than the SSE buffer do not
+wait for a session stream to open. WebSocket uses its existing bidirectional
+connection for both replay and subsequent messages.
+
+`resume_session()` (when the agent advertises `sessionCapabilities.resume`)
+follows the same HTTP routing without replaying history: the response uses the
+connection SSE stream, and the client then opens the session SSE stream.
+
+A failed load returns its JSON-RPC error on the connection stream and can be
+retried. The server removes streams provisioned only for failed loads, while
+preserving established sessions and overlapping loads. It does not change the
+agent's load response or automatically enable the agent's `loadSession` capability.
+
+## Server
+
+The server uses Starlette for HTTP requests, responses, routing, streaming,
+WebSocket handling, and application lifespan. Save the agent from
+[Build an agent](../../tutorials/agent.md) as `my_agent.py`, then create
+`web_server.py` beside it:
+
+```python
+from acp.http.asgi import create_asgi_app
+
+from my_agent import MyAgent
+
+# One agent instance is created per connection.
+app = create_asgi_app(lambda conn: MyAgent())
+```
+
+`app` is a `starlette.applications.Starlette` instance handling
+`POST`/`GET`/`DELETE` and WebSocket upgrades at `/acp` by default. Set the
+keyword-only `path` argument to use a different endpoint for both transports:
+
+```python
+app = create_asgi_app(lambda conn: MyAgent(), path="/rpc")
+```
+
+Other paths do not serve ACP. Starlette supplies
+`Request`, `JSONResponse`, `StreamingResponse`, and `WebSocket`; the SDK keeps
+ACP connection and session routing.
+
+### Mounting in another application
+
+Mount the app at the desired prefix. The parent must enter the child lifespan
+so HTTP connections are cleaned up during shutdown (mounted application
+lifespans are not run automatically):
+
+```python
+from contextlib import asynccontextmanager
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+acp_app = create_asgi_app(lambda conn: MyAgent())
+
+@asynccontextmanager
+async def lifespan(app):
+ async with acp_app.router.lifespan_context(acp_app):
+ yield
+
+app = Starlette(routes=[Mount("/agents", app=acp_app)], lifespan=lifespan)
+# Connect to /agents/acp using either HTTP or WebSocket.
+```
+
+With `path="/rpc"`, the mounted endpoint is `/agents/rpc`. Use `path="/"` to
+serve ACP at the mount root (`/agents/`).
+
+For the connection registry and stream routing, see [Transport architecture](../../explanation/transports.md).
+
+### Server API
+
+The experimental server API uses Starlette. `create_asgi_app(agent_factory, *, path="/acp")`
+returns a Starlette application, and `AcpServer.handle_post()` and `handle_delete()`
+return Starlette response objects. `open_stream()` yields SSE bytes and `close()`
+closes active connections.
+
+Direct WebSocket integrations use
+`handle_websocket(agent_factory, websocket)` with a Starlette `WebSocket`.
+The default route is `/acp`; pass `path="/"` explicitly if you need the mount root.
+
+### HTTP/2 server requirement
+
+The Streamable HTTP profile requires HTTP/2 for multiplexing POST requests and
+SSE streams. Serve HTTP/2 with Hypercorn and TLS, or terminate HTTP/2 at a proxy.
+The SDK enables HTTP/2 on its default client but does not reject HTTP/1.1
+responses. Plain local HTTP can therefore exercise the SDK without satisfying
+the profile's HTTP/2 requirement. WebSocket does not need an HTTP/2 server.
+
+Start the local WebSocket server with:
+
+```bash
+uv run --frozen --extra http --with hypercorn hypercorn web_server:app --bind localhost:8000
+```
+
+For HTTP/2 over TLS, supply `--certfile` and `--keyfile` for a certificate
+trusted by your client, then use an `https://` URL. See Hypercorn's
+[HTTP/2 guidance](https://hypercorn.readthedocs.io/en/latest/discussion/http2.html).
+
+## Identity model
+
+- `Acp-Connection-Id` (HTTP header) — returned by `initialize`; required on all
+ post-initialize HTTP requests and every GET stream.
+- `Acp-Session-Id` (HTTP header) — required on session-scoped POSTs and the
+ session-scoped GET stream.
+- `sessionId` (JSON-RPC field) — carried in params/results and used to route
+ messages to the correct stream.
+
+The connection and session headers identify routing state; they are not
+authentication credentials. Authentication and authorization belong to the
+application serving the endpoint.
+
+## Unsupported transport features
+
+- `Last-Event-ID` / SSE resumability and message sequencing.
+- Client-side automatic reconnect/backoff.
+- Batch JSON-RPC (the server returns `501`).
+- `Acp-Protocol-Version` header enforcement.
diff --git a/docs/reference/protocol.md b/docs/reference/protocol.md
new file mode 100644
index 00000000..16ea1695
--- /dev/null
+++ b/docs/reference/protocol.md
@@ -0,0 +1,103 @@
+# Protocol features and cancellation
+
+The default `acp` runtime uses protocol v1. This page documents the bundled
+[`schema-v1.23.0`](https://github.com/agentclientprotocol/agent-client-protocol/blob/schema-v1.23.0/schema/v1/schema.unstable.json) bindings on `main`. The official [v1 documentation](https://agentclientprotocol.com/protocol/v1/overview) describes the protocol. Check the peer's capabilities before using
+optional methods; schema support alone does not mean a peer implements them.
+For the separate draft runtime, see [experimental v2](experimental/protocol-v2.md).
+
+## Session notices (unstable)
+
+The v1 bindings use `schema-v1.23.0`. Clients that can display live advisory
+notices advertise `ClientCapabilities(session=ClientSessionCapabilities(notices=NoticeCapabilities()))`
+in `initialize`; these models are available from `acp.schema`.
+
+After checking that capability, agents can send a notice:
+
+```python
+from acp.schema import SessionUpdateNotice
+
+await client.session_update(
+ session_id=session_id,
+ update=SessionUpdateNotice(
+ severity="warning",
+ title="Context is nearly full",
+ description="Start a new session soon.",
+ ),
+)
+```
+
+Notices are live events, outside session history. The title must be non-empty;
+severity accepts `info`, `warning`, `error`, and custom or future strings.
+Custom severity values begin with `_`; other unknown values are reserved for
+future protocol variants. Agents send notices only to clients that advertise
+support, and cannot rely on them being displayed. Severity is a presentation
+hint: an `error` notice does not fail a request or end a turn. See the
+[session-notices contract](https://agentclientprotocol.com/rfds/session-notices).
+
+Tool calls include the stable `name` field. Terminal authentication methods use
+`TerminalAuthMethod`. Accepted elicitation content validates scalar values and
+string lists; nested objects are not valid form values.
+
+## Additional v1 protocol methods
+
+The SDK exposes all agent and client methods in the bundled `schema-v1.23.0`
+method tables. `session/delete` and `logout` are stable and do not require
+`use_unstable_protocol=True`. The other methods in the table below are unstable;
+enable that flag on the receiving connection (or `run_agent`) to route them.
+Check the peer's advertised capabilities before calling these methods.
+
+| Wire method | Python method | Receiver |
+| --- | --- | --- |
+| `session/delete` | `delete_session(session_id=...)` | Agent |
+| `providers/list`, `providers/set`, `providers/disable` | `list_providers`, `set_provider`, `disable_provider` | Agent |
+| `logout` | `logout()` | Agent |
+| `mcp/connect`, `mcp/disconnect` | `connect_mcp`, `disconnect_mcp` | Client |
+| `mcp/message` (request) | `mcp_message` | Either peer |
+| `mcp/message` (notification) | `notify_mcp` | Either peer |
+| `nes/start`, `nes/suggest`, `nes/close` | `start_nes`, `suggest_nes`, `close_nes` | Agent |
+| `nes/accept`, `nes/reject` | `accept_nes`, `reject_nes` | Agent |
+| `document/didOpen`, `document/didChange`, `document/didClose`, `document/didSave`, `document/didFocus` | `did_open`, `did_change`, `did_close`, `did_save`, `did_focus` | Agent |
+
+For example, an agent that advertises session deletion implements:
+
+```python
+from acp import DeleteSessionResponse
+
+async def delete_session(self, session_id: str, **kwargs) -> DeleteSessionResponse:
+ await self.session_store.delete(session_id)
+ return DeleteSessionResponse()
+```
+
+A client then calls `await connection.delete_session(session_id=session_id)`.
+Deletion removes stored session data; `close_session` only closes the active
+session. The SDK dispatches these calls to your implementation; it does not
+provide session storage or provider management itself. Implement each method
+you advertise; the interface declaration alone does not supply its behavior.
+For the session lifecycle contracts, see [session setup](https://agentclientprotocol.com/protocol/v1/session-setup)
+and [session deletion](https://agentclientprotocol.com/protocol/v1/session-delete).
+
+MCP requests return the inner JSON result unchanged, including `null`. Use
+`notify_mcp` for one-way MCP messages. Both APIs accept `connection_id`, `method`,
+and optional `params`. These methods share the same connections and routers
+across stdio, HTTP, and WebSocket transports.
+
+## Request cancellation
+
+Connections handle the protocol's `$/cancel_request` notification. When the
+peer cancels one of its requests, the SDK cancels the task running your handler.
+The handler may catch `asyncio.CancelledError` and return a (partial) result;
+otherwise the peer receives a `-32800` "Request cancelled" error. Cancellations
+for unknown or already finished requests are ignored; a cancelled request still
+gets exactly one response unless the connection is closed first.
+
+Cancelling the task that awaits an outgoing request raises `CancelledError`
+locally without waiting for the peer, and additionally sends a best-effort
+`$/cancel_request` for that request. When `asyncio.wait_for` times out, it cancels
+the awaiting task and raises `TimeoutError` to its caller.
+Cancellation support is optional for peers. See the protocol's
+[request-cancellation contract](https://agentclientprotocol.com/protocol/v1/cancellation).
+To stop a prompt turn, call `await connection.cancel(session_id=session_id)`.
+This sends `session/cancel` to your agent's handler; the agent stops its work
+and completes the original prompt with `stop_reason="cancelled"`. The client
+keeps handling updates until that response and returns cancellation outcomes
+for pending permission requests. See [turn cancellation](../explanation/lifecycle.md#cancel-a-turn).
diff --git a/docs/releasing.md b/docs/releasing.md
deleted file mode 100644
index 68fd50e6..00000000
--- a/docs/releasing.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Releasing
-
-Every package release tracks an upstream ACP schema tag from [`agentclientprotocol/agent-client-protocol`](https://github.com/agentclientprotocol/agent-client-protocol). Follow this checklist to stay in lockstep.
-
-## Prep checklist
-
-1. **Choose the schema tag** (e.g. `schema-v1.16.0`) and regenerate artifacts:
- ```bash
- ACP_SCHEMA_VERSION=schema-v1.16.0 make gen-all
- ```
- This refreshes `schema/` and the generated `src/acp/schema.py`.
-2. **Bump the SDK version** in `pyproject.toml` using a PEP 440 version string (for example `0.9.0a1` for an alpha release), and sync `uv.lock` if the lockfile is tracked.
-3. **Run the standard gates:**
- ```bash
- make check # Ruff format/lint, type analysis, dep hygiene
- make test # pytest + doctests
- ```
-4. **Refresh docs + examples** so user-facing flows (e.g. Gemini bridge) reflect behaviour in the new schema.
-
-## Commit & review
-
-- Keep the diff tight: regenerated schema files, version bumps, doc updates, and any required fixture refresh (goldens, RPC tests, etc.).
-- Use a Conventional Commit such as `release: 0.9.0a1`.
-- In the PR description, capture:
- - The ACP schema tag you targeted.
- - Output from `make check` / `make test` (and optional Gemini tests if you ran them).
- - Behavioural or API highlights that reviewers should focus on.
-
-## Publish via GitHub Release
-
-Releases are automated by `on-release-main.yml` once the PR lands on `main`.
-
-1. Draft a GitHub Release for the new tag (the UI creates the tag if missing).
- Use the exact package version as the tag, for example `0.9.0a1` or `0.9.0`.
-2. Publishing the release triggers the workflow, which:
- - Syncs the tag back into `pyproject.toml`.
- - Builds and uploads to PyPI via `uv publish` using `PYPI_TOKEN`.
- - Deploys updated docs with `mkdocs gh-deploy`.
-
-No local build/publish steps are needed—just provide a clear release summary (highlights, compatibility notes, migration tips).
-
-## Extra tips
-
-- Breaking schema bumps often mean updating `tests/test_golden.py`, `tests/test_rpc.py`, and any examples touched by new fields.
-- Use `make clean` if you need a fresh slate before re-running `make gen-all`.
-- When available, run the Gemini smoke test (`ACP_ENABLE_GEMINI_TESTS=1`, set `ACP_GEMINI_BIN`) to catch regressions early.
diff --git a/docs/tutorials/agent.md b/docs/tutorials/agent.md
new file mode 100644
index 00000000..abc961ff
--- /dev/null
+++ b/docs/tutorials/agent.md
@@ -0,0 +1,114 @@
+# Build an agent
+
+Write an agent that prefixes each text prompt with `You said:`. First complete the [quickstart](quickstart.md) so you have a repository checkout and a working client.
+
+## Implement the handlers
+
+Save this as `my_agent.py` in the repository root:
+
+```python
+import asyncio
+from typing import Any
+from uuid import uuid4
+
+from acp import (
+ PROTOCOL_VERSION,
+ Agent,
+ Client,
+ InitializeResponse,
+ NewSessionResponse,
+ PromptResponse,
+ run_agent,
+)
+from acp.exceptions import RequestError
+from acp.schema import AgentMessageChunk, ResourceContentBlock, TextContentBlock
+
+
+class MyAgent(Agent):
+ _client: Client
+
+ def __init__(self) -> None:
+ self._sessions: set[str] = set()
+ self._active: dict[str, asyncio.Event] = {}
+
+ def on_connect(self, conn: Client) -> None:
+ self._client = conn
+
+ async def initialize(
+ self, protocol_version: int, **kwargs: Any
+ ) -> InitializeResponse:
+ return InitializeResponse(protocol_version=PROTOCOL_VERSION)
+
+ async def new_session(self, cwd: str, **kwargs: Any) -> NewSessionResponse:
+ session_id = uuid4().hex
+ self._sessions.add(session_id)
+ return NewSessionResponse(session_id=session_id)
+
+ async def prompt(
+ self, session_id: str, prompt: list[Any], **kwargs: Any
+ ) -> PromptResponse:
+ if session_id not in self._sessions:
+ raise RequestError.invalid_params({"sessionId": session_id})
+ if session_id in self._active:
+ raise RequestError.invalid_request({"details": "A turn is already active"})
+
+ cancelled = asyncio.Event()
+ self._active[session_id] = cancelled
+ try:
+ for block in prompt:
+ if cancelled.is_set():
+ break
+ if isinstance(block, TextContentBlock):
+ text = f"You said: {block.text}"
+ elif isinstance(block, ResourceContentBlock):
+ text = f"You linked: {block.name} ({block.uri})"
+ else:
+ raise RequestError.invalid_params(
+ {"details": "Only text and resource links are supported"}
+ )
+ await self._client.session_update(
+ session_id=session_id,
+ update=AgentMessageChunk(content=TextContentBlock(text=text)),
+ )
+ return PromptResponse(
+ stop_reason="cancelled" if cancelled.is_set() else "end_turn"
+ )
+ finally:
+ self._active.pop(session_id, None)
+
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
+ if cancelled := self._active.get(session_id):
+ cancelled.set()
+
+
+if __name__ == "__main__":
+ asyncio.run(run_agent(MyAgent()))
+```
+
+`Agent` describes the handler interface. Subclassing it is optional; the SDK can use an object with the matching methods. The router validates prompt blocks into generated models before calling your handler. This example handles the protocol's baseline text and resource-link variants; it reports a link's name and URI without fetching the resource. Other content types require advertised prompt capabilities. The [API reference](../reference/api.md#agent-handlers) shows the complete content union.
+
+`on_connect` receives the connection used to send callbacks to the client. `initialize` reports the supported protocol version. `new_session` returns a session ID. During `prompt`, the agent sends text through `session_update`, then returns the turn's stop reason.
+
+`cancel` marks the session's active turn for cooperative cancellation. The prompt handler checks that flag and returns `stop_reason="cancelled"` after sending its remaining updates. The SDK dispatches the notification; your agent stops its own model and tool work. See the protocol's [prompt-turn contract](https://agentclientprotocol.com/protocol/v1/prompt-turn).
+
+## Run it through the client
+
+```bash
+uv run --frozen python examples/client.py python my_agent.py
+```
+
+Type `Hello ACP`:
+
+```text
+> Hello ACP
+| Agent: You said: Hello ACP
+>
+```
+
+The client owns the agent process. Keep agent logs on stderr because stdout carries ACP frames.
+
+## Add your own behavior
+
+Replace the text generation inside `prompt` with your agent's work. Send multiple text chunks as results become available. Implement the agent capabilities you advertise, such as supported prompt content and session persistence. Check the client's advertised filesystem and terminal capabilities before calling those services.
+
+This example tracks valid session IDs in memory but retains no conversation history and uses an empty MCP server list. A production runtime also handles the session's working directory and MCP server connections; the protocol requires stdio MCP support. Add session storage before advertising support for loading previous sessions. Long-running model and tool operations must observe cancellation and finish their cleanup before the prompt response. Use the [permission guide](../how-to/permissions.md) when an operation needs user approval. The [integration patterns](../integrations/index.md) show how Bub and PydanticAI adapters map an existing runtime's events and history to these handlers.
diff --git a/docs/tutorials/client.md b/docs/tutorials/client.md
new file mode 100644
index 00000000..c41920d6
--- /dev/null
+++ b/docs/tutorials/client.md
@@ -0,0 +1,59 @@
+# Build a client
+
+Implement `Client` callbacks in your application and use a connection helper to communicate with an agent. The client sends prompts and receives updates; it can also provide filesystem, terminal, and permission services that the agent calls.
+
+## Start from a complete exchange
+
+Follow the [quickstart](quickstart.md#make-the-calls-from-python) to create `hello_client.py` in the repository root. Run it from the checkout:
+
+```bash
+uv run --frozen python hello_client.py
+```
+
+Expected output:
+
+```text
+Hello ACP
+Turn finished: end_turn
+```
+
+That example uses the checked-out SDK, starts the repository's echo agent, and shuts down the child process when the context manager exits. It needs no external agent installation, model account, or credentials. Keep it as a smoke check while developing your callbacks.
+
+## Implement incoming callbacks
+
+The quickstart's `EchoClient` subclasses `Client` and implements two methods:
+
+| Callback | Your application's responsibility |
+| --- | --- |
+| `session_update(session_id, update, **kwargs)` | Route each event to the session's UI or state store; distinguish text, tool activity, and other update types |
+| `request_permission(session_id, tool_call, options, **kwargs)` | Present the supplied options and return the selected option ID or cancellation |
+
+Use generated models rather than hand-written response dictionaries. The [permission guide](../how-to/permissions.md) explains why selecting an option is not necessarily approval. The [session state guide](../how-to/session-state.md) shows how to keep interleaved sessions separate.
+
+Your callback signatures receive snake-case fields. Request `_meta` entries are expanded into keyword arguments; collect those your handler does not use in `**kwargs`. Read the [client handler reference](../reference/api.md#client-handlers) for the complete types.
+
+## Advertise the services you implement
+
+The minimal client does not advertise filesystem or terminal services. Add `client_capabilities` to its `initialize` call when those handlers exist in your application. Filesystem capabilities select `read_text_file` and `write_text_file`; `terminal=True` requires the terminal lifecycle handlers.
+
+Define how your application resolves workspace paths and enforces permissions before exposing those services. Protocol models validate message shape; they do not decide whether your application should allow an operation. Check the agent's advertised capabilities before calling optional methods such as loading a saved session.
+
+## Own the connection lifetime
+
+`spawn_agent_process` launches the agent and closes its connection and process when the async context exits. Use `connect_to_agent` when your application already owns byte streams or a message transport, and close the resulting connection during teardown.
+
+The spawn helper inherits a limited environment, including `PATH`. Pass additional variables required by the agent through `env=...`. Its stderr is piped by default; `transport_kwargs={"stderr": None}` exposes diagnostics in your terminal.
+
+Initialize the connection, create or load a session, and send prompts with that session ID. Response text arrives through updates; the v1 prompt response reports the stop reason. See [roles and session lifecycle](../explanation/lifecycle.md) for the flow, and [cancellation](../reference/protocol.md#request-cancellation) for stopping requests and prompt turns.
+
+## Existing client code
+
+[`examples/client.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/client.py) provides an interactive stdio client. Run it against the checked-out echo agent:
+
+```bash
+uv run --frozen python examples/client.py python examples/echo_agent.py
+```
+
+Type `Hello ACP`; it prints `| Agent: Hello ACP`. Its optional filesystem and terminal callbacks deliberately reject unsupported requests.
+
+[`examples/gemini.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/gemini.py) shows an interactive permission UI and filesystem callbacks. Its terminal methods are placeholders. Use it to study those implemented callbacks, rather than as a complete terminal service. The [integration patterns](../integrations/index.md) link another SDK client that connects protocol events to notebook output.
diff --git a/docs/tutorials/quickstart.md b/docs/tutorials/quickstart.md
new file mode 100644
index 00000000..5692d8eb
--- /dev/null
+++ b/docs/tutorials/quickstart.md
@@ -0,0 +1,136 @@
+# Run an agent and client
+
+Send a message from a Python client to an echo agent and receive a streamed reply. Both processes run locally over stdio. You need Python 3.10 through 3.14, Git, and uv.
+
+This tutorial uses the SDK in the repository checkout. It does not require an editor, an AI model, or an API key.
+
+## Get the examples
+
+```bash
+git clone https://github.com/agentclientprotocol/python-sdk.git
+cd python-sdk
+uv sync --frozen
+```
+
+Run the remaining commands from `python-sdk/`. The repository includes the examples and a lockfile for their dependencies.
+
+## Send your first message
+
+```bash
+uv run --frozen python examples/client.py python examples/echo_agent.py
+```
+
+The client starts the agent as a child process, initializes the connection, and creates a session. At the prompt, type `Hello ACP`:
+
+```text
+> Hello ACP
+| Agent: Hello ACP
+>
+```
+
+The `| Agent:` line comes from a `session/update` notification. After the agent finishes the turn, the client prompts for another message. Press Ctrl+D on Unix, or Ctrl+Z followed by Enter on Windows, to exit.
+
+Starting `echo_agent.py` by itself leaves it waiting for protocol messages on stdin. The client command above creates the pipes and sends those messages.
+
+
+
+## Make the calls from Python
+
+Save the following as `hello_client.py` in the repository root:
+
+```python
+import asyncio
+import sys
+from pathlib import Path
+from typing import Any
+
+from acp import PROTOCOL_VERSION, Client, spawn_agent_process
+from acp.schema import (
+ AgentMessageChunk,
+ DeniedOutcome,
+ PermissionOption,
+ RequestPermissionResponse,
+ TextContentBlock,
+ ToolCallUpdate,
+)
+
+
+class EchoClient(Client):
+ async def session_update(
+ self, session_id: str, update: Any, **kwargs: Any
+ ) -> None:
+ if isinstance(update, AgentMessageChunk) and isinstance(
+ update.content, TextContentBlock
+ ):
+ print(update.content.text)
+
+ async def request_permission(
+ self,
+ session_id: str,
+ tool_call: ToolCallUpdate,
+ options: list[PermissionOption],
+ **kwargs: Any,
+ ) -> RequestPermissionResponse:
+ return RequestPermissionResponse(outcome=DeniedOutcome())
+
+
+async def main() -> None:
+ root = Path(__file__).resolve().parent
+ agent_script = root / "examples" / "echo_agent.py"
+ async with spawn_agent_process(
+ EchoClient(), sys.executable, str(agent_script), cwd=root
+ ) as (connection, _process):
+ initialized = await connection.initialize(protocol_version=PROTOCOL_VERSION)
+ if initialized.protocol_version != PROTOCOL_VERSION:
+ raise RuntimeError("The agent selected an unsupported protocol version")
+ session = await connection.new_session(cwd=str(root), mcp_servers=[])
+ response = await connection.prompt(
+ session_id=session.session_id,
+ prompt=[TextContentBlock(text="Hello ACP")],
+ )
+ print(f"Turn finished: {response.stop_reason}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+Run it with the same environment as the agent:
+
+```bash
+uv run --frozen python hello_client.py
+```
+
+Expected output:
+
+```text
+Hello ACP
+Turn finished: end_turn
+```
+
+`initialize` negotiates the protocol version; this client continues only when the agent selects v1. `new_session` creates a session with an absolute working directory. `prompt` sends content to that session. The agent sends response text to `session_update` and then returns a `PromptResponse` with the turn's stop reason. See the protocol's [initialization contract](https://agentclientprotocol.com/protocol/v1/initialization).
+
+The callback accepts every update variant, but this example prints only agent text chunks. The echo agent does not request permissions; the permission handler returns a typed cancellation outcome if a request arrives. The `async with` block closes the connection and shuts down the child process when it exits. The [lifecycle explanation](../explanation/lifecycle.md) describes how these calls fit together.
+
+## If the example does not work
+
+| Symptom | Check |
+| --- | --- |
+| `examples/echo_agent.py` cannot be found | Use the cloned repository and save `hello_client.py` in its root |
+| `ModuleNotFoundError: acp` | Run `uv sync --frozen`, then use `uv run --frozen python` |
+| The agent waits without replying | Start it through the client; a standalone agent expects protocol messages on stdin |
+| JSON-RPC parsing errors after adding logging | Send agent logs to stderr; stdout carries protocol frames |
+
+## Extend the agent
+
+Read [`examples/echo_agent.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/echo_agent.py) for the text round trip. The [agent tutorial](agent.md) adds resource-link handling, session tracking, and turn cancellation to that structure.
+
+Follow [Build an agent](agent.md) to write your own handlers, or [Build a client](client.md) to implement callbacks and manage connections. For tool calls, see [Handle permissions](../how-to/permissions.md). The [integration patterns](../integrations/index.md) show how existing projects use the same SDK interfaces.
+
+## Protocol and maintenance topics
+
+- [Session notices](../reference/protocol.md#session-notices-unstable)
+- [Additional v1 methods](../reference/protocol.md#additional-v1-protocol-methods)
+- [Request cancellation](../reference/protocol.md#request-cancellation)
+- [Protocol route maintenance](../maintainers/protocol-routes.md)
+- [Existing client examples](client.md#existing-client-code)
diff --git a/docs/use-cases.md b/docs/use-cases.md
deleted file mode 100644
index dd7abd5d..00000000
--- a/docs/use-cases.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Use Cases
-
-This page mirrors the quick-read style of the README/index: skim the tables, copy links, and see how others apply the SDK. For the protocol overview itself, visit the official [agent](https://agentclientprotocol.com/overview/agents) and [client](https://agentclientprotocol.com/overview/clients) guides.
-
-## Agents
-
-| Project | What it showcases |
-| --- | --- |
-| [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) | A CLI-first ACP+MCP agent that helps with software dev and terminal workflows. Highlights streaming updates, permission prompts, and tool call UX. |
-| [MiniMax-AI/Mini-Agent](https://github.com/MiniMax-AI/Mini-Agent) | A minimal yet professional single agent demo project that showcases the core execution pipeline and production-grade features of agents. |
-| [OpenHands/OpenHands-CLI](https://github.com/OpenHands/OpenHands-CLI) | A lightweight, modern CLI to interact with the OpenHands agent. |
-| [evalstate/fast-agent](https://github.com/evalstate/fast-agent) | Define, Prompt and Test MCP enabled Agents and Workflows |
-
-## Clients
-
-| Project | What it showcases |
-| --- | --- |
-| [wiki3-ai/agent-client-kernel](https://github.com/wiki3-ai/agent-client-kernel) | A Jupyter kernel that speaks ACP so notebooks can chat with external agents. Great reference if you’re embedding ACP in notebook tooling. |
-| [OhadRubin/simple-acp-client](https://github.com/OhadRubin/simple-acp-client) | A Claude Agent SDK–style Python client that wraps ACP executables with a friendly API. Use it as a starting point for bespoke clients. |
-
-## Add your integration
-
-Shipping something with this SDK? Tell us!
-
-- Open an issue or PR with a short blurb and link.
-- Start a thread in [GitHub Discussions](https://github.com/agentclientprotocol/python-sdk/discussions).
-- Drop a note in [agentclientprotocol.zulipchat.com](https://agentclientprotocol.zulipchat.com/).
-
-We’ll keep this list current so newcomers can see what’s possible.
diff --git a/docs/web-transport.md b/docs/web-transport.md
deleted file mode 100644
index ce77eba4..00000000
--- a/docs/web-transport.md
+++ /dev/null
@@ -1,244 +0,0 @@
-# Web Transport (Streamable HTTP & WebSocket)
-
-> **Experimental.** The remote web transports are experimental and may change.
-> They ship as an optional extra and are import-guarded.
-
-The SDK can run ACP over two remote connectivity profiles in addition to stdio:
-
-- **Streamable HTTP** — `POST` for client→server messages, long-lived `GET` SSE
- streams for server→client messages (one connection-scoped stream plus one per
- session), and `DELETE` to terminate. `initialize` returns `200 OK` with a JSON
- body; all other POSTs return `202 Accepted`. **Requires HTTP/2.**
-- **WebSocket** — a `GET` upgrade on the same endpoint carrying full-duplex
- JSON-RPC text frames, one message per frame. Binary frames are ignored, as
- the transport RFD specifies; the server logs a warning on the first one.
-
-Both reuse the existing JSON-RPC message format and ACP lifecycle
-(`initialize` → session methods → close).
-
-## Installation
-
-```bash
-pip install "agent-client-protocol[http]"
-```
-
-This pulls in `httpx2[http2]` (HTTP/2 + SSE consumption), `websockets`, and
-`starlette` (the server application). The core SDK and stdio transport do not
-require these optional dependencies.
-
-## Client
-
-Both transports produce a message-level `Transport` that plugs into the existing
-`connect_to_agent`:
-
-```python
-from acp import connect_to_agent
-from acp.http import create_http_stream
-from acp.ws import create_websocket_stream
-
-# Streamable HTTP
-transport = create_http_stream("http://localhost:8000/acp")
-conn = connect_to_agent(my_client, transport)
-
-# ...or WebSocket
-transport = await create_websocket_stream("ws://localhost:8000/acp")
-conn = connect_to_agent(my_client, transport)
-
-init = await conn.initialize(protocol_version=1)
-session = await conn.new_session(cwd="/tmp", mcp_servers=[])
-await conn.prompt(session_id=session.session_id, prompt=[...])
-await conn.close()
-await transport.close()
-```
-
-The client sends `initialize` first, reads the `Acp-Connection-Id` response
-header, then opens the connection-scoped SSE stream. When a new `sessionId`
-appears it opens that session-scoped stream too. A single SSE attempt is made per
-stream; reconnect/retry is the caller's responsibility (v1 of the RFD).
-
-### Loading an existing session
-
-Both HTTP and WebSocket support `load_session()` when the agent advertises
-`loadSession` and implements session persistence:
-
-```python
-init = await conn.initialize(protocol_version=1)
-if init.agent_capabilities.load_session:
- await conn.load_session(session_id="saved-session-id", cwd="/workspace", mcp_servers=[])
- await conn.prompt(session_id="saved-session-id", prompt=[...])
-```
-
-For HTTP, history replay and the load response use the connection SSE stream.
-The client correlates the response with the session ID from the load request,
-then opens the session SSE stream for further prompts and agent callbacks. This
-also works with an empty history: a load response need not contain `sessionId`.
-Replay is consumed as it arrives, so histories larger than the SSE buffer do not
-wait for a session stream to open. WebSocket uses its existing bidirectional
-connection for both replay and subsequent messages.
-
-`resume_session()` (when the agent advertises `sessionCapabilities.resume`)
-follows the same HTTP routing without replaying history: the response uses the
-connection SSE stream, and the client then opens the session SSE stream.
-
-A failed load returns its JSON-RPC error on the connection stream and can be
-retried. The server removes streams provisioned only for failed loads, while
-preserving established sessions and overlapping loads. It does not change the
-agent's load response or automatically enable the agent's `loadSession` capability.
-
-## Server
-
-The server uses Starlette for HTTP requests, responses, routing, streaming,
-WebSocket handling, and application lifespan:
-
-```python
-from acp.http.asgi import create_asgi_app
-
-# One agent instance is created per connection.
-app = create_asgi_app(lambda conn: MyAgent())
-```
-
-`app` is a `starlette.applications.Starlette` instance handling
-`POST`/`GET`/`DELETE` and WebSocket upgrades at `/acp` by default. Set the
-keyword-only `path` argument to use a different endpoint for both transports:
-
-```python
-app = create_asgi_app(lambda conn: MyAgent(), path="/rpc")
-```
-
-Other paths do not serve ACP. Starlette supplies
-`Request`, `JSONResponse`, `StreamingResponse`, and `WebSocket`; the SDK keeps
-ACP connection and session routing.
-
-### Mounting in another application
-
-Mount the app at the desired prefix. The parent must enter the child lifespan
-so HTTP connections are cleaned up during shutdown (mounted application
-lifespans are not run automatically):
-
-```python
-from contextlib import asynccontextmanager
-from starlette.applications import Starlette
-from starlette.routing import Mount
-
-acp_app = create_asgi_app(lambda conn: MyAgent())
-
-@asynccontextmanager
-async def lifespan(app):
- async with acp_app.router.lifespan_context(acp_app):
- yield
-
-app = Starlette(routes=[Mount("/agents", app=acp_app)], lifespan=lifespan)
-# Connect to /agents/acp using either HTTP or WebSocket.
-```
-
-With `path="/rpc"`, the mounted endpoint is `/agents/rpc`. Use `path="/"` to
-serve ACP at the mount root (`/agents/`).
-
-### How the server fits together
-
-Start reading at `acp/http/asgi.py`. It creates Starlette routes, passes parsed
-HTTP requests to `AcpServer`, and binds WebSockets in `acp/ws/server.py`. Both use the existing
-`AgentSideConnection` and its message-level `Transport` interface:
-
-```text
-HTTP POST → _HttpTransport incoming queue → AgentSideConnection → agent
-HTTP GET ← StreamingResponse ← SSE buffer ← _HttpTransport.send() ← agent output
-
-Starlette WebSocket ↔ _WebSocketTransport ↔ AgentSideConnection ↔ agent
-```
-
-For HTTP, `AcpServer` owns a dictionary of active connections. Each connection
-has one incoming queue and one SSE buffer per stream. The incoming queue lets
-POST return `202` while the agent handles the request. Output goes directly to
-the relevant SSE buffer; there is no intermediate transport pair or pump task.
-
-HTTP output follows these routing rules:
-
-| Message | Destination | Why |
-| --- | --- | --- |
-| `initialize` response | POST body, via one Future | Establishes the connection before GET streams open |
-| Response containing a new `sessionId` | Connection SSE stream | The client needs the ID before it can open the session stream |
-| `session/load` replay and response, `session/resume` response | Connection SSE stream | Replay precedes the response; the client gets the session ID from the original request |
-| Other messages | Session SSE stream when known, otherwise connection stream | Responses use their request's recorded session; requests/notifications carry `sessionId` |
-
-`OutboundStream` retains a bounded buffer, backpressure, and close handling.
-Idle SSE streams emit keepalives. These support slow readers, streams that open
-after messages arrive, and orderly teardown. `DELETE` and server shutdown close
-the HTTP connections and cancel their agent work.
-
-WebSocket already provides one bidirectional stream. Its transport adapts
-Starlette's socket to JSON-RPC messages; it needs no HTTP connection registry,
-session routing, SSE buffers, or multiplex mode. The ASGI handler owns the agent
-connection and closes it on socket disconnect or handler cancellation.
-
-### Simplification experiment
-
-The original 80 HTTP, WebSocket, and RPC tests passed after each ablation.
-The Starlette migration also passes these behaviors; assertions now inspect
-Starlette response objects and WebSocket tests use the framework's socket:
-
-| Stage | Removed | Lines across the three server files |
-| --- | --- | ---: |
-| Baseline | — | 715 |
-| First ablation | `ConnectionRegistry`, WebSocket multiplex mode, WebSocket pump tasks, forwarding-only ASGI method | 647 |
-| Second ablation | HTTP memory transport pair and pump, `ConnectionState`, generic response-waiter map | 581 |
-| Starlette migration | Custom ASGI app, request/header parsing, response encoding, WebSocket state tracking, `PostResult` | 483 |
-
-This measures structural simplification and regression coverage, not throughput
-or latency. Additional tests cover interrupted initialization, closing a full
-SSE buffer, WebSocket cancellation/disconnect, invalid frames, and session routing
-of concurrent success/error responses.
-
-`create_asgi_app(agent_factory, *, path="/acp")` returns a Starlette application.
-The default route is now `/acp`, replacing the earlier catch-all route.
-`AcpServer.handle_post()` and `handle_delete()` return
-Starlette responses (`status_code`, byte `body`, and case-insensitive `headers`).
-`open_stream()` and `close()` keep their signatures.
-The experimental `AcpAsgiApp` and `PostResult` wrappers were removed, along with
-`ConnectionRegistry`, `ConnectionState`, `AcpServer.registry`, and
-`create_websocket_connection()`. Direct WebSocket integrations now use
-`handle_websocket(agent_factory, websocket)` with a Starlette `WebSocket`.
-WebSocket lifetimes belong to their ASGI handlers; `AcpServer.close()` manages
-HTTP connections.
-
-The migration adds checks for HTTP error statuses, unsupported methods, mounting,
-lifespan cleanup, and reopening an SSE stream after disconnect.
-
-### HTTP/2 server requirement
-
-> ⚠️ **Uvicorn does not serve HTTP/2.** For a spec-compliant Streamable HTTP
-> server, run an HTTP/2-capable ASGI server (**Hypercorn**, Daphne, Granian) or
-> terminate HTTP/2 at a proxy. The WebSocket profile works on Uvicorn.
-
-```python
-import asyncio
-import hypercorn.asyncio
-from hypercorn.config import Config
-
-config = Config()
-config.bind = ["localhost:8000"]
-config.alpn_protocols = ["h2", "http/1.1"]
-asyncio.run(hypercorn.asyncio.serve(app, config))
-```
-
-## Examples
-
-- [`examples/http_server.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/http_server.py) — serve an agent over HTTP + WS (Hypercorn).
-- [`examples/http_client.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/http_client.py) — connect over Streamable HTTP.
-- [`examples/ws_client.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/ws_client.py) — connect over WebSocket.
-
-## Identity model
-
-- `Acp-Connection-Id` (HTTP header) — returned by `initialize`; required on all
- post-initialize HTTP requests and every GET stream.
-- `Acp-Session-Id` (HTTP header) — required on session-scoped POSTs and the
- session-scoped GET stream.
-- `sessionId` (JSON-RPC field) — carried in params/results and used to route
- messages to the correct stream.
-
-## Not yet supported (deferred to a future revision)
-
-- `Last-Event-ID` / SSE resumability and message sequencing.
-- Client-side automatic reconnect/backoff.
-- Batch JSON-RPC (the server returns `501`).
-- `Acp-Protocol-Version` header enforcement.
diff --git a/mkdocs.yml b/mkdocs.yml
index 1938cf48..189426f6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,7 +1,7 @@
-site_name: Agent Client Protocol - Python SDK
+site_name: ACP Python SDK
repo_url: https://github.com/agentclientprotocol/python-sdk
site_url: https://agentclientprotocol.github.io/python-sdk
-site_description: A Python implement of Agent Client Protocol (ACP, by Zed Industries)
+site_description: Build agents and clients in Python with the Agent Client Protocol.
site_author: Chojan Shang
edit_uri: edit/main/docs/
repo_name: agentclientprotocol/python-sdk
@@ -9,25 +9,63 @@ copyright: Maintained by psiace.
nav:
- Home: index.md
- - Quick Start: quickstart.md
- - Use Cases: use-cases.md
- - Web Transport (HTTP/WS): web-transport.md
- - Experimental Protocol v2: experimental-v2.md
- - Experimental Contrib: contrib.md
- - Releasing: releasing.md
- - 0.11 Migration Guide: migration-guide-0.11.md
- - 0.7 Migration Guide: migration-guide-0.7.md
- - 0.8 Migration Guide: migration-guide-0.8.md
+ - Tutorials:
+ - Run an agent and client: tutorials/quickstart.md
+ - Build an agent: tutorials/agent.md
+ - Build a client: tutorials/client.md
+ - How-to guides:
+ - Handle permissions: how-to/permissions.md
+ - Accumulate session state: how-to/session-state.md
+ - Explanation:
+ - Roles and session lifecycle: explanation/lifecycle.md
+ - Transport architecture: explanation/transports.md
+ - Reference:
+ - Core API: reference/api.md
+ - Protocol features and cancellation: reference/protocol.md
+ - Experimental:
+ - Contrib helpers: reference/experimental/contrib.md
+ - HTTP and WebSocket: reference/experimental/web-transports.md
+ - Protocol v2: reference/experimental/protocol-v2.md
+ - Integration patterns: integrations/index.md
+ - Migration:
+ - Upgrade to 0.11: migration/0.11.md
+ - Upgrade to 0.8: migration/0.8.md
+ - Upgrade to 0.7: migration/0.7.md
+ - Maintainers:
+ - Release the SDK: maintainers/releasing.md
+ - Deploy documentation: maintainers/documentation.md
+ - Maintain protocol routes: maintainers/protocol-routes.md
plugins:
- search
+ - redirects:
+ redirect_maps:
+ quickstart.md: tutorials/quickstart.md
+ guides/build-agent.md: tutorials/agent.md
+ guides/connect-agent.md: tutorials/client.md
+ guides/permissions.md: how-to/permissions.md
+ guides/session-state.md: how-to/session-state.md
+ contrib.md: reference/experimental/contrib.md
+ experimental-v2.md: reference/experimental/protocol-v2.md
+ web-transport.md: reference/experimental/web-transports.md
+ use-cases.md: integrations/index.md
+ releasing.md: maintainers/releasing.md
+ migration-guide-0.11.md: migration/0.11.md
+ migration-guide-0.8.md: migration/0.8.md
+ migration-guide-0.7.md: migration/0.7.md
- mkdocstrings:
handlers:
python:
- paths: ["src/acp"]
+ paths: ["src"]
+ options:
+ show_source: false
+ show_root_heading: true
+ heading_level: 3
+ show_signature_annotations: true
theme:
name: material
- feature:
- tabs: true
+ features:
+ - navigation.sections
+ - content.code.copy
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
@@ -56,5 +94,9 @@ extra:
markdown_extensions:
- toc:
permalink: true
+ - admonition
+ - pymdownx.details
+ - pymdownx.highlight
+ - pymdownx.superfences
- pymdownx.arithmatex:
generic: true
diff --git a/pyproject.toml b/pyproject.toml
index 4438e7d8..05dfe8a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,6 +49,7 @@ dev = [
"websockets>=12.0",
"uvicorn>=0.30",
"starlette>=0.49.3",
+ "mkdocs-redirects==1.2.2",
]
[project.optional-dependencies]
diff --git a/uv.lock b/uv.lock
index 84de1f71..157a1310 100644
--- a/uv.lock
+++ b/uv.lock
@@ -34,6 +34,7 @@ dev = [
{ name = "httpx2", extra = ["http2"] },
{ name = "mkdocs" },
{ name = "mkdocs-material" },
+ { name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] },
{ name = "prek" },
{ name = "pytest" },
@@ -66,6 +67,7 @@ dev = [
{ name = "httpx2", extras = ["http2"], specifier = ">=2.12" },
{ name = "mkdocs", specifier = ">=1.4.2" },
{ name = "mkdocs-material", specifier = ">=8.5.10" },
+ { name = "mkdocs-redirects", specifier = "==1.2.2" },
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.26.1" },
{ name = "prek", specifier = ">=0.2.17" },
{ name = "pytest", specifier = ">=7.2.0" },
@@ -832,6 +834,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
]
+[[package]]
+name = "mkdocs-redirects"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" },
+]
+
[[package]]
name = "mkdocstrings"
version = "0.30.1"