agent2agent 1.1.1 → 2.0.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +98 -241
  3. data/lib/a2a/{faraday → client}/middleware/json_rpc/request.rb +10 -8
  4. data/lib/a2a/{faraday → client}/middleware/json_rpc/response.rb +10 -9
  5. data/lib/a2a/{faraday → client}/middleware/rest/request.rb +24 -18
  6. data/lib/a2a/{faraday → client}/middleware/rest/response.rb +12 -8
  7. data/lib/a2a/{faraday → client}/middleware/schema_request.rb +9 -8
  8. data/lib/a2a/client.rb +40 -39
  9. data/lib/a2a/errors/json_rpc_error.rb +1 -2
  10. data/lib/a2a/errors/rest_error.rb +1 -2
  11. data/lib/a2a/errors.rb +1 -2
  12. data/lib/a2a/protocol/json_schema/definition.rb +276 -0
  13. data/lib/a2a/protocol/json_schema/validation_error.rb +132 -0
  14. data/lib/a2a/{schema.rb → protocol/json_schema.rb} +211 -208
  15. data/lib/a2a/protocol/protobuf.rb +447 -0
  16. data/lib/a2a/protocol.rb +31 -0
  17. data/lib/a2a/server/bindings/grpc.rb +37 -0
  18. data/lib/a2a/server/bindings/json_rpc.rb +29 -9
  19. data/lib/a2a/server/bindings/rest.rb +26 -13
  20. data/lib/a2a/server/middleware/extract_message.rb +145 -0
  21. data/lib/a2a/{middleware → server/middleware}/fetch_task.rb +82 -89
  22. data/lib/a2a/{middleware → server/middleware}/limit_history_length.rb +40 -49
  23. data/lib/a2a/{middleware → server/middleware}/limit_pagination_size.rb +36 -42
  24. data/lib/a2a/server/middleware/sse_stream.rb +236 -0
  25. data/lib/a2a/{sse → server/sse}/event_parser.rb +70 -69
  26. data/lib/a2a/server/sse/json_rpc_stream.rb +89 -0
  27. data/lib/a2a/server/sse/rest_stream.rb +63 -0
  28. data/lib/a2a/server/sse/stream.rb +261 -0
  29. data/lib/a2a/server/triage.rb +35 -4
  30. data/lib/a2a/server/well_known.rb +27 -0
  31. data/lib/a2a/server.rb +44 -58
  32. data/lib/a2a/test_helpers.rb +34 -75
  33. data/lib/a2a/version.rb +1 -1
  34. data/lib/a2a.rb +7 -11
  35. data/lib/traces/provider/a2a/agent.rb +25 -0
  36. data/lib/traces/provider/a2a.rb +1 -1
  37. metadata +39 -37
  38. data/lib/a2a/middleware/extract_message.rb +0 -120
  39. data/lib/a2a/middleware/sse_stream.rb +0 -235
  40. data/lib/a2a/proto.rb +0 -444
  41. data/lib/a2a/schema/definition.rb +0 -148
  42. data/lib/a2a/schema/validation_error.rb +0 -129
  43. data/lib/a2a/server/dispatcher.rb +0 -127
  44. data/lib/a2a/sse/json_rpc_stream.rb +0 -88
  45. data/lib/a2a/sse/rest_stream.rb +0 -62
  46. data/lib/a2a/sse/stream.rb +0 -257
  47. data/lib/traces/provider/a2a/server/dispatcher.rb +0 -26
  48. /data/lib/a2a/{middleware.rb → server/middleware.rb} +0 -0
  49. /data/lib/a2a/{sse.rb → server/sse.rb} +0 -0
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "a2a"
5
-
6
- module A2A
7
- module Schema
8
- # Raised when a Definition instance fails schema validation.
9
- #
10
- # Collects JSONSchemer error details and formats them as a
11
- # human-readable list with dot-notation field paths.
12
- #
13
- # Agent Card validation failed:
14
- # - name is required but missing
15
- # - capabilities.streaming must be boolean, got string
16
- #
17
- class ValidationError < StandardError
18
- attr_reader :errors, :definition_name, :data
19
-
20
- def initialize(errors, definition_name:, data: nil)
21
- @errors = errors
22
- @definition_name = definition_name
23
- @data = data
24
-
25
- super(build_message)
26
- end
27
-
28
- private
29
-
30
- def build_message
31
- lines = errors.map { |e| " - #{format_error(e)}" }
32
- "#{definition_name} validation failed:\n#{lines.join("\n")}"
33
- end
34
-
35
- def format_error(error)
36
- path = format_path(error)
37
- type = error["type"]
38
-
39
- case type
40
- when "required"
41
- missing = error.dig("details", "missing_keys")&.join(", ") || "unknown"
42
- if path.empty?
43
- "#{missing} is required but missing"
44
- else
45
- "#{path}.#{missing} is required but missing"
46
- end
47
- when "type"
48
- expected = Array(error.dig("schema", "type")).join(" or ")
49
- "#{path.empty? ? "(root)" : path} must be #{expected}"
50
- when "enum"
51
- allowed = error.dig("schema", "enum")&.join(", ") || "?"
52
- "#{path.empty? ? "(root)" : path} must be one of: #{allowed}"
53
- when "pattern"
54
- pattern = error.dig("schema", "pattern")
55
- "#{path.empty? ? "(root)" : path} must match pattern #{pattern}"
56
- when "format"
57
- fmt = error.dig("schema", "format")
58
- "#{path.empty? ? "(root)" : path} must be a valid #{fmt}"
59
- when "minimum", "maximum"
60
- "#{path.empty? ? "(root)" : path} #{error["error"]}"
61
- when "additionalProperties"
62
- "#{path.empty? ? "(root)" : path} has unknown properties"
63
- else
64
- detail = error["error"] || error["type"] || "invalid"
65
- "#{path.empty? ? "(root)" : path} #{detail}"
66
- end
67
- end
68
-
69
- # Convert JSON pointer like "/properties/capabilities/streaming"
70
- # to dot notation like "capabilities.streaming"
71
- def format_path(error)
72
- pointer = error["data_pointer"].to_s
73
- return "" if pointer.empty? || pointer == "/"
74
-
75
- pointer.delete_prefix("/").gsub("/", ".")
76
- end
77
- end
78
- end
79
- end
80
-
81
- test do
82
- error_data = [
83
- {
84
- "data_pointer" => "",
85
- "type" => "required",
86
- "details" => { "missing_keys" => ["name"] },
87
- "schema" => {},
88
- "error" => "missing keys: name"
89
- }
90
- ]
91
-
92
- err = A2A::Schema::ValidationError.new(error_data, definition_name: "Agent Card")
93
-
94
- it "includes the definition name" do
95
- err.message.should.include?("Agent Card")
96
- end
97
-
98
- it "formats required errors" do
99
- err.message.should.include?("name is required but missing")
100
- end
101
-
102
- it "stores the errors array" do
103
- err.errors.should == error_data
104
- end
105
-
106
- it "stores the definition name" do
107
- err.definition_name.should == "Agent Card"
108
- end
109
-
110
- nested = A2A::Schema::ValidationError.new(
111
- [{ "data_pointer" => "/capabilities/streaming", "type" => "type",
112
- "schema" => { "type" => "boolean" }, "error" => "wrong type" }],
113
- definition_name: "Agent Card"
114
- )
115
-
116
- it "formats nested paths with dot notation" do
117
- nested.message.should.include?("capabilities.streaming must be boolean")
118
- end
119
-
120
- additional = A2A::Schema::ValidationError.new(
121
- [{ "data_pointer" => "/foo", "type" => "additionalProperties",
122
- "schema" => {}, "error" => "unexpected" }],
123
- definition_name: "Test"
124
- )
125
-
126
- it "formats additionalProperties errors" do
127
- additional.message.should.include?("foo has unknown properties")
128
- end
129
- end
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "console"
5
- require "a2a"
6
-
7
- module A2A
8
- class Server
9
- # Routes incoming A2A operations to their registered handler stacks.
10
- #
11
- # Each operation maps to exactly one Rack app (a compiled middleware
12
- # stack built by the Agent DSL). The Dispatcher is a terminal Rack app
13
- # that reads env["a2a.operation"] set by Triage, looks up the matching
14
- # route, and calls it.
15
- #
16
- # Returns domain objects (A2A::Schema::Definition or A2A::Error) —
17
- # the binding layer is responsible for formatting these into HTTP.
18
- #
19
- class Dispatcher
20
- def initialize
21
- @routes = {}
22
- end
23
-
24
- # Register a handler object.
25
- #
26
- # The handler must respond to:
27
- # #operations -> Array(String) (e.g. ["SendMessage", "GetTask"])
28
- # #call(env) -> A2A::Schema::Definition | A2A::Error
29
- #
30
- def register(handler)
31
- handler.operations.each do |op|
32
- @routes[op] = handler
33
- Console.info(self) { "Registered #{handler.class.name} for #{op}" }
34
- end
35
- end
36
-
37
- def call(env)
38
- operation = env["a2a.operation"]
39
- app = @routes[operation]
40
-
41
- unless app
42
- raise A2A::UnsupportedOperationError.new(
43
- message: "Operation not supported: #{operation}"
44
- )
45
- end
46
-
47
- app.call(env)
48
- rescue A2A::Error => e
49
- e
50
- rescue => e
51
- Console.error(self, "Handler raised #{e.class}: #{e.message}", e)
52
- A2A::Error.new("Internal error", code: -32603, http_status: 500)
53
- end
54
-
55
- def handler_count
56
- @routes.size
57
- end
58
- end
59
- end
60
- end
61
-
62
- test do
63
- describe "A2A::Server::Dispatcher" do
64
- it "registers and dispatches to a handler" do
65
- handler = Object.new
66
- handler.define_singleton_method(:operations) { ["SendMessage"] }
67
- handler.define_singleton_method(:call) { |env| :dispatched }
68
-
69
- dispatcher = A2A::Server::Dispatcher.new
70
- dispatcher.register(handler)
71
- dispatcher.handler_count.should == 1
72
-
73
- env = { "a2a.operation" => "SendMessage" }
74
- result = dispatcher.call(env)
75
- result.should == :dispatched
76
- end
77
-
78
- it "returns UnsupportedOperationError for unknown operations" do
79
- dispatcher = A2A::Server::Dispatcher.new
80
- env = { "a2a.operation" => "UnknownOp" }
81
- result = dispatcher.call(env)
82
- result.should.be.is_a(A2A::UnsupportedOperationError)
83
- result.code.should == -32004
84
- end
85
-
86
- it "returns A2A::Error when handler raises one" do
87
- handler = Object.new
88
- handler.define_singleton_method(:operations) { ["SendMessage"] }
89
- handler.define_singleton_method(:call) { |_| raise A2A::TaskNotFoundError.new("t-1") }
90
-
91
- dispatcher = A2A::Server::Dispatcher.new
92
- dispatcher.register(handler)
93
-
94
- result = dispatcher.call({ "a2a.operation" => "SendMessage" })
95
- result.should.be.is_a(A2A::TaskNotFoundError)
96
- result.code.should == -32001
97
- end
98
-
99
- it "returns internal error when handler raises unexpected exception" do
100
- handler = Object.new
101
- handler.define_singleton_method(:operations) { ["SendMessage"] }
102
- handler.define_singleton_method(:call) { |_| raise "boom" }
103
-
104
- dispatcher = A2A::Server::Dispatcher.new
105
- dispatcher.register(handler)
106
-
107
- result = dispatcher.call({ "a2a.operation" => "SendMessage" })
108
- result.should.be.is_a(A2A::Error)
109
- result.code.should == -32603
110
- result.http_status.should == 500
111
- end
112
-
113
- it "dispatches to multiple operations from one handler" do
114
- received = []
115
- handler = Object.new
116
- handler.define_singleton_method(:operations) { ["SendMessage", "GetTask"] }
117
- handler.define_singleton_method(:call) { |env| received << env["a2a.operation"]; :ok }
118
-
119
- dispatcher = A2A::Server::Dispatcher.new
120
- dispatcher.register(handler)
121
-
122
- dispatcher.call({ "a2a.operation" => "SendMessage" })
123
- dispatcher.call({ "a2a.operation" => "GetTask" })
124
- received.should == ["SendMessage", "GetTask"]
125
- end
126
- end
127
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "stream"
4
-
5
- module A2A
6
- module SSE
7
- # SSE stream that wraps each event in a JSON-RPC 2.0 response envelope.
8
- #
9
- # Per the A2A spec, JSON-RPC streaming returns SSE where each `data:` line
10
- # is a full JSON-RPC response: {"jsonrpc":"2.0","id":N,"result":{...}}
11
- #
12
- # Usage:
13
- #
14
- # stream = A2A::SSE::JsonRpcStream.new(
15
- # task_id: "t1", context_id: "c1", json_rpc_id: 1
16
- # )
17
- #
18
- # Async do
19
- # stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "..." })
20
- # stream.finish
21
- # end
22
- #
23
- # [200, A2A::SSE::Stream.headers, stream]
24
- #
25
- class JsonRpcStream < Stream
26
- def initialize(json_rpc_id:, **options)
27
- @json_rpc_id = json_rpc_id
28
- super(**options)
29
- end
30
-
31
- # Emit an SSE event wrapped in a JSON-RPC envelope.
32
- #
33
- # @param data [Hash] the StreamResponse payload (becomes "result")
34
- #
35
- def event(data, **opts)
36
- envelope = {
37
- "jsonrpc" => "2.0",
38
- "id" => @json_rpc_id,
39
- "result" => data.respond_to?(:to_h) ? data.to_h : data,
40
- }
41
- super(envelope, **opts)
42
- end
43
- end
44
- end
45
- end
46
-
47
- test do
48
- describe "A2A::SSE::JsonRpcStream" do
49
- it "wraps events in JSON-RPC 2.0 envelopes" do
50
- stream = A2A::SSE::JsonRpcStream.new(
51
- task_id: "t1", context_id: "c1", json_rpc_id: 42
52
- )
53
-
54
- stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "2025-01-01T00:00:00Z" })
55
- stream.finish
56
-
57
- chunk = stream.read
58
- chunk.should.include('"jsonrpc"')
59
- chunk.should.include('"2.0"')
60
- chunk.should.include('"id":42') # numeric id preserved
61
- chunk.should.include('"result"')
62
- end
63
-
64
- it "is a subclass of SSE::Stream" do
65
- stream = A2A::SSE::JsonRpcStream.new(
66
- task_id: "t1", context_id: "c1", json_rpc_id: 1
67
- )
68
- stream.is_a?(A2A::SSE::Stream).should == true
69
- stream.is_a?(Protocol::HTTP::Body::Readable).should == true
70
- end
71
-
72
- it "typed methods chain through JSON-RPC envelope" do
73
- stream = A2A::SSE::JsonRpcStream.new(
74
- task_id: "t1", context_id: "c1", json_rpc_id: 7
75
- )
76
-
77
- stream.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "2025-01-01T00:00:00Z" })
78
- stream.finish
79
-
80
- chunk = stream.read
81
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
82
- parsed["jsonrpc"].should == "2.0"
83
- parsed["id"].should == 7
84
- parsed["result"]["statusUpdate"]["taskId"].should == "t1"
85
- parsed["result"]["statusUpdate"]["contextId"].should == "c1"
86
- end
87
- end
88
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "stream"
4
-
5
- module A2A
6
- module SSE
7
- # SSE stream for the HTTP+JSON/REST binding.
8
- #
9
- # Per the A2A spec, REST streaming returns SSE where each `data:` line
10
- # is a bare StreamResponse JSON object (no envelope wrapping).
11
- #
12
- # This is essentially just an alias for Stream — the base class already
13
- # emits bare JSON. We keep this as a named class for symmetry with
14
- # JsonRpcStream and to make binding code self-documenting.
15
- #
16
- # Usage:
17
- #
18
- # stream = A2A::SSE::RestStream.new(task_id: "t1", context_id: "c1")
19
- #
20
- # Async do
21
- # stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "..." })
22
- # stream.finish
23
- # end
24
- #
25
- # [200, A2A::SSE::Stream.headers, stream]
26
- #
27
- class RestStream < Stream
28
- end
29
- end
30
- end
31
-
32
- test do
33
- describe "A2A::SSE::RestStream" do
34
- it "emits bare JSON events (no envelope)" do
35
- stream = A2A::SSE::RestStream.new(task_id: "t1", context_id: "c1")
36
-
37
- stream.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "2025-01-01T00:00:00Z" })
38
- stream.finish
39
-
40
- chunk = stream.read
41
- chunk.should.include('"statusUpdate"')
42
- chunk.should.not.include('"jsonrpc"')
43
- end
44
-
45
- it "is a subclass of SSE::Stream" do
46
- stream = A2A::SSE::RestStream.new(task_id: "t1", context_id: "c1")
47
- stream.is_a?(A2A::SSE::Stream).should == true
48
- end
49
-
50
- it "injects task_id and context_id into typed events" do
51
- stream = A2A::SSE::RestStream.new(task_id: "t1", context_id: "c1")
52
-
53
- stream.task(status: { state: "TASK_STATE_WORKING" })
54
- stream.finish
55
-
56
- chunk = stream.read
57
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
58
- parsed["task"]["id"].should == "t1"
59
- parsed["task"]["contextId"].should == "c1"
60
- end
61
- end
62
- end
@@ -1,257 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "protocol/http/body/writable"
4
- require "json"
5
-
6
- module A2A
7
- module SSE
8
- # Async-native SSE body built on Protocol::HTTP::Body::Writable.
9
- #
10
- # Falcon's protocol-rack passes Protocol::HTTP::Body::Readable subclasses
11
- # through untouched — no Enumerable wrapping, no intermediate buffering.
12
- # This gives us true async streaming with backpressure for free.
13
- #
14
- # The gospel (protocol-http) teaches:
15
- # - Writable is a producer/consumer queue body
16
- # - write() pushes chunks; read() pops them (blocks when empty)
17
- # - close_write signals EOF (reader gets nil)
18
- # - Client disconnect raises on next write()
19
- #
20
- # Usage:
21
- #
22
- # stream = A2A::SSE::RestStream.new(task_id: "t1", context_id: "c1")
23
- #
24
- # Async do
25
- # stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "..." })
26
- # stream.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "..." })
27
- # stream.finish
28
- # end
29
- #
30
- # # Return as Rack body — Falcon streams it natively
31
- # [200, { "content-type" => "text/event-stream" }, stream]
32
- #
33
- class Stream < Protocol::HTTP::Body::Writable
34
- SSE_HEADERS = {
35
- "content-type" => "text/event-stream",
36
- "cache-control" => "no-cache, no-transform",
37
- "x-accel-buffering" => "no",
38
- "connection" => "keep-alive",
39
- }.freeze
40
-
41
- attr_reader :task_id, :context_id
42
-
43
- def initialize(task_id:, context_id:, **options)
44
- @task_id = task_id
45
- @context_id = context_id
46
- super(**options)
47
- end
48
-
49
- # Emit an SSE event.
50
- #
51
- # @param data [Hash, String] the event payload (Hashes are JSON-encoded)
52
- # @param type [String, nil] optional SSE event type field
53
- # @param id [String, nil] optional SSE event id field
54
- #
55
- def event(data, type: nil, id: nil)
56
- payload = data.is_a?(String) ? data : JSON.generate(data)
57
-
58
- buf = String.new
59
- buf << "event: #{type}\n" if type
60
- buf << "id: #{id}\n" if id
61
-
62
- # SSE spec: each line of data gets its own `data:` prefix.
63
- # For single-line JSON this is one line; multi-line is handled correctly.
64
- payload.each_line do |line|
65
- buf << "data: #{line.chomp}\n"
66
- end
67
- buf << "\n" # blank line terminates the event
68
-
69
- write(buf)
70
- end
71
-
72
- # Signal end-of-stream.
73
- # The reader will receive nil on next read(), closing the SSE connection.
74
- def finish
75
- close_write
76
- end
77
-
78
- # Convenience: the SSE headers to return in the Rack response.
79
- def self.headers
80
- SSE_HEADERS
81
- end
82
-
83
- # --- Typed event emitters -------------------------------------------
84
- #
85
- # Dynamically generated from the StreamResponse schema.
86
- # Each property in StreamResponse (task, message, statusUpdate,
87
- # artifactUpdate) gets a snake_case method that:
88
- #
89
- # 1. Injects @task_id and @context_id (using the correct key name
90
- # for each type — Task uses `id`, others use `task_id`)
91
- # 2. Builds the schema Definition from kwargs
92
- # 3. Wraps it in the StreamResponse envelope
93
- # 4. Calls #event to emit the SSE wire format
94
- #
95
- # This means you never pass task_id/context_id manually:
96
- #
97
- # stream.task(status: { state: "TASK_STATE_WORKING" })
98
- # stream.artifact_update(artifact: { ... }, append: false, last_chunk: true)
99
- # stream.status_update(status: { state: "TASK_STATE_COMPLETED" })
100
- # stream.message(role: "ROLE_AGENT", parts: [{ text: "Hello" }])
101
- #
102
-
103
- stream_response = A2A::Schema["Stream Response"]
104
-
105
- stream_response.property_refs.each do |camel_key, (_kind, title)|
106
- target_props = A2A::Schema[title].schema_properties
107
-
108
- # Task uses `id` for the task identifier; the other three use `taskId`
109
- task_id_key = target_props.include?("id") ? :id : :task_id
110
-
111
- # camelCase -> snake_case method name
112
- method_name = camel_key
113
- .gsub(/([A-Z])/) { "_#{$1.downcase}" }
114
- .delete_prefix("_")
115
-
116
- define_method(method_name) do |**kwargs|
117
- merged = { task_id_key => @task_id, context_id: @context_id }.merge(kwargs)
118
- event({ camel_key => A2A::Schema[title].new(merged).to_h })
119
- end
120
- end
121
- end
122
- end
123
- end
124
-
125
- test do
126
- require "async"
127
-
128
- describe "A2A::SSE::Stream" do
129
- it "formats SSE events correctly" do
130
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
131
-
132
- stream.event({ "task" => { "id" => "t1" } })
133
- stream.finish
134
-
135
- chunk = stream.read
136
- chunk.should.include("data: ")
137
- chunk.should.include('"task"')
138
- chunk.should.end_with("\n\n")
139
-
140
- # After finish, read returns nil
141
- stream.read.should.be.nil
142
- end
143
-
144
- it "includes event type when provided" do
145
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
146
-
147
- stream.event("hello", type: "ping")
148
- stream.finish
149
-
150
- chunk = stream.read
151
- chunk.should.include("event: ping\n")
152
- chunk.should.include("data: hello\n")
153
- end
154
-
155
- it "includes event id when provided" do
156
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
157
-
158
- stream.event("test", id: "42")
159
- stream.finish
160
-
161
- chunk = stream.read
162
- chunk.should.include("id: 42\n")
163
- end
164
-
165
- it "is a Protocol::HTTP::Body::Readable" do
166
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
167
- stream.is_a?(Protocol::HTTP::Body::Readable).should == true
168
- end
169
-
170
- it "provides standard SSE headers" do
171
- headers = A2A::SSE::Stream.headers
172
- headers["content-type"].should == "text/event-stream"
173
- headers["cache-control"].should.include("no-cache")
174
- end
175
-
176
- it "stores task_id and context_id" do
177
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
178
- stream.task_id.should == "t1"
179
- stream.context_id.should == "c1"
180
- end
181
-
182
- # --- Typed emit methods ---
183
-
184
- it "#task emits a task event with injected id and context_id" do
185
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
186
-
187
- stream.task(status: { state: "TASK_STATE_WORKING", timestamp: "2025-01-01T00:00:00Z" })
188
- stream.finish
189
-
190
- chunk = stream.read
191
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
192
- parsed["task"]["id"].should == "t1"
193
- parsed["task"]["contextId"].should == "c1"
194
- parsed["task"]["status"]["state"].should == "TASK_STATE_WORKING"
195
- end
196
-
197
- it "#status_update emits with injected task_id and context_id" do
198
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
199
-
200
- stream.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: "2025-01-01T00:00:00Z" })
201
- stream.finish
202
-
203
- chunk = stream.read
204
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
205
- parsed["statusUpdate"]["taskId"].should == "t1"
206
- parsed["statusUpdate"]["contextId"].should == "c1"
207
- parsed["statusUpdate"]["status"]["state"].should == "TASK_STATE_COMPLETED"
208
- end
209
-
210
- it "#artifact_update emits with injected task_id and context_id" do
211
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
212
-
213
- stream.artifact_update(
214
- artifact: { artifact_id: "a1", parts: [{ text: "hello" }] },
215
- append: false,
216
- last_chunk: true
217
- )
218
- stream.finish
219
-
220
- chunk = stream.read
221
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
222
- parsed["artifactUpdate"]["taskId"].should == "t1"
223
- parsed["artifactUpdate"]["contextId"].should == "c1"
224
- parsed["artifactUpdate"]["artifact"]["artifactId"].should == "a1"
225
- parsed["artifactUpdate"]["append"].should == false
226
- parsed["artifactUpdate"]["lastChunk"].should == true
227
- end
228
-
229
- it "#message emits with injected task_id and context_id" do
230
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
231
-
232
- stream.message(
233
- message_id: "m1",
234
- role: "ROLE_AGENT",
235
- parts: [{ text: "Hello" }]
236
- )
237
- stream.finish
238
-
239
- chunk = stream.read
240
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
241
- parsed["message"]["taskId"].should == "t1"
242
- parsed["message"]["contextId"].should == "c1"
243
- parsed["message"]["role"].should == "ROLE_AGENT"
244
- end
245
-
246
- it "typed methods allow overriding injected values" do
247
- stream = A2A::SSE::Stream.new(task_id: "t1", context_id: "c1")
248
-
249
- stream.task(id: "override", status: { state: "TASK_STATE_WORKING" })
250
- stream.finish
251
-
252
- chunk = stream.read
253
- parsed = JSON.parse(chunk.sub(/\Adata: /, "").strip)
254
- parsed["task"]["id"].should == "override"
255
- end
256
- end
257
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../../../a2a/server/dispatcher"
4
- require "traces/provider"
5
-
6
- Traces::Provider(A2A::Server::Dispatcher) do
7
- def call(env)
8
- attributes = {
9
- "a2a.operation" => env["a2a.operation"],
10
- }
11
-
12
- Traces.trace("a2a.server.dispatcher.call", attributes: attributes) do |span|
13
- super.tap do |status, headers, body|
14
- span["http.status_code"] = status
15
-
16
- if env["a2a.stream"]
17
- span["a2a.response_type"] = "stream"
18
- elsif env["a2a.error"]
19
- span["a2a.response_type"] = "error"
20
- elsif env["a2a.result"]
21
- span["a2a.response_type"] = "result"
22
- end
23
- end
24
- end
25
- end
26
- end
File without changes
File without changes