feat(agenttool): group in-process sub-agents by conversation id (not shared session id)#156
Draft
alenkacz wants to merge 4 commits into
Draft
feat(agenttool): group in-process sub-agents by conversation id (not shared session id)#156alenkacz wants to merge 4 commits into
alenkacz wants to merge 4 commits into
Conversation
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 The previous approach made an in-process sub-agent reuse its parent's storage session.ID purely to group them as one conversation in observability. That overloads session.ID — a store primary key — with a telemetry-grouping role, and rests on an unenforced "never persisted" invariant: both Store impls key solely on session.ID with whole-record last-write-wins, and AgentTool.New accepts any agent.Agent, so a persisting sub-agent (or future persistence on this path) would silently clobber the parent / sibling sessions under the shared id. Default 3-way concurrent tool execution makes the collision concrete. Decouple the two concepts instead: - The sub-agent keeps its own freshly minted, globally unique session id, so it can never collide in a store. The "safe only while unpersisted" caveat is gone. - Conversation grouping is carried as a derived value: agenttool stamps the parent's conversation id into session metadata (propagated transitively, so nested sub-agents group under the root), and the otel plugin emits it as gen_ai.conversation.id / the injector SessionID on invocation, model and tool spans via a single session.ConversationID resolver. Within-trace nesting was already free via ctx propagation; this only aligns the conversation-id attribute value. - Linkage metadata keys are namespaced under "redpanda.agent." so they can't be confused with caller-supplied metadata, matching the existing span attribute names. Tests updated: sub-agent gets a unique id, groups under the parent's conversation id, propagates the root id through nesting, and stays context-isolated. go build, go vet, affected suites pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Found 6 test failures on Blacksmith runners: Failures
|
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)
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:
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 — plusparent_invocation_id/agent_pathlinkage.store/session: newConversationID(*State)resolver (single source of truth) — returns the conversation id from metadata, else the session's own id. Linkage metadata keys are namespaced underredpanda.agent.*so they can't be confused with caller-supplied metadata (and match the OTel attribute names).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 nesting was already free viactxpropagation; this 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.Linkage surface (maps to Claude's model)
ConversationID(metadataredpanda.agent.conversation_id, derived attr)sessionIdInvocationID+ uniquesession.IDagentIdredpanda.agent.parent_invocation_idparentUuid/toolUseIdTests
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.go(ctx helpers) unchanged.go build,go vet, and theagenttool/store/session/plugins/otel/agentsuites pass.🤖 Generated with Claude Code