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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8200755c9d568b3b1161815a2dfd8df190efe773ae946a00fafcfddd25b1ca4e
4
- data.tar.gz: a5f112a8138f111d137b1a443c674a309c0ad98c5c6f5b8473e4c5d3af42d316
3
+ metadata.gz: 69d7ae29ed8960745bbeaebfd8368cdd52162d81f5faa8779d9d5ffc00634e21
4
+ data.tar.gz: 1bc2957a22ec2de785da814cb8d87f1a776562f9fdb7fed7ad26eb060482d3a4
5
5
  SHA512:
6
- metadata.gz: 39509e60e4665ca244831830f6b9c5cb4e3de384c98c7e5d5ae74ab15bdce5c06fb942fb85a6738a11e2d6080f8ab2c3c6334d0be02f58e04bd1f364a9df1231
7
- data.tar.gz: d6c85963a895435cffa1b3ec28ad7bc7506d168274ae2281b4e7c590ff0d4b48387cb97a4f249aaf7df01621ade3b8aeaf7e3de14e2e35cfad42625600084635
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 " Name: #{card.name}"
21
- puts " Version: #{card.version}"
22
- puts " Description: #{card.description}"
23
- puts " Skills: #{card.skills.map(&:name).join(', ')}"
24
- puts
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 " id: #{retrieved.id}"
63
- puts " state: #{retrieved.status.state}"
64
- puts " reply: #{retrieved.artifacts.first&.parts&.first&.text.inspect}"
65
- puts
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 "Starting BasicAgent on http://localhost:9292"
54
- puts "Press Ctrl-C to stop."
55
- puts
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 "=== Agent Card ==="
20
- puts " Name: #{card.name}"
21
- puts " Description: #{card.description}"
22
- puts " Streaming: #{card.capabilities&.streaming}"
23
- puts " Source: https://lamplight.guide/blog/the-god-particle/"
24
- puts
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
- puts "=== Summary ==="
67
- puts " Words received : #{word_count}"
68
- puts " Events received : #{event_count}"
69
- puts " Elapsed : #{elapsed.round(1)}s — #{wpm} WPM effective"
70
- puts " Status : #{interrupted ? "interrupted" : "completed"}"
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 "Starting StreamingAgent on http://localhost:9292"
173
- puts "Streaming #{StreamingExecutor::WORDS.length} words at 600 WPM (~#{(StreamingExecutor::WORDS.length / 600.0).ceil} min)"
174
- puts "Press Ctrl-C to stop."
175
- puts
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 "┌#{bar}┐"
21
- puts "│ #{text} │"
22
- puts "└#{bar}┘"
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
- puts "Querying Anthropic (claude-sonnet-4-6) and OpenAI (gpt-5.4) in parallel…"
45
- puts "This may take several minutes for complex topics."
46
- puts
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 "Starting multi-agent research server on http://localhost:9292"
190
- puts " /anthropic → #{AnthropicResearchExecutor::MODEL}"
191
- puts " /openai → #{OpenAIResearchExecutor::MODEL}"
192
- puts " /evaluator → #{EvaluatorExecutor::MODEL} (evaluator)"
193
- puts "Press Ctrl-C to stop."
194
- puts
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 "=== Agent Card ==="
47
- puts " Name: #{card.name}"
48
- puts " Description: #{card.description}"
49
- puts " Streaming: #{card.capabilities&.streaming}"
50
- puts
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
- divider
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 " Subscriber 1 — events received : #{sub1_events.length}"
130
- puts " Subscriber 1 — artifact steps : #{sub1_artifacts.length}"
131
- puts
132
- puts " Subscriber 2 — events received : #{sub2_events.length}"
133
- puts " Subscriber 2task snapshot : #{snapshot ? 'yes' : 'no (unexpected)'}"
134
- puts " Subscriber 2 — artifact steps : #{sub2_artifacts.length}"
135
- puts " Subscriber 2 — joined at step : #{sub1_artifacts.length - sub2_artifacts.length + 1} of 5"
136
- puts
137
- puts " Both streams terminated cleanly: #{
138
- sub1_events.any? { |e| e.is_a?(A2A::Models::TaskStatusUpdateEvent) && e.final? } &&
139
- sub2_events.any? { |e| e.is_a?(A2A::Models::TaskStatusUpdateEvent) && e.final? }
140
- }"
126
+ divider
127
+ puts <<~HEREDOC
128
+
129
+ === Summary ===
130
+ Subscriber 1 — events received : #{sub1_events.length}
131
+ Subscriber 1artifact 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 "Starting AnalysisAgent on http://localhost:9292"
71
- puts "Task runs #{AnalysisExecutor::STEPS.length} steps × #{AnalysisExecutor::STEP_DELAY}s = ~#{total.round(0).to_i}s per run"
72
- puts "Press Ctrl-C to stop."
73
- puts
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
- puts "=== Agent Card ==="
36
- puts " Name: #{card.name}"
37
- puts " Description: #{card.description}"
38
- puts " Streaming: #{card.capabilities&.streaming}"
39
- puts
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
- puts "Starting tasks A, B, and C via tasks/sendSubscribe…"
51
- puts "(each would take 10 s; task B will be cancelled after #{CANCEL_SEC}s)"
52
- puts
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 " Task A completed normally : #{a_ok ? 'PASS' : 'FAIL'}"
147
- puts " Task B was cancelled : #{b_ok ? 'PASS' : 'FAIL'}"
148
- puts " Task C completed normally : #{c_ok ? 'PASS' : 'FAIL'}"
149
- puts
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 "Starting SlowAgent on http://localhost:9292"
73
- puts "Each task takes #{SlowExecutor::STEPS * SlowExecutor::STEP_SEC}s to complete without cancellation."
74
- puts "Press Ctrl-C to stop."
75
- puts
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
- puts "=== Webhook receiver listening on #{WEBHOOK_URL} ==="
49
- puts
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 "=== Agent Card ==="
58
- puts " Name: #{card.name}"
59
- puts " Description: #{card.description}"
60
- puts " push_notifications: #{card.capabilities&.push_notifications}"
61
- puts " streaming: #{card.capabilities&.streaming}"
62
- puts
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
- puts "Watching webhook for incoming push notifications…"
126
- puts "(the client has NO open SSE connection — all updates arrive out-of-band)"
127
- puts
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
- puts "=== Summary ==="
169
- puts " Push notifications received : #{received.length}"
170
- puts " States delivered : #{received.map { |p| p.dig("status", "state") }.join(" → ")}"
171
- puts " Final delivery seen : #{received.any? { |p| p["final"] } ? 'yes' : 'no'}"
172
- puts " Config cleaned up : #{list_after.empty? ? 'yes' : 'no'}"
173
- puts
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 " Push notifications received (≥2) : #{push_ok ? 'PASS' : 'FAIL'}"
188
- puts " Final push delivery seen : #{final_ok ? 'PASS' : 'FAIL'}"
189
- puts " Final state is completed : #{state_ok ? 'PASS' : 'FAIL'}"
190
- puts " Config deleted successfully : #{cleanup_ok ? 'PASS' : 'FAIL'}"
191
- puts
192
- puts(push_ok && final_ok && state_ok && cleanup_ok ? "All assertions passed." : "One or more assertions failed.")
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 "Starting PushAgent on http://localhost:9292"
113
- puts " push_notifications: true"
114
- puts " #{PushExecutor::STEPS} steps × #{PushExecutor::STEP_SEC}s = ~#{(PushExecutor::STEPS * PushExecutor::STEP_SEC).to_i}s per task"
115
- puts "Press Ctrl-C to stop."
116
- puts
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
- puts "=== Pipeline calls (client speaks only to /pipeline) ==="
59
- puts
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 "Input: #{text}"
65
- puts output.lines.map { |l| " #{l}" }.join
66
- puts
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
- puts "=== Sub-agents called directly (verification) ==="
77
- puts
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 " Input: #{sample}"
89
- puts " /reverse result: #{artifact_text(reverse_task)}"
90
- puts " /shout result: #{artifact_text(shout_task)}"
91
- puts
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 " All pipeline tasks completed : #{all_completed ? 'PASS' : 'FAIL'}"
114
- puts " Pipeline output matches chain : #{pipeline_correct ? 'PASS' : 'FAIL'}"
115
- puts " ReverseAgent standalone correct : #{standalone_reverse_ok ? 'PASS' : 'FAIL'}"
116
- puts " ShoutAgent standalone correct : #{standalone_shout_ok ? 'PASS' : 'FAIL'}"
117
- puts
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
- all_ok = all_completed && pipeline_correct && standalone_reverse_ok && standalone_shout_ok
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 "Starting multi-agent server on #{BASE_URL}"
137
- puts " /reverse → ReverseAgent"
138
- puts " /shout ShoutAgent"
139
- puts " /pipeline PipelineAgent (chains the other two)"
140
- puts "Press Ctrl-C to stop."
141
- puts
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
- puts "=== Scenario A: input_required (OrderAgent) ==="
56
- puts
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
- puts " Client reads the question and answers: 'pasta'"
72
- puts
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
- puts "=== Scenario B: auth_required (VaultAgent) ==="
85
- puts
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
- puts " Client sends the wrong token: 'wrong-token'"
101
- puts
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
- puts " Client sends the correct token: 'open-sesame'"
111
- puts
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 "Starting interrupted-states server on #{BASE_URL}"
131
- puts " /order → OrderAgent (demonstrates input_required)"
132
- puts " /vaultVaultAgent (demonstrates auth_required)"
133
- puts "Press Ctrl-C to stop."
134
- puts
130
+ puts <<~HEREDOC
131
+ Starting interrupted-states server on #{BASE_URL}
132
+ /orderOrderAgent (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
- puts "=== Agent Card ==="
36
- puts " Name: #{card.name}"
37
- puts " Description: #{card.description}"
38
- puts
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 " base64 length : #{part.raw.length} chars"
77
- puts " decoded bytes : #{bytes.bytesize}"
78
- puts " content:"
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
- puts "=== Verification ==="
101
- puts " text part present and non-empty : #{(text_part && !text_part.text.empty?) ? 'PASS' : 'FAIL'}"
102
- puts " json part present with hash : #{(json_part && json_part.data.is_a?(Hash)) ? 'PASS' : 'FAIL'}"
103
- puts " binary part decodes correctly : #{(binary_part && binary_part.decoded_bytes.bytesize > 0) ? 'PASS' : 'FAIL'}"
104
- puts " url part is a valid URL : #{(url_part && url_part.url.start_with?("https://")) ? 'PASS' : 'FAIL'}"
105
- puts " json part topic matches input : #{(json_part && json_part.data["topic"] == topic) ? 'PASS' : 'FAIL'}"
106
- puts " binary part is valid CSV : #{(binary_part && binary_part.decoded_bytes.include?("rank")) ? 'PASS' : 'FAIL'}"
107
- puts
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 "Starting ReportAgent on http://localhost:9292"
93
- puts "Returns a 4-part artifact (text + JSON + binary + URL) for any topic."
94
- puts "Press Ctrl-C to stop."
95
- puts
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 " Name: #{card.name}"
37
- puts " Description: #{card.description}"
38
- puts
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
- puts "=== Unauthenticated client — no Authorization header ==="
46
- puts
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
- puts "=== Authenticated client — Authorization: Bearer #{TOKEN} ==="
64
- puts
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 " Agent card discoverable without auth : #{card_ok ? 'PASS' : 'FAIL'}"
86
- puts " Unauthenticated call rejected : #{rejected_ok ? 'PASS' : 'FAIL'}"
87
- puts " Authenticated call accepted : #{accepted_ok ? 'PASS' : 'FAIL'}"
88
- puts " Authenticated result is correct : #{result_ok ? 'PASS' : 'FAIL'}"
89
- puts
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
- all_ok = card_ok && rejected_ok && accepted_ok && result_ok
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 "Starting SecureAgent on http://localhost:9292"
93
- puts " GET /agentCard — public (no auth required)"
94
- puts " POST / requires Authorization: Bearer #{VALID_TOKEN}"
95
- puts "Press Ctrl-C to stop."
96
- puts
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 " Agent : #{card.name}"
41
- puts " DB : shown in server output"
42
- puts
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 " sent : #{text.strip}"
55
- puts " id : #{task.id}"
56
- puts " state : #{task.status.state}"
57
- puts " reply : #{task.artifacts.first&.parts&.first&.text}"
58
- puts
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 "Populate complete. The server will now be stopped and restarted."
66
- puts "The same database file will be passed to the new server instance."
67
- puts
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
- puts "=== Phase 2: Verify — confirming persistence after server restart ==="
75
- puts
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 " id : #{id}"
88
- puts " state : #{task.status.state}"
89
- puts " reply : #{task.artifacts.first&.parts&.first&.text}"
90
- puts
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
- puts "=== Verification ==="
106
- puts " All tasks present after restart : #{all_present ? 'PASS' : 'FAIL'}"
107
- puts " All tasks in completed state : #{all_completed ? 'PASS' : 'FAIL'}"
108
- puts " Task count matches (#{ids.length}) : #{count_ok ? 'PASS' : 'FAIL'}"
109
- puts
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
- all_ok = all_present && all_completed && count_ok
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 "Starting PersistentEchoAgent on http://localhost:9292"
126
- puts " database: #{db_path}"
127
- puts " existing tasks in DB: #{storage.size}"
128
- puts "Press Ctrl-C to stop."
129
- puts
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 = Models::Task.new(
111
- status: Models::TaskStatus.new(state: Models::Types::TaskState::SUBMITTED)
112
- )
113
- self.class.storage.save(task)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module A2A
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
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.0
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: