simple_a2a 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/Rakefile +13 -0
- data/docs/api/client/index.md +124 -0
- data/docs/api/index.md +27 -0
- data/docs/api/models/index.md +233 -0
- data/docs/api/server/index.md +162 -0
- data/docs/api/storage/index.md +84 -0
- data/docs/architecture/index.md +63 -0
- data/docs/architecture/protocol.md +112 -0
- data/docs/assets/css/custom.css +6 -0
- data/docs/examples/basic-usage.md +77 -0
- data/docs/examples/index.md +92 -0
- data/docs/examples/llm-research.md +92 -0
- data/docs/examples/streaming.md +81 -0
- data/docs/getting-started/installation.md +48 -0
- data/docs/getting-started/quick-start.md +100 -0
- data/docs/guides/custom-storage.md +69 -0
- data/docs/guides/push-notifications.md +104 -0
- data/docs/guides/streaming.md +75 -0
- data/docs/index.md +98 -0
- data/examples/01_basic_usage/client.rb +75 -0
- data/examples/01_basic_usage/server.rb +57 -0
- data/examples/02_streaming/client.rb +70 -0
- data/examples/02_streaming/server.rb +177 -0
- data/examples/03_llm_research/client.rb +138 -0
- data/examples/03_llm_research/run +82 -0
- data/examples/03_llm_research/server.rb +203 -0
- data/examples/03_llm_research/web_client.rb +501 -0
- data/examples/common_config.rb +4 -0
- data/examples/run +108 -0
- data/lib/simple_a2a/client/base.rb +101 -0
- data/lib/simple_a2a/client/sse.rb +58 -0
- data/lib/simple_a2a/errors.rb +15 -0
- data/lib/simple_a2a/json_rpc.rb +89 -0
- data/lib/simple_a2a/models/agent_capabilities.rb +11 -0
- data/lib/simple_a2a/models/agent_card.rb +23 -0
- data/lib/simple_a2a/models/agent_interface.rb +11 -0
- data/lib/simple_a2a/models/agent_provider.rb +11 -0
- data/lib/simple_a2a/models/agent_skill.rb +12 -0
- data/lib/simple_a2a/models/artifact.rb +23 -0
- data/lib/simple_a2a/models/authentication_info.rb +11 -0
- data/lib/simple_a2a/models/base.rb +111 -0
- data/lib/simple_a2a/models/message.rb +45 -0
- data/lib/simple_a2a/models/part.rb +45 -0
- data/lib/simple_a2a/models/push_notification_config.rb +17 -0
- data/lib/simple_a2a/models/security_scheme.rb +16 -0
- data/lib/simple_a2a/models/send_message_configuration.rb +12 -0
- data/lib/simple_a2a/models/stream_response.rb +32 -0
- data/lib/simple_a2a/models/task.rb +57 -0
- data/lib/simple_a2a/models/task_artifact_update_event.rb +21 -0
- data/lib/simple_a2a/models/task_status.rb +20 -0
- data/lib/simple_a2a/models/task_status_update_event.rb +19 -0
- data/lib/simple_a2a/models/types.rb +39 -0
- data/lib/simple_a2a/server/agent_executor.rb +16 -0
- data/lib/simple_a2a/server/app.rb +227 -0
- data/lib/simple_a2a/server/base.rb +43 -0
- data/lib/simple_a2a/server/context.rb +44 -0
- data/lib/simple_a2a/server/event_router.rb +50 -0
- data/lib/simple_a2a/server/falcon_runner.rb +31 -0
- data/lib/simple_a2a/server/multi_agent.rb +50 -0
- data/lib/simple_a2a/server/push_sender.rb +80 -0
- data/lib/simple_a2a/server/resume_context.rb +14 -0
- data/lib/simple_a2a/storage/base.rb +12 -0
- data/lib/simple_a2a/storage/memory.rb +41 -0
- data/lib/simple_a2a/version.rb +5 -0
- data/lib/simple_a2a.rb +49 -0
- data/mkdocs.yml +143 -0
- data/sig/simple_a2a.rbs +4 -0
- metadata +353 -0
|
@@ -0,0 +1,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,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
|
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 © 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
|
data/sig/simple_a2a.rbs
ADDED