Skip to content
Merged
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
36 changes: 20 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A Python implementation of the Agent Client Protocol (ACP). Use it to build agen
- Package name: `agent-client-protocol` (import as `acp`)
- Repository: https://github.com/psiace/agent-client-protocol-python
- Docs: https://psiace.github.io/agent-client-protocol-python/
- Featured: Listed as the first third-party SDK on the official ACP site — see https://agentclientprotocol.com/libraries/community

## Install

Expand All @@ -24,51 +25,54 @@ make test # run tests

## Minimal agent example

See a complete streaming echo example in [examples/echo_agent.py](examples/echo_agent.py). It streams back each text block using `session/update` and ends the turn.

```python
import asyncio

from acp import (
Agent,
AgentSideConnection,
AuthenticateRequest,
CancelNotification,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
PromptResponse,
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2


class EchoAgent(Agent):
def __init__(self, conn):
self._conn = conn

async def initialize(self, params: InitializeRequest) -> InitializeResponse:
return InitializeResponse(protocolVersion=params.protocolVersion)

async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
return NewSessionResponse(sessionId="sess-1")

async def loadSession(self, params: LoadSessionRequest) -> None:
return None

async def authenticate(self, params: AuthenticateRequest) -> None:
return None

async def prompt(self, params: PromptRequest) -> PromptResponse:
# Normally you'd stream updates via sessionUpdate
for block in params.prompt:
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
),
)
)
return PromptResponse(stopReason="end_turn")

async def cancel(self, params: CancelNotification) -> None:
return None


async def main() -> None:
reader, writer = await stdio_streams()
# For an agent process, local writes go to client stdin (writer=stdout)
AgentSideConnection(lambda _conn: EchoAgent(), writer, reader)
# Keep running; in a real agent you would await tasks or add your own loop
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
await asyncio.Event().wait()


Expand Down
62 changes: 49 additions & 13 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,56 @@ pip install agent-client-protocol
## Minimal usage

```python
from acp import Agent, AgentSideConnection, Client, stdio_streams, PROTOCOL_VERSION, InitializeRequest, InitializeResponse, PromptRequest, PromptResponse
from acp.schema import ContentBlock1, SessionUpdate2, SessionNotification

class MyAgent(Agent):
def __init__(self, client: Client):
self.client = client
async def initialize(self, _p: InitializeRequest) -> InitializeResponse:
return InitializeResponse(protocolVersion=PROTOCOL_VERSION)
async def prompt(self, p: PromptRequest) -> PromptResponse:
await self.client.sessionUpdate(SessionNotification(
sessionId=p.sessionId,
update=SessionUpdate2(sessionUpdate="agent_message_chunk", content=ContentBlock1(type="text", text="Hello from ACP")),
))
import asyncio

from acp import (
Agent,
AgentSideConnection,
InitializeRequest,
InitializeResponse,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
PromptResponse,
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2


class EchoAgent(Agent):
def __init__(self, conn):
self._conn = conn

async def initialize(self, params: InitializeRequest) -> InitializeResponse:
return InitializeResponse(protocolVersion=params.protocolVersion)

async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
return NewSessionResponse(sessionId="sess-1")

async def prompt(self, params: PromptRequest) -> PromptResponse:
for block in params.prompt:
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
),
)
)
return PromptResponse(stopReason="end_turn")


async def main() -> None:
reader, writer = await stdio_streams()
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
await asyncio.Event().wait()


if __name__ == "__main__":
asyncio.run(main())
```

- Quickstart: [quickstart.md](quickstart.md)
Expand Down
39 changes: 30 additions & 9 deletions docs/mini-swe-agent.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
# Mini SWE Agent bridge

This example wraps mini-swe-agent behind ACP so Zed can run it as an external agent over stdio.
> Just a show of the bridge in action. Not a best-effort or absolutely-correct implementation of the agent.

This example wraps mini-swe-agent behind ACP so Zed can run it as an external agent over stdio. It also includes a local Textual UI client connected via a duet launcher

## Behavior

- Prompts: text blocks are concatenated into a single task string; referenced resources (`resource_link` / `resource`) are surfaced as hints in the task.
- Streaming: incremental text is sent via `session/update` with `agent_message_chunk`.
- Prompts: text blocks are concatenated into a single task string. (Resource embedding is not used in this example.)
- Streaming: only LM output is streamed via `session/update` `agent_message_chunk`.
- Tool calls: when the agent executes a shell command, the bridge sends:
- `tool_call` with `kind=execute`, pending status, and a bash code block containing the command
- `tool_call_update` upon completion, including output and a `rawOutput` object with `output` and `returncode`
- Final result: on task submission (mini-swe-agent prints `COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` as the first line), a final `agent_message_chunk` with the submission content is sent.

Non-terminating events (e.g. user rejection or timeouts) are surfaced both as a `tool_call_update` with `status=cancelled|failed` and as a text chunk so the session can continue.

## Configuration

Environment variables set in the Zed server config control the model:
Environment variables control the model:

- `MINI_SWE_MODEL`: model ID (e.g. `openrouter/openai/gpt-4o-mini`)
- `MINI_SWE_MODEL_KWARGS`: JSON string of extra parameters (e.g. `{ "api_base": "https://openrouter.ai/api/v1" }`)
- Vendor API keys (e.g. `OPENROUTER_API_KEY`) must be present in the environment
- `OPENROUTER_API_KEY` for OpenRouter; or `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` for native providers
- Optional `MINI_SWE_MODEL_KWARGS`: JSON, e.g. `{ "api_base": "https://openrouter.ai/api/v1" }` (auto-injected for OpenRouter if missing)

Agent behavior automatically maps the appropriate API key based on the chosen model and available environment variables.

If `mini-swe-agent` is not installed in the venv, the bridge attempts to import a vendored reference copy under `reference/mini-swe-agent/src`.

## How to run

- In Zed (editor integration): configure an agent server to launch `examples/mini_swe_agent/agent.py` and set the environment variables there. Use Zed’s “Open ACP Logs” to inspect `tool_call`/`tool_call_update` and message chunks.
- In terminal (local TUI): run the duet launcher to start both the agent and the Textual client with the same environment and dedicated pipes:

```bash
python examples/mini_swe_agent/duet.py
```

The launcher loads `.env` from the repo root (using python-dotenv) so both processes share the same configuration.

### TUI usage

- Hotkeys: `y` → YOLO, `c` → Confirm, `u` → Human, `Enter` → Continue.
- In Human mode, you’ll be prompted for a bash command; it will be executed and streamed back as a tool call.
- Each executed command appears in the “TOOL CALLS” section with live status and output.

## Files

- Example entry: [`examples/mini_swe_agent/agent.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/agent.py)
- Agent entry: [`examples/mini_swe_agent/agent.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/agent.py)
- Duet launcher: [`examples/mini_swe_agent/duet.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/duet.py)
- Textual client: [`examples/mini_swe_agent/client.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/client.py)
47 changes: 30 additions & 17 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,46 @@ import asyncio
from acp import (
Agent,
AgentSideConnection,
AuthenticateRequest,
CancelNotification,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
PromptResponse,
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2


class EchoAgent(Agent):
def __init__(self, conn):
self._conn = conn

async def initialize(self, params: InitializeRequest) -> InitializeResponse:
return InitializeResponse(protocolVersion=params.protocolVersion)

async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
return NewSessionResponse(sessionId="sess-1")

async def loadSession(self, params: LoadSessionRequest) -> None:
return None

async def authenticate(self, params: AuthenticateRequest) -> None:
return None

async def prompt(self, params: PromptRequest) -> PromptResponse:
# Normally you'd stream updates via sessionUpdate
for block in params.prompt:
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
),
)
)
return PromptResponse(stopReason="end_turn")

async def cancel(self, params: CancelNotification) -> None:
return None


async def main() -> None:
reader, writer = await stdio_streams()
# For an agent process, local writes go to client stdin (writer=stdout)
AgentSideConnection(lambda _conn: EchoAgent(), writer, reader)
# Keep running; in a real agent you would await tasks or add your own loop
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
await asyncio.Event().wait()


Expand Down Expand Up @@ -84,14 +85,26 @@ Add an agent server to Zed’s `settings.json`:
],
"env": {
"MINI_SWE_MODEL": "openrouter/openai/gpt-4o-mini",
"MINI_SWE_MODEL_KWARGS": "{\"api_base\":\"https://openrouter.ai/api/v1\"}",
"OPENROUTER_API_KEY": "sk-or-..."
}
}
}
}
```

- For OpenRouter, `api_base` is set automatically to `https://openrouter.ai/api/v1` if not provided.
- Alternatively, use native providers by setting `MINI_SWE_MODEL` accordingly and providing `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.

In Zed, open the Agents panel and select "Mini SWE Agent (Python)".

See [mini-swe-agent.md](mini-swe-agent.md) for behavior and message mapping details.

## Run locally with a TUI

Use the duet launcher to run both the agent and the local Textual client over dedicated pipes:

```bash
python examples/mini_swe_agent/duet.py
```

The launcher loads `.env` from the repo root so both processes share the same configuration (requires python-dotenv).
Loading