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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+
5
+ module A2A
6
+ module Server
7
+ # Hosts multiple A2A agents on a single server, each at its own URL path.
8
+ #
9
+ # Usage:
10
+ # A2A.multi_server(
11
+ # agents: {
12
+ # "/anthropic" => { agent_card: card1, executor: exec1 },
13
+ # "/openai" => { agent_card: card2, executor: exec2 }
14
+ # },
15
+ # port: 9292
16
+ # ).run
17
+ class MultiAgent
18
+ def initialize(agents:, host: "localhost", port: 9292)
19
+ @agents = agents
20
+ @host = host
21
+ @port = port
22
+ end
23
+
24
+ def run
25
+ FalconRunner.new(rack_app, host: @host, port: @port).run
26
+ end
27
+
28
+ private
29
+
30
+ def rack_app
31
+ url_map = @agents.transform_values { |cfg| build_app(cfg) }
32
+ Rack::URLMap.new(url_map)
33
+ end
34
+
35
+ # Each agent needs its own App subclass so class-level configure state
36
+ # doesn't bleed between agents.
37
+ def build_app(cfg)
38
+ klass = Class.new(App)
39
+ klass.configure(
40
+ agent_card: cfg[:agent_card],
41
+ storage: cfg[:storage] || Storage::Memory.new,
42
+ executor: cfg[:executor],
43
+ event_router: cfg[:event_router] || EventRouter.new,
44
+ push_sender: cfg[:push_sender]
45
+ )
46
+ klass.freeze.app
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module A2A
8
+ module Server
9
+ class PushSender
10
+ SUPPORTED_SCHEMES = %w[bearer token].freeze
11
+
12
+ def initialize(private_key: nil, key_id: nil, issuer: "simple_a2a")
13
+ @private_key = private_key
14
+ @key_id = key_id
15
+ @issuer = issuer
16
+ end
17
+
18
+ def deliver(config, event)
19
+ return unless config.is_a?(Models::PushNotificationConfig)
20
+ return unless config.valid?
21
+
22
+ payload = build_payload(event)
23
+ headers = build_headers(config, payload)
24
+ post(config.webhook_url, payload, headers)
25
+ rescue StandardError => e
26
+ A2A.logger&.warn("PushSender: delivery failed to #{config&.webhook_url} — #{e.class}: #{e.message}")
27
+ false
28
+ end
29
+
30
+ private
31
+
32
+ def build_payload(event)
33
+ JSON.generate(event.to_h)
34
+ end
35
+
36
+ def build_headers(config, payload)
37
+ headers = { "Content-Type" => "application/json" }
38
+ auth = config.authentication_info
39
+ return headers unless auth
40
+
41
+ case auth.scheme.to_s.downcase
42
+ when "bearer"
43
+ token = jwt_token(payload)
44
+ headers[auth.header_name || "Authorization"] = "Bearer #{token}"
45
+ when "token"
46
+ headers[auth.header_name || "Authorization"] = "Token #{auth.value}"
47
+ else
48
+ headers[auth.header_name || "Authorization"] = auth.value
49
+ end
50
+
51
+ headers
52
+ end
53
+
54
+ def jwt_token(payload)
55
+ return "no-key" unless @private_key
56
+
57
+ claims = {
58
+ iss: @issuer,
59
+ iat: Time.now.to_i,
60
+ exp: Time.now.to_i + 300,
61
+ payload_hash: Digest::SHA256.hexdigest(payload)
62
+ }
63
+ JWT.encode(claims, @private_key, "RS256", { kid: @key_id }.compact)
64
+ end
65
+
66
+ def post(url, body, headers)
67
+ uri = URI.parse(url)
68
+ http = Net::HTTP.new(uri.host, uri.port)
69
+ http.use_ssl = uri.scheme == "https"
70
+ http.open_timeout = 5
71
+ http.read_timeout = 10
72
+
73
+ request = Net::HTTP::Post.new(uri.request_uri, headers)
74
+ request.body = body
75
+ response = http.request(request)
76
+ response.code.to_i < 300
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ module Server
5
+ class ResumeContext < Context
6
+ attr_reader :resume_message
7
+
8
+ def initialize(resume_message:, **kwargs)
9
+ super(**kwargs)
10
+ @resume_message = resume_message
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ module Storage
5
+ class Base
6
+ def save(task) = raise NotImplementedError, "#{self.class}#save"
7
+ def find(id) = raise NotImplementedError, "#{self.class}#find"
8
+ def delete(id) = raise NotImplementedError, "#{self.class}#delete"
9
+ def list = raise NotImplementedError, "#{self.class}#list"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ module Storage
5
+ class Memory < Base
6
+ def initialize
7
+ @store = {}
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def save(task)
12
+ @mutex.synchronize { @store[task.id] = task }
13
+ task
14
+ end
15
+
16
+ def find(id)
17
+ @mutex.synchronize { @store[id] }
18
+ end
19
+
20
+ def find!(id)
21
+ find(id) or raise TaskNotFoundError, "Task #{id} not found"
22
+ end
23
+
24
+ def delete(id)
25
+ @mutex.synchronize { @store.delete(id) }
26
+ end
27
+
28
+ def list
29
+ @mutex.synchronize { @store.values.dup }
30
+ end
31
+
32
+ def size
33
+ @mutex.synchronize { @store.size }
34
+ end
35
+
36
+ def clear
37
+ @mutex.synchronize { @store.clear }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module A2A
4
+ VERSION = "0.1.0"
5
+ end
data/lib/simple_a2a.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "json"
5
+ require "securerandom"
6
+ require "time"
7
+ require "uri"
8
+ require "base64"
9
+
10
+ module A2A
11
+ class << self
12
+ attr_accessor :logger
13
+
14
+ def server(**opts)
15
+ Server::Base.new(**opts)
16
+ end
17
+
18
+ def multi_server(**opts)
19
+ Server::MultiAgent.new(**opts)
20
+ end
21
+
22
+ def client(**opts)
23
+ Client::Base.new(**opts)
24
+ end
25
+
26
+ def sse_client(**opts)
27
+ Client::SSE.new(**opts)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Require files that don't follow Zeitwerk naming conventions first
33
+ require_relative "simple_a2a/version"
34
+ require_relative "simple_a2a/errors"
35
+
36
+ loader = Zeitwerk::Loader.for_gem
37
+ loader.inflector.inflect(
38
+ "simple_a2a" => "A2A",
39
+ "sse" => "SSE"
40
+ )
41
+ loader.ignore(
42
+ "#{__dir__}/simple_a2a/version.rb",
43
+ "#{__dir__}/simple_a2a/errors.rb",
44
+ "#{__dir__}/simple_a2a/json_rpc.rb"
45
+ )
46
+ loader.setup
47
+
48
+ # json_rpc.rb depends on error classes; load after errors + loader setup
49
+ require_relative "simple_a2a/json_rpc"
data/mkdocs.yml ADDED
@@ -0,0 +1,143 @@
1
+ site_name: simple_a2a
2
+ site_description: Ruby gem implementing the Agent2Agent (A2A) protocol
3
+ site_author: Dewayne VanHoozer
4
+ site_url: https://madbomber.github.io/simple_a2a
5
+ copyright: Copyright &copy; 2026 Dewayne VanHoozer
6
+
7
+ repo_name: MadBomber/simple_a2a
8
+ repo_url: https://github.com/MadBomber/simple_a2a
9
+ edit_uri: edit/main/docs/
10
+
11
+ theme:
12
+ name: material
13
+
14
+ palette:
15
+ - scheme: default
16
+ primary: blue
17
+ accent: amber
18
+ toggle:
19
+ icon: material/brightness-7
20
+ name: Switch to dark mode
21
+
22
+ - scheme: slate
23
+ primary: blue
24
+ accent: amber
25
+ toggle:
26
+ icon: material/brightness-4
27
+ name: Switch to light mode
28
+
29
+ font:
30
+ text: Roboto
31
+ code: Roboto Mono
32
+
33
+ icon:
34
+ repo: fontawesome/brands/github
35
+ logo: material/connection
36
+
37
+ features:
38
+ - navigation.instant
39
+ - navigation.tracking
40
+ - navigation.tabs
41
+ - navigation.tabs.sticky
42
+ - navigation.path
43
+ - navigation.indexes
44
+ - navigation.top
45
+ - navigation.footer
46
+ - toc.follow
47
+ - search.suggest
48
+ - search.highlight
49
+ - search.share
50
+ - header.autohide
51
+ - content.code.copy
52
+ - content.code.annotate
53
+ - content.tabs.link
54
+ - content.tooltips
55
+ - content.action.edit
56
+ - content.action.view
57
+
58
+ plugins:
59
+ - search:
60
+ separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
61
+ - tags
62
+
63
+ markdown_extensions:
64
+ - abbr
65
+ - admonition
66
+ - attr_list
67
+ - def_list
68
+ - footnotes
69
+ - md_in_html
70
+ - tables
71
+ - toc:
72
+ permalink: true
73
+ title: On this page
74
+ - pymdownx.betterem:
75
+ smart_enable: all
76
+ - pymdownx.caret
77
+ - pymdownx.critic
78
+ - pymdownx.details
79
+ - pymdownx.emoji:
80
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
81
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
82
+ - pymdownx.highlight:
83
+ anchor_linenums: true
84
+ line_spans: __span
85
+ pygments_lang_class: true
86
+ - pymdownx.inlinehilite
87
+ - pymdownx.keys
88
+ - pymdownx.magiclink:
89
+ repo_url_shorthand: true
90
+ user: MadBomber
91
+ repo: simple_a2a
92
+ normalize_issue_symbols: true
93
+ - pymdownx.mark
94
+ - pymdownx.smartsymbols
95
+ - pymdownx.superfences:
96
+ custom_fences:
97
+ - name: mermaid
98
+ class: mermaid
99
+ format: !!python/name:pymdownx.superfences.fence_code_format
100
+ - pymdownx.tabbed:
101
+ alternate_style: true
102
+ - pymdownx.tasklist:
103
+ custom_checkbox: true
104
+ - pymdownx.tilde
105
+
106
+ extra_css:
107
+ - assets/css/custom.css
108
+
109
+ extra:
110
+ social:
111
+ - icon: fontawesome/brands/github
112
+ link: https://github.com/MadBomber/simple_a2a
113
+ name: simple_a2a on GitHub
114
+ - icon: fontawesome/solid/gem
115
+ link: https://rubygems.org/gems/simple_a2a
116
+ name: simple_a2a on RubyGems
117
+ - icon: fontawesome/solid/book
118
+ link: https://a2a-protocol.org/latest/
119
+ name: A2A Protocol Specification
120
+
121
+ nav:
122
+ - Home: index.md
123
+ - Getting Started:
124
+ - Installation: getting-started/installation.md
125
+ - Quick Start: getting-started/quick-start.md
126
+ - Architecture:
127
+ - Overview: architecture/index.md
128
+ - Protocol Details: architecture/protocol.md
129
+ - API Reference:
130
+ - Overview: api/index.md
131
+ - Models: api/models/index.md
132
+ - Server: api/server/index.md
133
+ - Client: api/client/index.md
134
+ - Storage: api/storage/index.md
135
+ - Guides:
136
+ - Streaming Responses: guides/streaming.md
137
+ - Push Notifications: guides/push-notifications.md
138
+ - Custom Storage: guides/custom-storage.md
139
+ - Examples:
140
+ - Overview: examples/index.md
141
+ - Basic Usage: examples/basic-usage.md
142
+ - Streaming: examples/streaming.md
143
+ - Multi-Agent LLM Research: examples/llm-research.md
@@ -0,0 +1,4 @@
1
+ module SimpleA2a
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end