a2claude 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- a2claude-0.1.0/PKG-INFO +188 -0
- a2claude-0.1.0/README.md +172 -0
- a2claude-0.1.0/pyproject.toml +51 -0
- a2claude-0.1.0/src/a2claude/__init__.py +9 -0
- a2claude-0.1.0/src/a2claude/backends/__init__.py +47 -0
- a2claude-0.1.0/src/a2claude/backends/base.py +96 -0
- a2claude-0.1.0/src/a2claude/backends/claude.py +125 -0
- a2claude-0.1.0/src/a2claude/backends/diff.py +70 -0
- a2claude-0.1.0/src/a2claude/backends/echo.py +47 -0
- a2claude-0.1.0/src/a2claude/backends/session.py +128 -0
- a2claude-0.1.0/src/a2claude/card.py +93 -0
- a2claude-0.1.0/src/a2claude/cli.py +173 -0
- a2claude-0.1.0/src/a2claude/executor.py +313 -0
- a2claude-0.1.0/src/a2claude/py.typed +0 -0
- a2claude-0.1.0/src/a2claude/server.py +62 -0
a2claude-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: a2claude
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run Claude Code as an A2A protocol agent server.
|
|
5
|
+
Keywords: a2a,claude-code,agent,agent2agent
|
|
6
|
+
Author: kanywst
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Dist: a2a-sdk[http-server]>=1.1,<2
|
|
9
|
+
Requires-Dist: claude-agent-sdk>=0.2.101
|
|
10
|
+
Requires-Dist: uvicorn>=0.49
|
|
11
|
+
Requires-Dist: httpx>=0.28
|
|
12
|
+
Requires-Dist: typer>=0.26
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Project-URL: Repository, https://github.com/kanywst/a2claude
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
<img src="assets/mascot.png" alt="a2claude" width="150" align="right">
|
|
18
|
+
|
|
19
|
+
# a2claude
|
|
20
|
+
|
|
21
|
+
Run Claude Code as an [A2A](https://a2aprotocol.ai/) agent server. Other agents
|
|
22
|
+
call it over the protocol; it drives a real Claude Code session in your project
|
|
23
|
+
and streams back the actual work — the tools it runs, the diffs it writes, what
|
|
24
|
+
it costs, and the permissions it needs.
|
|
25
|
+
|
|
26
|
+
[](https://github.com/kanywst/a2claude/actions/workflows/ci.yml)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](https://www.python.org/)
|
|
29
|
+
[](https://a2aprotocol.ai/)
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
Most "wrap a coding agent in A2A" adapters flatten everything to text in, text
|
|
34
|
+
out. a2claude keeps the parts that matter for the agent on the other end: which
|
|
35
|
+
tools ran, what files changed, what it cost, and how to continue the same
|
|
36
|
+
session on the next turn.
|
|
37
|
+
|
|
38
|
+
## How it maps to A2A
|
|
39
|
+
|
|
40
|
+
| Claude Code produces | A2A surface it lands on |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Assistant text | A streamed artifact (`append` / `last_chunk`) |
|
|
43
|
+
| A tool call (Bash, Edit) | A `working` status update for the action |
|
|
44
|
+
| A file edit | A named artifact carrying the diff |
|
|
45
|
+
| Run result | Cost, turns, and usage on the completion message |
|
|
46
|
+
| Session id | Mapped to the A2A `contextId` so follow-ups resume |
|
|
47
|
+
|
|
48
|
+
The mapping lives in one place (`executor.py`). Backends only emit
|
|
49
|
+
normalized events; they never touch the protocol.
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Python 3.13+
|
|
54
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
55
|
+
- Claude Code CLI on `PATH` (only for the `claude` backend)
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
Install:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv sync
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `echo` backend needs no API key and no Claude install, so you can
|
|
66
|
+
exercise the whole path offline first:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv run a2claude serve --backend echo &
|
|
70
|
+
# once the "Uvicorn running" line appears:
|
|
71
|
+
uv run a2claude call "fix the failing test"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
task 189b1c63-1a7b-4908-87c4-c8f3bba8f6b5
|
|
76
|
+
context 0b2a901e-2b6f-4c56-bba2-d0da546936e9
|
|
77
|
+
|
|
78
|
+
· Echo
|
|
79
|
+
fix the failing test
|
|
80
|
+
[completed] $0.0 · 1 turns
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then point it at a real project:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run a2claude serve --backend claude --cwd /path/to/project
|
|
87
|
+
uv run a2claude call "add a /health endpoint" --url http://localhost:9100/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Continue the same conversation by passing the `context` from a previous turn:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv run a2claude call "now add a test for it" --context <context-id>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Commands
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `a2claude serve` | Start the A2A server |
|
|
101
|
+
| `a2claude call TEXT` | Send a message and print the streamed events |
|
|
102
|
+
| `a2claude card` | Fetch and print the agent card |
|
|
103
|
+
|
|
104
|
+
The agent card is served at `/.well-known/agent-card.json` and advertises
|
|
105
|
+
Claude Code's abilities as discrete skills (generation, refactor, debug,
|
|
106
|
+
review, test, explain).
|
|
107
|
+
|
|
108
|
+
## Backends
|
|
109
|
+
|
|
110
|
+
A backend turns a prompt into a stream of normalized events. Two ship today:
|
|
111
|
+
|
|
112
|
+
- `echo` — no dependencies; mirrors the input. For wiring checks and tests.
|
|
113
|
+
- `claude` — drives Claude Code through the Claude Agent SDK.
|
|
114
|
+
|
|
115
|
+
The split keeps the A2A layer independent of how Claude Code is invoked, so
|
|
116
|
+
a raw-CLI backend can be added later without touching the server or the
|
|
117
|
+
protocol mapping.
|
|
118
|
+
|
|
119
|
+
## Authentication
|
|
120
|
+
|
|
121
|
+
The `claude` backend uses whatever the Claude CLI is configured with. When
|
|
122
|
+
the server answers on behalf of other agents, that has to be an Anthropic API
|
|
123
|
+
key (or Bedrock / Vertex) — Anthropic does not permit subscription
|
|
124
|
+
credentials for third-party serving. Set a per-run cost ceiling with
|
|
125
|
+
`--max-budget-usd`.
|
|
126
|
+
|
|
127
|
+
## Permissions
|
|
128
|
+
|
|
129
|
+
A tool that needs approval pauses the task in the A2A `input-required` state
|
|
130
|
+
instead of being skipped. The caller answers with a follow-up message on the
|
|
131
|
+
same task:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
uv run a2claude call "sudo reboot"
|
|
135
|
+
# ... [input-required] Permission requested for Bash: $ sudo reboot
|
|
136
|
+
# reply: a2claude call "allow" --task <id> --context <id>
|
|
137
|
+
uv run a2claude call "allow" --task <id> --context <id>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`allow` (or `yes`, `approve`, `ok`) approves; anything else denies. The Claude
|
|
141
|
+
session stays alive across the pause, so it resumes exactly where it stopped.
|
|
142
|
+
|
|
143
|
+
The server does not inherit your personal Claude settings, so it has no
|
|
144
|
+
pre-approved tool allowlist — every tool that needs approval routes through the
|
|
145
|
+
caller. Read-only actions Claude already treats as safe still run without a
|
|
146
|
+
prompt.
|
|
147
|
+
|
|
148
|
+
## Long-running tasks
|
|
149
|
+
|
|
150
|
+
The agent card advertises push notifications. A caller can register a webhook
|
|
151
|
+
for a task and receive status and artifact updates by HTTP POST instead of
|
|
152
|
+
holding a stream open — useful when a run takes minutes. Streaming and polling
|
|
153
|
+
(`tasks/get`) both work too.
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
uv sync --dev
|
|
159
|
+
uv run ruff check src tests
|
|
160
|
+
uv run ruff format src tests
|
|
161
|
+
uv run mypy
|
|
162
|
+
uv run pytest
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
CI runs these on Python 3.13 and 3.14, plus a Markdown lint, on every push and
|
|
166
|
+
pull request.
|
|
167
|
+
|
|
168
|
+
## Releasing
|
|
169
|
+
|
|
170
|
+
Pushing a `v*` tag builds the package, creates a GitHub release with the
|
|
171
|
+
artifacts, and publishes to PyPI via trusted publishing:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git tag v0.1.0
|
|
175
|
+
git push origin v0.1.0
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Status
|
|
179
|
+
|
|
180
|
+
The mapping is complete end to end and verified against real Claude: text round
|
|
181
|
+
trip, tool-progress updates, streaming artifacts, file diffs as artifacts, run
|
|
182
|
+
metadata, session continuity, the permission → `input-required` round trip, and
|
|
183
|
+
push notifications. The offline `echo` backend covers every path including
|
|
184
|
+
permissions, so it can all be exercised without an API key.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
Apache 2.0 — see [LICENSE](LICENSE).
|
a2claude-0.1.0/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<img src="assets/mascot.png" alt="a2claude" width="150" align="right">
|
|
2
|
+
|
|
3
|
+
# a2claude
|
|
4
|
+
|
|
5
|
+
Run Claude Code as an [A2A](https://a2aprotocol.ai/) agent server. Other agents
|
|
6
|
+
call it over the protocol; it drives a real Claude Code session in your project
|
|
7
|
+
and streams back the actual work — the tools it runs, the diffs it writes, what
|
|
8
|
+
it costs, and the permissions it needs.
|
|
9
|
+
|
|
10
|
+
[](https://github.com/kanywst/a2claude/actions/workflows/ci.yml)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://www.python.org/)
|
|
13
|
+
[](https://a2aprotocol.ai/)
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
Most "wrap a coding agent in A2A" adapters flatten everything to text in, text
|
|
18
|
+
out. a2claude keeps the parts that matter for the agent on the other end: which
|
|
19
|
+
tools ran, what files changed, what it cost, and how to continue the same
|
|
20
|
+
session on the next turn.
|
|
21
|
+
|
|
22
|
+
## How it maps to A2A
|
|
23
|
+
|
|
24
|
+
| Claude Code produces | A2A surface it lands on |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Assistant text | A streamed artifact (`append` / `last_chunk`) |
|
|
27
|
+
| A tool call (Bash, Edit) | A `working` status update for the action |
|
|
28
|
+
| A file edit | A named artifact carrying the diff |
|
|
29
|
+
| Run result | Cost, turns, and usage on the completion message |
|
|
30
|
+
| Session id | Mapped to the A2A `contextId` so follow-ups resume |
|
|
31
|
+
|
|
32
|
+
The mapping lives in one place (`executor.py`). Backends only emit
|
|
33
|
+
normalized events; they never touch the protocol.
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Python 3.13+
|
|
38
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
39
|
+
- Claude Code CLI on `PATH` (only for the `claude` backend)
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Install:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv sync
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `echo` backend needs no API key and no Claude install, so you can
|
|
50
|
+
exercise the whole path offline first:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run a2claude serve --backend echo &
|
|
54
|
+
# once the "Uvicorn running" line appears:
|
|
55
|
+
uv run a2claude call "fix the failing test"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
task 189b1c63-1a7b-4908-87c4-c8f3bba8f6b5
|
|
60
|
+
context 0b2a901e-2b6f-4c56-bba2-d0da546936e9
|
|
61
|
+
|
|
62
|
+
· Echo
|
|
63
|
+
fix the failing test
|
|
64
|
+
[completed] $0.0 · 1 turns
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then point it at a real project:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run a2claude serve --backend claude --cwd /path/to/project
|
|
71
|
+
uv run a2claude call "add a /health endpoint" --url http://localhost:9100/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Continue the same conversation by passing the `context` from a previous turn:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv run a2claude call "now add a test for it" --context <context-id>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
| Command | Description |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `a2claude serve` | Start the A2A server |
|
|
85
|
+
| `a2claude call TEXT` | Send a message and print the streamed events |
|
|
86
|
+
| `a2claude card` | Fetch and print the agent card |
|
|
87
|
+
|
|
88
|
+
The agent card is served at `/.well-known/agent-card.json` and advertises
|
|
89
|
+
Claude Code's abilities as discrete skills (generation, refactor, debug,
|
|
90
|
+
review, test, explain).
|
|
91
|
+
|
|
92
|
+
## Backends
|
|
93
|
+
|
|
94
|
+
A backend turns a prompt into a stream of normalized events. Two ship today:
|
|
95
|
+
|
|
96
|
+
- `echo` — no dependencies; mirrors the input. For wiring checks and tests.
|
|
97
|
+
- `claude` — drives Claude Code through the Claude Agent SDK.
|
|
98
|
+
|
|
99
|
+
The split keeps the A2A layer independent of how Claude Code is invoked, so
|
|
100
|
+
a raw-CLI backend can be added later without touching the server or the
|
|
101
|
+
protocol mapping.
|
|
102
|
+
|
|
103
|
+
## Authentication
|
|
104
|
+
|
|
105
|
+
The `claude` backend uses whatever the Claude CLI is configured with. When
|
|
106
|
+
the server answers on behalf of other agents, that has to be an Anthropic API
|
|
107
|
+
key (or Bedrock / Vertex) — Anthropic does not permit subscription
|
|
108
|
+
credentials for third-party serving. Set a per-run cost ceiling with
|
|
109
|
+
`--max-budget-usd`.
|
|
110
|
+
|
|
111
|
+
## Permissions
|
|
112
|
+
|
|
113
|
+
A tool that needs approval pauses the task in the A2A `input-required` state
|
|
114
|
+
instead of being skipped. The caller answers with a follow-up message on the
|
|
115
|
+
same task:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
uv run a2claude call "sudo reboot"
|
|
119
|
+
# ... [input-required] Permission requested for Bash: $ sudo reboot
|
|
120
|
+
# reply: a2claude call "allow" --task <id> --context <id>
|
|
121
|
+
uv run a2claude call "allow" --task <id> --context <id>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`allow` (or `yes`, `approve`, `ok`) approves; anything else denies. The Claude
|
|
125
|
+
session stays alive across the pause, so it resumes exactly where it stopped.
|
|
126
|
+
|
|
127
|
+
The server does not inherit your personal Claude settings, so it has no
|
|
128
|
+
pre-approved tool allowlist — every tool that needs approval routes through the
|
|
129
|
+
caller. Read-only actions Claude already treats as safe still run without a
|
|
130
|
+
prompt.
|
|
131
|
+
|
|
132
|
+
## Long-running tasks
|
|
133
|
+
|
|
134
|
+
The agent card advertises push notifications. A caller can register a webhook
|
|
135
|
+
for a task and receive status and artifact updates by HTTP POST instead of
|
|
136
|
+
holding a stream open — useful when a run takes minutes. Streaming and polling
|
|
137
|
+
(`tasks/get`) both work too.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv sync --dev
|
|
143
|
+
uv run ruff check src tests
|
|
144
|
+
uv run ruff format src tests
|
|
145
|
+
uv run mypy
|
|
146
|
+
uv run pytest
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
CI runs these on Python 3.13 and 3.14, plus a Markdown lint, on every push and
|
|
150
|
+
pull request.
|
|
151
|
+
|
|
152
|
+
## Releasing
|
|
153
|
+
|
|
154
|
+
Pushing a `v*` tag builds the package, creates a GitHub release with the
|
|
155
|
+
artifacts, and publishes to PyPI via trusted publishing:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git tag v0.1.0
|
|
159
|
+
git push origin v0.1.0
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Status
|
|
163
|
+
|
|
164
|
+
The mapping is complete end to end and verified against real Claude: text round
|
|
165
|
+
trip, tool-progress updates, streaming artifacts, file diffs as artifacts, run
|
|
166
|
+
metadata, session continuity, the permission → `input-required` round trip, and
|
|
167
|
+
push notifications. The offline `echo` backend covers every path including
|
|
168
|
+
permissions, so it can all be exercised without an API key.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
Apache 2.0 — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "a2claude"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Run Claude Code as an A2A protocol agent server."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [{ name = "kanywst" }]
|
|
9
|
+
keywords = ["a2a", "claude-code", "agent", "agent2agent"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"a2a-sdk[http-server]>=1.1,<2",
|
|
12
|
+
"claude-agent-sdk>=0.2.101",
|
|
13
|
+
"uvicorn>=0.49",
|
|
14
|
+
"httpx>=0.28",
|
|
15
|
+
"typer>=0.26",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
a2claude = "a2claude.cli:app"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Repository = "https://github.com/kanywst/a2claude"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"ruff>=0.15",
|
|
27
|
+
"pytest>=9",
|
|
28
|
+
"pytest-asyncio>=1.4",
|
|
29
|
+
"mypy>=2.1",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 88
|
|
38
|
+
target-version = "py313"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
asyncio_mode = "auto"
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
files = ["src"]
|
|
49
|
+
python_version = "3.13"
|
|
50
|
+
ignore_missing_imports = true
|
|
51
|
+
check_untyped_defs = true
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Run Claude Code as an A2A protocol agent server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .card import build_card
|
|
6
|
+
from .executor import ClaudeCodeExecutor
|
|
7
|
+
from .server import build_app
|
|
8
|
+
|
|
9
|
+
__all__ = ["build_app", "build_card", "ClaudeCodeExecutor"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Backends drive Claude Code and emit normalized events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import (
|
|
6
|
+
Backend,
|
|
7
|
+
BackendEvent,
|
|
8
|
+
FileChange,
|
|
9
|
+
PermissionDecision,
|
|
10
|
+
PermissionRequest,
|
|
11
|
+
Result,
|
|
12
|
+
RunRequest,
|
|
13
|
+
TextDelta,
|
|
14
|
+
ToolUse,
|
|
15
|
+
)
|
|
16
|
+
from .echo import EchoBackend
|
|
17
|
+
from .session import BackendSession
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Backend",
|
|
21
|
+
"BackendEvent",
|
|
22
|
+
"BackendSession",
|
|
23
|
+
"FileChange",
|
|
24
|
+
"PermissionDecision",
|
|
25
|
+
"PermissionRequest",
|
|
26
|
+
"Result",
|
|
27
|
+
"RunRequest",
|
|
28
|
+
"TextDelta",
|
|
29
|
+
"ToolUse",
|
|
30
|
+
"EchoBackend",
|
|
31
|
+
"make_backend",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_backend(name: str, **kwargs) -> Backend:
|
|
36
|
+
"""Construct a backend by name.
|
|
37
|
+
|
|
38
|
+
``claude`` is imported lazily so the echo backend works without the Claude
|
|
39
|
+
Agent SDK's runtime dependencies present.
|
|
40
|
+
"""
|
|
41
|
+
if name == "echo":
|
|
42
|
+
return EchoBackend()
|
|
43
|
+
if name == "claude":
|
|
44
|
+
from .claude import ClaudeBackend
|
|
45
|
+
|
|
46
|
+
return ClaudeBackend(**kwargs)
|
|
47
|
+
raise ValueError(f"unknown backend: {name!r} (expected 'echo' or 'claude')")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Backend abstraction.
|
|
2
|
+
|
|
3
|
+
A backend drives Claude Code and yields a normalized stream of events. The
|
|
4
|
+
A2A layer never imports the Claude Agent SDK directly — it only consumes these
|
|
5
|
+
events. That keeps the protocol mapping in one place and lets us swap the
|
|
6
|
+
underlying driver (Agent SDK today, raw CLI later) without touching the server.
|
|
7
|
+
|
|
8
|
+
Backends implement ``drive(session, request)``: they push events onto the
|
|
9
|
+
session and, when a tool needs approval, call ``session.request_permission(...)``
|
|
10
|
+
which parks until the A2A caller responds. This is what lets a permission prompt
|
|
11
|
+
become an A2A ``input-required`` round trip rather than being silently skipped.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .session import BackendSession
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class TextDelta:
|
|
25
|
+
"""A chunk of assistant-authored text."""
|
|
26
|
+
|
|
27
|
+
text: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class ToolUse:
|
|
32
|
+
"""The agent decided to run a tool (Bash, Edit, Read, ...)."""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
tool_input: dict[str, Any]
|
|
36
|
+
tool_use_id: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class FileChange:
|
|
41
|
+
"""A file was written or edited during the run."""
|
|
42
|
+
|
|
43
|
+
path: str
|
|
44
|
+
diff: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class PermissionRequest:
|
|
49
|
+
"""A tool needs the caller's approval before it can run."""
|
|
50
|
+
|
|
51
|
+
request_id: str
|
|
52
|
+
tool_name: str
|
|
53
|
+
tool_input: dict[str, Any]
|
|
54
|
+
description: str = ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(slots=True)
|
|
58
|
+
class PermissionDecision:
|
|
59
|
+
"""The caller's answer to a PermissionRequest."""
|
|
60
|
+
|
|
61
|
+
request_id: str
|
|
62
|
+
allow: bool
|
|
63
|
+
message: str = ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True)
|
|
67
|
+
class Result:
|
|
68
|
+
"""Terminal event carrying run metadata."""
|
|
69
|
+
|
|
70
|
+
session_id: str | None = None
|
|
71
|
+
cost_usd: float | None = None
|
|
72
|
+
num_turns: int | None = None
|
|
73
|
+
usage: dict[str, Any] | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
BackendEvent = TextDelta | ToolUse | FileChange | PermissionRequest | Result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(slots=True)
|
|
80
|
+
class RunRequest:
|
|
81
|
+
"""One turn of work handed to a backend."""
|
|
82
|
+
|
|
83
|
+
prompt: str
|
|
84
|
+
context_id: str | None = None
|
|
85
|
+
resume: str | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@runtime_checkable
|
|
89
|
+
class Backend(Protocol):
|
|
90
|
+
"""Anything that can drive Claude Code and emit normalized events."""
|
|
91
|
+
|
|
92
|
+
name: str
|
|
93
|
+
|
|
94
|
+
async def drive(self, session: BackendSession, request: RunRequest) -> None:
|
|
95
|
+
"""Run one turn, emitting events onto ``session`` until it returns."""
|
|
96
|
+
...
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Claude backend.
|
|
2
|
+
|
|
3
|
+
Drives Claude Code through the Claude Agent SDK's bidirectional client and
|
|
4
|
+
normalizes its typed message stream into backend events. Tool calls, file edits,
|
|
5
|
+
run cost, and the session id — everything the "text in, text out" wrappers
|
|
6
|
+
discard — are preserved for the A2A layer to map onto the protocol.
|
|
7
|
+
|
|
8
|
+
Permission prompts are routed through ``can_use_tool`` into the session's
|
|
9
|
+
``request_permission``, so the caller approves or denies a tool over A2A instead
|
|
10
|
+
of the server skipping it.
|
|
11
|
+
|
|
12
|
+
Authentication follows whatever the Claude CLI is configured with. For a server
|
|
13
|
+
that answers on behalf of other agents that means an Anthropic API key (or
|
|
14
|
+
Bedrock/Vertex); subscription credentials are not permitted for third-party
|
|
15
|
+
serving.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from collections.abc import Iterator
|
|
22
|
+
|
|
23
|
+
from claude_agent_sdk import (
|
|
24
|
+
AssistantMessage,
|
|
25
|
+
ClaudeAgentOptions,
|
|
26
|
+
ClaudeSDKClient,
|
|
27
|
+
PermissionMode,
|
|
28
|
+
PermissionResultAllow,
|
|
29
|
+
PermissionResultDeny,
|
|
30
|
+
ResultMessage,
|
|
31
|
+
SettingSource,
|
|
32
|
+
TextBlock,
|
|
33
|
+
ToolUseBlock,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from .base import BackendEvent, Result, RunRequest, TextDelta, ToolUse
|
|
37
|
+
from .diff import file_changes
|
|
38
|
+
from .session import BackendSession
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def events_from_message(message: object) -> Iterator[BackendEvent]:
|
|
42
|
+
"""Map one Claude Agent SDK message to normalized backend events.
|
|
43
|
+
|
|
44
|
+
Pure and side-effect free so the translation can be unit tested without a
|
|
45
|
+
live Claude session.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(message, AssistantMessage):
|
|
48
|
+
for block in message.content:
|
|
49
|
+
if isinstance(block, TextBlock):
|
|
50
|
+
if block.text:
|
|
51
|
+
yield TextDelta(text=block.text)
|
|
52
|
+
elif isinstance(block, ToolUseBlock):
|
|
53
|
+
tool_input = dict(block.input or {})
|
|
54
|
+
yield ToolUse(block.name, tool_input, block.id)
|
|
55
|
+
yield from file_changes(block.name, tool_input)
|
|
56
|
+
elif isinstance(message, ResultMessage):
|
|
57
|
+
yield Result(
|
|
58
|
+
session_id=message.session_id,
|
|
59
|
+
cost_usd=message.total_cost_usd,
|
|
60
|
+
num_turns=message.num_turns,
|
|
61
|
+
usage=message.usage,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ClaudeBackend:
|
|
66
|
+
name = "claude"
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
cwd: str | None = None,
|
|
72
|
+
allowed_tools: list[str] | None = None,
|
|
73
|
+
permission_mode: PermissionMode | None = None,
|
|
74
|
+
model: str | None = None,
|
|
75
|
+
max_budget_usd: float | None = None,
|
|
76
|
+
setting_sources: list[SettingSource] | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self.cwd = os.path.abspath(cwd or os.getcwd())
|
|
79
|
+
self.allowed_tools = allowed_tools
|
|
80
|
+
self.permission_mode = permission_mode
|
|
81
|
+
self.model = model
|
|
82
|
+
self.max_budget_usd = max_budget_usd
|
|
83
|
+
# A server should not inherit a developer's personal tool allowlist:
|
|
84
|
+
# default to loading no settings so every tool routes through the A2A
|
|
85
|
+
# permission round trip. Pass e.g. ["project"] to opt back in.
|
|
86
|
+
self.setting_sources: list[SettingSource] = (
|
|
87
|
+
[] if setting_sources is None else setting_sources
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _options(self, request: RunRequest, can_use_tool) -> ClaudeAgentOptions:
|
|
91
|
+
options = ClaudeAgentOptions(
|
|
92
|
+
cwd=self.cwd,
|
|
93
|
+
can_use_tool=can_use_tool,
|
|
94
|
+
setting_sources=self.setting_sources,
|
|
95
|
+
)
|
|
96
|
+
if request.resume:
|
|
97
|
+
options.resume = request.resume
|
|
98
|
+
if self.allowed_tools:
|
|
99
|
+
options.allowed_tools = self.allowed_tools
|
|
100
|
+
if self.permission_mode:
|
|
101
|
+
options.permission_mode = self.permission_mode
|
|
102
|
+
if self.model:
|
|
103
|
+
options.model = self.model
|
|
104
|
+
if self.max_budget_usd is not None:
|
|
105
|
+
options.max_budget_usd = self.max_budget_usd
|
|
106
|
+
return options
|
|
107
|
+
|
|
108
|
+
async def drive(self, session: BackendSession, request: RunRequest) -> None:
|
|
109
|
+
async def can_use_tool(tool_name, tool_input, context):
|
|
110
|
+
description = getattr(context, "display_name", "") or tool_name
|
|
111
|
+
decision = await session.request_permission(
|
|
112
|
+
tool_name, dict(tool_input or {}), description
|
|
113
|
+
)
|
|
114
|
+
if decision.allow:
|
|
115
|
+
return PermissionResultAllow()
|
|
116
|
+
return PermissionResultDeny(
|
|
117
|
+
message=decision.message or "Denied by A2A caller"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
options = self._options(request, can_use_tool)
|
|
121
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
122
|
+
await client.query(request.prompt)
|
|
123
|
+
async for message in client.receive_response():
|
|
124
|
+
for event in events_from_message(message):
|
|
125
|
+
await session.emit(event)
|