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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +192 -0
  6. data/Rakefile +13 -0
  7. data/docs/api/client/index.md +124 -0
  8. data/docs/api/index.md +27 -0
  9. data/docs/api/models/index.md +233 -0
  10. data/docs/api/server/index.md +162 -0
  11. data/docs/api/storage/index.md +84 -0
  12. data/docs/architecture/index.md +63 -0
  13. data/docs/architecture/protocol.md +112 -0
  14. data/docs/assets/css/custom.css +6 -0
  15. data/docs/examples/basic-usage.md +77 -0
  16. data/docs/examples/index.md +92 -0
  17. data/docs/examples/llm-research.md +92 -0
  18. data/docs/examples/streaming.md +81 -0
  19. data/docs/getting-started/installation.md +48 -0
  20. data/docs/getting-started/quick-start.md +100 -0
  21. data/docs/guides/custom-storage.md +69 -0
  22. data/docs/guides/push-notifications.md +104 -0
  23. data/docs/guides/streaming.md +75 -0
  24. data/docs/index.md +98 -0
  25. data/examples/01_basic_usage/client.rb +75 -0
  26. data/examples/01_basic_usage/server.rb +57 -0
  27. data/examples/02_streaming/client.rb +70 -0
  28. data/examples/02_streaming/server.rb +177 -0
  29. data/examples/03_llm_research/client.rb +138 -0
  30. data/examples/03_llm_research/run +82 -0
  31. data/examples/03_llm_research/server.rb +203 -0
  32. data/examples/03_llm_research/web_client.rb +501 -0
  33. data/examples/common_config.rb +4 -0
  34. data/examples/run +108 -0
  35. data/lib/simple_a2a/client/base.rb +101 -0
  36. data/lib/simple_a2a/client/sse.rb +58 -0
  37. data/lib/simple_a2a/errors.rb +15 -0
  38. data/lib/simple_a2a/json_rpc.rb +89 -0
  39. data/lib/simple_a2a/models/agent_capabilities.rb +11 -0
  40. data/lib/simple_a2a/models/agent_card.rb +23 -0
  41. data/lib/simple_a2a/models/agent_interface.rb +11 -0
  42. data/lib/simple_a2a/models/agent_provider.rb +11 -0
  43. data/lib/simple_a2a/models/agent_skill.rb +12 -0
  44. data/lib/simple_a2a/models/artifact.rb +23 -0
  45. data/lib/simple_a2a/models/authentication_info.rb +11 -0
  46. data/lib/simple_a2a/models/base.rb +111 -0
  47. data/lib/simple_a2a/models/message.rb +45 -0
  48. data/lib/simple_a2a/models/part.rb +45 -0
  49. data/lib/simple_a2a/models/push_notification_config.rb +17 -0
  50. data/lib/simple_a2a/models/security_scheme.rb +16 -0
  51. data/lib/simple_a2a/models/send_message_configuration.rb +12 -0
  52. data/lib/simple_a2a/models/stream_response.rb +32 -0
  53. data/lib/simple_a2a/models/task.rb +57 -0
  54. data/lib/simple_a2a/models/task_artifact_update_event.rb +21 -0
  55. data/lib/simple_a2a/models/task_status.rb +20 -0
  56. data/lib/simple_a2a/models/task_status_update_event.rb +19 -0
  57. data/lib/simple_a2a/models/types.rb +39 -0
  58. data/lib/simple_a2a/server/agent_executor.rb +16 -0
  59. data/lib/simple_a2a/server/app.rb +227 -0
  60. data/lib/simple_a2a/server/base.rb +43 -0
  61. data/lib/simple_a2a/server/context.rb +44 -0
  62. data/lib/simple_a2a/server/event_router.rb +50 -0
  63. data/lib/simple_a2a/server/falcon_runner.rb +31 -0
  64. data/lib/simple_a2a/server/multi_agent.rb +50 -0
  65. data/lib/simple_a2a/server/push_sender.rb +80 -0
  66. data/lib/simple_a2a/server/resume_context.rb +14 -0
  67. data/lib/simple_a2a/storage/base.rb +12 -0
  68. data/lib/simple_a2a/storage/memory.rb +41 -0
  69. data/lib/simple_a2a/version.rb +5 -0
  70. data/lib/simple_a2a.rb +49 -0
  71. data/mkdocs.yml +143 -0
  72. data/sig/simple_a2a.rbs +4 -0
  73. 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,6 @@
1
+ /* simple_a2a documentation custom styles */
2
+
3
+ :root {
4
+ --md-primary-fg-color: #1565C0;
5
+ --md-accent-fg-color: #F57F17;
6
+ }
@@ -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.