fix(a2a): fix legacy streaming chunk, agent card test, and metadata merge

- providers/litellm_completion: move 'final' out of the message object
  into the result envelope per the A2A spec (matches the bridge fix).
- agent endpoints test: the runtime invocation path now preserves the
  top-level 'url' on the stored card, so update the assertion to match.
- completion bridge metadata: when forwarding A2A metadata via
  extra_body.metadata, merge into any existing extra_body.metadata
  instead of replacing it, so an agent-configured metadata block is
  preserved (forward metadata still wins on key conflicts).

Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
Cursor Agent
2026-05-26 09:43:54 +00:00
parent 20129d4405
commit afc8b10f00
38 changed files with 22 additions and 8 deletions
@@ -100,7 +100,15 @@ class A2ACompletionBridgeTransformation:
extra_body = completion_params.get("extra_body")
if not isinstance(extra_body, dict):
extra_body = {}
extra_body = {**extra_body, "metadata": forward_metadata}
# Merge into any existing ``extra_body.metadata`` so an
# agent-configured ``extra_body: {metadata: {...}}`` is preserved;
# forwarded A2A metadata takes precedence on key conflicts.
existing_metadata = extra_body.get("metadata")
merged_metadata: Dict[str, Any] = (
{**existing_metadata} if isinstance(existing_metadata, dict) else {}
)
merged_metadata.update(forward_metadata)
extra_body = {**extra_body, "metadata": merged_metadata}
completion_params["extra_body"] = extra_body
verbose_logger.debug(
@@ -266,15 +266,19 @@ class A2ACompletionBridgeTransformation:
if not content and not is_final:
return None
# Build A2A streaming chunk (legacy format)
# Build A2A streaming chunk (legacy format). ``final`` is an
# envelope-level streaming property per the A2A spec and must live
# alongside ``message`` in ``result``, not inside the message object.
a2a_chunk = {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"kind": "message",
"role": "agent",
"parts": [{"kind": "text", "text": content}],
"messageId": uuid4().hex,
"message": {
"kind": "message",
"role": "agent",
"parts": [{"kind": "text", "text": content}],
"messageId": uuid4().hex,
},
"final": is_final,
},
}
@@ -414,8 +414,10 @@ class TestAgentRBACProxyAdmin:
stored_card = call_kwargs["agent"]["agent_card_params"]
new_agent_id = call_kwargs["agent_id"]
# Top-level url is dropped; supportedInterfaces points at the proxy.
assert "url" not in stored_card
# Top-level url is retained for runtime A2A invocation (the public
# well-known endpoint rewrites it before exposing to clients);
# supportedInterfaces points at the proxy.
assert stored_card["url"] == "http://localhost"
assert stored_card["supportedInterfaces"][0]["protocolBinding"] == "JSONRPC"
assert stored_card["supportedInterfaces"][0]["url"].endswith(
f"/a2a/{new_agent_id}"