fix(a2a): reformat SseEventFormatter output to JSON-RPC 2.0 notification structure
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Failing after 17s
CI / helm (pull_request) Successful in 23s
CI / security (pull_request) Failing after 49s
CI / typecheck (pull_request) Failing after 50s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 1m55s
CI / docker (pull_request) Has been skipped
CI / quality (pull_request) Successful in 3m41s
CI / e2e_tests (pull_request) Failing after 15m43s
CI / integration_tests (pull_request) Failing after 21m39s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Failing after 17s
CI / helm (pull_request) Successful in 23s
CI / security (pull_request) Failing after 49s
CI / typecheck (pull_request) Failing after 50s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 1m55s
CI / docker (pull_request) Has been skipped
CI / quality (pull_request) Successful in 3m41s
CI / e2e_tests (pull_request) Failing after 15m43s
CI / integration_tests (pull_request) Failing after 21m39s
CI / status-check (pull_request) Failing after 1s
- Updated SseEventFormatter.format() to produce JSON-RPC 2.0 compliant data payload - Added mapping from A2A event types to JSON-RPC method names (task/statusUpdate, task/artifactUpdate) - Moved event data fields into params object, with taskId included when plan_id is present - Updated BDD tests to verify JSON-RPC 2.0 structure compliance - Added test scenarios for events with/without plan_id and with custom data Fixes #1502
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
@mock_only
|
||||
Feature: A2A SSE streaming for task updates and artifacts
|
||||
As a CleverAgents developer
|
||||
I want the A2A event system to support SSE streaming
|
||||
So that clients can receive real-time task status and artifact updates
|
||||
I want the A2A event system to support SSE streaming with JSON-RPC 2.0 compliance
|
||||
So that clients can receive real-time task status and artifact updates in a standard format
|
||||
|
||||
Scenario: SSE event formatter produces valid text/event-stream output
|
||||
Given an A2aEvent with type "TaskStatusUpdateEvent" and plan_id "plan-001"
|
||||
@@ -44,9 +44,37 @@ Feature: A2A SSE streaming for task updates and artifacts
|
||||
Scenario: TaskArtifactUpdateEvent type constant is defined
|
||||
Then the TASK_ARTIFACT_UPDATE constant should equal "TaskArtifactUpdateEvent"
|
||||
|
||||
Scenario: SseEventFormatter data payload is valid JSON
|
||||
Scenario: SseEventFormatter produces JSON-RPC 2.0 compliant data payload
|
||||
Given an A2aEvent with type "TaskStatusUpdateEvent" and plan_id "plan-002"
|
||||
When I format the event as SSE
|
||||
Then the SSE data line should contain valid JSON
|
||||
And the JSON should have key "event_type" with value "TaskStatusUpdateEvent"
|
||||
And the JSON should have key "plan_id" with value "plan-002"
|
||||
And the JSON should have key "jsonrpc" with value "2.0"
|
||||
And the JSON should have key "method" with value "task/statusUpdate"
|
||||
And the JSON should have a "params" object
|
||||
And the JSON params should have key "taskId" with value "plan-002"
|
||||
|
||||
Scenario: SseEventFormatter produces JSON-RPC 2.0 for TaskArtifactUpdateEvent
|
||||
Given an A2aEvent with type "TaskArtifactUpdateEvent" and plan_id "plan-003"
|
||||
When I format the event as SSE
|
||||
Then the SSE data line should contain valid JSON
|
||||
And the JSON should have key "jsonrpc" with value "2.0"
|
||||
And the JSON should have key "method" with value "task/artifactUpdate"
|
||||
And the JSON should have a "params" object
|
||||
And the JSON params should have key "taskId" with value "plan-003"
|
||||
|
||||
Scenario: SseEventFormatter includes event data in params
|
||||
Given an A2aEvent with type "TaskStatusUpdateEvent" and plan_id "plan-004" and data {"state": "working", "progress": 0.5}
|
||||
When I format the event as SSE
|
||||
Then the SSE data line should contain valid JSON
|
||||
And the JSON params should have key "state" with value "working"
|
||||
And the JSON params should have key "progress" with value 0.5
|
||||
And the JSON params should have key "taskId" with value "plan-004"
|
||||
|
||||
Scenario: SseEventFormatter handles events without plan_id
|
||||
Given an A2aEvent with type "TaskStatusUpdateEvent" and no plan_id
|
||||
When I format the event as SSE
|
||||
Then the SSE data line should contain valid JSON
|
||||
And the JSON should have key "jsonrpc" with value "2.0"
|
||||
And the JSON should have key "method" with value "task/statusUpdate"
|
||||
And the JSON should have a "params" object
|
||||
And the JSON params should not have key "taskId"
|
||||
|
||||
@@ -161,3 +161,59 @@ def step_json_has_key_value(context: Any, key: str, value: str) -> None:
|
||||
assert context.sse_json.get(key) == value, (
|
||||
f"Expected JSON['{key}'] == '{value}', got: {context.sse_json.get(key)!r}"
|
||||
)
|
||||
|
||||
|
||||
@given('an A2aEvent with type "{event_type}" and plan_id "{plan_id}" and data {data_json}')
|
||||
def step_create_event_with_data(context: Any, event_type: str, plan_id: str, data_json: str) -> None:
|
||||
data = json.loads(data_json)
|
||||
context.event = A2aEvent(
|
||||
event_type=event_type,
|
||||
plan_id=plan_id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
@given('an A2aEvent with type "{event_type}" and no plan_id')
|
||||
def step_create_event_no_plan(context: Any, event_type: str) -> None:
|
||||
context.event = A2aEvent(
|
||||
event_type=event_type,
|
||||
plan_id=None,
|
||||
data={"status": "working"},
|
||||
)
|
||||
|
||||
|
||||
@then('the JSON should have a "{key}" object')
|
||||
def step_json_has_object(context: Any, key: str) -> None:
|
||||
assert key in context.sse_json, (
|
||||
f"Expected JSON to have key '{key}', got: {list(context.sse_json.keys())}"
|
||||
)
|
||||
assert isinstance(context.sse_json[key], dict), (
|
||||
f"Expected JSON['{key}'] to be a dict, got: {type(context.sse_json[key])}"
|
||||
)
|
||||
|
||||
|
||||
@then('the JSON params should have key "{key}" with value "{value}"')
|
||||
def step_json_params_has_string(context: Any, key: str, value: str) -> None:
|
||||
params = context.sse_json.get("params", {})
|
||||
assert params.get(key) == value, (
|
||||
f"Expected params['{key}'] == '{value}', got: {params.get(key)!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the JSON params should have key "{key}" with value {value}')
|
||||
def step_json_params_has_value(context: Any, key: str, value: str) -> None:
|
||||
params = context.sse_json.get("params", {})
|
||||
# Parse numeric values
|
||||
expected = json.loads(value)
|
||||
actual = params.get(key)
|
||||
assert actual == expected, (
|
||||
f"Expected params['{key}'] == {expected}, got: {actual!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the JSON params should not have key "{key}"')
|
||||
def step_json_params_missing_key(context: Any, key: str) -> None:
|
||||
params = context.sse_json.get("params", {})
|
||||
assert key not in params, (
|
||||
f"Expected params not to have key '{key}', but it does: {params}"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ local mode and a stub for remote subscriptions that raises
|
||||
|
||||
:class:`SseEventFormatter` converts :class:`A2aEvent` instances into
|
||||
``text/event-stream`` formatted strings per the Server-Sent Events spec.
|
||||
The data payload follows JSON-RPC 2.0 notification format.
|
||||
|
||||
:class:`EventBusBridge` subscribes to the internal ``EventBus`` and
|
||||
publishes translated :class:`A2aEvent` instances to an event queue.
|
||||
@@ -146,21 +147,45 @@ class SseEventFormatter:
|
||||
data: <json payload>
|
||||
|
||||
Two trailing newlines terminate the event per the EventSource spec.
|
||||
The data payload follows JSON-RPC 2.0 notification format.
|
||||
"""
|
||||
|
||||
# Mapping from A2A event types to JSON-RPC 2.0 method names
|
||||
_EVENT_TYPE_TO_METHOD = {
|
||||
"TaskStatusUpdateEvent": "task/statusUpdate",
|
||||
"TaskArtifactUpdateEvent": "task/artifactUpdate",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format(event: A2aEvent) -> str:
|
||||
"""Format an :class:`A2aEvent` as an SSE text block."""
|
||||
"""Format an :class:`A2aEvent` as an SSE text block.
|
||||
|
||||
The data payload is formatted as a JSON-RPC 2.0 notification:
|
||||
{"jsonrpc": "2.0", "method": "...", "params": {...}}
|
||||
"""
|
||||
# Map event_type to JSON-RPC method name
|
||||
method = SseEventFormatter._EVENT_TYPE_TO_METHOD.get(
|
||||
event.event_type,
|
||||
f"task/{event.event_type}", # fallback for unknown types
|
||||
)
|
||||
|
||||
# Build params object from event fields
|
||||
params: dict[str, Any] = dict(event.data) # Start with event.data contents
|
||||
|
||||
# Add taskId if plan_id is present
|
||||
if event.plan_id:
|
||||
params["taskId"] = event.plan_id
|
||||
|
||||
# Build JSON-RPC 2.0 notification envelope
|
||||
data_payload = json.dumps(
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
"event_type": event.event_type,
|
||||
"plan_id": event.plan_id,
|
||||
"data": event.data,
|
||||
"timestamp": event.timestamp,
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
},
|
||||
default=str,
|
||||
)
|
||||
|
||||
lines = [
|
||||
f"event: {event.event_type}",
|
||||
f"id: {event.event_id}",
|
||||
|
||||
Reference in New Issue
Block a user