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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Server API
|
|
2
|
+
|
|
3
|
+
## Server::Base
|
|
4
|
+
|
|
5
|
+
The server entry point. Creates and wires all server components, then runs Falcon.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
server = A2A::Server::Base.new(
|
|
9
|
+
agent_card: card, # A2A::Models::AgentCard (required)
|
|
10
|
+
executor: MyExecutor.new, # A2A::Server::AgentExecutor subclass (required)
|
|
11
|
+
storage: A2A::Storage::Memory.new, # default
|
|
12
|
+
push_sender: nil, # A2A::Server::PushSender instance, optional
|
|
13
|
+
host: "localhost", # default
|
|
14
|
+
port: 9292 # default
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
server.run # blocks — starts Falcon
|
|
18
|
+
server.rack_app # returns the Rack app (useful for embedding in other servers)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Convenience factory:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
server = A2A.server(agent_card: card, executor: MyExecutor.new)
|
|
25
|
+
server.run
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Server::MultiAgent
|
|
31
|
+
|
|
32
|
+
Hosts multiple A2A agents in one Falcon process by mounting each agent at its own URL path. Use this when you want independent AgentCards, executors, storage, and SSE channels behind one port.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
server = A2A.multi_server(
|
|
36
|
+
agents: {
|
|
37
|
+
"/anthropic" => { agent_card: anthropic_card, executor: AnthropicExecutor.new },
|
|
38
|
+
"/openai" => { agent_card: openai_card, executor: OpenAIExecutor.new },
|
|
39
|
+
"/evaluator" => { agent_card: evaluator_card, executor: EvaluatorExecutor.new }
|
|
40
|
+
},
|
|
41
|
+
host: "localhost",
|
|
42
|
+
port: 9292
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
server.run
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Each entry in `agents` accepts the same core configuration used by `Server::Base`:
|
|
49
|
+
|
|
50
|
+
| Key | Required | Description |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `:agent_card` | Yes | AgentCard returned by that path's `/agentCard` endpoint |
|
|
53
|
+
| `:executor` | Yes | Executor that handles requests for that path |
|
|
54
|
+
| `:storage` | No | Storage backend for that path; defaults to `A2A::Storage::Memory.new` |
|
|
55
|
+
| `:event_router` | No | SSE event router for that path; defaults to a new router |
|
|
56
|
+
| `:push_sender` | No | Push notification sender for that path |
|
|
57
|
+
|
|
58
|
+
For a runnable example, see the [Multi-Agent LLM Research demo](../../examples/llm-research.md).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Server::AgentExecutor
|
|
63
|
+
|
|
64
|
+
Base class for your agent logic. Subclass and implement `#call`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class MyExecutor < A2A::Server::AgentExecutor
|
|
68
|
+
def call(ctx)
|
|
69
|
+
# ctx is an A2A::Server::Context
|
|
70
|
+
input = ctx.message.text_content
|
|
71
|
+
ctx.task.start!
|
|
72
|
+
# … do work …
|
|
73
|
+
ctx.task.complete!(artifacts: [ … ])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Optional: handle task cancellation
|
|
77
|
+
def cancel(ctx)
|
|
78
|
+
# default implementation calls ctx.task.cancel! and emits a final status event
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`#call` runs synchronously inside the Falcon reactor. Long-running work should use `Async::Task` internally to stay non-blocking.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Server::Context
|
|
89
|
+
|
|
90
|
+
Passed to `AgentExecutor#call`. Provides access to the request and helper methods.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
ctx.task # => A2A::Models::Task
|
|
94
|
+
ctx.message # => A2A::Models::Message (the incoming message)
|
|
95
|
+
ctx.storage # => A2A::Storage::Base
|
|
96
|
+
ctx.event_router # => A2A::Server::EventRouter
|
|
97
|
+
ctx.config # => Hash (arbitrary per-request config, default {})
|
|
98
|
+
|
|
99
|
+
ctx.save_task # persists task to storage
|
|
100
|
+
ctx.emit_status(final: false) # publishes TaskStatusUpdateEvent
|
|
101
|
+
ctx.emit_artifact(artifact, append: false, last_chunk: false) # publishes TaskArtifactUpdateEvent
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Server::ResumeContext
|
|
107
|
+
|
|
108
|
+
A `Context` subclass for resumed tasks (after `input_required` or `auth_required`).
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Additional attribute:
|
|
112
|
+
ctx.resume_message # => A2A::Models::Message (the new input from the user)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Server::EventRouter
|
|
118
|
+
|
|
119
|
+
Manages per-task SSE channels using `TypedBus`. You rarely interact with this directly — use `ctx.emit_status` and `ctx.emit_artifact` instead.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
router = A2A::Server::EventRouter.new
|
|
123
|
+
router.open(task_id) # creates a channel
|
|
124
|
+
router.publish(task_id, event) # sends an event to subscribers
|
|
125
|
+
router.subscribe(task_id) { |event| … } # block receives raw event objects
|
|
126
|
+
router.close(task_id) # removes the channel
|
|
127
|
+
router.channel?(task_id) # => true/false
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Server::PushSender
|
|
133
|
+
|
|
134
|
+
Delivers webhook push notifications.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
sender = A2A::Server::PushSender.new(
|
|
138
|
+
private_key: OpenSSL::PKey::RSA.generate(2048), # for JWT signing
|
|
139
|
+
key_id: "my-key-id",
|
|
140
|
+
issuer: "my-agent"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
sender.deliver(push_config, event) # => true (success) or false (failure)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Schemes:
|
|
147
|
+
|
|
148
|
+
- `"bearer"` — signs a JWT with `RS256` and sends `Authorization: Bearer <token>`
|
|
149
|
+
- `"token"` — sends the static value as `Authorization: Token <value>` (or custom header)
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Server::App
|
|
154
|
+
|
|
155
|
+
The Roda-based Rack application. You don't instantiate this directly — `Server::Base` configures and freezes it.
|
|
156
|
+
|
|
157
|
+
**Routes:**
|
|
158
|
+
|
|
159
|
+
| Method | Path | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `GET` | `/agentCard` | Returns the AgentCard as JSON |
|
|
162
|
+
| `POST` | `/` | JSON-RPC 2.0 dispatch |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Storage API
|
|
2
|
+
|
|
3
|
+
## Storage::Base
|
|
4
|
+
|
|
5
|
+
Abstract interface. Subclass this to implement custom storage backends.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class MyStorage < A2A::Storage::Base
|
|
9
|
+
def save(task) = … # persist, return task
|
|
10
|
+
def find(id) = … # return task or nil
|
|
11
|
+
def find!(id) = … # return task or raise A2A::TaskNotFoundError
|
|
12
|
+
def delete(id) = … # remove task
|
|
13
|
+
def list = … # return array of all tasks
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Pass your custom storage to `Server::Base`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
A2A::Server::Base.new(
|
|
21
|
+
agent_card: card,
|
|
22
|
+
executor: MyExecutor.new,
|
|
23
|
+
storage: MyStorage.new
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Storage::Memory
|
|
30
|
+
|
|
31
|
+
Thread-safe in-process hash store backed by a `Mutex`. Sufficient for single-process servers. Data is lost on restart.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
storage = A2A::Storage::Memory.new
|
|
35
|
+
|
|
36
|
+
storage.save(task) # => task
|
|
37
|
+
storage.find("task-id") # => task or nil
|
|
38
|
+
storage.find!("task-id") # => task or raises A2A::TaskNotFoundError
|
|
39
|
+
storage.delete("task-id") # => removed task or nil
|
|
40
|
+
storage.list # => [task, …]
|
|
41
|
+
storage.size # => Integer
|
|
42
|
+
storage.clear # clears all tasks
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
All methods acquire a mutex lock — safe for concurrent Falcon fibers.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Custom storage example (Redis sketch)
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
require "redis"
|
|
53
|
+
|
|
54
|
+
class RedisStorage < A2A::Storage::Base
|
|
55
|
+
def initialize(redis: Redis.new)
|
|
56
|
+
@redis = redis
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def save(task)
|
|
60
|
+
@redis.set("a2a:task:#{task.id}", task.to_h.to_json)
|
|
61
|
+
task
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def find(id)
|
|
65
|
+
raw = @redis.get("a2a:task:#{id}")
|
|
66
|
+
return nil unless raw
|
|
67
|
+
A2A::Models::Task.from_hash(JSON.parse(raw))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find!(id)
|
|
71
|
+
find(id) or raise A2A::TaskNotFoundError, "Task #{id} not found"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete(id)
|
|
75
|
+
@redis.del("a2a:task:#{id}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def list
|
|
79
|
+
keys = @redis.keys("a2a:task:*")
|
|
80
|
+
return [] if keys.empty?
|
|
81
|
+
@redis.mget(*keys).compact.map { |raw| A2A::Models::Task.from_hash(JSON.parse(raw)) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
`simple_a2a` is organized into three top-level namespaces under the `A2A` module:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
A2A
|
|
7
|
+
├── Models — data classes (Task, Message, Part, Artifact, AgentCard, …)
|
|
8
|
+
├── Server — HTTP server, routing, executor base, event fan-out
|
|
9
|
+
│ ├── App (Roda JSON-RPC router)
|
|
10
|
+
│ ├── Base (server bootstrap + Falcon runner)
|
|
11
|
+
│ ├── AgentExecutor (base class for your agent logic)
|
|
12
|
+
│ ├── Context (per-request helper passed to executor)
|
|
13
|
+
│ ├── ResumeContext (Context + resume_message for interrupted tasks)
|
|
14
|
+
│ ├── EventRouter (TypedBus SSE fan-out)
|
|
15
|
+
│ ├── PushSender (webhook delivery with JWT signing)
|
|
16
|
+
│ └── FalconRunner (Falcon adapter)
|
|
17
|
+
├── Client — async HTTP client
|
|
18
|
+
│ ├── Base (JSON-RPC client over async/http)
|
|
19
|
+
│ └── SSE (streaming subscribe client)
|
|
20
|
+
├── Storage — task persistence
|
|
21
|
+
│ ├── Base (abstract interface)
|
|
22
|
+
│ └── Memory (in-process, thread-safe)
|
|
23
|
+
└── JsonRpc — JSON-RPC 2.0 layer (request/response/error)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Request lifecycle
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Client POST /
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
App (Roda)
|
|
33
|
+
│ JSON-RPC parse + dispatch
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
handle_send
|
|
37
|
+
│ creates Task (submitted)
|
|
38
|
+
│ builds Context
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
Executor#call(ctx) ← your code lives here
|
|
42
|
+
│ ctx.task.start!
|
|
43
|
+
│ ctx.emit_artifact(…) → EventRouter → SSE subscribers
|
|
44
|
+
│ ctx.task.complete!(…)
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
Storage#save(task)
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
JsonRpc::Response (task hash) → HTTP response
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Key design decisions
|
|
54
|
+
|
|
55
|
+
**Zeitwerk autoloading** — all files under `lib/simple_a2a/` load on demand. The top-level module is `A2A` (not `SimpleA2a`), achieved via a custom Zeitwerk inflector.
|
|
56
|
+
|
|
57
|
+
**Async-first** — the server runs inside Falcon's async reactor. The client uses `Async::HTTP::Internet` and wraps calls in `Async { }.wait` when invoked outside a reactor, keeping the public API synchronous.
|
|
58
|
+
|
|
59
|
+
**One executor instance** — `Server::Base` holds a single executor object. For concurrent requests, executors must be stateless (or use per-call state inside `#call`).
|
|
60
|
+
|
|
61
|
+
**Pluggable storage** — `Storage::Base` defines the interface (`save`, `find!`, `list`, `delete`). Swap in Redis or PostgreSQL by subclassing and passing your implementation to `Server::Base`.
|
|
62
|
+
|
|
63
|
+
**EventRouter** — wraps `TypedBus::MessageBus` to provide per-task SSE channels. Channels are opened on first publish and closed when the SSE connection ends. `subscribe` transparently unwraps `TypedBus::Delivery` and calls `ack!`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Protocol Details
|
|
2
|
+
|
|
3
|
+
## Transport
|
|
4
|
+
|
|
5
|
+
All A2A communication uses **JSON-RPC 2.0 over HTTP POST** to a single endpoint (the server root `/`). The AgentCard is served at `GET /agentCard`.
|
|
6
|
+
|
|
7
|
+
### Request format
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"jsonrpc": "2.0",
|
|
12
|
+
"id": "abc-123",
|
|
13
|
+
"method": "tasks/send",
|
|
14
|
+
"params": {
|
|
15
|
+
"message": {
|
|
16
|
+
"messageId": "msg-1",
|
|
17
|
+
"role": "user",
|
|
18
|
+
"parts": [{ "text": "hello", "kind": "text" }]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Response format
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"jsonrpc": "2.0",
|
|
29
|
+
"id": "abc-123",
|
|
30
|
+
"result": {
|
|
31
|
+
"id": "task-uuid",
|
|
32
|
+
"contextId": "ctx-uuid",
|
|
33
|
+
"status": { "state": "completed", "timestamp": "2026-01-01T00:00:00Z" },
|
|
34
|
+
"artifacts": [ ... ]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Methods
|
|
40
|
+
|
|
41
|
+
| Method | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `tasks/send` | Send a message, execute synchronously, return completed task |
|
|
44
|
+
| `tasks/sendSubscribe` | Send a message, stream events via SSE |
|
|
45
|
+
| `tasks/get` | Retrieve a task by ID |
|
|
46
|
+
| `tasks/list` | List all tasks |
|
|
47
|
+
| `tasks/cancel` | Cancel a non-terminal task |
|
|
48
|
+
| `tasks/pushNotification/set` | Register a push notification endpoint |
|
|
49
|
+
| `tasks/pushNotification/get` | Retrieve push notification config |
|
|
50
|
+
| `tasks/pushNotification/delete` | Remove push notification config |
|
|
51
|
+
| `tasks/pushNotification/list` | List push notification configs |
|
|
52
|
+
|
|
53
|
+
## Error codes
|
|
54
|
+
|
|
55
|
+
| Code | Constant | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| -32700 | `PARSE_ERROR` | Invalid JSON |
|
|
58
|
+
| -32600 | `INVALID_REQUEST` | Not a valid JSON-RPC 2.0 object |
|
|
59
|
+
| -32601 | `METHOD_NOT_FOUND` | Unknown method |
|
|
60
|
+
| -32602 | `INVALID_PARAMS` | Missing or invalid parameters |
|
|
61
|
+
| -32603 | `INTERNAL_ERROR` | Unhandled server error |
|
|
62
|
+
| -32001 | `TASK_NOT_FOUND` | No task with the given ID |
|
|
63
|
+
| -32002 | `TASK_NOT_CANCELABLE` | Task is already in a terminal state |
|
|
64
|
+
| -32003 | `PUSH_NOT_SUPPORTED` | Agent doesn't support push notifications |
|
|
65
|
+
| -32004 | `UNSUPPORTED_OPERATION` | Operation not supported (e.g. SSE on non-streaming agent) |
|
|
66
|
+
| -32005 | `CONTENT_TYPE_NOT_SUPPORTED` | |
|
|
67
|
+
| -32006 | `INVALID_AGENT_RESPONSE` | |
|
|
68
|
+
| -32007 | `EXTENSION_REQUIRED` | |
|
|
69
|
+
| -32008 | `VERSION_NOT_SUPPORTED` | Unsupported A2A-Version header value |
|
|
70
|
+
|
|
71
|
+
## Version negotiation
|
|
72
|
+
|
|
73
|
+
Clients may include an `A2A-Version` header. The server accepts `1.0` and `0.3`. Any other value returns a `VERSION_NOT_SUPPORTED` error.
|
|
74
|
+
|
|
75
|
+
```http
|
|
76
|
+
POST / HTTP/1.1
|
|
77
|
+
A2A-Version: 1.0
|
|
78
|
+
Content-Type: application/json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Task lifecycle
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
submitted
|
|
85
|
+
│
|
|
86
|
+
▼
|
|
87
|
+
working
|
|
88
|
+
│
|
|
89
|
+
├──→ completed (terminal)
|
|
90
|
+
├──→ failed (terminal)
|
|
91
|
+
├──→ canceled (terminal)
|
|
92
|
+
├──→ rejected (terminal)
|
|
93
|
+
├──→ input_required (interrupted — waiting for user)
|
|
94
|
+
└──→ auth_required (interrupted — waiting for credentials)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Terminal tasks cannot be canceled (`TASK_NOT_CANCELABLE`).
|
|
98
|
+
Interrupted tasks can be resumed by sending a new message.
|
|
99
|
+
|
|
100
|
+
## Streaming (SSE)
|
|
101
|
+
|
|
102
|
+
`tasks/sendSubscribe` keeps the HTTP connection open and streams `text/event-stream` events:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
data: {"jsonrpc":"2.0","result":{"type":"TaskStatusUpdateEvent","taskId":"…","status":{"state":"working"},"final":false}}
|
|
106
|
+
|
|
107
|
+
data: {"jsonrpc":"2.0","result":{"type":"TaskArtifactUpdateEvent","taskId":"…","artifact":{…},"final":false}}
|
|
108
|
+
|
|
109
|
+
data: {"jsonrpc":"2.0","result":{"type":"TaskStatusUpdateEvent","taskId":"…","status":{"state":"completed"},"final":true}}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Events with `"final": true` signal that the stream is complete.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Basic Usage Demo
|
|
2
|
+
|
|
3
|
+
`examples/01_basic_usage` is the smallest complete client/server demo. It shows the non-streaming JSON-RPC flow for an A2A agent.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `examples/01_basic_usage/server.rb` | Defines `BasicExecutor`, builds the `BasicAgent` card, and starts the A2A server |
|
|
10
|
+
| `examples/01_basic_usage/client.rb` | Discovers the agent, sends tasks, lists tasks, retrieves a task, and handles an expected error |
|
|
11
|
+
|
|
12
|
+
## Run it
|
|
13
|
+
|
|
14
|
+
From the repository root:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle exec ruby examples/run 01_basic_usage
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The launcher starts the server on `http://localhost:9292`, runs the client, and stops the server afterward.
|
|
21
|
+
|
|
22
|
+
Manual run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle exec ruby examples/01_basic_usage/server.rb
|
|
26
|
+
bundle exec ruby examples/01_basic_usage/client.rb
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Server behavior
|
|
30
|
+
|
|
31
|
+
`BasicExecutor` subclasses `A2A::Server::AgentExecutor` and implements `#call(ctx)`. It reads the incoming user message with `ctx.message.text_content`, prepends a random greeting, and completes the task with one text artifact.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class BasicExecutor < A2A::Server::AgentExecutor
|
|
35
|
+
GREETINGS = %w[Hello Greetings Salutations Hey Howdy].freeze
|
|
36
|
+
|
|
37
|
+
def call(ctx)
|
|
38
|
+
input = ctx.message.text_content.strip
|
|
39
|
+
reply = "#{GREETINGS.sample}: #{input}"
|
|
40
|
+
|
|
41
|
+
ctx.task.complete!(artifacts: [
|
|
42
|
+
A2A::Models::Artifact.new(
|
|
43
|
+
name: "reply",
|
|
44
|
+
parts: [A2A::Models::Part.text(reply)]
|
|
45
|
+
)
|
|
46
|
+
])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The server advertises one skill named `greet` and one JSON-RPC interface at `http://localhost:9292`.
|
|
52
|
+
|
|
53
|
+
## Client flow
|
|
54
|
+
|
|
55
|
+
The client demonstrates the core client API:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
client = A2A.client(url: "http://localhost:9292")
|
|
59
|
+
|
|
60
|
+
card = client.agent_card
|
|
61
|
+
task = client.send_task(message: A2A::Models::Message.user("world"))
|
|
62
|
+
tasks = client.list_tasks
|
|
63
|
+
retrieved = client.get_task(task.id)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It also calls `client.get_task("no-such-task-id")` and rescues `A2A::Error`, which is a compact example of handling protocol or server errors from a client.
|
|
67
|
+
|
|
68
|
+
## What to study next
|
|
69
|
+
|
|
70
|
+
Use this demo when learning the minimal server shape:
|
|
71
|
+
|
|
72
|
+
| Concept | Where to look |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Executor contract | `BasicExecutor#call` in `server.rb` |
|
|
75
|
+
| Agent discovery | `client.agent_card` in `client.rb` |
|
|
76
|
+
| Task submission | `client.send_task` in `client.rb` |
|
|
77
|
+
| Storage-backed task lookup | `client.list_tasks` and `client.get_task` |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
The `examples/` directory contains runnable demo applications that exercise the gem from a client and server process. Each demo uses `examples/common_config.rb`, which adds the repository `lib/` directory to `$LOAD_PATH` before requiring `simple_a2a`, so the examples run against the local checkout.
|
|
4
|
+
|
|
5
|
+
<svg viewBox="0 0 900 330" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="examples-title examples-desc">
|
|
6
|
+
<title id="examples-title">simple_a2a example applications</title>
|
|
7
|
+
<desc id="examples-desc">Dark themed transparent-background diagram showing the three example applications and the A2A capabilities they demonstrate.</desc>
|
|
8
|
+
<defs>
|
|
9
|
+
<linearGradient id="blue" x1="0" x2="1">
|
|
10
|
+
<stop offset="0" stop-color="#38bdf8"/>
|
|
11
|
+
<stop offset="1" stop-color="#2563eb"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="green" x1="0" x2="1">
|
|
14
|
+
<stop offset="0" stop-color="#34d399"/>
|
|
15
|
+
<stop offset="1" stop-color="#16a34a"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<linearGradient id="amber" x1="0" x2="1">
|
|
18
|
+
<stop offset="0" stop-color="#fbbf24"/>
|
|
19
|
+
<stop offset="1" stop-color="#f97316"/>
|
|
20
|
+
</linearGradient>
|
|
21
|
+
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
22
|
+
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#000000" flood-opacity="0.35"/>
|
|
23
|
+
</filter>
|
|
24
|
+
</defs>
|
|
25
|
+
<g fill="none" stroke="#334155" stroke-width="2">
|
|
26
|
+
<path d="M295 165H365"/>
|
|
27
|
+
<path d="M535 165H605"/>
|
|
28
|
+
</g>
|
|
29
|
+
<g filter="url(#glow)">
|
|
30
|
+
<rect x="45" y="65" width="250" height="200" rx="14" fill="#0f172a" stroke="url(#blue)" stroke-width="2"/>
|
|
31
|
+
<rect x="325" y="65" width="250" height="200" rx="14" fill="#0f172a" stroke="url(#green)" stroke-width="2"/>
|
|
32
|
+
<rect x="605" y="65" width="250" height="200" rx="14" fill="#0f172a" stroke="url(#amber)" stroke-width="2"/>
|
|
33
|
+
</g>
|
|
34
|
+
<g font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">
|
|
35
|
+
<text x="70" y="108" fill="#e2e8f0" font-size="24" font-weight="700">01 Basic Usage</text>
|
|
36
|
+
<text x="70" y="145" fill="#93c5fd" font-size="16">JSON-RPC request/response</text>
|
|
37
|
+
<text x="70" y="180" fill="#cbd5e1" font-size="15">agent card discovery</text>
|
|
38
|
+
<text x="70" y="205" fill="#cbd5e1" font-size="15">send, list, and get tasks</text>
|
|
39
|
+
<text x="70" y="230" fill="#cbd5e1" font-size="15">client error handling</text>
|
|
40
|
+
<text x="350" y="108" fill="#e2e8f0" font-size="24" font-weight="700">02 Streaming</text>
|
|
41
|
+
<text x="350" y="145" fill="#86efac" font-size="16">SSE task subscription</text>
|
|
42
|
+
<text x="350" y="180" fill="#cbd5e1" font-size="15">working/final statuses</text>
|
|
43
|
+
<text x="350" y="205" fill="#cbd5e1" font-size="15">append artifact chunks</text>
|
|
44
|
+
<text x="350" y="230" fill="#cbd5e1" font-size="15">incremental client output</text>
|
|
45
|
+
<text x="630" y="108" fill="#e2e8f0" font-size="24" font-weight="700">03 LLM Research</text>
|
|
46
|
+
<text x="630" y="145" fill="#fcd34d" font-size="16">multi-agent orchestration</text>
|
|
47
|
+
<text x="630" y="180" fill="#cbd5e1" font-size="15">Anthropic + OpenAI agents</text>
|
|
48
|
+
<text x="630" y="205" fill="#cbd5e1" font-size="15">evaluator agent</text>
|
|
49
|
+
<text x="630" y="230" fill="#cbd5e1" font-size="15">CLI and web clients</text>
|
|
50
|
+
</g>
|
|
51
|
+
</svg>
|
|
52
|
+
|
|
53
|
+
## Run a demo
|
|
54
|
+
|
|
55
|
+
From the repository root:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bundle exec ruby examples/run 01_basic_usage
|
|
59
|
+
bundle exec ruby examples/run 02_streaming
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The launcher starts the demo server on `http://localhost:9292`, waits for it to accept connections, runs the demo client, and then shuts the server down.
|
|
63
|
+
|
|
64
|
+
To run a demo manually, start its `server.rb` in one terminal and its `client.rb` in another:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle exec ruby examples/01_basic_usage/server.rb
|
|
68
|
+
bundle exec ruby examples/01_basic_usage/client.rb
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Demo-specific dependencies
|
|
72
|
+
|
|
73
|
+
The basic and streaming demos use only the gem and its normal development setup. The LLM research demo intentionally keeps its LLM and web UI dependencies out of the gem runtime dependency list. Install the demo-specific gems before running it:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bundle add ruby_llm async-http-faraday sinatra
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then set API keys:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export ANTHROPIC_API_KEY=your_key_here
|
|
83
|
+
export OPENAI_API_KEY=your_key_here
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Demo index
|
|
87
|
+
|
|
88
|
+
| Demo | Command | Documentation |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| Basic Usage | `bundle exec ruby examples/run 01_basic_usage` | [Basic Usage](basic-usage.md) |
|
|
91
|
+
| Streaming | `bundle exec ruby examples/run 02_streaming` | [Streaming](streaming.md) |
|
|
92
|
+
| Multi-Agent LLM Research | `bundle exec ruby examples/run 03_llm_research` | [Multi-Agent LLM Research](llm-research.md) |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Multi-Agent LLM Research Demo
|
|
2
|
+
|
|
3
|
+
`examples/03_llm_research` demonstrates a multi-agent A2A server with two streaming research agents and one evaluator agent. It includes both a CLI client and a Sinatra web client.
|
|
4
|
+
|
|
5
|
+
## What it runs
|
|
6
|
+
|
|
7
|
+
| Path | Agent | Model |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `http://localhost:9292/anthropic` | `AnthropicResearchAgent` | `claude-sonnet-4-6` |
|
|
10
|
+
| `http://localhost:9292/openai` | `OpenAIResearchAgent` | `gpt-5.4` |
|
|
11
|
+
| `http://localhost:9292/evaluator` | `EvaluatorAgent` | `claude-sonnet-4-6` |
|
|
12
|
+
| `http://localhost:4567` | Sinatra web UI | Streams both responses and the evaluation to the browser |
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
| File | Purpose |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `examples/03_llm_research/server.rb` | Configures RubyLLM, defines three executors, and starts a path-routed multi-agent A2A server |
|
|
19
|
+
| `examples/03_llm_research/client.rb` | CLI client that queries both research agents in parallel and sends both responses to the evaluator |
|
|
20
|
+
| `examples/03_llm_research/web_client.rb` | Sinatra UI that streams both research responses and the evaluator response to the browser |
|
|
21
|
+
| `examples/03_llm_research/run` | Lifecycle script that starts the A2A server and web client together |
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
The demo uses LLM and web UI libraries that are intentionally not runtime dependencies of the `simple_a2a` gem. Add them to your local bundle before running the demo:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle add ruby_llm async-http-faraday sinatra
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Set both provider keys:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export ANTHROPIC_API_KEY=your_key_here
|
|
35
|
+
export OPENAI_API_KEY=your_key_here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Run the web demo
|
|
39
|
+
|
|
40
|
+
From the repository root:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle exec ruby examples/run 03_llm_research
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The custom runner starts:
|
|
47
|
+
|
|
48
|
+
| Service | URL |
|
|
49
|
+
|---|---|
|
|
50
|
+
| A2A multi-agent server | `http://localhost:9292` |
|
|
51
|
+
| Web client | `http://localhost:4567` |
|
|
52
|
+
|
|
53
|
+
Open `http://localhost:4567`, enter a topic, and the page streams output from both research agents followed by the evaluator.
|
|
54
|
+
|
|
55
|
+
## Run the CLI client
|
|
56
|
+
|
|
57
|
+
Start the multi-agent server:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bundle exec ruby examples/03_llm_research/server.rb
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then run the CLI client from another terminal:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bundle exec ruby examples/03_llm_research/client.rb "compare the practical tradeoffs of A2A and MCP"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If no topic is supplied, the client uses its built-in default topic.
|
|
70
|
+
|
|
71
|
+
## Server design
|
|
72
|
+
|
|
73
|
+
The server uses `A2A.multi_server` to mount multiple agents under one Falcon process:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
A2A.multi_server(
|
|
77
|
+
agents: {
|
|
78
|
+
"/anthropic" => { agent_card: anthropic_card, executor: AnthropicResearchExecutor.new },
|
|
79
|
+
"/openai" => { agent_card: openai_card, executor: OpenAIResearchExecutor.new },
|
|
80
|
+
"/evaluator" => { agent_card: evaluator_card, executor: EvaluatorExecutor.new }
|
|
81
|
+
},
|
|
82
|
+
port: 9292
|
|
83
|
+
).run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each executor includes a shared streaming helper that calls RubyLLM and emits `TaskArtifactUpdateEvent` chunks while the model response arrives.
|
|
87
|
+
|
|
88
|
+
## Web client design
|
|
89
|
+
|
|
90
|
+
The web client exposes a browser-facing SSE endpoint at `/research`. Internally it opens A2A SSE subscriptions to the Anthropic and OpenAI agents in parallel, buffers both complete responses, then streams the evaluator response back to the browser.
|
|
91
|
+
|
|
92
|
+
This is useful as a reference for bridging A2A streaming into another application protocol while keeping the A2A agents independently addressable.
|