simple_a2a 0.1.0
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.
- checksums.yaml +7 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/Rakefile +13 -0
- data/docs/api/client/index.md +124 -0
- data/docs/api/index.md +27 -0
- data/docs/api/models/index.md +233 -0
- data/docs/api/server/index.md +162 -0
- data/docs/api/storage/index.md +84 -0
- data/docs/architecture/index.md +63 -0
- data/docs/architecture/protocol.md +112 -0
- data/docs/assets/css/custom.css +6 -0
- data/docs/examples/basic-usage.md +77 -0
- data/docs/examples/index.md +92 -0
- data/docs/examples/llm-research.md +92 -0
- data/docs/examples/streaming.md +81 -0
- data/docs/getting-started/installation.md +48 -0
- data/docs/getting-started/quick-start.md +100 -0
- data/docs/guides/custom-storage.md +69 -0
- data/docs/guides/push-notifications.md +104 -0
- data/docs/guides/streaming.md +75 -0
- data/docs/index.md +98 -0
- data/examples/01_basic_usage/client.rb +75 -0
- data/examples/01_basic_usage/server.rb +57 -0
- data/examples/02_streaming/client.rb +70 -0
- data/examples/02_streaming/server.rb +177 -0
- data/examples/03_llm_research/client.rb +138 -0
- data/examples/03_llm_research/run +82 -0
- data/examples/03_llm_research/server.rb +203 -0
- data/examples/03_llm_research/web_client.rb +501 -0
- data/examples/common_config.rb +4 -0
- data/examples/run +108 -0
- data/lib/simple_a2a/client/base.rb +101 -0
- data/lib/simple_a2a/client/sse.rb +58 -0
- data/lib/simple_a2a/errors.rb +15 -0
- data/lib/simple_a2a/json_rpc.rb +89 -0
- data/lib/simple_a2a/models/agent_capabilities.rb +11 -0
- data/lib/simple_a2a/models/agent_card.rb +23 -0
- data/lib/simple_a2a/models/agent_interface.rb +11 -0
- data/lib/simple_a2a/models/agent_provider.rb +11 -0
- data/lib/simple_a2a/models/agent_skill.rb +12 -0
- data/lib/simple_a2a/models/artifact.rb +23 -0
- data/lib/simple_a2a/models/authentication_info.rb +11 -0
- data/lib/simple_a2a/models/base.rb +111 -0
- data/lib/simple_a2a/models/message.rb +45 -0
- data/lib/simple_a2a/models/part.rb +45 -0
- data/lib/simple_a2a/models/push_notification_config.rb +17 -0
- data/lib/simple_a2a/models/security_scheme.rb +16 -0
- data/lib/simple_a2a/models/send_message_configuration.rb +12 -0
- data/lib/simple_a2a/models/stream_response.rb +32 -0
- data/lib/simple_a2a/models/task.rb +57 -0
- data/lib/simple_a2a/models/task_artifact_update_event.rb +21 -0
- data/lib/simple_a2a/models/task_status.rb +20 -0
- data/lib/simple_a2a/models/task_status_update_event.rb +19 -0
- data/lib/simple_a2a/models/types.rb +39 -0
- data/lib/simple_a2a/server/agent_executor.rb +16 -0
- data/lib/simple_a2a/server/app.rb +227 -0
- data/lib/simple_a2a/server/base.rb +43 -0
- data/lib/simple_a2a/server/context.rb +44 -0
- data/lib/simple_a2a/server/event_router.rb +50 -0
- data/lib/simple_a2a/server/falcon_runner.rb +31 -0
- data/lib/simple_a2a/server/multi_agent.rb +50 -0
- data/lib/simple_a2a/server/push_sender.rb +80 -0
- data/lib/simple_a2a/server/resume_context.rb +14 -0
- data/lib/simple_a2a/storage/base.rb +12 -0
- data/lib/simple_a2a/storage/memory.rb +41 -0
- data/lib/simple_a2a/version.rb +5 -0
- data/lib/simple_a2a.rb +49 -0
- data/mkdocs.yml +143 -0
- data/sig/simple_a2a.rbs +4 -0
- metadata +353 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d1b4e2d504e01c9511e9334010acfb9e1ea99314990586789aff6db82f7c31fd
|
|
4
|
+
data.tar.gz: eda5925b5f758c25980af95081f214d652387d9cc61bbd37f0bb6f31d2aa7dbc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1074249a5196cd4f0c533ef12b4662f4897d03a64f9d626429f0e7cbbd1653c6822e566afa4467793fe3d934d5e9d2451079fdcd5e1ad00159b505af814f4e02
|
|
7
|
+
data.tar.gz: 003b13cec22786ee19363f1afa1bdeee064a79a0a600b0b73a3343df6167611dc5b93c8c45fc6522b637c5ae5fde6c322836d6b05743fd53a0a49275ce12786b
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Deploy Documentation to GitHub Pages
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
- develop
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "mkdocs.yml"
|
|
10
|
+
- ".github/workflows/deploy-github-pages.yml"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pages: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
|
|
27
|
+
- name: Setup Python
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: 3.x
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
pip install mkdocs
|
|
35
|
+
pip install mkdocs-material
|
|
36
|
+
pip install mkdocs-macros-plugin
|
|
37
|
+
pip install mike
|
|
38
|
+
|
|
39
|
+
- name: Configure Git
|
|
40
|
+
run: |
|
|
41
|
+
git config --local user.email "action@github.com"
|
|
42
|
+
git config --local user.name "GitHub Action"
|
|
43
|
+
|
|
44
|
+
- name: Build MkDocs site
|
|
45
|
+
run: mkdocs build
|
|
46
|
+
|
|
47
|
+
- name: Deploy to GitHub Pages
|
|
48
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
49
|
+
with:
|
|
50
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
publish_dir: ./site
|
|
52
|
+
keep_files: true
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dewayne VanHoozer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# simple_a2a
|
|
2
|
+
|
|
3
|
+
A Ruby gem implementing the [Agent2Agent (A2A) protocol](https://a2a-protocol.org/latest/) — an open standard by Google and the Linux Foundation for interoperability between AI agents.
|
|
4
|
+
|
|
5
|
+
`simple_a2a` provides a complete A2A client and server in a single package, built on the async Ruby ecosystem with [Falcon](https://github.com/socketry/falcon) as the recommended HTTP server.
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
The full documentation website is available at [https://madbomber.github.io/simple_a2a](https://madbomber.github.io/simple_a2a).
|
|
10
|
+
|
|
11
|
+
## Lineage
|
|
12
|
+
|
|
13
|
+
`simple_a2a` is the successor to my earlier Ruby gem, `simple_acp`. That gem implemented the Agent Communication Protocol (ACP), which IBM Research introduced through the BeeAI project for interoperable agent communication. ACP later merged into A2A under the Linux Foundation, with the ACP team contributing its technology and expertise to the A2A effort.
|
|
14
|
+
|
|
15
|
+
A2A itself was created by Google and then donated to the Linux Foundation for neutral, open governance. The current A2A specification is maintained by the Linux Foundation-hosted [Agent2Agent project](https://github.com/a2aproject/A2A) and published at [a2a-protocol.org](https://a2a-protocol.org/latest/).
|
|
16
|
+
|
|
17
|
+
> My opinion: the A2A specification is still a little jagged in places. A simple example is that it does not clearly cover whether an A2A server is expected to host only one agent or may host multiple agents. That is a minor example, but it points to the kind of operational detail the specification still needs to tighten up.
|
|
18
|
+
|
|
19
|
+
References:
|
|
20
|
+
|
|
21
|
+
- IBM Research: [Agent Communication Protocol](https://research.ibm.com/projects/agent-communication-protocol)
|
|
22
|
+
- BeeAI announcement: [ACP Joins Forces with A2A Under the Linux Foundation](https://github.com/orgs/i-am-bee/discussions/5)
|
|
23
|
+
- Linux Foundation: [Launch of the Agent2Agent Protocol Project](https://www.linuxfoundation.org/press/linux-foundation-launches-the-agent2agent-protocol-project-to-enable-secure-intelligent-communication-between-ai-agents)
|
|
24
|
+
|
|
25
|
+
## Protocol Reference
|
|
26
|
+
|
|
27
|
+
- **Official A2A Specification:** [https://a2a-protocol.org/latest/](https://a2a-protocol.org/latest/)
|
|
28
|
+
- **A2A Project on GitHub:** [https://github.com/a2aproject/A2A](https://github.com/a2aproject/A2A)
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- Full A2A v1.0 protocol support (backward compatible with v0.3)
|
|
33
|
+
- JSON-RPC 2.0 over HTTP(S) — primary binding
|
|
34
|
+
- Server-Sent Events (SSE) for streaming responses
|
|
35
|
+
- Push notifications via webhooks (RS256 JWT)
|
|
36
|
+
- Task lifecycle management (`submitted → working → completed/failed/canceled`)
|
|
37
|
+
- AgentCard discovery endpoint at `GET /agentCard`
|
|
38
|
+
- Multi-agent hosting with path-based routing via `A2A.multi_server`
|
|
39
|
+
- Async-first via the `async` gem ecosystem (Falcon + async-http)
|
|
40
|
+
- Rack-compatible server with Roda routing
|
|
41
|
+
- Zeitwerk autoloading — top-level module is `A2A`
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Add to your Gemfile:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
gem "simple_a2a"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or install directly:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
gem install simple_a2a
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "simple_a2a"
|
|
61
|
+
|
|
62
|
+
# Define your agent logic
|
|
63
|
+
class MyExecutor < A2A::Server::AgentExecutor
|
|
64
|
+
def call(ctx)
|
|
65
|
+
input = ctx.message.text_content
|
|
66
|
+
ctx.task.complete!(artifacts: [
|
|
67
|
+
A2A::Models::Artifact.new(
|
|
68
|
+
parts: [A2A::Models::Part.text("You said: #{input}")]
|
|
69
|
+
)
|
|
70
|
+
])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Describe your agent
|
|
75
|
+
card = A2A::Models::AgentCard.new(
|
|
76
|
+
name: "MyAgent",
|
|
77
|
+
version: "1.0",
|
|
78
|
+
capabilities: A2A::Models::AgentCapabilities.new,
|
|
79
|
+
skills: [A2A::Models::AgentSkill.new(name: "reply")],
|
|
80
|
+
interfaces: [A2A::Models::AgentInterface.new(
|
|
81
|
+
type: "json-rpc", url: "http://localhost:9292", version: "1.0"
|
|
82
|
+
)]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Start the server (blocks — runs Falcon on port 9292)
|
|
86
|
+
A2A.server(agent_card: card, executor: MyExecutor.new).run
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# Client — talk to any A2A agent
|
|
91
|
+
client = A2A.client(url: "http://localhost:9292")
|
|
92
|
+
|
|
93
|
+
task = client.send_task(message: A2A::Models::Message.user("hello"))
|
|
94
|
+
puts task.status.state # => "completed"
|
|
95
|
+
puts task.artifacts.first.parts.first.text # => "You said: hello"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Task lifecycle
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
submitted → working → completed (terminal)
|
|
102
|
+
→ failed (terminal)
|
|
103
|
+
→ canceled (terminal)
|
|
104
|
+
→ rejected (terminal)
|
|
105
|
+
→ input_required (interrupted)
|
|
106
|
+
→ auth_required (interrupted)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Terminal tasks cannot be canceled. Interrupted tasks can be resumed by sending a new message.
|
|
110
|
+
|
|
111
|
+
## Streaming
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# Server — emit incremental events
|
|
115
|
+
class StreamingExecutor < A2A::Server::AgentExecutor
|
|
116
|
+
def call(ctx)
|
|
117
|
+
ctx.task.start!
|
|
118
|
+
ctx.emit_status
|
|
119
|
+
|
|
120
|
+
ctx.emit_artifact(A2A::Models::Artifact.new(
|
|
121
|
+
parts: [A2A::Models::Part.text("thinking… ")]
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
ctx.task.complete!
|
|
125
|
+
ctx.emit_status(final: true)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Client — consume SSE events
|
|
132
|
+
client = A2A.sse_client(url: "http://localhost:9292")
|
|
133
|
+
|
|
134
|
+
client.send_subscribe(message: A2A::Models::Message.user("go")) do |event|
|
|
135
|
+
case event
|
|
136
|
+
when A2A::Models::TaskStatusUpdateEvent
|
|
137
|
+
puts "status: #{event.status.state}"
|
|
138
|
+
when A2A::Models::TaskArtifactUpdateEvent
|
|
139
|
+
print event.artifact.parts.map(&:text).join
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Multi-agent server
|
|
145
|
+
|
|
146
|
+
Use `A2A.multi_server` to host multiple A2A agents in one Falcon process, with each agent mounted at its own path:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
A2A.multi_server(
|
|
150
|
+
agents: {
|
|
151
|
+
"/research" => { agent_card: research_card, executor: ResearchExecutor.new },
|
|
152
|
+
"/evaluator" => { agent_card: evaluator_card, executor: EvaluatorExecutor.new }
|
|
153
|
+
},
|
|
154
|
+
port: 9292
|
|
155
|
+
).run
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Each mounted agent has its own AgentCard, executor, storage, and event router.
|
|
159
|
+
|
|
160
|
+
## Examples
|
|
161
|
+
|
|
162
|
+
The repository includes three runnable demo apps:
|
|
163
|
+
|
|
164
|
+
| Demo | Shows |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `01_basic_usage` | Agent discovery, `tasks/send`, task listing, task lookup, and error handling |
|
|
167
|
+
| `02_streaming` | `tasks/sendSubscribe` with Server-Sent Events and incremental artifact chunks |
|
|
168
|
+
| `03_llm_research` | Multi-agent routing, parallel streaming LLM calls, evaluator agent, and a Sinatra web client |
|
|
169
|
+
|
|
170
|
+
Run the basic and streaming demos end-to-end:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bundle exec ruby examples/run 01_basic_usage
|
|
174
|
+
bundle exec ruby examples/run 02_streaming
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The LLM research demo requires `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and demo-specific gems. See the full documentation for setup details.
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
bin/setup # install dependencies
|
|
183
|
+
bundle exec rake test # run the test suite
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
Bug reports and pull requests are welcome at [https://github.com/MadBomber/simple_a2a](https://github.com/MadBomber/simple_a2a).
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "minitest/test_task"
|
|
5
|
+
|
|
6
|
+
Minitest::TestTask.create do |t|
|
|
7
|
+
# Load SimpleCov before minitest/autorun so its at_exit fires AFTER
|
|
8
|
+
# Minitest's (LIFO order), meaning coverage is reported after tests run.
|
|
9
|
+
t.test_prelude = %(require_relative "test/test_helper")
|
|
10
|
+
t.framework = "" # test_helper already requires minitest/autorun
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :test
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Client API
|
|
2
|
+
|
|
3
|
+
## Client::Base
|
|
4
|
+
|
|
5
|
+
Synchronous JSON-RPC client backed by `async/http`. Works inside or outside an async reactor.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
client = A2A.client(url: "http://localhost:9292")
|
|
9
|
+
# or
|
|
10
|
+
client = A2A::Client::Base.new(
|
|
11
|
+
url: "http://localhost:9292",
|
|
12
|
+
headers: { "Authorization" => "Bearer token" }
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Methods
|
|
17
|
+
|
|
18
|
+
#### `#agent_card` → `A2A::Models::AgentCard`
|
|
19
|
+
|
|
20
|
+
Fetches and parses the AgentCard from `GET /agentCard`.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
card = client.agent_card
|
|
24
|
+
puts card.name
|
|
25
|
+
puts card.capabilities.streaming
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### `#send_task(message:, **opts)` → `A2A::Models::Task`
|
|
29
|
+
|
|
30
|
+
Sends a `tasks/send` request and waits for the completed task.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
task = client.send_task(
|
|
34
|
+
message: A2A::Models::Message.user("hello"),
|
|
35
|
+
task_id: "my-task-id", # optional — server assigns if omitted
|
|
36
|
+
context_id: "ctx-1", # optional
|
|
37
|
+
metadata: { source: "web" } # optional
|
|
38
|
+
)
|
|
39
|
+
puts task.status.state # => "completed"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### `#get_task(task_id)` → `A2A::Models::Task`
|
|
43
|
+
|
|
44
|
+
Retrieves a task by ID (`tasks/get`). Raises `A2A::Error` if not found.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
task = client.get_task("abc-123")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### `#list_tasks` → `[A2A::Models::Task]`
|
|
51
|
+
|
|
52
|
+
Returns all tasks from the server (`tasks/list`).
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
tasks = client.list_tasks
|
|
56
|
+
tasks.each { |t| puts "#{t.id}: #{t.status.state}" }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### `#cancel_task(task_id)` → `A2A::Models::Task`
|
|
60
|
+
|
|
61
|
+
Cancels a non-terminal task (`tasks/cancel`). Raises `A2A::Error` if not cancelable.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
task = client.cancel_task("abc-123")
|
|
65
|
+
puts task.status.state # => "canceled"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Error handling
|
|
69
|
+
|
|
70
|
+
All RPC errors raise `A2A::Error` with the server's error message:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
begin
|
|
74
|
+
client.get_task("no-such-id")
|
|
75
|
+
rescue A2A::Error => e
|
|
76
|
+
puts e.message # => "Task no-such-id not found"
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Client::SSE
|
|
83
|
+
|
|
84
|
+
Extends `Client::Base` with streaming support via `tasks/sendSubscribe`.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
client = A2A.sse_client(url: "http://localhost:9292")
|
|
88
|
+
# or
|
|
89
|
+
client = A2A::Client::SSE.new(url: "http://localhost:9292")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `#send_subscribe(message:, **opts, &block)`
|
|
93
|
+
|
|
94
|
+
Opens an SSE connection and yields events as they arrive. Blocks until the stream closes.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
client.send_subscribe(message: A2A::Models::Message.user("process this")) do |event|
|
|
98
|
+
case event
|
|
99
|
+
when A2A::Models::TaskStatusUpdateEvent
|
|
100
|
+
puts "Status: #{event.status.state} (final=#{event.final})"
|
|
101
|
+
when A2A::Models::TaskArtifactUpdateEvent
|
|
102
|
+
puts "Artifact chunk: #{event.artifact.parts.map(&:text).join}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Events are instances of:
|
|
108
|
+
- `A2A::Models::TaskStatusUpdateEvent`
|
|
109
|
+
- `A2A::Models::TaskArtifactUpdateEvent`
|
|
110
|
+
- `Hash` — for unrecognized event types
|
|
111
|
+
|
|
112
|
+
The stream ends when the server sends a `final: true` event. The block is not called for malformed or comment-only SSE frames.
|
|
113
|
+
|
|
114
|
+
### Using inside an Async reactor
|
|
115
|
+
|
|
116
|
+
Both `Base` and `SSE` detect whether they're already inside an `Async` reactor via `Async::Task.current?`. Inside a reactor, they call the underlying `Async::HTTP::Internet` directly. Outside, they wrap the call in `Async { }.wait`.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Async do
|
|
120
|
+
client = A2A.client(url: "http://localhost:9292")
|
|
121
|
+
task = client.send_task(message: A2A::Models::Message.user("hi"))
|
|
122
|
+
puts task.id
|
|
123
|
+
end
|
|
124
|
+
```
|
data/docs/api/index.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## Namespaces
|
|
4
|
+
|
|
5
|
+
| Namespace | Description |
|
|
6
|
+
|---|---|
|
|
7
|
+
| [`A2A::Models`](models/index.md) | Data classes — Task, Message, Part, Artifact, AgentCard, events |
|
|
8
|
+
| [`A2A::Server`](server/index.md) | Server bootstrap, routing, executor base, context, event routing |
|
|
9
|
+
| [`A2A::Client`](client/index.md) | HTTP client (sync JSON-RPC + SSE streaming) |
|
|
10
|
+
| [`A2A::Storage`](storage/index.md) | Task persistence — Memory backend, pluggable base |
|
|
11
|
+
| `A2A::JsonRpc` | JSON-RPC 2.0 request/response/error layer |
|
|
12
|
+
|
|
13
|
+
## Top-level module
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
A2A.logger = Logger.new($stdout) # optional — logs internal warnings
|
|
17
|
+
|
|
18
|
+
A2A.server(**opts) # → A2A::Server::Base.new(**opts)
|
|
19
|
+
A2A.client(**opts) # → A2A::Client::Base.new(**opts)
|
|
20
|
+
A2A.sse_client(**opts) # → A2A::Client::SSE.new(**opts)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Constants
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
A2A::VERSION # => "0.1.0"
|
|
27
|
+
```
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Models
|
|
2
|
+
|
|
3
|
+
All model classes live under `A2A::Models` and inherit from `A2A::Models::Base`, which provides a lightweight attribute DSL with camelCase JSON serialization.
|
|
4
|
+
|
|
5
|
+
## Part
|
|
6
|
+
|
|
7
|
+
The atomic content unit in a message or artifact.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
A2A::Models::Part.text("hello") # text part
|
|
11
|
+
A2A::Models::Part.json({ key: "value" }) # JSON data part
|
|
12
|
+
A2A::Models::Part.from_url("https://…", media_type: "image/png") # URL reference
|
|
13
|
+
A2A::Models::Part.binary(File.binread("file.pdf"),
|
|
14
|
+
media_type: "application/pdf",
|
|
15
|
+
filename: "file.pdf") # base64-encoded binary
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Attribute | Type | Description |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `text` | String | Plain text content |
|
|
21
|
+
| `data` | Hash | Structured JSON data |
|
|
22
|
+
| `url` | String | URL reference |
|
|
23
|
+
| `raw` | String | Base64-encoded binary |
|
|
24
|
+
| `media_type` | String | MIME type |
|
|
25
|
+
| `filename` | String | Suggested filename |
|
|
26
|
+
| `metadata` | Hash | Arbitrary metadata |
|
|
27
|
+
|
|
28
|
+
**Predicates:** `#text?`, `#json?`, `#url?`, `#raw?`, `#valid?`
|
|
29
|
+
**Decoding:** `#decoded_bytes` → binary string (Base64 decode of `raw`)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Message
|
|
34
|
+
|
|
35
|
+
A message from a user or agent, composed of one or more Parts.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
A2A::Models::Message.user("hello") # user message with text part
|
|
39
|
+
A2A::Models::Message.user("prefix", Part.text("…")) # mixed content
|
|
40
|
+
A2A::Models::Message.agent("I understand") # agent message
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Attribute | Type | Required | Description |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `message_id` | String | auto | UUID, auto-generated |
|
|
46
|
+
| `role` | String | yes | `"user"` or `"agent"` |
|
|
47
|
+
| `parts` | [Part] | yes | Content parts |
|
|
48
|
+
| `context_id` | String | | Task context ID |
|
|
49
|
+
| `task_id` | String | | Task this message belongs to |
|
|
50
|
+
| `reference_task_ids` | [String] | | Related task IDs |
|
|
51
|
+
| `metadata` | Hash | | Arbitrary metadata |
|
|
52
|
+
|
|
53
|
+
**Predicates:** `#user?`, `#agent?`, `#valid?`
|
|
54
|
+
**Helper:** `#text_content` → concatenates all text parts with newlines
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Artifact
|
|
59
|
+
|
|
60
|
+
A structured output produced by an agent.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
A2A::Models::Artifact.new(
|
|
64
|
+
name: "report",
|
|
65
|
+
parts: [A2A::Models::Part.text("The answer is 42")]
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Attribute | Type | Description |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `name` | String | Artifact identifier |
|
|
72
|
+
| `description` | String | Human-readable description |
|
|
73
|
+
| `parts` | [Part] | Content parts |
|
|
74
|
+
| `index` | Integer | Position in artifact sequence |
|
|
75
|
+
| `append` | Boolean | True if this is a streaming chunk appended to a prior artifact |
|
|
76
|
+
| `last_chunk` | Boolean | True if this is the final streaming chunk |
|
|
77
|
+
| `metadata` | Hash | Arbitrary metadata |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Task
|
|
82
|
+
|
|
83
|
+
Represents a unit of work. Tasks are created by the server on each `tasks/send` call.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
task = A2A::Models::Task.new(
|
|
87
|
+
status: A2A::Models::TaskStatus.new(state: "submitted")
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Method | Description |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `task.start!` | Transition to `working` |
|
|
94
|
+
| `task.complete!(artifacts: […])` | Transition to `completed`, attach artifacts |
|
|
95
|
+
| `task.fail!(message: "…")` | Transition to `failed` |
|
|
96
|
+
| `task.cancel!` | Transition to `canceled` |
|
|
97
|
+
| `task.reject!(message: "…")` | Transition to `rejected` |
|
|
98
|
+
| `task.require_input!` | Transition to `input_required` |
|
|
99
|
+
| `task.require_auth!` | Transition to `auth_required` |
|
|
100
|
+
| `task.terminal?` | True if state is completed/failed/canceled/rejected |
|
|
101
|
+
| `task.interrupted?` | True if state is input_required/auth_required |
|
|
102
|
+
| `task.state` | Current state string |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## TaskStatus
|
|
107
|
+
|
|
108
|
+
Attached to every Task. Created automatically on transition.
|
|
109
|
+
|
|
110
|
+
| Attribute | Type | Description |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `state` | String | One of the `Types::TaskState` constants |
|
|
113
|
+
| `message` | Message | Optional status message |
|
|
114
|
+
| `timestamp` | String | ISO 8601, auto-set to `Time.now.iso8601` |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Types
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
A2A::Models::Types::TaskState::SUBMITTED # => "submitted"
|
|
122
|
+
A2A::Models::Types::TaskState::WORKING # => "working"
|
|
123
|
+
A2A::Models::Types::TaskState::COMPLETED # => "completed"
|
|
124
|
+
A2A::Models::Types::TaskState::FAILED # => "failed"
|
|
125
|
+
A2A::Models::Types::TaskState::CANCELED # => "canceled"
|
|
126
|
+
A2A::Models::Types::TaskState::REJECTED # => "rejected"
|
|
127
|
+
A2A::Models::Types::TaskState::INPUT_REQUIRED # => "input_required"
|
|
128
|
+
A2A::Models::Types::TaskState::AUTH_REQUIRED # => "auth_required"
|
|
129
|
+
|
|
130
|
+
A2A::Models::Types::TaskState.terminal?(state) # => true/false
|
|
131
|
+
A2A::Models::Types::TaskState.interrupted?(state) # => true/false
|
|
132
|
+
A2A::Models::Types::TaskState.active?(state) # => true/false
|
|
133
|
+
|
|
134
|
+
A2A::Models::Types::Role::USER # => "user"
|
|
135
|
+
A2A::Models::Types::Role::AGENT # => "agent"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## AgentCard
|
|
141
|
+
|
|
142
|
+
Describes an agent's identity and capabilities for discovery.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
card = A2A::Models::AgentCard.new(
|
|
146
|
+
name: "MyAgent",
|
|
147
|
+
version: "1.0",
|
|
148
|
+
description: "An example agent",
|
|
149
|
+
capabilities: A2A::Models::AgentCapabilities.new(streaming: true),
|
|
150
|
+
skills: [A2A::Models::AgentSkill.new(name: "greet")],
|
|
151
|
+
interfaces: [A2A::Models::AgentInterface.new(
|
|
152
|
+
type: "json-rpc", url: "http://localhost:9292", version: "1.0"
|
|
153
|
+
)]
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### AgentCapabilities
|
|
158
|
+
|
|
159
|
+
| Attribute | Type | Default | Description |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `streaming` | Boolean | false | Supports `tasks/sendSubscribe` |
|
|
162
|
+
| `push_notifications` | Boolean | false | Supports webhook push |
|
|
163
|
+
| `state_transition_history` | Boolean | false | Preserves task history |
|
|
164
|
+
|
|
165
|
+
### AgentSkill
|
|
166
|
+
|
|
167
|
+
| Attribute | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `name` | Skill identifier |
|
|
170
|
+
| `description` | Human-readable description |
|
|
171
|
+
| `tags` | Searchable tags |
|
|
172
|
+
| `examples` | Example prompts |
|
|
173
|
+
|
|
174
|
+
### AgentInterface
|
|
175
|
+
|
|
176
|
+
| Attribute | Description |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `type` | Binding type: `"json-rpc"`, `"http"`, `"grpc"` |
|
|
179
|
+
| `url` | Endpoint URL |
|
|
180
|
+
| `version` | Protocol version |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Events
|
|
185
|
+
|
|
186
|
+
Events are emitted during streaming (`tasks/sendSubscribe`) and via push notifications.
|
|
187
|
+
|
|
188
|
+
### TaskStatusUpdateEvent
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
A2A::Models::TaskStatusUpdateEvent.new(
|
|
192
|
+
task_id: task.id,
|
|
193
|
+
context_id: task.context_id,
|
|
194
|
+
status: task.status,
|
|
195
|
+
final: true
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### TaskArtifactUpdateEvent
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
A2A::Models::TaskArtifactUpdateEvent.new(
|
|
203
|
+
task_id: task.id,
|
|
204
|
+
context_id: task.context_id,
|
|
205
|
+
artifact: artifact,
|
|
206
|
+
append: false,
|
|
207
|
+
last_chunk: false
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Push notification models
|
|
214
|
+
|
|
215
|
+
### PushNotificationConfig
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
config = A2A::Models::PushNotificationConfig.new(
|
|
219
|
+
webhook_url: "https://example.com/hook",
|
|
220
|
+
authentication_info: A2A::Models::AuthenticationInfo.new(
|
|
221
|
+
scheme: "bearer"
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
config.valid? # => true
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### AuthenticationInfo
|
|
228
|
+
|
|
229
|
+
| Attribute | Description |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `scheme` | `"bearer"` (JWT), `"token"` (static), or custom |
|
|
232
|
+
| `value` | Token value (used for `"token"` scheme) |
|
|
233
|
+
| `header_name` | Override header name (default: `"Authorization"`) |
|