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,81 @@
1
+ # Streaming Demo
2
+
3
+ `examples/02_streaming` demonstrates `tasks/sendSubscribe` and Server-Sent Events (SSE). The server streams an article word by word, and the client prints chunks as they arrive.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |---|---|
9
+ | `examples/02_streaming/server.rb` | Defines `StreamingExecutor`, advertises streaming support, and emits status and artifact events |
10
+ | `examples/02_streaming/client.rb` | Uses `A2A.sse_client` and `send_subscribe` to consume streamed events |
11
+
12
+ ## Run it
13
+
14
+ From the repository root:
15
+
16
+ ```bash
17
+ bundle exec ruby examples/run 02_streaming
18
+ ```
19
+
20
+ Manual run:
21
+
22
+ ```bash
23
+ bundle exec ruby examples/02_streaming/server.rb
24
+ bundle exec ruby examples/02_streaming/client.rb
25
+ ```
26
+
27
+ ## Server behavior
28
+
29
+ The streaming executor starts the task, emits a working status, sends artifact chunks, completes the task, and emits a final status.
30
+
31
+ ```ruby
32
+ def call(ctx)
33
+ ctx.task.start!
34
+ ctx.emit_status
35
+
36
+ WORDS.each_with_index do |word, i|
37
+ text = i.zero? ? word : " #{word}"
38
+
39
+ artifact = A2A::Models::Artifact.new(
40
+ index: 0,
41
+ parts: [A2A::Models::Part.text(text)],
42
+ append: i > 0,
43
+ last_chunk: i == WORDS.length - 1
44
+ )
45
+
46
+ ctx.emit_artifact(artifact, append: i > 0, last_chunk: i == WORDS.length - 1)
47
+ end
48
+
49
+ ctx.task.complete!
50
+ ctx.emit_status(final: true)
51
+ end
52
+ ```
53
+
54
+ The agent card declares streaming support:
55
+
56
+ ```ruby
57
+ A2A::Models::AgentCapabilities.new(streaming: true)
58
+ ```
59
+
60
+ ## Client behavior
61
+
62
+ The client uses `A2A.sse_client` instead of `A2A.client`:
63
+
64
+ ```ruby
65
+ client = A2A.sse_client(url: "http://localhost:9292")
66
+
67
+ client.send_subscribe(message: A2A::Models::Message.user("stream")) do |event|
68
+ case event
69
+ when A2A::Models::TaskStatusUpdateEvent
70
+ # task state changed
71
+ when A2A::Models::TaskArtifactUpdateEvent
72
+ print event.artifact.parts.map(&:text).join
73
+ end
74
+ end
75
+ ```
76
+
77
+ It also tracks event count, word count, elapsed time, and effective words per minute, which makes it useful for checking end-to-end streaming behavior.
78
+
79
+ ## Relationship to the guide
80
+
81
+ The [Streaming Responses guide](../guides/streaming.md) explains the API in isolation. This demo shows the same flow as a runnable pair of scripts.
@@ -0,0 +1,48 @@
1
+ # Installation
2
+
3
+ ## Requirements
4
+
5
+ - Ruby 3.2 or higher (tested on Ruby 4.0)
6
+ - Bundler 2.x
7
+
8
+ ## Gemfile
9
+
10
+ ```ruby
11
+ gem "simple_a2a"
12
+ ```
13
+
14
+ Then run:
15
+
16
+ ```bash
17
+ bundle install
18
+ ```
19
+
20
+ ## Direct install
21
+
22
+ ```bash
23
+ gem install simple_a2a
24
+ ```
25
+
26
+ ## Dependencies
27
+
28
+ `simple_a2a` pulls in the following gems automatically:
29
+
30
+ | Gem | Purpose |
31
+ |---|---|
32
+ | `async` | Async fiber runtime |
33
+ | `async-http` | Non-blocking HTTP client (used by `Client::Base` and `Client::SSE`) |
34
+ | `falcon` | Async-native Rack HTTP server |
35
+ | `roda` | Rack router for the server endpoint |
36
+ | `rack` | WSGI-style Ruby web interface |
37
+ | `zeitwerk` | Autoloading |
38
+ | `jwt` | RS256 JWT signing for push notification webhooks |
39
+ | `logger` | Ruby standard logger (bundled gem in Ruby 4.0+) |
40
+ | `simple_flow` | Pipeline composition for executor chains |
41
+ | `typed_bus` | Per-task SSE event fan-out |
42
+
43
+ ## Verifying the install
44
+
45
+ ```ruby
46
+ require "simple_a2a"
47
+ puts A2A::VERSION # => "0.1.0"
48
+ ```
@@ -0,0 +1,100 @@
1
+ # Quick Start
2
+
3
+ This guide walks through building a minimal echo agent and a client that talks to it.
4
+
5
+ ## 1. Create the executor
6
+
7
+ An **executor** contains your agent's logic. Subclass `A2A::Server::AgentExecutor` and implement `#call`:
8
+
9
+ ```ruby
10
+ require "simple_a2a"
11
+
12
+ class EchoExecutor < A2A::Server::AgentExecutor
13
+ def call(ctx)
14
+ input = ctx.message.text_content
15
+ ctx.task.complete!(artifacts: [
16
+ A2A::Models::Artifact.new(
17
+ name: "reply",
18
+ parts: [A2A::Models::Part.text("Echo: #{input}")]
19
+ )
20
+ ])
21
+ end
22
+ end
23
+ ```
24
+
25
+ `ctx` is an `A2A::Server::Context` that gives you access to the incoming message, the task object, and helpers for emitting streaming events.
26
+
27
+ ## 2. Build the agent card
28
+
29
+ The **AgentCard** describes your agent to clients:
30
+
31
+ ```ruby
32
+ card = A2A::Models::AgentCard.new(
33
+ name: "EchoAgent",
34
+ version: "1.0",
35
+ capabilities: A2A::Models::AgentCapabilities.new,
36
+ skills: [A2A::Models::AgentSkill.new(name: "echo", description: "Echoes your input")],
37
+ interfaces: [A2A::Models::AgentInterface.new(
38
+ type: "json-rpc",
39
+ url: "http://localhost:9292",
40
+ version: "1.0"
41
+ )]
42
+ )
43
+ ```
44
+
45
+ ## 3. Start the server
46
+
47
+ ```ruby
48
+ server = A2A.server(agent_card: card, executor: EchoExecutor.new)
49
+ server.run # starts Falcon on localhost:9292
50
+ ```
51
+
52
+ Or with custom host/port:
53
+
54
+ ```ruby
55
+ server = A2A::Server::Base.new(
56
+ agent_card: card,
57
+ executor: EchoExecutor.new,
58
+ host: "0.0.0.0",
59
+ port: 8080
60
+ )
61
+ server.run
62
+ ```
63
+
64
+ ## 4. Send a task from a client
65
+
66
+ In a separate process or script:
67
+
68
+ ```ruby
69
+ require "simple_a2a"
70
+
71
+ client = A2A.client(url: "http://localhost:9292")
72
+
73
+ task = client.send_task(message: A2A::Models::Message.user("hello there"))
74
+ puts task.status.state # => "completed"
75
+ puts task.artifacts.first.parts.first.text # => "Echo: hello there"
76
+ ```
77
+
78
+ ## 5. Discover the agent card
79
+
80
+ ```ruby
81
+ card = client.agent_card
82
+ puts card.name # => "EchoAgent"
83
+ puts card.version # => "1.0"
84
+ ```
85
+
86
+ ## 6. List and retrieve tasks
87
+
88
+ ```ruby
89
+ tasks = client.list_tasks
90
+ task = client.get_task(tasks.first.id)
91
+ puts task.id
92
+ ```
93
+
94
+ ## Next steps
95
+
96
+ - [Architecture overview](../architecture/index.md) — understand the components
97
+ - [Streaming responses](../guides/streaming.md) — emit incremental SSE events
98
+ - [Runnable examples](../examples/index.md) - run the demo apps in `examples/`
99
+ - [Push notifications](../guides/push-notifications.md) — webhook delivery
100
+ - [API reference](../api/index.md) — full class and method docs
@@ -0,0 +1,69 @@
1
+ # Custom Storage
2
+
3
+ The default `Storage::Memory` is ephemeral — tasks are lost on restart. For production, implement `Storage::Base`.
4
+
5
+ ## Interface
6
+
7
+ ```ruby
8
+ class A2A::Storage::Base
9
+ def save(task) = raise NotImplementedError
10
+ def find(id) = raise NotImplementedError # nil if missing
11
+ def find!(id) = raise NotImplementedError # raises TaskNotFoundError if missing
12
+ def delete(id) = raise NotImplementedError
13
+ def list = raise NotImplementedError # returns Array
14
+ end
15
+ ```
16
+
17
+ ## Minimal example
18
+
19
+ ```ruby
20
+ class HashStorage < A2A::Storage::Base
21
+ def initialize
22
+ @store = {}
23
+ end
24
+
25
+ def save(task)
26
+ @store[task.id] = task
27
+ end
28
+
29
+ def find(id)
30
+ @store[id]
31
+ end
32
+
33
+ def find!(id)
34
+ @store.fetch(id) { raise A2A::TaskNotFoundError, "Task #{id} not found" }
35
+ end
36
+
37
+ def delete(id)
38
+ @store.delete(id)
39
+ end
40
+
41
+ def list
42
+ @store.values
43
+ end
44
+ end
45
+ ```
46
+
47
+ ## Serialization
48
+
49
+ `A2A::Models::Task` supports round-trip serialization via `to_h` and `from_hash`:
50
+
51
+ ```ruby
52
+ hash = task.to_h # => { "id" => "…", "status" => {…}, … }
53
+ task = A2A::Models::Task.from_hash(hash) # => reconstructed Task
54
+ ```
55
+
56
+ Use this for any storage backend that persists JSON (databases, Redis, files):
57
+
58
+ ```ruby
59
+ def save(task)
60
+ db.set(task.id, task.to_h.to_json)
61
+ task
62
+ end
63
+
64
+ def find(id)
65
+ raw = db.get(id)
66
+ return nil unless raw
67
+ A2A::Models::Task.from_hash(JSON.parse(raw))
68
+ end
69
+ ```
@@ -0,0 +1,104 @@
1
+ # Push Notifications
2
+
3
+ Push notifications let an A2A server deliver task events to a client-registered webhook, rather than requiring the client to maintain an open SSE connection.
4
+
5
+ ## Overview
6
+
7
+ 1. Client registers a webhook URL via `tasks/pushNotification/set`
8
+ 2. Server delivers `TaskStatusUpdateEvent` and `TaskArtifactUpdateEvent` to the URL
9
+ 3. Requests are signed with RS256 JWT (optional but recommended)
10
+
11
+ ## Server setup
12
+
13
+ Create a `PushSender` with an RS256 private key:
14
+
15
+ ```ruby
16
+ require "openssl"
17
+
18
+ private_key = OpenSSL::PKey::RSA.generate(2048)
19
+
20
+ push_sender = A2A::Server::PushSender.new(
21
+ private_key: private_key,
22
+ key_id: "my-key-2026",
23
+ issuer: "my-agent"
24
+ )
25
+
26
+ server = A2A::Server::Base.new(
27
+ agent_card: card,
28
+ executor: MyExecutor.new,
29
+ push_sender: push_sender
30
+ )
31
+ ```
32
+
33
+ Advertise push notification support in your AgentCard:
34
+
35
+ ```ruby
36
+ capabilities = A2A::Models::AgentCapabilities.new(
37
+ push_notifications: true
38
+ )
39
+ ```
40
+
41
+ ## Delivering events from your executor
42
+
43
+ Call `push_sender.deliver` with the stored `PushNotificationConfig` and an event:
44
+
45
+ ```ruby
46
+ class MyExecutor < A2A::Server::AgentExecutor
47
+ def initialize(push_sender:)
48
+ @push_sender = push_sender
49
+ end
50
+
51
+ def call(ctx)
52
+ ctx.task.complete!(artifacts: [ … ])
53
+ event = A2A::Models::TaskStatusUpdateEvent.new(
54
+ task_id: ctx.task.id,
55
+ context_id: ctx.task.context_id,
56
+ status: ctx.task.status,
57
+ final: true
58
+ )
59
+ config = # retrieve PushNotificationConfig registered by the client
60
+ @push_sender.deliver(config, event)
61
+ end
62
+ end
63
+ ```
64
+
65
+ `deliver` returns `true` on HTTP 2xx, `false` on failure. Failures are logged via `A2A.logger` but not raised.
66
+
67
+ ## Authentication schemes
68
+
69
+ ### Bearer (RS256 JWT)
70
+
71
+ A JWT is generated and sent as `Authorization: Bearer <token>`. The token payload includes:
72
+
73
+ ```json
74
+ {
75
+ "iss": "my-agent",
76
+ "iat": 1700000000,
77
+ "exp": 1700000300,
78
+ "payload_hash": "<SHA-256 hex of the request body>"
79
+ }
80
+ ```
81
+
82
+ ```ruby
83
+ auth = A2A::Models::AuthenticationInfo.new(scheme: "bearer")
84
+ ```
85
+
86
+ ### Static token
87
+
88
+ ```ruby
89
+ auth = A2A::Models::AuthenticationInfo.new(
90
+ scheme: "token",
91
+ value: "secret-webhook-token",
92
+ header_name: "X-Webhook-Token" # optional, defaults to "Authorization"
93
+ )
94
+ ```
95
+
96
+ Sent as `X-Webhook-Token: Token secret-webhook-token`.
97
+
98
+ ## PushSender without a private key
99
+
100
+ If no `private_key` is provided, the JWT token is the literal string `"no-key"`. This is useful for local development without RSA setup:
101
+
102
+ ```ruby
103
+ push_sender = A2A::Server::PushSender.new # no args
104
+ ```
@@ -0,0 +1,75 @@
1
+ # Streaming Responses
2
+
3
+ `tasks/sendSubscribe` keeps the HTTP connection open and streams events as your executor progresses. This is ideal for long-running tasks where the client needs incremental feedback.
4
+
5
+ ## Server side — emitting events
6
+
7
+ Use `ctx.emit_status` and `ctx.emit_artifact` inside your executor:
8
+
9
+ ```ruby
10
+ class StreamingExecutor < A2A::Server::AgentExecutor
11
+ def call(ctx)
12
+ ctx.task.start!
13
+ ctx.emit_status # publishes TaskStatusUpdateEvent(state: "working", final: false)
14
+
15
+ # Stream artifact chunks
16
+ ["Thinking… ", "Processing… ", "Done!"].each_with_index do |chunk, i|
17
+ last = i == 2
18
+ artifact = A2A::Models::Artifact.new(
19
+ index: 0,
20
+ parts: [A2A::Models::Part.text(chunk)],
21
+ append: i > 0, # true for chunks after the first
22
+ last_chunk: last
23
+ )
24
+ ctx.emit_artifact(artifact, append: i > 0, last_chunk: last)
25
+ end
26
+
27
+ ctx.task.complete!
28
+ ctx.emit_status(final: true) # signals end of stream
29
+ end
30
+ end
31
+ ```
32
+
33
+ ### Emit methods
34
+
35
+ | Method | Publishes | `final` |
36
+ |---|---|---|
37
+ | `ctx.emit_status` | `TaskStatusUpdateEvent` | pass `final: true` to close the stream |
38
+ | `ctx.emit_artifact(artifact, append:, last_chunk:)` | `TaskArtifactUpdateEvent` | always `false` |
39
+
40
+ Always emit `ctx.emit_status(final: true)` as your last event to close the SSE connection cleanly.
41
+
42
+ ---
43
+
44
+ ## Client side — consuming events
45
+
46
+ Use `Client::SSE#send_subscribe`:
47
+
48
+ ```ruby
49
+ client = A2A.sse_client(url: "http://localhost:9292")
50
+
51
+ client.send_subscribe(message: A2A::Models::Message.user("go")) do |event|
52
+ case event
53
+ when A2A::Models::TaskStatusUpdateEvent
54
+ puts "[status] #{event.status.state} final=#{event.final}"
55
+ break if event.final
56
+ when A2A::Models::TaskArtifactUpdateEvent
57
+ print event.artifact.parts.map(&:text).join
58
+ $stdout.flush
59
+ end
60
+ end
61
+ ```
62
+
63
+ The block is called for each parsed SSE event. Unrecognized event types yield a plain `Hash`.
64
+
65
+ ---
66
+
67
+ ## AgentCard declaration
68
+
69
+ Advertise streaming support in your AgentCard:
70
+
71
+ ```ruby
72
+ capabilities = A2A::Models::AgentCapabilities.new(streaming: true)
73
+ ```
74
+
75
+ Clients can check `card.capabilities.streaming` before using `send_subscribe`.
data/docs/index.md ADDED
@@ -0,0 +1,98 @@
1
+ # simple_a2a
2
+
3
+ A Ruby gem implementing the [Agent2Agent (A2A) protocol](https://a2a-protocol.org/latest/) — an open standard by Google and the Linux Foundation for interoperability between AI agents.
4
+
5
+ `simple_a2a` provides a complete A2A client and server in a single package, built on the async Ruby ecosystem with [Falcon](https://github.com/socketry/falcon) as the recommended HTTP server.
6
+
7
+ ---
8
+
9
+ ## What is A2A?
10
+
11
+ The Agent2Agent (A2A) protocol defines how AI agents running on different platforms, frameworks, and vendors can discover each other, exchange tasks, and stream results — without vendor lock-in.
12
+
13
+ - Agents expose a JSON-RPC 2.0 over HTTP endpoint
14
+ - Clients send tasks and receive structured results
15
+ - Streaming uses Server-Sent Events (SSE)
16
+ - Push notifications use webhooks (RS256 JWT)
17
+ - AgentCards describe capabilities and skills
18
+
19
+ **Protocol Reference:** [https://a2a-protocol.org/latest/](https://a2a-protocol.org/latest/)
20
+
21
+ ---
22
+
23
+ ## At a Glance
24
+
25
+ ```ruby
26
+ require "simple_a2a"
27
+
28
+ # 1. Implement your agent logic
29
+ class MyExecutor < A2A::Server::AgentExecutor
30
+ def call(ctx)
31
+ input = ctx.message.text_content
32
+ ctx.task.complete!(artifacts: [
33
+ A2A::Models::Artifact.new(
34
+ parts: [A2A::Models::Part.text("You said: #{input}")]
35
+ )
36
+ ])
37
+ end
38
+ end
39
+
40
+ # 2. Describe your agent
41
+ card = A2A::Models::AgentCard.new(
42
+ name: "MyAgent",
43
+ version: "1.0",
44
+ capabilities: A2A::Models::AgentCapabilities.new,
45
+ skills: [A2A::Models::AgentSkill.new(name: "reply")],
46
+ interfaces: [A2A::Models::AgentInterface.new(
47
+ type: "json-rpc", url: "http://localhost:9292", version: "1.0"
48
+ )]
49
+ )
50
+
51
+ # 3. Start the server
52
+ A2A.server(agent_card: card, executor: MyExecutor.new).run
53
+ ```
54
+
55
+ ```ruby
56
+ # Client — send a task to any A2A agent
57
+ client = A2A.client(url: "http://localhost:9292")
58
+ task = client.send_task(message: A2A::Models::Message.user("hello"))
59
+ puts task.status.state # => "completed"
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Features
65
+
66
+ | Feature | Details |
67
+ |---|---|
68
+ | Protocol | A2A v1.0, backward compatible with v0.3 |
69
+ | Transport | JSON-RPC 2.0 over HTTP(S) |
70
+ | Streaming | Server-Sent Events (SSE) |
71
+ | Push notifications | Webhooks with RS256 JWT signing |
72
+ | Task lifecycle | `submitted → working → completed/failed/canceled` |
73
+ | Discovery | AgentCard endpoint at `GET /agentCard` |
74
+ | Async runtime | `async` gem ecosystem — non-blocking I/O |
75
+ | HTTP server | Falcon (recommended), any Rack-compatible server |
76
+ | HTTP client | `async-http` (`Async::HTTP::Internet`) |
77
+ | Storage | In-memory (thread-safe); pluggable via `Storage::Base` |
78
+ | Routing | Roda with JSON-RPC dispatch |
79
+ | Autoloading | Zeitwerk |
80
+
81
+ ## Runnable demos
82
+
83
+ The repository includes three demo applications under `examples/`:
84
+
85
+ | Demo | Shows |
86
+ |---|---|
87
+ | `01_basic_usage` | Agent discovery, `tasks/send`, task listing, task lookup, and error handling |
88
+ | `02_streaming` | `tasks/sendSubscribe` with Server-Sent Events and incremental artifact chunks |
89
+ | `03_llm_research` | Multi-agent routing, parallel streaming LLM calls, evaluator agent, and a Sinatra web client |
90
+
91
+ Run the basic and streaming demos end-to-end with:
92
+
93
+ ```bash
94
+ bundle exec ruby examples/run 01_basic_usage
95
+ bundle exec ruby examples/run 02_streaming
96
+ ```
97
+
98
+ The LLM research demo requires `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and its demo-specific gems. See the [examples overview](examples/index.md) for setup details.
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Usage: bundle exec ruby examples/01_basic_usage/client.rb
5
+ #
6
+ # Start the server first:
7
+ # bundle exec ruby examples/01_basic_usage/server.rb
8
+
9
+ require_relative "../common_config"
10
+
11
+ URL = "http://localhost:9292"
12
+
13
+ client = A2A.client(url: URL)
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # 1. Discover the agent
17
+ # ---------------------------------------------------------------------------
18
+ puts "=== Agent Card ==="
19
+ card = client.agent_card
20
+ puts " Name: #{card.name}"
21
+ puts " Version: #{card.version}"
22
+ puts " Description: #{card.description}"
23
+ puts " Skills: #{card.skills.map(&:name).join(', ')}"
24
+ puts
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # 2. Send a few tasks
28
+ # ---------------------------------------------------------------------------
29
+ messages = [
30
+ "world",
31
+ "Ruby developers",
32
+ "A2A protocol"
33
+ ]
34
+
35
+ puts "=== Sending Tasks ==="
36
+ task_ids = messages.map do |text|
37
+ task = client.send_task(message: A2A::Models::Message.user(text))
38
+
39
+ reply = task.artifacts.first&.parts&.first&.text || "(no reply)"
40
+ puts " [#{task.status.state}] sent: #{text.inspect}"
41
+ puts " got: #{reply.inspect}"
42
+ task.id
43
+ end
44
+ puts
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # 3. List all tasks
48
+ # ---------------------------------------------------------------------------
49
+ puts "=== Task List ==="
50
+ all_tasks = client.list_tasks
51
+ all_tasks.each do |t|
52
+ puts " #{t.id} state=#{t.status.state}"
53
+ end
54
+ puts " Total: #{all_tasks.size}"
55
+ puts
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # 4. Retrieve a single task by ID
59
+ # ---------------------------------------------------------------------------
60
+ puts "=== Retrieve Task ==="
61
+ retrieved = client.get_task(task_ids.first)
62
+ puts " id: #{retrieved.id}"
63
+ puts " state: #{retrieved.status.state}"
64
+ puts " reply: #{retrieved.artifacts.first&.parts&.first&.text.inspect}"
65
+ puts
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # 5. Cancel a non-existent task (demonstrates error handling)
69
+ # ---------------------------------------------------------------------------
70
+ puts "=== Error Handling ==="
71
+ begin
72
+ client.get_task("no-such-task-id")
73
+ rescue A2A::Error => e
74
+ puts " Caught expected error: #{e.message}"
75
+ end