Skip to content

[FEATURE] Propagate parent + root context_id headers on outbound A2A calls #1926

Closed

Description

@Prefix

📋 Prerequisites

  • I have searched the existing issues to avoid creating a duplicate
  • By submitting this issue, you agree to follow our Code of Conduct

📝 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

  1. 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.

  2. 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.

  3. 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.

  4. 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?

  • I am willing to submit a PR for this feature

Activity

  1. added a commit that references this issue on Jun 3, 2026
    95b6754
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions