langgraph-acp
Use ACP-compatible agents — Codex, Claude, and anything else that speaks the protocol — as first-class LangGraph nodes.
LangGraph owns orchestration and workflow durability.
ACP owns the agent conversation and agent-side session state.
langgraph-acp owns the binding between the two.
The design and the ticket sequence live in docs/langgraph-acp Architecture and Implementation Plan.md.
Status
Tickets 1 to 4 of that plan: the core types, a connection to a real agent, the
store that remembers which conversation belongs to which node, and the first
working ACPNode end-to-end path.
from langgraph_acp import ACPNode
result = await ACPNode(agent="codex")("Review this change")
print(result.message)
Available now:
ACPNode |
a LangGraph-compatible async callable that runs one ACP turn |
ACPAgentProvider, ACPAgentRegistry, default_registry |
names resolve to agents |
StdioACPProvider, CodexACPProvider |
how an agent is launched |
ACPClient, ACPCapabilities |
one live connection, and what it can do |
ACPSession |
one conversation, and the turns run in it |
ACPSessionSpec, ACPSessionStrategy |
which conversation a node speaks in |
ACPSessionStore, InMemoryACPSessionStore |
which ACP session a node's identity resolves to |
ACPWorkspace |
the filesystem context a session is given |
ACPConfig, ACPRequirements |
settings requested, capabilities demanded |
ACPEvent, ACPEventType |
streamed activity, normalized |
ACPResult, ACPUsage |
what a turn returns, and what it cost |
ACPError and its subclasses |
typed failures |
Registering another ACP agent is a command line rather than a class:
default_registry().register(
StdioACPProvider(name="gemini", command=["gemini", "--experimental-acp"])
)
The minimal node takes its invocation input as the prompt and starts a fresh session each time. Runtime prompt resolvers, session reuse, MCP injection, and LangGraph event streaming arrive in the following tickets.
Session bindings
ACPSessionStore maps a node's logical identity to the ACP session it speaks
in, so that a workflow can pick a conversation back up later:
store = InMemoryACPSessionStore()
await store.put("pr-918", "reviewer", "sess_abc123")
await store.get("pr-918", "reviewer") # -> "sess_abc123"
await store.delete("pr-918", "reviewer")
The identity is a pair, not a thread id, because one LangGraph thread routinely runs several agents — an implementer and three reviewers on the same pull request — and none of them may resume another's conversation.
A store holds ACP session identifiers and nothing else. No conversation
history, no agent messages, no tool history, no model context, no agent memory:
the ACP agent owns all of that and restores it itself when asked to load
sess_abc123. That is what makes a reply arriving on a webhook days later
resume a live conversation without reconstructing a transcript — and why the
store is the piece that has to be durable. InMemoryACPSessionStore is not; it
lives as long as its process, which is right for tests, examples, and
single-process graphs. The durable implementations arrive with Ticket 17 and
change nothing about the interface.
Two behaviours are placeholders that later tickets replace. Permission requests
are streamed as acp.permission.requested and then declined, because the policy
that could approve them does not exist yet; and the history an agent replays
while loading a session is dropped, because no turn is streaming when it
arrives.
Development
A standalone distribution, deliberately outside the uv workspace at the root
of this repository: it depends on nothing OpenEngine ships, targets a newer
Python than the rest of the tree, and is intended to be published on its own.
cd langgraph-acp
uv run pytest
uv run mypy # strict, over src/ and tests/
The package ships py.typed, so its annotations are a promise to a downstream
type checker; mypy --strict runs in CI on both supported interpreters to keep
it. Each field is annotated with what the constructor accepts, since
__post_init__ normalizes — a Path becomes a str, "reuse" becomes
ACPSessionStrategy.REUSE.
The dependency list is empty and stays empty: ACP is JSON-RPC over a pipe, so
the client is stdlib asyncio written here rather than a second protocol
library in every application that installs this one. The client tests launch a
real child process — tests/fake_agent.py, an ACP agent that does nothing,
correctly — because the thing under test is a process boundary.
CI runs both only when something under langgraph-acp/ changes.
Release files for langgraph-acp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| langgraph_acp-0.1.0.tar.gz | 167.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langgraph_acp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 217.2 kB
Release files / langgraph_acp-0.1.0.tar.gz
| Download URL | langgraph_acp-0.1.0.tar.gz |
|---|---|
| Size | 167.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b6586fe8580fc41cb97e2fa9faae1ae22565016ec9c700c8d4ad01128f8fd99a
|
|
BLAKE2b-256 checksum How to use checksums |
d6c375b076ea2232af29c67bccdd426e6a031f297f332e50e86295b42e9d6019
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / langgraph_acp-0.1.0-py3-none-any.whl
| Download URL | langgraph_acp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 49.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7aaaeef13013f1f7269016ef3a1404aae9a3d2d945395300b7ff0896e710ada7
|
|
BLAKE2b-256 checksum How to use checksums |
5311689fd626485fedb3e1fbf8385fd70956f261f7c76cb1ec216823d5ab8ee5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|