📋 Prerequisites
📝 Feature Summary
Propagate two conversation-lineage HTTP headers (x-kagent-parent-context-id + x-kagent-root-context-id) on outbound KAgentRemoteA2ATool calls so a peer agent can correlate a turn with the originating chat conversation across a chain of A2A hops.
❓ Problem Statement / Motivation
Today KAgentRemoteA2ATool.run_async ships the outbound A2A message with context_id = self._last_context_id, which is a uuid4() minted once per tool instance at construction (_remote_a2a_tool.py line 159). The receiving peer sees that opaque uuid as its own A2A context_id and has no way to correlate the turn back to the originating chat conversation.
_SubagentInterceptor only forwards two headers today: x-user-id and x-kagent-source: agent (_remote_a2a_tool.py lines 73-84). The receive side already copies all inbound headers into session.state["headers"] in _agent_executor.py lines 541-545 — so the plumbing to consume new headers exists; it is only the outbound-stamping side that lacks lineage data.
Concrete failure mode that motivated this issue: a chat-tier Declarative kagent Agent delegates to a router agent over A2A; the router spawns a per-conversation worker pod (e.g. a kubernetes-sigs/agent-sandbox SandboxClaim running claude --print) and wants the same pod to serve every subsequent turn of the same chat thread. Without a stable cross-hop identifier the router has to re-create the worker on every turn — because the tool's self-generated context_id is opaque to the chat-tier and changes the moment the chat-tier's tool list is rebuilt. The user-visible symptom is "my coding agent loses memory between turns" even though the kagent UI thread is the same.
Anyone building multi-hop kagent fleets that key state on a conversation id today either has to hand-roll a header_provider callback on every KAgentRemoteA2ATool instance or accept losing per-conversation continuity at every A2A hop. Both options are friction that should not exist for the obvious correlation case.
💡 Proposed Solution
Add two outbound HTTP headers, set automatically inside KAgentRemoteA2ATool._build_call_context:
x-kagent-parent-context-id — immediate caller's session id (= the agent
that just ran this tool). Changes with every
hop.
x-kagent-root-context-id — top-of-chain context_id. Forwarded unchanged
when an inbound `x-kagent-root-context-id`
header is present in this agent's session
state; otherwise set to the caller's own
session id (this agent is the chain root).
Stays stable across hops and across turns of
the same conversation.
Derivation logic (self-healing, no per-agent configuration needed):
parent_context_id = tool_context.session.id
inbound_headers = tool_context.session.state.get("headers", {}) or {}
root_context_id = (
inbound_headers.get("x-kagent-root-context-id") # forwarded
or inbound_headers.get("x-kagent-parent-context-id") # legacy fallback
or parent_context_id # we are root
)
Receiving agents already see every inbound HTTP header in session.state["headers"] (set by A2aAgentExecutor.execute), so no server-side plumbing change is required — a peer that wants to use the root context_id just reads state["headers"]["x-kagent-root-context-id"].
Header names are exported as module-level constants (PARENT_CONTEXT_ID_HEADER, ROOT_CONTEXT_ID_HEADER) so downstream BYO agents can consume them by reference rather than by string literal.
The patch preserves backward compatibility:
- Existing
x-user-id and x-kagent-source propagation is unchanged.
- Constructor-supplied
header_provider callbacks still win — lineage headers are layered underneath the provider so a custom provider can override them when it really needs to.
- When the caller cannot resolve a session id (older
tool_context shapes used in tests / unusual integration paths), no lineage headers are emitted — outbound request matches pre-feature behavior.
A draft PR with the implementation, unit-test coverage (root, mid-chain, legacy-inbound, missing session id, provider-override, end-to-end through _SubagentInterceptor) and a DCO sign-off is attached to this issue.
🔄 Alternatives Considered
-
Per-tool header_provider callback — already exists; works but every consumer has to write the same lineage-extraction code, and most consumers don't know they should. Friction tax on the common case.
-
Reuse context_id field on A2AMessage itself to carry root identity — would mis-use a field the a2a-sdk uses for per-call session correlation, breaks the abstraction.
-
Add the data to tool_context.session.state (a session-state key) — would only round-trip if the receiving agent runs in the same kagent process, which is not the case for cross-pod A2A calls.
-
OTEL trace context propagation — already there (gen_ai.conversation.id span attribute set in _agent_executor.py) but trace IDs are not exposed to agent-level code and are not a stable per-conversation identifier across UI turns.
The explicit-header approach is the smallest viable change that solves the correlation problem at the right layer, mirrors how x-user-id is already propagated, and stays inside the HTTP plane where downstream code already reads it.
🎯 Affected Service(s)
App Service
📚 Additional Context
- Receiving-side helper code already copies all inbound HTTP headers into
session.state["headers"], so this PR only changes the outbound stamping logic plus tests.
- Draft PR with full diff + unit tests: linked above.
🙋 Are you willing to contribute?
📋 Prerequisites
📝 Feature Summary
Propagate two conversation-lineage HTTP headers (
x-kagent-parent-context-id+x-kagent-root-context-id) on outboundKAgentRemoteA2AToolcalls so a peer agent can correlate a turn with the originating chat conversation across a chain of A2A hops.❓ Problem Statement / Motivation
Today
KAgentRemoteA2ATool.run_asyncships the outbound A2A message withcontext_id = self._last_context_id, which is auuid4()minted once per tool instance at construction (_remote_a2a_tool.pyline 159). The receiving peer sees that opaque uuid as its own A2Acontext_idand has no way to correlate the turn back to the originating chat conversation._SubagentInterceptoronly forwards two headers today:x-user-idandx-kagent-source: agent(_remote_a2a_tool.pylines 73-84). The receive side already copies all inbound headers intosession.state["headers"]in_agent_executor.pylines 541-545 — so the plumbing to consume new headers exists; it is only the outbound-stamping side that lacks lineage data.Concrete failure mode that motivated this issue: a chat-tier Declarative kagent Agent delegates to a router agent over A2A; the router spawns a per-conversation worker pod (e.g. a kubernetes-sigs/agent-sandbox
SandboxClaimrunningclaude --print) and wants the same pod to serve every subsequent turn of the same chat thread. Without a stable cross-hop identifier the router has to re-create the worker on every turn — because the tool's self-generatedcontext_idis opaque to the chat-tier and changes the moment the chat-tier's tool list is rebuilt. The user-visible symptom is "my coding agent loses memory between turns" even though the kagent UI thread is the same.Anyone building multi-hop kagent fleets that key state on a conversation id today either has to hand-roll a
header_providercallback on everyKAgentRemoteA2AToolinstance or accept losing per-conversation continuity at every A2A hop. Both options are friction that should not exist for the obvious correlation case.💡 Proposed Solution
Add two outbound HTTP headers, set automatically inside
KAgentRemoteA2ATool._build_call_context:Derivation logic (self-healing, no per-agent configuration needed):
Receiving agents already see every inbound HTTP header in
session.state["headers"](set byA2aAgentExecutor.execute), so no server-side plumbing change is required — a peer that wants to use the root context_id just readsstate["headers"]["x-kagent-root-context-id"].Header names are exported as module-level constants (
PARENT_CONTEXT_ID_HEADER,ROOT_CONTEXT_ID_HEADER) so downstream BYO agents can consume them by reference rather than by string literal.The patch preserves backward compatibility:
x-user-idandx-kagent-sourcepropagation is unchanged.header_providercallbacks still win — lineage headers are layered underneath the provider so a custom provider can override them when it really needs to.tool_contextshapes used in tests / unusual integration paths), no lineage headers are emitted — outbound request matches pre-feature behavior.A draft PR with the implementation, unit-test coverage (root, mid-chain, legacy-inbound, missing session id, provider-override, end-to-end through
_SubagentInterceptor) and a DCO sign-off is attached to this issue.🔄 Alternatives Considered
Per-tool
header_providercallback — already exists; works but every consumer has to write the same lineage-extraction code, and most consumers don't know they should. Friction tax on the common case.Reuse
context_idfield onA2AMessageitself to carry root identity — would mis-use a field the a2a-sdk uses for per-call session correlation, breaks the abstraction.Add the data to
tool_context.session.state(a session-state key) — would only round-trip if the receiving agent runs in the same kagent process, which is not the case for cross-pod A2A calls.OTEL trace context propagation — already there (
gen_ai.conversation.idspan attribute set in_agent_executor.py) but trace IDs are not exposed to agent-level code and are not a stable per-conversation identifier across UI turns.The explicit-header approach is the smallest viable change that solves the correlation problem at the right layer, mirrors how
x-user-idis already propagated, and stays inside the HTTP plane where downstream code already reads it.🎯 Affected Service(s)
App Service
📚 Additional Context
session.state["headers"], so this PR only changes the outbound stamping logic plus tests.🙋 Are you willing to contribute?