simple_a2a 0.3.0 → 0.3.1
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 +4 -4
- data/Rakefile +79 -0
- data/examples/01_basic_usage/client.rb +15 -11
- data/examples/01_basic_usage/server.rb +5 -3
- data/examples/02_streaming/client.rb +16 -12
- data/examples/02_streaming/server.rb +6 -4
- data/examples/03_llm_research/client.rb +11 -7
- data/examples/03_llm_research/server.rb +8 -6
- data/examples/04_resubscribe/client.rb +25 -23
- data/examples/04_resubscribe/server.rb +6 -4
- data/examples/05_cancellation/client.rb +21 -16
- data/examples/05_cancellation/server.rb +6 -4
- data/examples/06_push_notifications/client.rb +38 -29
- data/examples/06_push_notifications/server.rb +7 -5
- data/examples/07_agent_chaining/client.rb +28 -22
- data/examples/07_agent_chaining/server.rb +8 -6
- data/examples/08_interrupted_states/client.rb +40 -29
- data/examples/08_interrupted_states/server.rb +7 -5
- data/examples/09_multipart/client.rb +23 -17
- data/examples/09_multipart/server.rb +6 -4
- data/examples/10_auth_headers/client.rb +25 -19
- data/examples/10_auth_headers/server.rb +7 -5
- data/examples/11_sqlite_storage/client.rb +38 -29
- data/examples/11_sqlite_storage/server.rb +7 -5
- data/lib/simple_a2a/server/app.rb +28 -10
- data/lib/simple_a2a/version.rb +1 -1
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69d7ae29ed8960745bbeaebfd8368cdd52162d81f5faa8779d9d5ffc00634e21
|
|
4
|
+
data.tar.gz: 1bc2957a22ec2de785da814cb8d87f1a776562f9fdb7fed7ad26eb060482d3a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3fa6b8af3c6bcdb450c05263add6687fd3aac6bb338d80d17d1775715fea95419290f7ebe1ec6fc4b436ee5594a68276fa91d4a9edfc47f4d8084ea3d5a12092
|
|
7
|
+
data.tar.gz: 64dacdabd05f8347418e7fdb63609d9f6750bb7c4eb4377f69cae20e3e9d52a994df7e52f104d4d1a7cddb696914e5cd8de0d5998d07dfbbc73181a72a385a8e
|
data/Rakefile
CHANGED
|
@@ -11,3 +11,82 @@ Minitest::TestTask.create do |t|
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
task default: :test
|
|
14
|
+
|
|
15
|
+
desc 'Check code style with RuboCop'
|
|
16
|
+
task :rubocop do
|
|
17
|
+
sh 'bundle exec rubocop'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc 'Auto-correct RuboCop offenses'
|
|
21
|
+
task :rubocop_fix do
|
|
22
|
+
sh 'bundle exec rubocop -a'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
26
|
+
task :flog_check do
|
|
27
|
+
require 'flog'
|
|
28
|
+
|
|
29
|
+
method_warn = 20.0
|
|
30
|
+
method_fail = 50.0
|
|
31
|
+
|
|
32
|
+
flogger = Flog.new(all: true)
|
|
33
|
+
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
34
|
+
|
|
35
|
+
warnings = []
|
|
36
|
+
failures = []
|
|
37
|
+
|
|
38
|
+
flogger.each_by_score do |method, score|
|
|
39
|
+
next if method.end_with?('#none')
|
|
40
|
+
|
|
41
|
+
if score > method_fail
|
|
42
|
+
failures << "#{format('%.1f', score)}: #{method}"
|
|
43
|
+
elsif score > method_warn
|
|
44
|
+
warnings << "#{format('%.1f', score)}: #{method}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless warnings.empty?
|
|
49
|
+
puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
|
|
50
|
+
warnings.each { |v| puts " #{v}" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if failures.empty?
|
|
54
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
|
|
55
|
+
else
|
|
56
|
+
puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
|
|
57
|
+
failures.each { |v| puts " #{v}" }
|
|
58
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc 'Run all quality checks: tests (with coverage), RuboCop, and Flog'
|
|
63
|
+
task :quality do
|
|
64
|
+
results = {}
|
|
65
|
+
|
|
66
|
+
puts "\n#{'=' * 60}"
|
|
67
|
+
puts 'Quality Gate: Tests + Coverage'
|
|
68
|
+
puts '=' * 60
|
|
69
|
+
results[:tests] = system('bundle exec rake test') ? :pass : :fail
|
|
70
|
+
|
|
71
|
+
puts "\n#{'=' * 60}"
|
|
72
|
+
puts 'Quality Gate: RuboCop'
|
|
73
|
+
puts '=' * 60
|
|
74
|
+
results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
|
|
75
|
+
|
|
76
|
+
puts "\n#{'=' * 60}"
|
|
77
|
+
puts 'Quality Gate: Flog Complexity'
|
|
78
|
+
puts '=' * 60
|
|
79
|
+
results[:flog] = system('bundle exec rake flog_check') ? :pass : :fail
|
|
80
|
+
|
|
81
|
+
puts "\n#{'=' * 60}"
|
|
82
|
+
puts 'Quality Summary'
|
|
83
|
+
puts '=' * 60
|
|
84
|
+
results.each do |gate, status|
|
|
85
|
+
icon = status == :pass ? 'PASS' : 'FAIL'
|
|
86
|
+
puts " [#{icon}] #{gate}"
|
|
87
|
+
end
|
|
88
|
+
puts '=' * 60
|
|
89
|
+
|
|
90
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
91
|
+
puts "\nAll quality gates passed."
|
|
92
|
+
end
|
|
@@ -15,13 +15,15 @@ client = A2A.client(url: URL)
|
|
|
15
15
|
# ---------------------------------------------------------------------------
|
|
16
16
|
# 1. Discover the agent
|
|
17
17
|
# ---------------------------------------------------------------------------
|
|
18
|
-
puts "=== Agent Card ==="
|
|
19
18
|
card = client.agent_card
|
|
20
|
-
puts
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
puts <<~HEREDOC
|
|
20
|
+
=== Agent Card ===
|
|
21
|
+
Name: #{card.name}
|
|
22
|
+
Version: #{card.version}
|
|
23
|
+
Description: #{card.description}
|
|
24
|
+
Skills: #{card.skills.map(&:name).join(', ')}
|
|
25
|
+
|
|
26
|
+
HEREDOC
|
|
25
27
|
|
|
26
28
|
# ---------------------------------------------------------------------------
|
|
27
29
|
# 2. Send a few tasks
|
|
@@ -57,12 +59,14 @@ puts
|
|
|
57
59
|
# ---------------------------------------------------------------------------
|
|
58
60
|
# 4. Retrieve a single task by ID
|
|
59
61
|
# ---------------------------------------------------------------------------
|
|
60
|
-
puts "=== Retrieve Task ==="
|
|
61
62
|
retrieved = client.get_task(task_ids.first)
|
|
62
|
-
puts
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
puts <<~HEREDOC
|
|
64
|
+
=== Retrieve Task ===
|
|
65
|
+
id: #{retrieved.id}
|
|
66
|
+
state: #{retrieved.status.state}
|
|
67
|
+
reply: #{retrieved.artifacts.first&.parts&.first&.text.inspect}
|
|
68
|
+
|
|
69
|
+
HEREDOC
|
|
66
70
|
|
|
67
71
|
# ---------------------------------------------------------------------------
|
|
68
72
|
# 5. Cancel a non-existent task (demonstrates error handling)
|
|
@@ -50,8 +50,10 @@ card = A2A::Models::AgentCard.new(
|
|
|
50
50
|
# ---------------------------------------------------------------------------
|
|
51
51
|
# Start the server (blocks; Ctrl-C to stop).
|
|
52
52
|
# ---------------------------------------------------------------------------
|
|
53
|
-
puts
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
puts <<~HEREDOC
|
|
54
|
+
Starting BasicAgent on http://localhost:9292
|
|
55
|
+
Press Ctrl-C to stop.
|
|
56
|
+
|
|
57
|
+
HEREDOC
|
|
56
58
|
|
|
57
59
|
A2A.server(agent_card: card, executor: BasicExecutor.new).run
|
|
@@ -16,12 +16,14 @@ client = A2A.sse_client(url: URL)
|
|
|
16
16
|
# 1. Confirm the agent advertises streaming support
|
|
17
17
|
# ---------------------------------------------------------------------------
|
|
18
18
|
card = client.agent_card
|
|
19
|
-
puts
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
puts <<~HEREDOC
|
|
20
|
+
=== Agent Card ===
|
|
21
|
+
Name: #{card.name}
|
|
22
|
+
Description: #{card.description}
|
|
23
|
+
Streaming: #{card.capabilities&.streaming}
|
|
24
|
+
Source: https://lamplight.guide/blog/the-god-particle/
|
|
25
|
+
|
|
26
|
+
HEREDOC
|
|
25
27
|
|
|
26
28
|
# ---------------------------------------------------------------------------
|
|
27
29
|
# 2. Subscribe and print words as they stream in
|
|
@@ -62,9 +64,11 @@ end
|
|
|
62
64
|
elapsed = start_time ? (Time.now - start_time) : 0
|
|
63
65
|
wpm = elapsed > 0 ? (word_count / (elapsed / 60.0)).round : 0
|
|
64
66
|
|
|
65
|
-
puts
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
puts <<~HEREDOC
|
|
68
|
+
|
|
69
|
+
=== Summary ===
|
|
70
|
+
Words received : #{word_count}
|
|
71
|
+
Events received : #{event_count}
|
|
72
|
+
Elapsed : #{elapsed.round(1)}s — #{wpm} WPM effective
|
|
73
|
+
Status : #{interrupted ? "interrupted" : "completed"}
|
|
74
|
+
HEREDOC
|
|
@@ -169,9 +169,11 @@ card = A2A::Models::AgentCard.new(
|
|
|
169
169
|
]
|
|
170
170
|
)
|
|
171
171
|
|
|
172
|
-
puts
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
puts <<~HEREDOC
|
|
173
|
+
Starting StreamingAgent on http://localhost:9292
|
|
174
|
+
Streaming #{StreamingExecutor::WORDS.length} words at 600 WPM (~#{(StreamingExecutor::WORDS.length / 600.0).ceil} min)
|
|
175
|
+
Press Ctrl-C to stop.
|
|
176
|
+
|
|
177
|
+
HEREDOC
|
|
176
178
|
|
|
177
179
|
A2A.server(agent_card: card, executor: StreamingExecutor.new).run
|
|
@@ -17,9 +17,11 @@ topic = ARGV.first || "research all shortcomings and defects and criticisms of t
|
|
|
17
17
|
|
|
18
18
|
def banner(text)
|
|
19
19
|
bar = "─" * (text.length + 4)
|
|
20
|
-
puts
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
puts <<~HEREDOC
|
|
21
|
+
┌#{bar}┐
|
|
22
|
+
│ #{text} │
|
|
23
|
+
└#{bar}┘
|
|
24
|
+
HEREDOC
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def collect_streaming_response(url, topic)
|
|
@@ -40,10 +42,12 @@ end
|
|
|
40
42
|
# 1. Research phase — query both agents in parallel
|
|
41
43
|
# ---------------------------------------------------------------------------
|
|
42
44
|
banner "Research Phase: \"#{topic}\""
|
|
43
|
-
puts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
puts <<~HEREDOC
|
|
46
|
+
|
|
47
|
+
Querying Anthropic (claude-sonnet-4-6) and OpenAI (gpt-5.4) in parallel…
|
|
48
|
+
This may take several minutes for complex topics.
|
|
49
|
+
|
|
50
|
+
HEREDOC
|
|
47
51
|
|
|
48
52
|
anthropic_text = nil
|
|
49
53
|
openai_text = nil
|
|
@@ -186,12 +186,14 @@ evaluator_card = A2A::Models::AgentCard.new(
|
|
|
186
186
|
# ---------------------------------------------------------------------------
|
|
187
187
|
# Start multi-agent server
|
|
188
188
|
# ---------------------------------------------------------------------------
|
|
189
|
-
puts
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
puts <<~HEREDOC
|
|
190
|
+
Starting multi-agent research server on http://localhost:9292
|
|
191
|
+
/anthropic → #{AnthropicResearchExecutor::MODEL}
|
|
192
|
+
/openai → #{OpenAIResearchExecutor::MODEL}
|
|
193
|
+
/evaluator → #{EvaluatorExecutor::MODEL} (evaluator)
|
|
194
|
+
Press Ctrl-C to stop.
|
|
195
|
+
|
|
196
|
+
HEREDOC
|
|
195
197
|
|
|
196
198
|
A2A.multi_server(
|
|
197
199
|
agents: {
|
|
@@ -43,11 +43,13 @@ end
|
|
|
43
43
|
base = A2A.client(url: URL)
|
|
44
44
|
card = base.agent_card
|
|
45
45
|
|
|
46
|
-
puts
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
puts <<~HEREDOC
|
|
47
|
+
=== Agent Card ===
|
|
48
|
+
Name: #{card.name}
|
|
49
|
+
Description: #{card.description}
|
|
50
|
+
Streaming: #{card.capabilities&.streaming}
|
|
51
|
+
|
|
52
|
+
HEREDOC
|
|
51
53
|
|
|
52
54
|
abort "Agent does not advertise streaming support." unless card.capabilities&.streaming
|
|
53
55
|
|
|
@@ -118,23 +120,23 @@ end
|
|
|
118
120
|
# ---------------------------------------------------------------------------
|
|
119
121
|
# Summary
|
|
120
122
|
# ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
puts
|
|
123
|
-
puts "=== Summary ==="
|
|
124
|
-
|
|
125
|
-
snapshot = sub2_events.find { |e| e.is_a?(Hash) && e.key?("status") }
|
|
123
|
+
snapshot = sub2_events.find { |e| e.is_a?(Hash) && e.key?("status") }
|
|
126
124
|
sub2_artifacts = sub2_events.select { |e| e.is_a?(A2A::Models::TaskArtifactUpdateEvent) }
|
|
127
125
|
sub1_artifacts = sub1_events.select { |e| e.is_a?(A2A::Models::TaskArtifactUpdateEvent) }
|
|
128
|
-
|
|
129
|
-
puts
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
126
|
+
divider
|
|
127
|
+
puts <<~HEREDOC
|
|
128
|
+
|
|
129
|
+
=== Summary ===
|
|
130
|
+
Subscriber 1 — events received : #{sub1_events.length}
|
|
131
|
+
Subscriber 1 — artifact steps : #{sub1_artifacts.length}
|
|
132
|
+
|
|
133
|
+
Subscriber 2 — events received : #{sub2_events.length}
|
|
134
|
+
Subscriber 2 — task snapshot : #{snapshot ? 'yes' : 'no (unexpected)'}
|
|
135
|
+
Subscriber 2 — artifact steps : #{sub2_artifacts.length}
|
|
136
|
+
Subscriber 2 — joined at step : #{sub1_artifacts.length - sub2_artifacts.length + 1} of 5
|
|
137
|
+
|
|
138
|
+
Both streams terminated cleanly: #{
|
|
139
|
+
sub1_events.any? { |e| e.is_a?(A2A::Models::TaskStatusUpdateEvent) && e.final? } &&
|
|
140
|
+
sub2_events.any? { |e| e.is_a?(A2A::Models::TaskStatusUpdateEvent) && e.final? }
|
|
141
|
+
}
|
|
142
|
+
HEREDOC
|
|
@@ -67,9 +67,11 @@ card = A2A::Models::AgentCard.new(
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
total = AnalysisExecutor::STEPS.length * AnalysisExecutor::STEP_DELAY
|
|
70
|
-
puts
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
puts <<~HEREDOC
|
|
71
|
+
Starting AnalysisAgent on http://localhost:9292
|
|
72
|
+
Task runs #{AnalysisExecutor::STEPS.length} steps × #{AnalysisExecutor::STEP_DELAY}s = ~#{total.round(0).to_i}s per run
|
|
73
|
+
Press Ctrl-C to stop.
|
|
74
|
+
|
|
75
|
+
HEREDOC
|
|
74
76
|
|
|
75
77
|
A2A.server(agent_card: card, executor: AnalysisExecutor.new).run
|
|
@@ -31,12 +31,14 @@ end
|
|
|
31
31
|
# ---------------------------------------------------------------------------
|
|
32
32
|
card = A2A.client(url: URL).agent_card
|
|
33
33
|
|
|
34
|
-
puts
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
puts <<~HEREDOC
|
|
35
|
+
|
|
36
|
+
=== Agent Card ===
|
|
37
|
+
Name: #{card.name}
|
|
38
|
+
Description: #{card.description}
|
|
39
|
+
Streaming: #{card.capabilities&.streaming}
|
|
40
|
+
|
|
41
|
+
HEREDOC
|
|
40
42
|
abort "Agent does not advertise streaming support." unless card.capabilities&.streaming
|
|
41
43
|
divider
|
|
42
44
|
|
|
@@ -46,10 +48,12 @@ divider
|
|
|
46
48
|
# A shared, mutex-protected hash lets the main thread see task IDs as soon
|
|
47
49
|
# as the first status event arrives from each task.
|
|
48
50
|
# ---------------------------------------------------------------------------
|
|
49
|
-
puts
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
puts <<~HEREDOC
|
|
52
|
+
|
|
53
|
+
Starting tasks A, B, and C via tasks/sendSubscribe…
|
|
54
|
+
(each would take 10 s; task B will be cancelled after #{CANCEL_SEC}s)
|
|
55
|
+
|
|
56
|
+
HEREDOC
|
|
53
57
|
|
|
54
58
|
task_ids = {}
|
|
55
59
|
id_mutex = Mutex.new
|
|
@@ -132,8 +136,6 @@ puts
|
|
|
132
136
|
# ---------------------------------------------------------------------------
|
|
133
137
|
# Assertions
|
|
134
138
|
# ---------------------------------------------------------------------------
|
|
135
|
-
puts "=== Verification ==="
|
|
136
|
-
|
|
137
139
|
def final_state(events)
|
|
138
140
|
events.select { |e| e.is_a?(A2A::Models::TaskStatusUpdateEvent) }
|
|
139
141
|
.last&.status&.state
|
|
@@ -143,8 +145,11 @@ a_ok = final_state(all_events["A"]) == "completed"
|
|
|
143
145
|
b_ok = final_state(all_events["B"]) == "canceled"
|
|
144
146
|
c_ok = final_state(all_events["C"]) == "completed"
|
|
145
147
|
|
|
146
|
-
puts
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
puts <<~HEREDOC
|
|
149
|
+
=== Verification ===
|
|
150
|
+
Task A completed normally : #{a_ok ? 'PASS' : 'FAIL'}
|
|
151
|
+
Task B was cancelled : #{b_ok ? 'PASS' : 'FAIL'}
|
|
152
|
+
Task C completed normally : #{c_ok ? 'PASS' : 'FAIL'}
|
|
153
|
+
|
|
154
|
+
HEREDOC
|
|
150
155
|
puts(a_ok && b_ok && c_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
@@ -69,9 +69,11 @@ card = A2A::Models::AgentCard.new(
|
|
|
69
69
|
]
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
-
puts
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
puts <<~HEREDOC
|
|
73
|
+
Starting SlowAgent on http://localhost:9292
|
|
74
|
+
Each task takes #{SlowExecutor::STEPS * SlowExecutor::STEP_SEC}s to complete without cancellation.
|
|
75
|
+
Press Ctrl-C to stop.
|
|
76
|
+
|
|
77
|
+
HEREDOC
|
|
76
78
|
|
|
77
79
|
A2A.server(agent_card: card, executor: SlowExecutor.new).run
|
|
@@ -44,9 +44,11 @@ end
|
|
|
44
44
|
webhook_thread = Thread.new { webhook_server.start }
|
|
45
45
|
at_exit { webhook_server.shutdown }
|
|
46
46
|
|
|
47
|
-
puts
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
puts <<~HEREDOC
|
|
48
|
+
|
|
49
|
+
=== Webhook receiver listening on #{WEBHOOK_URL} ===
|
|
50
|
+
|
|
51
|
+
HEREDOC
|
|
50
52
|
|
|
51
53
|
# ---------------------------------------------------------------------------
|
|
52
54
|
# Confirm the server is up and supports push notifications
|
|
@@ -54,12 +56,14 @@ puts
|
|
|
54
56
|
base_client = A2A.client(url: A2A_URL)
|
|
55
57
|
card = base_client.agent_card
|
|
56
58
|
|
|
57
|
-
puts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
puts <<~HEREDOC
|
|
60
|
+
=== Agent Card ===
|
|
61
|
+
Name: #{card.name}
|
|
62
|
+
Description: #{card.description}
|
|
63
|
+
push_notifications: #{card.capabilities&.push_notifications}
|
|
64
|
+
streaming: #{card.capabilities&.streaming}
|
|
65
|
+
|
|
66
|
+
HEREDOC
|
|
63
67
|
abort "Agent does not advertise push notification support." unless card.capabilities&.push_notifications
|
|
64
68
|
divider
|
|
65
69
|
|
|
@@ -121,10 +125,12 @@ divider
|
|
|
121
125
|
# The server posts after each step; we print each payload as it lands.
|
|
122
126
|
# Stop when we see a final=true delivery.
|
|
123
127
|
# ---------------------------------------------------------------------------
|
|
124
|
-
puts
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
puts <<~HEREDOC
|
|
129
|
+
|
|
130
|
+
Watching webhook for incoming push notifications…
|
|
131
|
+
(the client has NO open SSE connection — all updates arrive out-of-band)
|
|
132
|
+
|
|
133
|
+
HEREDOC
|
|
128
134
|
|
|
129
135
|
received = []
|
|
130
136
|
loop do
|
|
@@ -164,29 +170,32 @@ sse_thread.join(5)
|
|
|
164
170
|
# Summary
|
|
165
171
|
# ---------------------------------------------------------------------------
|
|
166
172
|
divider
|
|
167
|
-
puts
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
puts <<~HEREDOC
|
|
174
|
+
|
|
175
|
+
=== Summary ===
|
|
176
|
+
Push notifications received : #{received.length}
|
|
177
|
+
States delivered : #{received.map { |p| p.dig("status", "state") }.join(" → ")}
|
|
178
|
+
Final delivery seen : #{received.any? { |p| p["final"] } ? 'yes' : 'no'}
|
|
179
|
+
Config cleaned up : #{list_after.empty? ? 'yes' : 'no'}
|
|
180
|
+
|
|
181
|
+
HEREDOC
|
|
174
182
|
|
|
175
183
|
# ---------------------------------------------------------------------------
|
|
176
184
|
# Verification
|
|
177
185
|
# ---------------------------------------------------------------------------
|
|
178
|
-
puts "=== Verification ==="
|
|
179
186
|
states = received.map { |p| p.dig("status", "state") }
|
|
180
187
|
last_state = states.last
|
|
181
|
-
|
|
182
188
|
push_ok = received.length >= 2
|
|
183
189
|
final_ok = received.last&.fetch("final", false)
|
|
184
190
|
state_ok = last_state == "completed"
|
|
185
191
|
cleanup_ok = list_after.empty?
|
|
186
|
-
|
|
187
|
-
puts
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
all_ok = push_ok && final_ok && state_ok && cleanup_ok
|
|
193
|
+
puts <<~HEREDOC
|
|
194
|
+
=== Verification ===
|
|
195
|
+
Push notifications received (≥2) : #{push_ok ? 'PASS' : 'FAIL'}
|
|
196
|
+
Final push delivery seen : #{final_ok ? 'PASS' : 'FAIL'}
|
|
197
|
+
Final state is completed : #{state_ok ? 'PASS' : 'FAIL'}
|
|
198
|
+
Config deleted successfully : #{cleanup_ok ? 'PASS' : 'FAIL'}
|
|
199
|
+
|
|
200
|
+
HEREDOC
|
|
201
|
+
puts(all_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
@@ -109,11 +109,13 @@ card = A2A::Models::AgentCard.new(
|
|
|
109
109
|
]
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
-
puts
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
puts <<~HEREDOC
|
|
113
|
+
Starting PushAgent on http://localhost:9292
|
|
114
|
+
push_notifications: true
|
|
115
|
+
#{PushExecutor::STEPS} steps × #{PushExecutor::STEP_SEC}s = ~#{(PushExecutor::STEPS * PushExecutor::STEP_SEC).to_i}s per task
|
|
116
|
+
Press Ctrl-C to stop.
|
|
117
|
+
|
|
118
|
+
HEREDOC
|
|
117
119
|
|
|
118
120
|
A2A.server(
|
|
119
121
|
agent_card: card,
|
|
@@ -54,16 +54,19 @@ inputs = [
|
|
|
54
54
|
"simple is better than complex"
|
|
55
55
|
]
|
|
56
56
|
|
|
57
|
-
puts
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
puts <<~HEREDOC
|
|
58
|
+
|
|
59
|
+
=== Pipeline calls (client speaks only to /pipeline) ===
|
|
60
|
+
|
|
61
|
+
HEREDOC
|
|
60
62
|
|
|
61
63
|
results = inputs.map do |text|
|
|
62
64
|
task = pipeline.send_task(message: A2A::Models::Message.user(text))
|
|
63
65
|
output = artifact_text(task)
|
|
64
|
-
puts
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
puts <<~HEREDOC
|
|
67
|
+
Input: #{text}
|
|
68
|
+
#{output.lines.map { |l| " #{l}" }.join}
|
|
69
|
+
HEREDOC
|
|
67
70
|
{ input: text, output: output, state: task.status.state }
|
|
68
71
|
end
|
|
69
72
|
|
|
@@ -72,10 +75,11 @@ divider
|
|
|
72
75
|
# ---------------------------------------------------------------------------
|
|
73
76
|
# Call sub-agents directly to show they work standalone
|
|
74
77
|
# ---------------------------------------------------------------------------
|
|
75
|
-
puts
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
puts <<~HEREDOC
|
|
79
|
+
|
|
80
|
+
=== Sub-agents called directly (verification) ===
|
|
78
81
|
|
|
82
|
+
HEREDOC
|
|
79
83
|
sample = "hello world from A2A"
|
|
80
84
|
|
|
81
85
|
reverse_task = A2A.client(url: REVERSE_URL).send_task(
|
|
@@ -85,19 +89,18 @@ shout_task = A2A.client(url: SHOUT_URL).send_task(
|
|
|
85
89
|
message: A2A::Models::Message.user(sample)
|
|
86
90
|
)
|
|
87
91
|
|
|
88
|
-
puts
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
puts <<~HEREDOC
|
|
93
|
+
Input: #{sample}
|
|
94
|
+
/reverse result: #{artifact_text(reverse_task)}
|
|
95
|
+
/shout result: #{artifact_text(shout_task)}
|
|
96
|
+
|
|
97
|
+
HEREDOC
|
|
92
98
|
|
|
93
99
|
divider
|
|
94
100
|
|
|
95
101
|
# ---------------------------------------------------------------------------
|
|
96
102
|
# Verification
|
|
97
103
|
# ---------------------------------------------------------------------------
|
|
98
|
-
puts
|
|
99
|
-
puts "=== Verification ==="
|
|
100
|
-
|
|
101
104
|
all_completed = results.all? { |r| r[:state] == "completed" }
|
|
102
105
|
|
|
103
106
|
pipeline_correct = results.all? do |r|
|
|
@@ -109,12 +112,15 @@ end
|
|
|
109
112
|
|
|
110
113
|
standalone_reverse_ok = artifact_text(reverse_task) == sample.split.reverse.join(" ")
|
|
111
114
|
standalone_shout_ok = artifact_text(shout_task) == sample.upcase
|
|
115
|
+
all_ok = all_completed && pipeline_correct && standalone_reverse_ok && standalone_shout_ok
|
|
112
116
|
|
|
113
|
-
puts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
puts <<~HEREDOC
|
|
118
|
+
|
|
119
|
+
=== Verification ===
|
|
120
|
+
All pipeline tasks completed : #{all_completed ? 'PASS' : 'FAIL'}
|
|
121
|
+
Pipeline output matches chain : #{pipeline_correct ? 'PASS' : 'FAIL'}
|
|
122
|
+
ReverseAgent standalone correct : #{standalone_reverse_ok ? 'PASS' : 'FAIL'}
|
|
123
|
+
ShoutAgent standalone correct : #{standalone_shout_ok ? 'PASS' : 'FAIL'}
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
HEREDOC
|
|
120
126
|
puts(all_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
@@ -133,12 +133,14 @@ pipeline_card = make_card(
|
|
|
133
133
|
# ---------------------------------------------------------------------------
|
|
134
134
|
# Start the multi-agent server
|
|
135
135
|
# ---------------------------------------------------------------------------
|
|
136
|
-
puts
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
puts <<~HEREDOC
|
|
137
|
+
Starting multi-agent server on #{BASE_URL}
|
|
138
|
+
/reverse → ReverseAgent
|
|
139
|
+
/shout → ShoutAgent
|
|
140
|
+
/pipeline → PipelineAgent (chains the other two)
|
|
141
|
+
Press Ctrl-C to stop.
|
|
142
|
+
|
|
143
|
+
HEREDOC
|
|
142
144
|
|
|
143
145
|
A2A.multi_server(
|
|
144
146
|
agents: {
|
|
@@ -51,9 +51,11 @@ end
|
|
|
51
51
|
# ---------------------------------------------------------------------------
|
|
52
52
|
# Scenario A — input_required
|
|
53
53
|
# ---------------------------------------------------------------------------
|
|
54
|
-
puts
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
puts <<~HEREDOC
|
|
55
|
+
|
|
56
|
+
=== Scenario A: input_required (OrderAgent) ===
|
|
57
|
+
|
|
58
|
+
HEREDOC
|
|
57
59
|
|
|
58
60
|
order = A2A.client(url: ORDER_URL)
|
|
59
61
|
conv_a = SecureRandom.uuid
|
|
@@ -67,9 +69,11 @@ show_task("turn 1", task_a1)
|
|
|
67
69
|
|
|
68
70
|
abort "Expected input_required, got #{task_a1.status.state}" unless task_a1.status.state == "input_required"
|
|
69
71
|
|
|
70
|
-
puts
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
puts <<~HEREDOC
|
|
73
|
+
|
|
74
|
+
Client reads the question and answers: 'pasta'
|
|
75
|
+
|
|
76
|
+
HEREDOC
|
|
73
77
|
|
|
74
78
|
# Turn 2 — client answers with the same context_id
|
|
75
79
|
task_a2 = order.send_task(message: msg("pasta", context_id: conv_a))
|
|
@@ -80,9 +84,11 @@ divider
|
|
|
80
84
|
# ---------------------------------------------------------------------------
|
|
81
85
|
# Scenario B — auth_required (with a wrong token first)
|
|
82
86
|
# ---------------------------------------------------------------------------
|
|
83
|
-
puts
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
puts <<~HEREDOC
|
|
88
|
+
|
|
89
|
+
=== Scenario B: auth_required (VaultAgent) ===
|
|
90
|
+
|
|
91
|
+
HEREDOC
|
|
86
92
|
|
|
87
93
|
vault = A2A.client(url: VAULT_URL)
|
|
88
94
|
conv_b = SecureRandom.uuid
|
|
@@ -96,9 +102,11 @@ show_task("turn 1", task_b1)
|
|
|
96
102
|
|
|
97
103
|
abort "Expected auth_required, got #{task_b1.status.state}" unless task_b1.status.state == "auth_required"
|
|
98
104
|
|
|
99
|
-
puts
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
puts <<~HEREDOC
|
|
106
|
+
|
|
107
|
+
Client sends the wrong token: 'wrong-token'
|
|
108
|
+
|
|
109
|
+
HEREDOC
|
|
102
110
|
|
|
103
111
|
# Turn 2 — wrong token; agent stays blocked
|
|
104
112
|
task_b2 = vault.send_task(message: msg("wrong-token", context_id: conv_b))
|
|
@@ -106,9 +114,11 @@ show_task("turn 2", task_b2)
|
|
|
106
114
|
|
|
107
115
|
abort "Expected auth_required, got #{task_b2.status.state}" unless task_b2.status.state == "auth_required"
|
|
108
116
|
|
|
109
|
-
puts
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
puts <<~HEREDOC
|
|
118
|
+
|
|
119
|
+
Client sends the correct token: 'open-sesame'
|
|
120
|
+
|
|
121
|
+
HEREDOC
|
|
112
122
|
|
|
113
123
|
# Turn 3 — correct token; agent unlocks
|
|
114
124
|
task_b3 = vault.send_task(message: msg("open-sesame", context_id: conv_b))
|
|
@@ -119,9 +129,6 @@ divider
|
|
|
119
129
|
# ---------------------------------------------------------------------------
|
|
120
130
|
# Verification
|
|
121
131
|
# ---------------------------------------------------------------------------
|
|
122
|
-
puts
|
|
123
|
-
puts "=== Verification ==="
|
|
124
|
-
|
|
125
132
|
a_paused = task_a1.status.state == "input_required"
|
|
126
133
|
a_asked = task_a1.status.message&.include?("Options:")
|
|
127
134
|
a_complete = task_a2.status.state == "completed"
|
|
@@ -132,17 +139,21 @@ b_blocked2 = task_b2.status.state == "auth_required"
|
|
|
132
139
|
b_complete = task_b3.status.state == "completed"
|
|
133
140
|
b_artifact = task_b3.artifacts&.first&.parts&.first&.text&.include?("treasure")
|
|
134
141
|
|
|
135
|
-
puts " [order] turn 1 paused with input_required : #{a_paused ? 'PASS' : 'FAIL'}"
|
|
136
|
-
puts " [order] turn 1 included a question : #{a_asked ? 'PASS' : 'FAIL'}"
|
|
137
|
-
puts " [order] turn 2 completed after answer : #{a_complete ? 'PASS' : 'FAIL'}"
|
|
138
|
-
puts " [order] turn 2 artifact mentions pasta : #{a_artifact ? 'PASS' : 'FAIL'}"
|
|
139
|
-
puts
|
|
140
|
-
puts " [vault] turn 1 paused with auth_required : #{b_blocked1 ? 'PASS' : 'FAIL'}"
|
|
141
|
-
puts " [vault] turn 2 stayed blocked (bad token) : #{b_blocked2 ? 'PASS' : 'FAIL'}"
|
|
142
|
-
puts " [vault] turn 3 completed after good token : #{b_complete ? 'PASS' : 'FAIL'}"
|
|
143
|
-
puts " [vault] turn 3 artifact contains secret : #{b_artifact ? 'PASS' : 'FAIL'}"
|
|
144
|
-
puts
|
|
145
|
-
|
|
146
142
|
all_ok = a_paused && a_asked && a_complete && a_artifact &&
|
|
147
143
|
b_blocked1 && b_blocked2 && b_complete && b_artifact
|
|
144
|
+
|
|
145
|
+
puts <<~HEREDOC
|
|
146
|
+
|
|
147
|
+
=== Verification ===
|
|
148
|
+
[order] turn 1 paused with input_required : #{a_paused ? 'PASS' : 'FAIL'}
|
|
149
|
+
[order] turn 1 included a question : #{a_asked ? 'PASS' : 'FAIL'}
|
|
150
|
+
[order] turn 2 completed after answer : #{a_complete ? 'PASS' : 'FAIL'}
|
|
151
|
+
[order] turn 2 artifact mentions pasta : #{a_artifact ? 'PASS' : 'FAIL'}
|
|
152
|
+
|
|
153
|
+
[vault] turn 1 paused with auth_required : #{b_blocked1 ? 'PASS' : 'FAIL'}
|
|
154
|
+
[vault] turn 2 stayed blocked (bad token) : #{b_blocked2 ? 'PASS' : 'FAIL'}
|
|
155
|
+
[vault] turn 3 completed after good token : #{b_complete ? 'PASS' : 'FAIL'}
|
|
156
|
+
[vault] turn 3 artifact contains secret : #{b_artifact ? 'PASS' : 'FAIL'}
|
|
157
|
+
|
|
158
|
+
HEREDOC
|
|
148
159
|
puts(all_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
@@ -127,11 +127,13 @@ vault_card = make_card(
|
|
|
127
127
|
path: "/vault"
|
|
128
128
|
)
|
|
129
129
|
|
|
130
|
-
puts
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
puts <<~HEREDOC
|
|
131
|
+
Starting interrupted-states server on #{BASE_URL}
|
|
132
|
+
/order → OrderAgent (demonstrates input_required)
|
|
133
|
+
/vault → VaultAgent (demonstrates auth_required)
|
|
134
|
+
Press Ctrl-C to stop.
|
|
135
|
+
|
|
136
|
+
HEREDOC
|
|
135
137
|
|
|
136
138
|
A2A.multi_server(
|
|
137
139
|
agents: {
|
|
@@ -31,11 +31,13 @@ def header(title) = puts("\n ── #{title} ──\n")
|
|
|
31
31
|
client = A2A.client(url: URL)
|
|
32
32
|
card = client.agent_card
|
|
33
33
|
|
|
34
|
-
puts
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
puts <<~HEREDOC
|
|
35
|
+
|
|
36
|
+
=== Agent Card ===
|
|
37
|
+
Name: #{card.name}
|
|
38
|
+
Description: #{card.description}
|
|
39
|
+
|
|
40
|
+
HEREDOC
|
|
39
41
|
|
|
40
42
|
# ---------------------------------------------------------------------------
|
|
41
43
|
# Send the task
|
|
@@ -73,9 +75,11 @@ artifact.parts.each_with_index do |part, i|
|
|
|
73
75
|
elsif part.raw?
|
|
74
76
|
header "binary (media_type: #{part.media_type}, filename: #{part.filename})"
|
|
75
77
|
bytes = part.decoded_bytes
|
|
76
|
-
puts
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
puts <<~HEREDOC
|
|
79
|
+
base64 length : #{part.raw.length} chars
|
|
80
|
+
decoded bytes : #{bytes.bytesize}
|
|
81
|
+
content:
|
|
82
|
+
HEREDOC
|
|
79
83
|
bytes.force_encoding("UTF-8").each_line { |l| puts " #{l}" }
|
|
80
84
|
|
|
81
85
|
elsif part.url?
|
|
@@ -96,15 +100,17 @@ json_part = parts.find(&:json?)
|
|
|
96
100
|
binary_part = parts.find(&:raw?)
|
|
97
101
|
url_part = parts.find(&:url?)
|
|
98
102
|
|
|
99
|
-
puts
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
puts <<~HEREDOC
|
|
104
|
+
|
|
105
|
+
=== Verification ===
|
|
106
|
+
text part present and non-empty : #{(text_part && !text_part.text.empty?) ? 'PASS' : 'FAIL'}
|
|
107
|
+
json part present with hash : #{(json_part && json_part.data.is_a?(Hash)) ? 'PASS' : 'FAIL'}
|
|
108
|
+
binary part decodes correctly : #{(binary_part && binary_part.decoded_bytes.bytesize > 0) ? 'PASS' : 'FAIL'}
|
|
109
|
+
url part is a valid URL : #{(url_part && url_part.url.start_with?("https://")) ? 'PASS' : 'FAIL'}
|
|
110
|
+
json part topic matches input : #{(json_part && json_part.data["topic"] == topic) ? 'PASS' : 'FAIL'}
|
|
111
|
+
binary part is valid CSV : #{(binary_part && binary_part.decoded_bytes.include?("rank")) ? 'PASS' : 'FAIL'}
|
|
112
|
+
|
|
113
|
+
HEREDOC
|
|
108
114
|
|
|
109
115
|
all_ok = text_part && json_part && binary_part && url_part &&
|
|
110
116
|
!text_part.text.empty? &&
|
|
@@ -89,9 +89,11 @@ card = A2A::Models::AgentCard.new(
|
|
|
89
89
|
]
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
-
puts
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
puts <<~HEREDOC
|
|
93
|
+
Starting ReportAgent on http://localhost:9292
|
|
94
|
+
Returns a 4-part artifact (text + JSON + binary + URL) for any topic.
|
|
95
|
+
Press Ctrl-C to stop.
|
|
96
|
+
|
|
97
|
+
HEREDOC
|
|
96
98
|
|
|
97
99
|
A2A.server(agent_card: card, executor: ReportExecutor.new).run
|
|
@@ -31,19 +31,23 @@ auth_client = A2A.client(url: URL, headers: { "Authorization" => "Bearer #{TOK
|
|
|
31
31
|
# Agent card — public endpoint, both clients can discover it
|
|
32
32
|
# ---------------------------------------------------------------------------
|
|
33
33
|
puts
|
|
34
|
-
puts "=== Agent Card (public — no auth required) ==="
|
|
35
34
|
card = unauth_client.agent_card
|
|
36
|
-
puts
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
puts <<~HEREDOC
|
|
36
|
+
=== Agent Card (public — no auth required) ===
|
|
37
|
+
Name: #{card.name}
|
|
38
|
+
Description: #{card.description}
|
|
39
|
+
|
|
40
|
+
HEREDOC
|
|
39
41
|
divider
|
|
40
42
|
|
|
41
43
|
# ---------------------------------------------------------------------------
|
|
42
44
|
# Unauthenticated client — rejected on RPC call
|
|
43
45
|
# ---------------------------------------------------------------------------
|
|
44
|
-
puts
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
puts <<~HEREDOC
|
|
47
|
+
|
|
48
|
+
=== Unauthenticated client — no Authorization header ===
|
|
49
|
+
|
|
50
|
+
HEREDOC
|
|
47
51
|
|
|
48
52
|
unauth_error = nil
|
|
49
53
|
begin
|
|
@@ -59,9 +63,11 @@ divider
|
|
|
59
63
|
# ---------------------------------------------------------------------------
|
|
60
64
|
# Authenticated client — accepted
|
|
61
65
|
# ---------------------------------------------------------------------------
|
|
62
|
-
puts
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
puts <<~HEREDOC
|
|
67
|
+
|
|
68
|
+
=== Authenticated client — Authorization: Bearer #{TOKEN} ===
|
|
69
|
+
|
|
70
|
+
HEREDOC
|
|
65
71
|
|
|
66
72
|
auth_task = auth_client.send_task(message: A2A::Models::Message.user("hello from authorized client"))
|
|
67
73
|
auth_result = auth_task.artifacts&.first&.parts&.first&.text
|
|
@@ -74,19 +80,19 @@ divider
|
|
|
74
80
|
# ---------------------------------------------------------------------------
|
|
75
81
|
# Verification
|
|
76
82
|
# ---------------------------------------------------------------------------
|
|
77
|
-
puts
|
|
78
|
-
puts "=== Verification ==="
|
|
79
|
-
|
|
80
83
|
card_ok = card.name == "SecureAgent"
|
|
81
84
|
rejected_ok = unauth_error&.message&.include?("Unauthorized")
|
|
82
85
|
accepted_ok = auth_task.status.state == "completed"
|
|
83
86
|
result_ok = auth_result&.include?("[authorized]")
|
|
87
|
+
all_ok = card_ok && rejected_ok && accepted_ok && result_ok
|
|
84
88
|
|
|
85
|
-
puts
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
puts <<~HEREDOC
|
|
90
|
+
|
|
91
|
+
=== Verification ===
|
|
92
|
+
Agent card discoverable without auth : #{card_ok ? 'PASS' : 'FAIL'}
|
|
93
|
+
Unauthenticated call rejected : #{rejected_ok ? 'PASS' : 'FAIL'}
|
|
94
|
+
Authenticated call accepted : #{accepted_ok ? 'PASS' : 'FAIL'}
|
|
95
|
+
Authenticated result is correct : #{result_ok ? 'PASS' : 'FAIL'}
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
HEREDOC
|
|
92
98
|
puts(all_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
@@ -89,10 +89,12 @@ card = A2A::Models::AgentCard.new(
|
|
|
89
89
|
inner_app = A2A.server(agent_card: card, executor: SecureEchoExecutor.new).rack_app
|
|
90
90
|
auth_app = BearerAuthMiddleware.new(inner_app, token: VALID_TOKEN)
|
|
91
91
|
|
|
92
|
-
puts
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
puts <<~HEREDOC
|
|
93
|
+
Starting SecureAgent on http://localhost:9292
|
|
94
|
+
GET /agentCard — public (no auth required)
|
|
95
|
+
POST / — requires Authorization: Bearer #{VALID_TOKEN}
|
|
96
|
+
Press Ctrl-C to stop.
|
|
97
|
+
|
|
98
|
+
HEREDOC
|
|
97
99
|
|
|
98
100
|
A2A::Server::FalconRunner.new(auth_app, port: 9292).run
|
|
@@ -32,14 +32,14 @@ client = A2A.client(url: URL)
|
|
|
32
32
|
# Phase: populate
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
if PHASE == "populate"
|
|
35
|
-
puts
|
|
36
|
-
puts "=== Phase 1: Populate — sending tasks to SQLite-backed server ==="
|
|
37
|
-
puts
|
|
38
|
-
|
|
39
35
|
card = client.agent_card
|
|
40
|
-
puts
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
puts <<~HEREDOC
|
|
37
|
+
|
|
38
|
+
=== Phase 1: Populate — sending tasks to SQLite-backed server ===
|
|
39
|
+
Agent : #{card.name}
|
|
40
|
+
DB : shown in server output
|
|
41
|
+
|
|
42
|
+
HEREDOC
|
|
43
43
|
|
|
44
44
|
messages = [
|
|
45
45
|
"alpha — first message",
|
|
@@ -51,28 +51,34 @@ if PHASE == "populate"
|
|
|
51
51
|
messages.each do |text|
|
|
52
52
|
task = client.send_task(message: A2A::Models::Message.user(text))
|
|
53
53
|
ids << task.id
|
|
54
|
-
puts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
puts <<~HEREDOC
|
|
55
|
+
sent : #{text.strip}
|
|
56
|
+
id : #{task.id}
|
|
57
|
+
state : #{task.status.state}
|
|
58
|
+
reply : #{task.artifacts.first&.parts&.first&.text}
|
|
59
|
+
|
|
60
|
+
HEREDOC
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
File.write(IDS_FILE, JSON.generate(ids))
|
|
62
64
|
puts " IDs written to #{IDS_FILE}"
|
|
63
65
|
divider
|
|
64
66
|
puts
|
|
65
|
-
puts
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
puts <<~HEREDOC
|
|
68
|
+
Populate complete. The server will now be stopped and restarted.
|
|
69
|
+
The same database file will be passed to the new server instance.
|
|
70
|
+
|
|
71
|
+
HEREDOC
|
|
68
72
|
|
|
69
73
|
# ---------------------------------------------------------------------------
|
|
70
74
|
# Phase: verify
|
|
71
75
|
# ---------------------------------------------------------------------------
|
|
72
76
|
else
|
|
73
|
-
puts
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
puts <<~HEREDOC
|
|
78
|
+
|
|
79
|
+
=== Phase 2: Verify — confirming persistence after server restart ===
|
|
80
|
+
|
|
81
|
+
HEREDOC
|
|
76
82
|
|
|
77
83
|
unless File.exist?(IDS_FILE)
|
|
78
84
|
abort "IDs file not found: #{IDS_FILE} — run the populate phase first."
|
|
@@ -84,10 +90,12 @@ else
|
|
|
84
90
|
|
|
85
91
|
results = ids.map do |id|
|
|
86
92
|
task = client.get_task(id)
|
|
87
|
-
puts
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
puts <<~HEREDOC
|
|
94
|
+
id : #{id}
|
|
95
|
+
state : #{task.status.state}
|
|
96
|
+
reply : #{task.artifacts.first&.parts&.first&.text}
|
|
97
|
+
|
|
98
|
+
HEREDOC
|
|
91
99
|
task
|
|
92
100
|
rescue A2A::Error => e
|
|
93
101
|
puts " id : #{id} MISSING — #{e.message}"
|
|
@@ -100,15 +108,16 @@ else
|
|
|
100
108
|
all_present = results.none?(&:nil?)
|
|
101
109
|
all_completed = results.compact.all? { |t| t.status.state == "completed" }
|
|
102
110
|
count_ok = results.length == ids.length
|
|
111
|
+
all_ok = all_present && all_completed && count_ok
|
|
103
112
|
|
|
104
|
-
puts
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
puts <<~HEREDOC
|
|
114
|
+
|
|
115
|
+
=== Verification ===
|
|
116
|
+
All tasks present after restart : #{all_present ? 'PASS' : 'FAIL'}
|
|
117
|
+
All tasks in completed state : #{all_completed ? 'PASS' : 'FAIL'}
|
|
118
|
+
Task count matches (#{ids.length}) : #{count_ok ? 'PASS' : 'FAIL'}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
HEREDOC
|
|
112
121
|
puts(all_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
113
122
|
exit(all_ok ? 0 : 1)
|
|
114
123
|
end
|
|
@@ -122,10 +122,12 @@ card = A2A::Models::AgentCard.new(
|
|
|
122
122
|
]
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
puts
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
puts <<~HEREDOC
|
|
126
|
+
Starting PersistentEchoAgent on http://localhost:9292
|
|
127
|
+
database: #{db_path}
|
|
128
|
+
existing tasks in DB: #{storage.size}
|
|
129
|
+
Press Ctrl-C to stop.
|
|
130
|
+
|
|
131
|
+
HEREDOC
|
|
130
132
|
|
|
131
133
|
A2A.server(agent_card: card, executor: EchoExecutor.new, storage: storage).run
|
|
@@ -106,18 +106,36 @@ module A2A
|
|
|
106
106
|
msg_hash = params["message"]
|
|
107
107
|
raise JsonRpc::InvalidParamsError, "message is required" unless msg_hash.is_a?(Hash)
|
|
108
108
|
message = Models::Message.from_hash(msg_hash)
|
|
109
|
+
task_id = params["id"]
|
|
109
110
|
|
|
110
|
-
task =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
task, ctx = if task_id
|
|
112
|
+
existing = self.class.storage.find!(task_id)
|
|
113
|
+
unless existing.status.interrupted?
|
|
114
|
+
raise UnsupportedOperationError,
|
|
115
|
+
"Task #{task_id} cannot be resumed: state is #{existing.status.state}"
|
|
116
|
+
end
|
|
117
|
+
resume_ctx = Server::ResumeContext.new(
|
|
118
|
+
task: existing,
|
|
119
|
+
message: message,
|
|
120
|
+
resume_message: message,
|
|
121
|
+
storage: self.class.storage,
|
|
122
|
+
event_router: TaskBroadcast.new
|
|
123
|
+
)
|
|
124
|
+
[existing, resume_ctx]
|
|
125
|
+
else
|
|
126
|
+
new_task = Models::Task.new(
|
|
127
|
+
status: Models::TaskStatus.new(state: Models::Types::TaskState::SUBMITTED)
|
|
128
|
+
)
|
|
129
|
+
self.class.storage.save(new_task)
|
|
130
|
+
new_ctx = Server::Context.new(
|
|
131
|
+
task: new_task,
|
|
132
|
+
message: message,
|
|
133
|
+
storage: self.class.storage,
|
|
134
|
+
event_router: TaskBroadcast.new
|
|
135
|
+
)
|
|
136
|
+
[new_task, new_ctx]
|
|
137
|
+
end
|
|
114
138
|
|
|
115
|
-
ctx = Server::Context.new(
|
|
116
|
-
task: task,
|
|
117
|
-
message: message,
|
|
118
|
-
storage: self.class.storage,
|
|
119
|
-
event_router: TaskBroadcast.new
|
|
120
|
-
)
|
|
121
139
|
self.class.executor.call(ctx)
|
|
122
140
|
self.class.storage.save(task)
|
|
123
141
|
|
data/lib/simple_a2a/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simple_a2a
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -233,6 +233,34 @@ dependencies:
|
|
|
233
233
|
- - ">="
|
|
234
234
|
- !ruby/object:Gem::Version
|
|
235
235
|
version: '0'
|
|
236
|
+
- !ruby/object:Gem::Dependency
|
|
237
|
+
name: rubocop
|
|
238
|
+
requirement: !ruby/object:Gem::Requirement
|
|
239
|
+
requirements:
|
|
240
|
+
- - ">="
|
|
241
|
+
- !ruby/object:Gem::Version
|
|
242
|
+
version: '0'
|
|
243
|
+
type: :development
|
|
244
|
+
prerelease: false
|
|
245
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
246
|
+
requirements:
|
|
247
|
+
- - ">="
|
|
248
|
+
- !ruby/object:Gem::Version
|
|
249
|
+
version: '0'
|
|
250
|
+
- !ruby/object:Gem::Dependency
|
|
251
|
+
name: flog
|
|
252
|
+
requirement: !ruby/object:Gem::Requirement
|
|
253
|
+
requirements:
|
|
254
|
+
- - ">="
|
|
255
|
+
- !ruby/object:Gem::Version
|
|
256
|
+
version: '0'
|
|
257
|
+
type: :development
|
|
258
|
+
prerelease: false
|
|
259
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
260
|
+
requirements:
|
|
261
|
+
- - ">="
|
|
262
|
+
- !ruby/object:Gem::Version
|
|
263
|
+
version: '0'
|
|
236
264
|
description: Client and server for the A2A protocol — async-first, Rack-compatible,
|
|
237
265
|
built on Falcon.
|
|
238
266
|
email:
|