agent2agent 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +165 -117
  3. data/lib/a2a/client.rb +470 -51
  4. data/lib/a2a/errors/json_rpc_error.rb +71 -0
  5. data/lib/a2a/errors/rest_error.rb +68 -0
  6. data/lib/a2a/errors.rb +535 -0
  7. data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
  8. data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
  9. data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
  10. data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
  11. data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
  12. data/lib/a2a/middleware/extract_message.rb +120 -0
  13. data/lib/a2a/middleware/fetch_task.rb +228 -0
  14. data/lib/a2a/middleware/limit_history_length.rb +123 -0
  15. data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
  16. data/lib/a2a/middleware/sse_stream.rb +235 -0
  17. data/lib/a2a/middleware.rb +7 -0
  18. data/lib/a2a/schema/definition.rb +35 -1
  19. data/lib/a2a/schema.rb +126 -0
  20. data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
  21. data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
  22. data/lib/a2a/server/dispatcher.rb +52 -54
  23. data/lib/a2a/server/env.rb +4 -6
  24. data/lib/a2a/server/triage.rb +1 -1
  25. data/lib/a2a/server.rb +10 -10
  26. data/lib/a2a/sse/event_parser.rb +202 -0
  27. data/lib/a2a/sse/json_rpc_stream.rb +27 -5
  28. data/lib/a2a/sse/rest_stream.rb +17 -5
  29. data/lib/a2a/sse/stream.rb +135 -7
  30. data/lib/a2a/sse.rb +1 -0
  31. data/lib/a2a/test_helpers.rb +89 -0
  32. data/lib/a2a/version.rb +1 -1
  33. data/lib/a2a.rb +6 -2
  34. data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
  35. data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
  36. data/lib/traces/provider/a2a.rb +2 -2
  37. metadata +49 -22
  38. data/lib/a2a/server/cancel_task.rb +0 -14
  39. data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
  40. data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
  41. data/lib/a2a/server/get_extended_agent_card.rb +0 -15
  42. data/lib/a2a/server/get_task.rb +0 -14
  43. data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
  44. data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
  45. data/lib/a2a/server/list_tasks.rb +0 -14
  46. data/lib/a2a/server/send_message.rb +0 -14
  47. data/lib/a2a/server/send_streaming_message.rb +0 -14
  48. data/lib/a2a/server/subscribe_to_task.rb +0 -14
  49. data/lib/a2a/store/processor.rb +0 -136
  50. data/lib/a2a/store/pub_sub.rb +0 -149
  51. data/lib/a2a/store/sqlite.rb +0 -533
  52. data/lib/a2a/store/webhooks.rb +0 -94
  53. data/lib/a2a/store.rb +0 -6
  54. data/lib/a2a/task_store.rb +0 -315
@@ -1,136 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async"
4
- require "console"
5
-
6
- module A2A
7
- module Store
8
- # Async background task processor, modeled after async-job's Inline processor.
9
- #
10
- # The gospel (async-job) teaches:
11
- # - Async::Idler schedules tasks when the event loop is idle (backpressure)
12
- # - Each job runs in its own fiber (not thread)
13
- # - The returned Async::Task can be .wait'd for completion notification
14
- # - Errors are caught and logged, not re-raised
15
- # - task.defer_stop protects critical sections from interruption
16
- #
17
- # This processor enables A2A's non-blocking mode (return_immediately: true).
18
- # The handler can enqueue work that executes after the HTTP response is sent.
19
- #
20
- # Usage:
21
- #
22
- # processor = A2A::Store::Processor.new
23
- #
24
- # # Fire and forget:
25
- # processor.call { store.update_state(task_id, "WORKING"); do_work; store.complete(task_id, result) }
26
- #
27
- # # Wait for completion:
28
- # task = processor.call { do_work }
29
- # task.wait
30
- #
31
- class Processor
32
- attr_reader :call_count, :complete_count, :failed_count
33
-
34
- def initialize(parent: nil)
35
- @parent = parent
36
- @call_count = 0
37
- @complete_count = 0
38
- @failed_count = 0
39
- end
40
-
41
- # Execute a block asynchronously in a background fiber.
42
- #
43
- # Returns the Async::Task so callers can optionally .wait on it.
44
- #
45
- # @yield the work to perform
46
- # @return [Async::Task]
47
- #
48
- def call(&block)
49
- @call_count += 1
50
-
51
- parent.async do |task|
52
- task.defer_stop do
53
- yield
54
- end
55
- @complete_count += 1
56
- rescue => error
57
- @failed_count += 1
58
- Console.error(self, "Background task failed", error)
59
- ensure
60
- @call_count -= 1
61
- end
62
- end
63
-
64
- def start
65
- # Ensure we have an async context available
66
- end
67
-
68
- def stop
69
- # Allow in-flight tasks to drain naturally
70
- end
71
-
72
- def status
73
- {
74
- in_flight: @call_count,
75
- completed: @complete_count,
76
- failed: @failed_count,
77
- }
78
- end
79
-
80
- private
81
-
82
- def parent
83
- @parent || Async::Task.current
84
- end
85
- end
86
- end
87
- end
88
-
89
- test do
90
- require "async"
91
-
92
- describe "A2A::Store::Processor" do
93
- it "executes blocks asynchronously" do
94
- executed = false
95
-
96
- Sync do
97
- processor = A2A::Store::Processor.new
98
- task = processor.call { executed = true }
99
- task.wait
100
- end
101
-
102
- executed.should == true
103
- end
104
-
105
- it "tracks call counts" do
106
- Sync do
107
- processor = A2A::Store::Processor.new
108
- task = processor.call { "work" }
109
- task.wait
110
-
111
- processor.complete_count.should == 1
112
- processor.call_count.should == 0 # decremented after completion
113
- end
114
- end
115
-
116
- it "handles errors gracefully" do
117
- Sync do
118
- processor = A2A::Store::Processor.new
119
- task = processor.call { raise "boom" }
120
- task.wait rescue nil # task may re-raise
121
-
122
- processor.failed_count.should == 1
123
- end
124
- end
125
-
126
- it "returns status" do
127
- Sync do
128
- processor = A2A::Store::Processor.new
129
- status = processor.status
130
- status[:in_flight].should == 0
131
- status[:completed].should == 0
132
- status[:failed].should == 0
133
- end
134
- end
135
- end
136
- end
@@ -1,149 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async"
4
- require "async/queue"
5
-
6
- module A2A
7
- module Store
8
- # Fiber-safe pub/sub for task update streaming.
9
- #
10
- # Following the gospel (async-job / protocol-http):
11
- # - Async::Queue is fiber-safe (no locks needed)
12
- # - enqueue/dequeue yield the fiber cooperatively
13
- # - nil sentinel signals end of stream
14
- #
15
- # Each subscriber gets an Async::Queue. State mutations push events
16
- # to all subscribers for the affected task. Terminal states close
17
- # all subscribers for that task.
18
- #
19
- # Usage:
20
- #
21
- # pub_sub = A2A::Store::PubSub.new
22
- #
23
- # # Subscribe (returns an Async::Queue)
24
- # queue = pub_sub.subscribe("task-123")
25
- #
26
- # # In another fiber, consume events:
27
- # Async do
28
- # while event = queue.dequeue
29
- # process(event)
30
- # end
31
- # end
32
- #
33
- # # Publish an event to all subscribers:
34
- # pub_sub.notify("task-123", { type: :status, data: { ... } })
35
- #
36
- # # Close all subscribers for a task (terminal state):
37
- # pub_sub.close("task-123")
38
- #
39
- class PubSub
40
- def initialize
41
- @subscribers = Hash.new { |h, k| h[k] = [] }
42
- end
43
-
44
- # Subscribe to updates for a task.
45
- # Returns an Async::Queue that will receive events.
46
- # A nil sentinel signals end of stream.
47
- def subscribe(task_id)
48
- queue = Async::Queue.new
49
- @subscribers[task_id] << queue
50
- queue
51
- end
52
-
53
- # Remove a specific subscriber queue.
54
- def unsubscribe(task_id, queue)
55
- @subscribers[task_id].delete(queue)
56
- @subscribers.delete(task_id) if @subscribers[task_id].empty?
57
- end
58
-
59
- # Push an event to all subscribers for a task.
60
- #
61
- # @param task_id [String]
62
- # @param event [Hash] e.g. { type: :status, data: { ... } }
63
- #
64
- def notify(task_id, event)
65
- @subscribers[task_id].each do |queue|
66
- queue.enqueue(event)
67
- end
68
- end
69
-
70
- # Close all subscribers for a task.
71
- # Sends nil sentinel and removes all subscriptions.
72
- def close(task_id)
73
- @subscribers[task_id].each do |queue|
74
- queue.enqueue(nil) # sentinel: end of stream
75
- end
76
- @subscribers.delete(task_id)
77
- end
78
-
79
- # Number of active subscribers for a task.
80
- def subscriber_count(task_id)
81
- @subscribers[task_id].size
82
- end
83
-
84
- # Total number of tasks with active subscribers.
85
- def task_count
86
- @subscribers.size
87
- end
88
- end
89
- end
90
- end
91
-
92
- test do
93
- require "async"
94
-
95
- describe "A2A::Store::PubSub" do
96
- it "subscribe returns an Async::Queue" do
97
- ps = A2A::Store::PubSub.new
98
- q = ps.subscribe("t1")
99
- q.is_a?(Async::Queue).should == true
100
- end
101
-
102
- it "notify pushes events to all subscribers" do
103
- ps = A2A::Store::PubSub.new
104
- q1 = ps.subscribe("t1")
105
- q2 = ps.subscribe("t1")
106
-
107
- ps.notify("t1", { type: :status, data: "hello" })
108
-
109
- Sync do
110
- q1.dequeue.should == { type: :status, data: "hello" }
111
- q2.dequeue.should == { type: :status, data: "hello" }
112
- end
113
- end
114
-
115
- it "close sends nil sentinel and removes subscriptions" do
116
- ps = A2A::Store::PubSub.new
117
- q = ps.subscribe("t1")
118
-
119
- ps.close("t1")
120
-
121
- Sync do
122
- q.dequeue.should.be.nil
123
- end
124
- ps.subscriber_count("t1").should == 0
125
- end
126
-
127
- it "unsubscribe removes a single subscriber" do
128
- ps = A2A::Store::PubSub.new
129
- q1 = ps.subscribe("t1")
130
- q2 = ps.subscribe("t1")
131
-
132
- ps.unsubscribe("t1", q1)
133
- ps.subscriber_count("t1").should == 1
134
-
135
- ps.notify("t1", { data: "only q2" })
136
-
137
- Sync do
138
- q2.dequeue[:data].should == "only q2"
139
- end
140
- end
141
-
142
- it "does not leak when all subscribers unsubscribed" do
143
- ps = A2A::Store::PubSub.new
144
- q = ps.subscribe("t1")
145
- ps.unsubscribe("t1", q)
146
- ps.task_count.should == 0
147
- end
148
- end
149
- end