simple_a2a 0.1.0 → 0.3.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/README.md +78 -38
  4. data/compare_agent2agent.md +460 -0
  5. data/docs/api/client/index.md +19 -0
  6. data/docs/api/index.md +4 -3
  7. data/docs/api/models/index.md +13 -11
  8. data/docs/api/server/index.md +42 -10
  9. data/docs/api/storage/index.md +0 -1
  10. data/docs/architecture/index.md +17 -15
  11. data/docs/architecture/protocol.md +16 -1
  12. data/docs/assets/images/simple_a2a.jpg +0 -0
  13. data/docs/examples/agent-chaining.md +107 -0
  14. data/docs/examples/auth-headers.md +105 -0
  15. data/docs/examples/cancellation.md +105 -0
  16. data/docs/examples/index.md +123 -52
  17. data/docs/examples/interrupted-states.md +114 -0
  18. data/docs/examples/multipart.md +103 -0
  19. data/docs/examples/push-notifications.md +117 -0
  20. data/docs/examples/resubscribe.md +129 -0
  21. data/docs/examples/sqlite-storage.md +131 -0
  22. data/docs/examples/streaming.md +1 -4
  23. data/docs/guides/push-notifications.md +4 -1
  24. data/docs/guides/streaming.md +34 -5
  25. data/docs/index.md +55 -27
  26. data/examples/04_resubscribe/client.rb +140 -0
  27. data/examples/04_resubscribe/server.rb +75 -0
  28. data/examples/05_cancellation/client.rb +150 -0
  29. data/examples/05_cancellation/server.rb +77 -0
  30. data/examples/06_push_notifications/client.rb +192 -0
  31. data/examples/06_push_notifications/server.rb +123 -0
  32. data/examples/07_agent_chaining/client.rb +120 -0
  33. data/examples/07_agent_chaining/server.rb +150 -0
  34. data/examples/08_interrupted_states/client.rb +148 -0
  35. data/examples/08_interrupted_states/server.rb +142 -0
  36. data/examples/09_multipart/client.rb +117 -0
  37. data/examples/09_multipart/server.rb +97 -0
  38. data/examples/10_auth_headers/client.rb +92 -0
  39. data/examples/10_auth_headers/server.rb +98 -0
  40. data/examples/11_sqlite_storage/Brewfile +1 -0
  41. data/examples/11_sqlite_storage/Gemfile +9 -0
  42. data/examples/11_sqlite_storage/client.rb +114 -0
  43. data/examples/11_sqlite_storage/run +154 -0
  44. data/examples/11_sqlite_storage/server.rb +131 -0
  45. data/examples/README.md +384 -0
  46. data/lib/simple_a2a/client/sse.rb +15 -0
  47. data/lib/simple_a2a/server/app.rb +131 -45
  48. data/lib/simple_a2a/server/base.rb +19 -17
  49. data/lib/simple_a2a/server/broadcast_registry.rb +24 -0
  50. data/lib/simple_a2a/server/multi_agent.rb +1 -1
  51. data/lib/simple_a2a/server/push_config_store.rb +29 -0
  52. data/lib/simple_a2a/server/push_sender.rb +1 -0
  53. data/lib/simple_a2a/server/task_broadcast.rb +46 -0
  54. data/lib/simple_a2a/version.rb +1 -1
  55. metadata +38 -20
  56. data/lib/simple_a2a/server/event_router.rb +0 -50
@@ -6,17 +6,18 @@
6
6
  A2A
7
7
  ├── Models — data classes (Task, Message, Part, Artifact, AgentCard, …)
8
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)
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
+ │ ├── TaskBroadcast (RactorQueue-based SSE fan-out per running task)
15
+ │ ├── BroadcastRegistry (task_id TaskBroadcast map, shared across requests)
16
+ ├── PushSender (webhook delivery with JWT signing)
17
+ │ └── FalconRunner (Falcon adapter)
17
18
  ├── Client — async HTTP client
18
19
  │ ├── Base (JSON-RPC client over async/http)
19
- │ └── SSE (streaming subscribe client)
20
+ │ └── SSE (streaming subscribe + resubscribe client)
20
21
  ├── Storage — task persistence
21
22
  │ ├── Base (abstract interface)
22
23
  │ └── Memory (in-process, thread-safe)
@@ -33,21 +34,22 @@ App (Roda)
33
34
  │ JSON-RPC parse + dispatch
34
35
 
35
36
 
36
- handle_send
37
+ handle_send / handle_send_subscribe
37
38
  │ creates Task (submitted)
38
- │ builds Context
39
+ │ builds Context (with TaskBroadcast as event_router)
39
40
 
40
41
 
41
42
  Executor#call(ctx) ← your code lives here
42
43
  │ ctx.task.start!
43
- │ ctx.emit_artifact(…) → EventRouter → SSE subscribers
44
+ │ ctx.emit_artifact(…) → TaskBroadcastRactorQueue(s) → SSE subscriber(s)
44
45
  │ ctx.task.complete!(…)
45
46
 
46
47
 
47
48
  Storage#save(task)
48
49
 
49
50
 
50
- JsonRpc::Response (task hash) → HTTP response
51
+ JsonRpc::Response (task hash) → HTTP response [tasks/send]
52
+ SSE stream closes → HTTP body ends [tasks/sendSubscribe / tasks/resubscribe]
51
53
  ```
52
54
 
53
55
  ## Key design decisions
@@ -58,6 +60,6 @@ JsonRpc::Response (task hash) → HTTP response
58
60
 
59
61
  **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
62
 
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`.
63
+ **Pluggable storage** — `Storage::Base` defines the interface (`save`, `find`, `list`, `delete`). `find!` (raises on missing) is a convenience method on `Storage::Memory` only — not part of the required interface. Swap in Redis or PostgreSQL by subclassing and passing your implementation to `Server::Base`.
62
64
 
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!`.
65
+ **TaskBroadcast + BroadcastRegistry** — each streaming task gets one `TaskBroadcast`, which holds one `RactorQueue` per SSE subscriber. The broadcast is registered in `BroadcastRegistry` for the duration of the task so that `tasks/resubscribe` and `tasks/cancel` can locate it by task ID. `RactorQueue#async_push` / `#async_pop` cooperate with the Falcon fiber scheduler via `sleep(0)`, keeping the event loop non-blocking. Multiple concurrent subscribers (original `sendSubscribe` and any number of `resubscribe` clients) each receive every event independently.
@@ -42,6 +42,7 @@ All A2A communication uses **JSON-RPC 2.0 over HTTP POST** to a single endpoint
42
42
  |---|---|
43
43
  | `tasks/send` | Send a message, execute synchronously, return completed task |
44
44
  | `tasks/sendSubscribe` | Send a message, stream events via SSE |
45
+ | `tasks/resubscribe` | Attach an SSE stream to an existing running task |
45
46
  | `tasks/get` | Retrieve a task by ID |
46
47
  | `tasks/list` | List all tasks |
47
48
  | `tasks/cancel` | Cancel a non-terminal task |
@@ -99,7 +100,9 @@ Interrupted tasks can be resumed by sending a new message.
99
100
 
100
101
  ## Streaming (SSE)
101
102
 
102
- `tasks/sendSubscribe` keeps the HTTP connection open and streams `text/event-stream` events:
103
+ Both `tasks/sendSubscribe` and `tasks/resubscribe` keep the HTTP connection open and stream `text/event-stream` events.
104
+
105
+ **`tasks/sendSubscribe`** — creates a new task and immediately begins streaming:
103
106
 
104
107
  ```
105
108
  data: {"jsonrpc":"2.0","result":{"type":"TaskStatusUpdateEvent","taskId":"…","status":{"state":"working"},"final":false}}
@@ -109,4 +112,16 @@ data: {"jsonrpc":"2.0","result":{"type":"TaskArtifactUpdateEvent","taskId":"…"
109
112
  data: {"jsonrpc":"2.0","result":{"type":"TaskStatusUpdateEvent","taskId":"…","status":{"state":"completed"},"final":true}}
110
113
  ```
111
114
 
115
+ **`tasks/resubscribe`** — attaches to an existing running task. The first event is always the current Task snapshot (no `type` field); subsequent events are the live stream:
116
+
117
+ ```
118
+ data: {"jsonrpc":"2.0","result":{"id":"…","contextId":"…","status":{"state":"working",…},"artifacts":[]}}
119
+
120
+ data: {"jsonrpc":"2.0","result":{"type":"TaskArtifactUpdateEvent","taskId":"…","artifact":{…},"final":false}}
121
+
122
+ data: {"jsonrpc":"2.0","result":{"type":"TaskStatusUpdateEvent","taskId":"…","status":{"state":"completed"},"final":true}}
123
+ ```
124
+
125
+ `tasks/resubscribe` returns `UnsupportedOperationError` if the task is already in a terminal state or is not currently streaming. Multiple clients may subscribe to the same task concurrently.
126
+
112
127
  Events with `"final": true` signal that the stream is complete.
Binary file
@@ -0,0 +1,107 @@
1
+ # 07 Agent Chaining
2
+
3
+ **Run it:**
4
+
5
+ ```bash
6
+ bundle exec ruby examples/run 07_agent_chaining
7
+ ```
8
+
9
+ **What it shows:** one agent calling peer agents using `A2A.client` inside its executor — the same client interface an external caller would use.
10
+
11
+ ---
12
+
13
+ ## Files
14
+
15
+ | File | Purpose |
16
+ |---|---|
17
+ | `examples/07_agent_chaining/server.rb` | Three agents at `/reverse`, `/shout`, `/pipeline`; `PipelineExecutor` chains the other two |
18
+ | `examples/07_agent_chaining/client.rb` | External client that speaks only to `/pipeline`; the internal delegation is invisible to it |
19
+
20
+ ---
21
+
22
+ ## The scenario
23
+
24
+ The demo mounts three agents on one server:
25
+
26
+ | Path | Agent | What it does |
27
+ |---|---|---|
28
+ | `/reverse` | `ReverseAgent` | Returns the input string with characters reversed |
29
+ | `/shout` | `ShoutAgent` | Returns the input string uppercased with `!!!` appended |
30
+ | `/pipeline` | `PipelineAgent` | Calls `/reverse` then `/shout` internally and returns the final result |
31
+
32
+ The external client sends one message to `/pipeline`. The pipeline executor calls the other two agents in sequence using `A2A.client` and returns their combined output. The external client never learns that two additional A2A calls were made internally.
33
+
34
+ ---
35
+
36
+ ## Server — `PipelineExecutor`
37
+
38
+ The executor holds pre-built `A2A.client` instances pointing to its peer agents:
39
+
40
+ ```ruby
41
+ class PipelineExecutor < A2A::Server::AgentExecutor
42
+ def initialize(reverse_url:, shout_url:)
43
+ @reverse_client = A2A.client(url: reverse_url)
44
+ @shout_client = A2A.client(url: shout_url)
45
+ end
46
+
47
+ def call(ctx)
48
+ input = ctx.message.text_content.strip
49
+
50
+ reversed = @reverse_client
51
+ .send_task(message: A2A::Models::Message.user(input))
52
+ .artifacts.first&.parts&.first&.text
53
+
54
+ shouted = @shout_client
55
+ .send_task(message: A2A::Models::Message.user(reversed))
56
+ .artifacts.first&.parts&.first&.text
57
+
58
+ ctx.task.complete!(artifacts: [
59
+ A2A::Models::Artifact.new(
60
+ name: "result",
61
+ parts: [A2A::Models::Part.text(shouted)]
62
+ )
63
+ ])
64
+ end
65
+ end
66
+ ```
67
+
68
+ All three agents are mounted on one port via `A2A.multi_server`:
69
+
70
+ ```ruby
71
+ A2A.multi_server(
72
+ agents: {
73
+ "/reverse" => { agent_card: reverse_card, executor: ReverseExecutor.new },
74
+ "/shout" => { agent_card: shout_card, executor: ShoutExecutor.new },
75
+ "/pipeline" => { agent_card: pipeline_card, executor: pipeline_executor }
76
+ },
77
+ port: 9292
78
+ ).run
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Client flow
84
+
85
+ The external client discovers all three agent cards then calls the pipeline:
86
+
87
+ ```ruby
88
+ client = A2A.client(url: "http://localhost:9292/pipeline")
89
+ task = client.send_task(message: A2A::Models::Message.user("hello world"))
90
+ puts task.artifacts.first.parts.first.text
91
+ # => "DLROW OLLEH!!!"
92
+ ```
93
+
94
+ The client also fetches agent cards from all three paths to show they are independently discoverable.
95
+
96
+ ---
97
+
98
+ ## Protocol coverage
99
+
100
+ | Spec section | What the demo shows |
101
+ |---|---|
102
+ | Agent-to-agent delegation | An executor uses `A2A.client` to call peer agents during task execution |
103
+ | `A2A.multi_server` | Three agents hosted at `/reverse`, `/shout`, `/pipeline` on one port |
104
+ | Agent Card discovery | Client discovers all three cards; pipeline card describes its composed capability |
105
+ | `tasks/send` | Sub-agents called synchronously within the pipeline executor's fiber |
106
+ | Protocol transparency | Internal A2A calls use the same JSON-RPC wire format as external calls |
107
+ | Composability | Any agent can act as both a server (to its caller) and a client (to its dependencies) |
@@ -0,0 +1,105 @@
1
+ # 10 Auth Headers
2
+
3
+ **Run it:**
4
+
5
+ ```bash
6
+ bundle exec ruby examples/run 10_auth_headers
7
+ ```
8
+
9
+ **What it shows:** injecting custom HTTP headers (Bearer token) into every client request, and wrapping the server with Rack middleware to enforce them.
10
+
11
+ ---
12
+
13
+ ## Files
14
+
15
+ | File | Purpose |
16
+ |---|---|
17
+ | `examples/10_auth_headers/server.rb` | `BearerAuthMiddleware` wraps the standard `rack_app`; agent card stays public |
18
+ | `examples/10_auth_headers/client.rb` | Two clients — one without headers (rejected) and one with (accepted) |
19
+
20
+ ---
21
+
22
+ ## The scenario
23
+
24
+ The server enforces a Bearer token on all `POST /` (RPC) requests. `GET /agentCard` is deliberately left public so agents remain discoverable without credentials.
25
+
26
+ Two clients are created with the same URL:
27
+
28
+ ```ruby
29
+ unauth_client = A2A.client(url: URL)
30
+ auth_client = A2A.client(url: URL, headers: { "Authorization" => "Bearer #{TOKEN}" })
31
+ ```
32
+
33
+ - `unauth_client.agent_card` → succeeds (GET is public)
34
+ - `unauth_client.send_task(...)` → raises `A2A::Error` ("Unauthorized")
35
+ - `auth_client.send_task(...)` → succeeds
36
+
37
+ ---
38
+
39
+ ## Server — Rack middleware composition
40
+
41
+ The middleware pattern keeps the library unchanged:
42
+
43
+ ```ruby
44
+ class BearerAuthMiddleware
45
+ def initialize(app, token:)
46
+ @app = app
47
+ @token = token
48
+ end
49
+
50
+ def call(env)
51
+ if env["REQUEST_METHOD"] == "POST"
52
+ auth = env["HTTP_AUTHORIZATION"].to_s
53
+ unless auth == "Bearer #{@token}"
54
+ body = JSON.generate({
55
+ "jsonrpc" => "2.0", "id" => nil,
56
+ "error" => { "code" => -32_000, "message" => "Unauthorized: valid Bearer token required" }
57
+ })
58
+ return [200, { "Content-Type" => "application/json" }, [body]]
59
+ end
60
+ end
61
+ @app.call(env)
62
+ end
63
+ end
64
+
65
+ inner_app = A2A.server(agent_card: card, executor: SecureEchoExecutor.new).rack_app
66
+ auth_app = BearerAuthMiddleware.new(inner_app, token: VALID_TOKEN)
67
+ A2A::Server::FalconRunner.new(auth_app, port: 9292).run
68
+ ```
69
+
70
+ The middleware returns a JSON-RPC shaped error body (not a bare HTTP 401) so the `A2A::Error` rescue path on the client works without special-casing.
71
+
72
+ ---
73
+
74
+ ## The `headers:` option
75
+
76
+ `A2A.client(headers:)` and `A2A.sse_client(headers:)` accept any `Hash` of header name → value pairs, which are merged into every request:
77
+
78
+ ```ruby
79
+ # Bearer token
80
+ A2A.client(url: URL, headers: { "Authorization" => "Bearer secret" })
81
+
82
+ # API key
83
+ A2A.client(url: URL, headers: { "X-Api-Key" => "key123" })
84
+
85
+ # Multiple headers
86
+ A2A.client(url: URL, headers: {
87
+ "Authorization" => "Bearer token",
88
+ "X-Tenant-Id" => "acme"
89
+ })
90
+ ```
91
+
92
+ The same option applies to `A2A.sse_client` for streaming subscriptions.
93
+
94
+ ---
95
+
96
+ ## Protocol coverage
97
+
98
+ | Spec section | What the demo shows |
99
+ |---|---|
100
+ | `A2A.client(headers:)` | `headers: { "Authorization" => "Bearer token" }` appended to every request |
101
+ | `AgentCard` public discovery | `GET /agentCard` succeeds without authentication — agents are discoverable |
102
+ | Bearer token authentication | `Authorization: Bearer <token>` header checked on all POST (RPC) requests |
103
+ | Rack middleware composition | `BearerAuthMiddleware.new(rack_app, token:)` wraps the standard app without library changes |
104
+ | JSON-RPC error on rejection | Middleware returns a well-formed JSON-RPC error body so the client can rescue `A2A::Error` |
105
+ | Header flexibility | The same `headers:` option supports API keys, custom schemes, and any HTTP header |
@@ -0,0 +1,105 @@
1
+ # 05 Cancellation
2
+
3
+ **Run it:**
4
+
5
+ ```bash
6
+ bundle exec ruby examples/run 05_cancellation
7
+ ```
8
+
9
+ **What it shows:** how to cancel an in-flight task mid-execution while other concurrent tasks run to completion.
10
+
11
+ ---
12
+
13
+ ## Files
14
+
15
+ | File | Purpose |
16
+ |---|---|
17
+ | `examples/05_cancellation/server.rb` | `SlowExecutor` that runs 10 one-second steps and checks `ctx.task.terminal?` between each step |
18
+ | `examples/05_cancellation/client.rb` | Starts three concurrent SSE subscriptions, cancels the middle task after 3 s, verifies final states |
19
+
20
+ ---
21
+
22
+ ## The scenario
23
+
24
+ Three tasks (A, B, C) start simultaneously via `tasks/sendSubscribe`. Each runs a 10-step loop with 1-second pauses — a 10-second total runtime without intervention. After 3 seconds the client calls `tasks/cancel` on Task B. Task B transitions to `canceled`; Tasks A and C run to completion unaffected.
25
+
26
+ This demonstrates three protocol guarantees:
27
+
28
+ 1. **Mid-flight cancellation** — `tasks/cancel` interrupts a running task without touching sibling tasks.
29
+ 2. **Cooperative cancellation** — the executor checks `ctx.task.terminal?` between steps and exits cleanly when cancelled.
30
+ 3. **Terminal state isolation** — the `canceled` state is terminal; subsequent executor steps are skipped.
31
+
32
+ ---
33
+
34
+ ## Server — `SlowExecutor`
35
+
36
+ The executor emits one status event per step and checks for cancellation between steps:
37
+
38
+ ```ruby
39
+ class SlowExecutor < A2A::Server::AgentExecutor
40
+ def call(ctx)
41
+ ctx.task.start!
42
+ ctx.emit_status
43
+
44
+ 10.times do |i|
45
+ break if ctx.task.terminal? # exit early if cancelled
46
+ sleep 1
47
+ ctx.emit_artifact(A2A::Models::Artifact.new(
48
+ parts: [A2A::Models::Part.text("step #{i + 1}/10")]
49
+ ))
50
+ end
51
+
52
+ return if ctx.task.terminal?
53
+ ctx.task.complete!
54
+ ctx.emit_status(final: true)
55
+ end
56
+ end
57
+ ```
58
+
59
+ The `AgentCapabilities` declares `streaming: true` so clients know to use `tasks/sendSubscribe`.
60
+
61
+ ---
62
+
63
+ ## Client — concurrent tasks with mid-flight cancel
64
+
65
+ Three SSE subscriptions run in separate threads. Each thread captures its task ID from the first status event:
66
+
67
+ ```ruby
68
+ task_ids = {}
69
+ mutex = Mutex.new
70
+
71
+ threads = %w[A B C].map do |label|
72
+ Thread.new do
73
+ A2A.sse_client(url: URL).send_subscribe(message: ...) do |event|
74
+ if event.is_a?(A2A::Models::TaskStatusUpdateEvent)
75
+ mutex.synchronize { task_ids[label] ||= event.task_id }
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ sleep 3 until task_ids.key?("B")
82
+ client.cancel_task(task_ids["B"])
83
+ threads.each(&:join)
84
+ ```
85
+
86
+ After all threads finish, the client calls `client.get_task` for each ID and asserts the expected states:
87
+
88
+ ```
89
+ Task A: completed ✓
90
+ Task B: canceled ✓
91
+ Task C: completed ✓
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Protocol coverage
97
+
98
+ | Spec section | What the demo shows |
99
+ |---|---|
100
+ | `tasks/cancel` | Client sends a cancel request by task ID while the task is mid-execution |
101
+ | `canceled` terminal state | Task B transitions to `canceled`; its SSE stream receives a final status event and closes |
102
+ | Concurrent task isolation | Tasks A and C are unaffected by the cancellation of task B |
103
+ | `AgentExecutor#cancel` | Default implementation calls `task.cancel!` and emits a final status event |
104
+ | `TaskState` lifecycle | `submitted → working → canceled` vs `submitted → working → completed` |
105
+ | Cooperative cancellation | Executor checks `ctx.task.terminal?` between steps and exits early when cancelled |
@@ -1,52 +1,112 @@
1
1
  # Examples
2
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.
3
+ The `examples/` directory contains eleven 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
4
 
5
- <svg viewBox="0 0 900 330" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="examples-title examples-desc">
5
+ <svg viewBox="0 0 830 510" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="examples-title examples-desc">
6
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>
7
+ <desc id="examples-desc">Dark themed diagram showing all eleven example applications in a 3×4 grid with the A2A capabilities each demonstrates.</desc>
8
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"/>
9
+ <linearGradient id="eg-blue" x1="0" x2="1"><stop offset="0" stop-color="#38bdf8"/><stop offset="1" stop-color="#2563eb"/></linearGradient>
10
+ <linearGradient id="eg-green" x1="0" x2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#16a34a"/></linearGradient>
11
+ <linearGradient id="eg-amber" x1="0" x2="1"><stop offset="0" stop-color="#fbbf24"/><stop offset="1" stop-color="#f97316"/></linearGradient>
12
+ <linearGradient id="eg-violet" x1="0" x2="1"><stop offset="0" stop-color="#a78bfa"/><stop offset="1" stop-color="#7c3aed"/></linearGradient>
13
+ <linearGradient id="eg-teal" x1="0" x2="1"><stop offset="0" stop-color="#2dd4bf"/><stop offset="1" stop-color="#0891b2"/></linearGradient>
14
+ <linearGradient id="eg-rose" x1="0" x2="1"><stop offset="0" stop-color="#fb7185"/><stop offset="1" stop-color="#e11d48"/></linearGradient>
15
+ <filter id="eg-glow" x="-10%" y="-10%" width="120%" height="120%">
16
+ <feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000" flood-opacity="0.35"/>
23
17
  </filter>
24
18
  </defs>
25
- <g fill="none" stroke="#334155" stroke-width="2">
26
- <path d="M295 165H365"/>
27
- <path d="M535 165H605"/>
19
+
20
+ <!-- Row 0 -->
21
+ <g filter="url(#eg-glow)">
22
+ <rect x="15" y="15" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-blue)" stroke-width="2"/>
23
+ <rect x="285" y="15" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-green)" stroke-width="2"/>
24
+ <rect x="555" y="15" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-amber)" stroke-width="2"/>
25
+ </g>
26
+ <!-- Row 1 -->
27
+ <g filter="url(#eg-glow)">
28
+ <rect x="15" y="135" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-blue)" stroke-width="2"/>
29
+ <rect x="285" y="135" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-amber)" stroke-width="2"/>
30
+ <rect x="555" y="135" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-green)" stroke-width="2"/>
28
31
  </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"/>
32
+ <!-- Row 2 -->
33
+ <g filter="url(#eg-glow)">
34
+ <rect x="15" y="255" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-violet)" stroke-width="2"/>
35
+ <rect x="285" y="255" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-amber)" stroke-width="2"/>
36
+ <rect x="555" y="255" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-teal)" stroke-width="2"/>
33
37
  </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>
38
+ <!-- Row 3 -->
39
+ <g filter="url(#eg-glow)">
40
+ <rect x="15" y="375" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-rose)" stroke-width="2"/>
41
+ <rect x="285" y="375" width="260" height="110" rx="10" fill="#0f172a" stroke="url(#eg-violet)" stroke-width="2"/>
42
+ </g>
43
+
44
+ <g font-family="Inter, ui-sans-serif, system-ui, sans-serif">
45
+ <!-- 01 Basic Usage -->
46
+ <text x="30" y="47" fill="#e2e8f0" font-size="17" font-weight="700">01 Basic Usage</text>
47
+ <text x="30" y="67" fill="#93c5fd" font-size="12">JSON-RPC request/response</text>
48
+ <text x="30" y="85" fill="#cbd5e1" font-size="12">agent card discovery</text>
49
+ <text x="30" y="103" fill="#cbd5e1" font-size="12">tasks/send · get · list · errors</text>
50
+
51
+ <!-- 02 Streaming -->
52
+ <text x="300" y="47" fill="#e2e8f0" font-size="17" font-weight="700">02 Streaming</text>
53
+ <text x="300" y="67" fill="#86efac" font-size="12">tasks/sendSubscribe · SSE</text>
54
+ <text x="300" y="85" fill="#cbd5e1" font-size="12">working / completed status</text>
55
+ <text x="300" y="103" fill="#cbd5e1" font-size="12">incremental artifact chunks</text>
56
+
57
+ <!-- 03 LLM Research -->
58
+ <text x="570" y="47" fill="#e2e8f0" font-size="17" font-weight="700">03 LLM Research</text>
59
+ <text x="570" y="67" fill="#fcd34d" font-size="12">multi-agent · multi_server</text>
60
+ <text x="570" y="85" fill="#cbd5e1" font-size="12">parallel SSE · evaluator</text>
61
+ <text x="570" y="103" fill="#cbd5e1" font-size="12">CLI + Sinatra web client</text>
62
+
63
+ <!-- 04 Resubscribe -->
64
+ <text x="30" y="167" fill="#e2e8f0" font-size="17" font-weight="700">04 Resubscribe</text>
65
+ <text x="30" y="187" fill="#93c5fd" font-size="12">tasks/resubscribe</text>
66
+ <text x="30" y="205" fill="#cbd5e1" font-size="12">mid-stream join · snapshot</text>
67
+ <text x="30" y="223" fill="#cbd5e1" font-size="12">concurrent subscribers</text>
68
+
69
+ <!-- 05 Cancellation -->
70
+ <text x="300" y="167" fill="#e2e8f0" font-size="17" font-weight="700">05 Cancellation</text>
71
+ <text x="300" y="187" fill="#fcd34d" font-size="12">tasks/cancel</text>
72
+ <text x="300" y="205" fill="#cbd5e1" font-size="12">concurrent tasks · lifecycle</text>
73
+ <text x="300" y="223" fill="#cbd5e1" font-size="12">cooperative cancellation</text>
74
+
75
+ <!-- 06 Push Notifications -->
76
+ <text x="570" y="167" fill="#e2e8f0" font-size="17" font-weight="700">06 Push Notifications</text>
77
+ <text x="570" y="187" fill="#86efac" font-size="12">pushNotification/set/get/del</text>
78
+ <text x="570" y="205" fill="#cbd5e1" font-size="12">webhook delivery · PushSender</text>
79
+ <text x="570" y="223" fill="#cbd5e1" font-size="12">out-of-band events</text>
80
+
81
+ <!-- 07 Agent Chaining -->
82
+ <text x="30" y="287" fill="#e2e8f0" font-size="17" font-weight="700">07 Agent Chaining</text>
83
+ <text x="30" y="307" fill="#c4b5fd" font-size="12">A2A.client inside executor</text>
84
+ <text x="30" y="325" fill="#cbd5e1" font-size="12">agent-to-agent delegation</text>
85
+ <text x="30" y="343" fill="#cbd5e1" font-size="12">composable pipelines</text>
86
+
87
+ <!-- 08 Interrupted States -->
88
+ <text x="300" y="287" fill="#e2e8f0" font-size="17" font-weight="700">08 Interrupted States</text>
89
+ <text x="300" y="307" fill="#fcd34d" font-size="12">input_required · auth_required</text>
90
+ <text x="300" y="325" fill="#cbd5e1" font-size="12">multi-turn conversations</text>
91
+ <text x="300" y="343" fill="#cbd5e1" font-size="12">message context_id threading</text>
92
+
93
+ <!-- 09 Multipart -->
94
+ <text x="570" y="287" fill="#e2e8f0" font-size="17" font-weight="700">09 Multipart</text>
95
+ <text x="570" y="307" fill="#5eead4" font-size="12">text · json · binary · url</text>
96
+ <text x="570" y="325" fill="#cbd5e1" font-size="12">Part predicates · base64</text>
97
+ <text x="570" y="343" fill="#cbd5e1" font-size="12">multi-type artifact</text>
98
+
99
+ <!-- 10 Auth Headers -->
100
+ <text x="30" y="407" fill="#e2e8f0" font-size="17" font-weight="700">10 Auth Headers</text>
101
+ <text x="30" y="427" fill="#fda4af" font-size="12">A2A.client(headers:)</text>
102
+ <text x="30" y="445" fill="#cbd5e1" font-size="12">Bearer token middleware</text>
103
+ <text x="30" y="463" fill="#cbd5e1" font-size="12">Rack middleware composition</text>
104
+
105
+ <!-- 11 SQLite Storage -->
106
+ <text x="300" y="407" fill="#e2e8f0" font-size="17" font-weight="700">11 SQLite Storage</text>
107
+ <text x="300" y="427" fill="#c4b5fd" font-size="12">Storage::Base injection</text>
108
+ <text x="300" y="445" fill="#cbd5e1" font-size="12">WAL persistence · Brewfile</text>
109
+ <text x="300" y="463" fill="#cbd5e1" font-size="12">cross-restart task survival</text>
50
110
  </g>
51
111
  </svg>
52
112
 
@@ -56,7 +116,8 @@ From the repository root:
56
116
 
57
117
  ```bash
58
118
  bundle exec ruby examples/run 01_basic_usage
59
- bundle exec ruby examples/run 02_streaming
119
+ bundle exec ruby examples/run 05_cancellation
120
+ bundle exec ruby examples/run 11_sqlite_storage
60
121
  ```
61
122
 
62
123
  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.
@@ -68,25 +129,35 @@ bundle exec ruby examples/01_basic_usage/server.rb
68
129
  bundle exec ruby examples/01_basic_usage/client.rb
69
130
  ```
70
131
 
71
- ## Demo-specific dependencies
132
+ Some demos (`03_llm_research`, `11_sqlite_storage`) have custom `run` scripts that manage a more complex lifecycle; the top-level launcher detects and delegates to them automatically.
72
133
 
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:
134
+ ## Demo-specific dependencies
74
135
 
75
- ```bash
76
- bundle add ruby_llm async-http-faraday sinatra
77
- ```
136
+ Most demos run with the gem's standard development setup (`bundle install`).
78
137
 
79
- Then set API keys:
138
+ **Demo 03 — LLM Research** requires LLM provider API keys and additional gems not in the gem's runtime dependency list:
80
139
 
81
140
  ```bash
141
+ bundle add ruby_llm async-http-faraday sinatra
82
142
  export ANTHROPIC_API_KEY=your_key_here
83
143
  export OPENAI_API_KEY=your_key_here
144
+ bundle exec ruby examples/run 03_llm_research
84
145
  ```
85
146
 
147
+ **Demo 11 — SQLite Storage** has its own `Gemfile` and `Brewfile`. The `run` script installs the sqlite3 binary (via Homebrew on macOS) and runs `bundle install` with the local Gemfile before starting the server. No manual setup is required.
148
+
86
149
  ## Demo index
87
150
 
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) |
151
+ | # | Demo | Run command | Documentation |
152
+ |---|---|---|---|
153
+ | 01 | Basic Usage | `examples/run 01_basic_usage` | [Basic Usage](basic-usage.md) |
154
+ | 02 | Streaming | `examples/run 02_streaming` | [Streaming](streaming.md) |
155
+ | 03 | LLM Research | `examples/run 03_llm_research` | [LLM Research](llm-research.md) |
156
+ | 04 | Resubscribe | `examples/run 04_resubscribe` | [Resubscribe](resubscribe.md) |
157
+ | 05 | Cancellation | `examples/run 05_cancellation` | [Cancellation](cancellation.md) |
158
+ | 06 | Push Notifications | `examples/run 06_push_notifications` | [Push Notifications](push-notifications.md) |
159
+ | 07 | Agent Chaining | `examples/run 07_agent_chaining` | [Agent Chaining](agent-chaining.md) |
160
+ | 08 | Interrupted States | `examples/run 08_interrupted_states` | [Interrupted States](interrupted-states.md) |
161
+ | 09 | Multipart Artifacts | `examples/run 09_multipart` | [Multipart](multipart.md) |
162
+ | 10 | Auth Headers | `examples/run 10_auth_headers` | [Auth Headers](auth-headers.md) |
163
+ | 11 | SQLite Storage | `examples/run 11_sqlite_storage` | [SQLite Storage](sqlite-storage.md) |