Summary
A2A.Client.send_message/3 and stream_message/3 place :task_id and
:context_id only at the JSON-RPC params top level (params.id /
params.contextId). They are never set on the message object itself.
Per the A2A spec, MessageSendParams is { message, configuration, metadata }
and taskId / contextId are fields of the Message. Spec-compliant
servers therefore read them from params.message, not from the top-level
params.
See: https://a2a-protocol.org/latest/specification/#414-message
For v0.2.0
See: https://a2a-protocol.org/v0.2.0/specification/#64-message-object
Impact
When the Elixir client talks to a server that reads the ids only from the
message — e.g. the reference JS SDK @a2a-js/sdk, whose
handler does const contextId = incomingMessage.contextId || task?.contextId || uuidv4()
against incomingMessage = params.message — the client-supplied contextId
is invisible. The server mints a fresh contextId (uuidv4()) on every
request, so multi-turn conversation continuity is lost: each message/send
starts a brand-new context.
This works today only because the Elixir A2A.Plug server is lenient and reads
the top-level params as a fallback (build_call_opts/2 +
maybe_put_fallback(:context_id, message.context_id)), which masks the bug in
Elixir-to-Elixir setups.
Reproduction
{:ok, _} = A2A.Client.send_message(client, "Remember 42", context_id: "ctx-1")
{:ok, _} = A2A.Client.send_message(client, "What number?", context_id: "ctx-1")
Against a JS-SDK-based agent, the second call does not share context with the
first — the agent has no memory of "42".
Fix
Mirror :task_id / :context_id onto the Message struct in
build_send_params/2 before encoding, so they serialize as message.taskId /
message.contextId (spec-compliant). Keeping the top-level params is
backward-compatible with the current Elixir server.
Summary
A2A.Client.send_message/3andstream_message/3place:task_idand:context_idonly at the JSON-RPC params top level (params.id/params.contextId). They are never set on themessageobject itself.Per the A2A spec,
MessageSendParamsis{ message, configuration, metadata }and
taskId/contextIdare fields of theMessage. Spec-compliantservers therefore read them from
params.message, not from the top-levelparams.
See: https://a2a-protocol.org/latest/specification/#414-message
For v0.2.0
See: https://a2a-protocol.org/v0.2.0/specification/#64-message-object
Impact
When the Elixir client talks to a server that reads the ids only from the
message — e.g. the reference JS SDK
@a2a-js/sdk, whosehandler does
const contextId = incomingMessage.contextId || task?.contextId || uuidv4()against
incomingMessage = params.message— the client-suppliedcontextIdis invisible. The server mints a fresh
contextId(uuidv4()) on everyrequest, so multi-turn conversation continuity is lost: each
message/sendstarts a brand-new context.
This works today only because the Elixir
A2A.Plugserver is lenient and readsthe top-level params as a fallback (
build_call_opts/2+maybe_put_fallback(:context_id, message.context_id)), which masks the bug inElixir-to-Elixir setups.
Reproduction
Against a JS-SDK-based agent, the second call does not share context with the
first — the agent has no memory of "42".
Fix
Mirror
:task_id/:context_idonto theMessagestruct inbuild_send_params/2before encoding, so they serialize asmessage.taskId/message.contextId(spec-compliant). Keeping the top-level params isbackward-compatible with the current Elixir server.