Skip to content

feat(agenttool): group in-process sub-agents by conversation id (not shared session id)#156

Merged
alenkacz merged 10 commits into
mainfrom
av/subagent-conversation-grouping
Jul 17, 2026
Merged

feat(agenttool): group in-process sub-agents by conversation id (not shared session id)#156
alenkacz merged 10 commits into
mainfrom
av/subagent-conversation-grouping

Conversation

@alenkacz

@alenkacz alenkacz commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Alternative to #153. Same goal — make an in-process agenttool sub-agent group with its parent as one conversation in session-oriented observability (Langfuse) — but without overloading the storage session.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:

  • Both session.Store impls (InMemoryStore, Kafka KVStore) key solely on session.ID with whole-record last-write-wins; there is no compound key or merge.
  • AgentTool.New accepts any agent.Agent, so a persisting sub-agent (or future persistence on this path) would write under the shared id.
  • Tools execute concurrently (default 3-way), so multiple sub-agents would adopt the same parent id at once → silent lost-update clobbering of sibling / parent sessions.

#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's InvocationMetadata on the tool-execution ctx via new ContextWithInvocation / InvocationFromContext helpers. llmagent sets 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 unique session.ID (can never collide in a store). When a parent invocation is in ctx, it records the parent's conversation id in session metadata — propagated transitively, so nested sub-agents group under the root conversation.
  • store/session: new ConversationID(*State) resolver (single source of truth) — returns the conversation id from metadata, else the session's own id. The metadata key is namespaced under redpanda.agent.conversation_id so it can't be confused with caller-supplied metadata (and matches the OTel attribute name).
  • plugins/otel: emit gen_ai.conversation.id and the injector SessionID (→ langfuse.session.id) from ConversationID on 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 ctx span propagation — the sub-agent's invoke_agent span is a child of the parent's execute_tool span. 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.ID stays 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_id back-reference, or the spawning gen_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.go covers: 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 covers the ctx invocation round-trip. go build, go vet, and the agent / agenttool / store/session / plugins/otel suites pass.

🤖 Generated with Claude Code

@blacksmith-sh

This comment has been minimized.

Comment thread plugins/otel/attributes.go Outdated
// 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"

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.

why not use agent.id / agent.name ?

Comment thread plugins/otel/attributes.go Outdated
// 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"

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.

if we go for otel traceparent for assocation this is not needed (we could do it still if needed)

@alenkacz
alenkacz force-pushed the av/subagent-conversation-grouping branch from 455639a to 5dfdb6e Compare July 7, 2026 16:50
alenkacz and others added 5 commits July 8, 2026 10:10
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
alenkacz force-pushed the av/subagent-conversation-grouping branch from 6076442 to da65ee4 Compare July 8, 2026 08:13
alenkacz and others added 3 commits July 8, 2026 13:10
@birdayz
birdayz marked this pull request as ready for review July 16, 2026 17:38
alenkacz and others added 2 commits July 16, 2026 19:53
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alenkacz
alenkacz merged commit 56491d3 into main Jul 17, 2026
3 checks passed
@alenkacz
alenkacz deleted the av/subagent-conversation-grouping branch July 17, 2026 05:55
alenkacz added a commit that referenced this pull request Jul 17, 2026
…-cleanup

refactor(agent,session,otel): tighten the conversation-id API from #156
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants