Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/experimental-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Experimental Protocol v2

> **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 v2 runtime is separate from the stable v1 API. Its methods accept and return
generated request and response models directly:

```python
from acp.experimental import v2

connection = v2.connect_to_agent(MyClient(), transport)
initialized = await connection.initialize(
v2.schema.InitializeRequest(
protocol_version=v2.PROTOCOL_VERSION,
info=v2.schema.Implementation(name="my-client", version="1.0.0"),
)
)
session = await connection.open_session(
v2.schema.NewSessionRequest(cwd="/workspace")
)
```

`open_session()` returns an `ActiveSession`. A v2 prompt is accepted before the
agent finishes it, so consume session updates until `SessionStop` marks the
`running` to `idle` transition:

```python
await session.prompt(
v2.schema.PromptRequest(
session_id=session.session_id,
prompt=[v2.schema.TextContentBlock(text="Hello")],
)
)
stopped = await session.wait_for_idle()
```

## Negotiate v1 or v2

Use `ClientNegotiator` when the same client can speak both versions. It sends
exactly one `initialize` request and returns a version-tagged connection:

```python
from acp.experimental import (
ClientNegotiator,
NegotiatedV2,
V1ClientConfig,
V2ClientConfig,
)

negotiator = ClientNegotiator(
transport,
v1=V1ClientConfig(client=v1_client, initialize=v1_initialize),
v2=V2ClientConfig(client=v2_client, initialize=v2_initialize),
)
negotiated = await negotiator.negotiate()

if isinstance(negotiated, NegotiatedV2):
session = await negotiated.connection.open_session(v2_new_session)
else:
session = await negotiated.connection.new_session(cwd="/workspace")
```

Agents that serve both versions use `AgentProtocolRouter`:

```python
from acp.experimental import AgentProtocolRouter

router = AgentProtocolRouter(v1=v1_agent, v2=v2_agent)
await router.run()
```

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.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ nav:
- 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
Expand Down
31 changes: 28 additions & 3 deletions src/acp/agent/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pydantic import TypeAdapter

from .._transport import Transport
from ..connection import Connection
from ..connection import Connection, MethodHandler
from ..interfaces import Agent, Client
from ..meta import CLIENT_METHODS
from ..schema import (
Expand Down Expand Up @@ -88,8 +88,7 @@ def __init__(
use_unstable_protocol: bool = False,
**connection_kwargs: Any,
) -> None:
agent = to_agent(self) if callable(to_agent) else to_agent
handler = build_agent_router(cast(Agent, agent), use_unstable_protocol=use_unstable_protocol)
agent, handler = self._prepare(to_agent, use_unstable_protocol=use_unstable_protocol)
if isinstance(input_stream, Transport):
if output_stream is not None:
raise TypeError(_AGENT_CONNECTION_ERROR)
Expand All @@ -100,6 +99,32 @@ def __init__(
):
raise TypeError(_AGENT_CONNECTION_ERROR)
self._conn = Connection(handler, input_stream, output_stream, listening=listening, **connection_kwargs)
self._notify_connected(agent)

@classmethod
def _attach(
cls,
to_agent: Callable[[Client], Agent] | Agent,
connection: Connection,
*,
use_unstable_protocol: bool = False,
) -> tuple[AgentSideConnection, MethodHandler]:
self = cls.__new__(cls)
agent, handler = self._prepare(to_agent, use_unstable_protocol=use_unstable_protocol)
self._conn = connection
self._notify_connected(agent)
return self, handler

def _prepare(
self,
to_agent: Callable[[Client], Agent] | Agent,
*,
use_unstable_protocol: bool,
) -> tuple[Agent, MethodHandler]:
agent = cast(Agent, to_agent(self) if callable(to_agent) else to_agent)
return agent, build_agent_router(agent, use_unstable_protocol=use_unstable_protocol)

def _notify_connected(self, agent: Agent) -> None:
if on_connect := getattr(agent, "on_connect", None):
on_connect(self)

Expand Down
34 changes: 30 additions & 4 deletions src/acp/client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Any, cast, final

from .._transport import Transport
from ..connection import Connection
from ..connection import Connection, MethodHandler
from ..exceptions import RequestError
from ..interfaces import Agent, Client
from ..meta import AGENT_METHODS, CLIENT_METHODS
Expand Down Expand Up @@ -122,9 +122,7 @@ def __init__(
use_unstable_protocol: bool = False,
**connection_kwargs: Any,
) -> None:
client = to_client(self) if callable(to_client) else to_client
self._session_updates = _SessionUpdateTracker(cast(Client, client))
handler = build_client_router(cast(Client, self._session_updates), use_unstable_protocol=use_unstable_protocol)
client, handler = self._prepare(to_client, use_unstable_protocol=use_unstable_protocol)

if isinstance(input_stream, Transport):
if output_stream is not None:
Expand All @@ -136,6 +134,34 @@ def __init__(
):
raise TypeError(_CLIENT_CONNECTION_ERROR)
self._conn = Connection(handler, input_stream, output_stream, **connection_kwargs)
self._notify_connected(client)

@classmethod
def _attach(
cls,
to_client: Callable[[Agent], Client] | Client,
connection: Connection,
*,
use_unstable_protocol: bool = False,
) -> tuple[ClientSideConnection, MethodHandler]:
self = cls.__new__(cls)
client, handler = self._prepare(to_client, use_unstable_protocol=use_unstable_protocol)
self._conn = connection
self._notify_connected(client)
return self, handler

def _prepare(
self,
to_client: Callable[[Agent], Client] | Client,
*,
use_unstable_protocol: bool,
) -> tuple[Client, MethodHandler]:
client = cast(Client, to_client(self) if callable(to_client) else to_client)
self._session_updates = _SessionUpdateTracker(client)
handler = build_client_router(cast(Client, self._session_updates), use_unstable_protocol=use_unstable_protocol)
return client, handler

def _notify_connected(self, client: Client) -> None:
if on_connect := getattr(client, "on_connect", None):
on_connect(self)

Expand Down
26 changes: 26 additions & 0 deletions src/acp/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
"""Experimental ACP APIs."""

from . import v2
from .negotiation import (
AgentProtocolConnection,
AgentProtocolRouter,
ClientNegotiator,
NegotiatedClient,
NegotiatedV1,
NegotiatedV2,
UnsupportedProtocolVersionError,
V1ClientConfig,
V2ClientConfig,
)

__all__ = [
"AgentProtocolConnection",
"AgentProtocolRouter",
"ClientNegotiator",
"NegotiatedClient",
"NegotiatedV1",
"NegotiatedV2",
"UnsupportedProtocolVersionError",
"V1ClientConfig",
"V2ClientConfig",
"v2",
]
Loading
Loading