Skip to content

feat(adk): propagate parent + root context_id headers on outbound A2A calls - #1927

Merged
EItanya merged 8 commits into
kagent-dev:mainfrom
Prefix:feat/a2a-conversation-lineage-headers
Jun 3, 2026
Merged

EItanya merged 8 commits into
kagent-dev:mainfrom
Prefix:feat/a2a-conversation-lineage-headers

Conversation

@Prefix

@Prefix Prefix commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stamps two conversation-lineage HTTP headers on every outbound KAgentRemoteA2ATool call so a remote peer can correlate the turn with the originating chat conversation across a chain of A2A hops.

  • x-kagent-parent-context-id — immediate caller's session id (changes per hop)
  • x-kagent-root-context-id — top-of-chain context_id (stable across hops + across turns of the same conversation)

Closes #1926.

Why

KAgentRemoteA2ATool.run_async ships the outbound A2A message with context_id = self._last_context_id, a uuid4() minted once per tool instance. The receiving peer has no way to correlate the turn with the chat that started it. Anyone building multi-hop kagent fleets that key per-conversation state (sessions, worker pods, idempotency tokens, cache entries) on a stable identifier today either hand-rolls a header_provider on every tool or accepts losing continuity at every hop.

Motivating concrete failure mode: a chat-tier Declarative agent delegates to a router that spawns a per-conversation worker pod (e.g. a kubernetes-sigs/agent-sandbox SandboxClaim running claude --print); the router has no stable identifier to key the claim on, so a fresh pod spawns every turn and the user-visible symptom is "the agent loses memory between turns" even though the kagent UI thread is the same.

The receive side already copies all inbound HTTP headers into session.state["headers"] (set by A2aAgentExecutor.execute, lines 541-545), so no server-side plumbing change is needed — a peer that wants the root context_id just reads it from session state.

What changed

python/packages/kagent-adk/src/kagent/adk/_remote_a2a_tool.py

  • Two new module-level header constants: PARENT_CONTEXT_ID_HEADER and ROOT_CONTEXT_ID_HEADER.
  • _build_call_context now also derives a lineage-headers dict and merges it into the outbound ClientCallContext state. The existing header_provider callback still wins on overlap, so callers can override lineage when they need to.
  • Helper _build_lineage_headers derives parent_context_id from tool_context.session.id and root_context_id from the inbound state["headers"] (forwarded unchanged when present, falls back to legacy parent header for older callers, falls back to caller's own session id when this agent is the chain root). Returns {} when no session id is resolvable so the outbound request matches pre-feature behavior on stub tool contexts.

python/packages/kagent-adk/tests/unittests/test_remote_a2a_tool.py

  • Extends _MockSession/MockToolContext to expose id and state so lineage logic is testable.
  • Extends _make_tool helper to forward an optional header_provider.
  • New TestLineageHeaderPropagation class covers six cases:
    • Root agent stamps own id as parent + root.
    • Mid-chain agent forwards root unchanged + overrides parent with own id.
    • Legacy inbound with only parent header promotes it to root.
    • No session id → no lineage headers (non-breaking).
    • header_provider overrides lineage when callers need to.
    • End-to-end through _SubagentInterceptor onto outbound http_kwargs.headers.

Test plan

  • uv run ruff format --diff → clean
  • uv run ruff check → "All checks passed!"
  • uv run pytest packages/kagent-adk/tests/unittests/test_remote_a2a_tool.py -q → 28 passed

Notes for reviewers

  • Marked draft to gather design feedback first per CONTRIBUTING.md guidance. Happy to iterate on header naming, derivation rules, or scope.
  • Backward-compatible: callers that ignore the new headers see no behavior change; callers that opt in just read session.state["headers"][ROOT_CONTEXT_ID_HEADER].
  • The export of header-name constants is intentional so BYO consumers (from kagent.adk._remote_a2a_tool import ROOT_CONTEXT_ID_HEADER) avoid string-literal drift.

@github-actions github-actions Bot added the enhancement New feature or request label May 26, 2026
@Prefix
Prefix marked this pull request as ready for review May 26, 2026 16:15
Copilot AI review requested due to automatic review settings May 26, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR adds propagation of conversation lineage headers to outbound A2A calls so downstream agents can correlate requests across multi-hop subagent chains.

Changes:

  • Introduced x-kagent-parent-context-id and x-kagent-root-context-id header derivation and propagation in KAgentRemoteA2ATool.
  • Updated call-context header merging logic to allow header_provider overrides while preserving lineage by default.
  • Added unit tests covering lineage semantics and verifying headers reach outbound HTTP via _SubagentInterceptor.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
python/packages/kagent-adk/src/kagent/adk/_remote_a2a_tool.py Adds lineage header constants and logic to compute/merge outbound propagation headers.
python/packages/kagent-adk/tests/unittests/test_remote_a2a_tool.py Extends mocks to support session id/state and adds focused tests for lineage propagation & override behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@EItanya EItanya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes make sense to me overall. @supreme-gg-gg is the expert on this flow so I'd like his take first. The one thing I'd like to say is that we should add this to the go runtime as well.

@Prefix
Prefix force-pushed the feat/a2a-conversation-lineage-headers branch from 0034e12 to ee2f61b Compare May 27, 2026 13:23
@Prefix

Prefix commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

added go lang

@Prefix
Prefix force-pushed the feat/a2a-conversation-lineage-headers branch from ee2f61b to 9f8ea4d Compare May 27, 2026 18:22

@supreme-gg-gg supreme-gg-gg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm overall, one small comment

@Prefix
Prefix requested a review from supreme-gg-gg May 29, 2026 07:25
Prefix and others added 4 commits May 29, 2026 12:49
… calls

When an agent calls a peer via KAgentRemoteA2ATool, the outbound A2A message
today carries only the tool's own pre-generated context_id (a uuid4() minted
at tool construction time). The receiving agent has no way to correlate
that turn back to the originating chat conversation, which makes it
impossible to key per-conversation state (sessions, sandbox pods, cache
entries, idempotency tokens) on a stable identifier across a chain of A2A
hops.

This change has KAgentRemoteA2ATool stamp two HTTP headers on every
outbound call:

  x-kagent-parent-context-id  — the immediate caller's session id (the
                                agent that just ran this tool). Changes
                                with every hop.

  x-kagent-root-context-id    — the top-of-chain context_id, forwarded
                                unchanged through every hop. Stays stable
                                across hops and across turns of the same
                                conversation. Set to the caller's own
                                session id when this agent is the root.

Receiving agents already see all inbound HTTP headers in
session.state['headers'] (set by A2aAgentExecutor.execute), so no
server-side plumbing change is needed; a peer that wants to use the root
context_id just reads it from session state.

Includes new unit-test coverage for the root, mid-chain, legacy-inbound,
empty-session-id, and provider-override paths plus an end-to-end check
that the headers make it through _SubagentInterceptor onto the outbound
http_kwargs.

Header constants are exported from the module so downstream BYO agents
can consume them by reference rather than string-literal.

The existing x-user-id propagation and any caller-supplied header_provider
output are preserved; lineage headers are layered underneath the provider
so a custom provider can still override them when needed.

Why this matters in practice — example use case that motivated the
change: a chat agent delegates to a router agent via A2A; the router
spawns a per-conversation sandbox pod and wants the same pod to serve
every subsequent turn of the same chat. Without a stable identifier the
router has to re-create the sandbox on every turn (because the tool's
self-generated context_id is opaque to the chat agent). With the root
header, the router keys the sandbox claim on x-kagent-root-context-id
and reuses the same pod across turns.

Signed-off-by: Lukas <[email protected]>
…g + annotate test helper

Addresses Copilot review feedback on kagent-dev#1927.

- _build_lineage_headers now hard-returns {} when the local session id is
  unresolvable. Without this guard the function would emit a root-only
  header forwarded from inbound state, contradicting the docstring
  contract "no session id → no lineage headers" and matching the
  existing unit-test assertion.

- _make_tool's header_provider parameter gains Callable type annotation
  matching the production signature.

Signed-off-by: Lukas <[email protected]>
Mirrors the Python ADK change from this PR over into the Go adk
KAgentRemoteA2ATool path so multi-hop A2A fleets get the same
cross-hop conversation lineage in both runtimes.

Single file, no executor change needed: the Go interceptor still has
access to the inbound a2asrv.CallContext on ctx (already used by the
authzForwardingInterceptor), so the root header can be forwarded
unchanged from inbound RequestMeta without mirroring Python's
session.state["headers"] copy in _agent_executor.py.

Wiring follows the existing kagent precedent in remote_a2a_tool.go:
- parent_context_id comes from ctx.SessionID() stashed via a context
  value at the same call sites that already stash userIDContextKey,
  matching userIDForwardingInterceptor's pattern;
- root_context_id is read from the inbound a2asrv.CallContext via
  CallContextFrom, matching authzForwardingInterceptor's pattern;
- pre-existing headers on req.Meta win, which gives callers using
  extraHeaders the same override knob Python exposes via
  header_provider.

Test coverage mirrors the five derivation cases in the Python
TestLineageHeaderPropagation suite: chain root, mid-chain forward,
legacy-parent promotion, no-session-id no-op, caller override wins.

Signed-off-by: Lukas Urbonas <[email protected]>
Both lineage headers ship in this PR, so no caller exists that sends
x-kagent-parent-context-id without x-kagent-root-context-id. The
fallback that promoted an inbound parent header to root covered a
population that cannot occur, so root derivation is now simply: forward
the inbound root header when present, otherwise use our own session id.

Applied to both the Python and Go runtimes; tests repurposed to assert
an inbound parent header alone no longer seeds root.

Addresses review feedback on kagent-dev#1927.

Signed-off-by: Lukas Urbonas <[email protected]>
@Prefix
Prefix force-pushed the feat/a2a-conversation-lineage-headers branch from 5da89f5 to 3d043bc Compare May 29, 2026 09:49
@Prefix
Prefix requested a review from EItanya May 31, 2026 08:47
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 2, 2026
@EItanya
EItanya merged commit 95b6754 into kagent-dev:main Jun 3, 2026
23 checks passed
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
kagent 0.9.6

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre><!-- Release notes generated using configuration in .github/release.yml at v0.9.6 -->

## What's Changed
### Features
* feat(mcp): expose agents as MCP resource with change notifications by @onematchfox in kagent-dev/kagent#1856
* feat(adk): propagate parent + root context_id headers on outbound A2A calls by @Prefix in kagent-dev/kagent#1927
### Bug Fixes
* fix(compaction): preserve namespaced tool names in LLM summarizer prompt by @yashrajshuklaaa in kagent-dev/kagent#1907
* fix: preserve Ollama streaming tool calls by @andreivince in kagent-dev/kagent#1948
* fix(bedrock): preserve thinking blocks in multi-turn tool use by @mesutoezdil in kagent-dev/kagent#1873
* fix(translator): forward Anthropic config fields to model config by @mesutoezdil in kagent-dev/kagent#1962
* fix(openai): apply ReasoningEffort in applyOpenAIConfig by @mesutoezdil in kagent-dev/kagent#1959
* fix(mcp): handle null tools field in MCP server configs by @mesutoezdil in kagent-dev/kagent#1960
### Documentation
* docs(api): add field descriptions for BYO agent spec by @marians in kagent-dev/kagent#1969
### Other Changes
* Fix/1945 fix for wrong validation being triggered by @gauravshinde1729 in kagent-dev/kagent#1946
* openclaw substrate support  by @peterj in kagent-dev/kagent#1939
* chore(deps): bump the npm-minor-patch group in /ui with 14 updates by @dependabot[bot] in kagent-dev/kagent#1951
* Link fixes by @kristin-kronstain-brown in kagent-dev/kagent#1950
* RemoteMCPServer TLS + companion-Secret API + plaintext egress rewrite by @iplay88keys in kagent-dev/kagent#1905

## New Contributors
* @yashrajshuklaaa made their first contribution in kagent-dev/kagent#1907
* @andreivince made their first contribution in kagent-dev/kagent#1948
* @kristin-kronstain-brown made their first contribution in kagent-dev/kagent#1950
* @Prefix made their first contribution in kagent-dev/kagent#1927
* @marians made their first contribution in kagent-dev/kagent#1969

**Full Changelog**: https://github.com/kagent-dev/kagent/compare/v0.9.5...v0.9.6</pre>
  <p>View the full release notes at <a href="https://github.com/kagent-dev/kagent/releases/tag/v0.9.6">https://github.com/kagent-dev/kagent/releases/tag/v0.9.6</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!10637
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants