feat(agenttool): group in-process sub-agents by conversation id (not shared session id)#156
Merged
Merged
Conversation
This comment has been minimized.
This comment has been minimized.
birdayz
reviewed
Jun 22, 2026
| // its activity can be told apart from the parent's even though they group | ||
| // under the same conversation (gen_ai.conversation.id). | ||
| attrAgentParentInvocationID = "redpanda.agent.parent_invocation_id" | ||
| attrAgentPath = "redpanda.agent.path" |
Contributor
There was a problem hiding this comment.
why not use agent.id / agent.name ?
birdayz
reviewed
Jun 22, 2026
| // Sub-agent linkage attributes. Emitted on a sub-agent's invocation span so | ||
| // its activity can be told apart from the parent's even though they group | ||
| // under the same conversation (gen_ai.conversation.id). | ||
| attrAgentParentInvocationID = "redpanda.agent.parent_invocation_id" |
Contributor
There was a problem hiding this comment.
if we go for otel traceparent for assocation this is not needed (we could do it still if needed)
alenkacz
force-pushed
the
av/subagent-conversation-grouping
branch
from
July 7, 2026 16:50
455639a to
5dfdb6e
Compare
In-process sub-agents (agenttool) minted a fresh `agent-tool-<name>-<nano>` session id per call, so the OTel conversation id differed from the parent and the two showed up as separate conversations in session-oriented observability (e.g. Langfuse), even though the trace tree is already connected via ctx. When a parent invocation is present in context, the sub-agent now shares the parent's session id so the two group into one conversation, and stamps linkage (is_sidechain / parent_invocation_id / agent_path) for reconstruction. Context stays isolated: the sub-agent's Messages are still built fresh and never loaded, so it cannot observe the parent's history. Falls back to the minted id when no parent invocation is in context. - agent: ContextWithInvocation / InvocationFromContext; llmagent injects the invocation into ctx before executing tools - store/session: canonical Metadata* linkage key constants - plugins/otel: emit redpanda.agent.* linkage span attributes on the invocation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent_invocation_id "sidechain" was borrowed from Claude Code's internal transcript field and reads as jargon outside that context. The boolean was also redundant: parent_invocation_id is only set on sub-agent runs, so its presence already signals a sub-agent. Drop the flag and rely on the linkage fields (parent_invocation_id + agent_path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…isted Preserve the intent of the removed "unique id prevents store collisions" comment: the id may now be shared with the parent, which is safe only because the sub-agent session is never loaded or persisted. If a store is ever added on this path, it must key on the unique invocation id / agent_path, not the shared session id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…session id Sub-agents keep their own unique session.ID and propagate only the parent's conversation id (transitively, the root) via session metadata, so the whole parent→sub-agent tree groups under one gen_ai.conversation.id without overloading the storage id. Within-trace parent/child nesting is already free via ctx propagation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
golangci-lint (unparam) flagged the second return value as never used by any caller. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alenkacz
force-pushed
the
av/subagent-conversation-grouping
branch
from
July 8, 2026 08:13
6076442 to
da65ee4
Compare
…data Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion grouping id Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
birdayz
marked this pull request as ready for review
July 16, 2026 17:38
birdayz
approved these changes
Jul 16, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alenkacz
added a commit
that referenced
this pull request
Jul 17, 2026
…-cleanup refactor(agent,session,otel): tighten the conversation-id API from #156
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Alternative to #153. Same goal — make an in-process
agenttoolsub-agent group with its parent as one conversation in session-oriented observability (Langfuse) — but without overloading the storagesession.ID.#153 made the sub-agent reuse the parent's
session.ID. That conflates a storage primary key with a telemetry grouping id, and is safe only while the sub-agent session is never persisted — an invariant nothing enforces:session.Storeimpls (InMemoryStore, KafkaKVStore) key solely onsession.IDwith whole-record last-write-wins; there is no compound key or merge.AgentTool.Newaccepts anyagent.Agent, so a persisting sub-agent (or future persistence on this path) would write under the shared id.#153's own code comment concedes this ("safe ONLY because this session is never loaded or persisted"). This PR removes the hazard instead of documenting it.
How
Decouple the conversation/grouping id from the storage id:
agent(+llmagent): expose the calling agent'sInvocationMetadataon the tool-executionctxvia newContextWithInvocation/InvocationFromContexthelpers.llmagentsets it before every tool call, so a tool can read the parent invocation (its session, conversation id) without a new tool interface; tools that ignore it are unaffected.agenttool: the sub-agent keeps its own freshly minted, globally uniquesession.ID(can never collide in a store). When a parent invocation is inctx, it records the parent's conversation id in session metadata — propagated transitively, so nested sub-agents group under the root conversation.store/session: newConversationID(*State)resolver (single source of truth) — returns the conversation id from metadata, else the session's own id. The metadata key is namespaced underredpanda.agent.conversation_idso it can't be confused with caller-supplied metadata (and matches the OTel attribute name).plugins/otel: emitgen_ai.conversation.idand the injectorSessionID(→langfuse.session.id) fromConversationIDon invocation, model and tool spans, so the whole sub-agent subtree reports one consistent conversation id.Within-trace parent/child nesting (the "sub-agent was spawned by this tool call" edge) is already free via
ctxspan propagation — the sub-agent'sinvoke_agentspan is a child of the parent'sexecute_toolspan. This PR only aligns the conversation-id value for cross-span / Langfuse session grouping.Net: grouping is identical to #153 at the telemetry layer, but every storage
session.IDstays unique — the "safe only while unpersisted" caveat is gone.Scope
Deliberately limited to sharing the conversation id — the one thing session grouping needs. Direct cross-trace parent linkage (a
parent_invocation_idback-reference, or the spawninggen_ai.tool.call.id) is not included: within a trace it is redundant with span parentage, and the only consumer that would need it across traces is the downstream ADP transcript-assembly query path. It can be added there, when a query actually requires it.Tests
tool/agenttool/session_grouping_test.gocovers: unique sub-agent id (never the parent's), grouping under the parent's conversation id, transitive propagation of the root id through nesting, context isolation, and no-parent / nil-session fallback.agent/context_test.gocovers the ctx invocation round-trip.go build,go vet, and theagent/agenttool/store/session/plugins/otelsuites pass.🤖 Generated with Claude Code