Skip to content

[release/0.1.9] fix(trajectory): record subagent runs under parent's conversation_id (follow-up to #112)#114

Merged
justrach merged 1 commit into
release/0.1.9from
backport/0.1.9/subagent-trajectory-conv-scope
May 20, 2026
Merged

[release/0.1.9] fix(trajectory): record subagent runs under parent's conversation_id (follow-up to #112)#114
justrach merged 1 commit into
release/0.1.9from
backport/0.1.9/subagent-trajectory-conv-scope

Conversation

@justrach

Copy link
Copy Markdown
Owner

Summary

Backports #113 (follow-up to #111, closes #33) to release/0.1.9.
Clean cherry-pick of d94dc46ff, no merge conflicts.

The earlier backport (#112) plumbed the trajectory backend through
ForgeApp.tool_registry, but the orchestrator's actual dispatch path
goes via services.call() which constructs a fresh ToolRegistry
per call — so subagent runs still bypassed recording. This PR
finishes the fix:

  • adds Services::trajectory_repo() so the blanket
    AgentService::call impl can thread the repo into the per-call
    registry, and
  • threads parent_conversation_id through ToolCallContext so the
    child agent's events land under the root's conversation_id,
    making /trace <root_id> walk the whole subagent tree.

Live e2e verified with the 0.1.9 release binary built from this
branch: SELECT DISTINCT agent_id FROM trajectory_events WHERE conversation_id = ? now returns the parent + every dispatched
child, with child rows carrying parent_agent_id linked back to
the dispatcher.

See #113 for the full rationale, before/after table, and
implementation notes.

Test plan

Generated with Devin

Follow-up to #111. The earlier fix wired `Arc<dyn TrajectoryRepo>`
through `ForgeApp.tool_registry`, but the orchestrator's actual
tool-dispatch path goes through `services.call()` — the blanket
`AgentService::call` impl in `crate::agent` constructs a *fresh*
`ToolRegistry` per call without that repo. So even with the wiring
from #111, every Task dispatch still built a registry with
`AgentExecutor::trajectory_repo == None`, and the child run silently
bypassed recording exactly as before.

End-to-end verification (graff 0.1.9 release binary, real LLM
dispatch of `sage` from the `forge` parent):

  Before #111:           4 rows, 1 distinct agent_id, 0 parent_agent_id
  After #111:            same row count for parent conv, child rows
                         appeared but under a *new* conversation_id —
                         so `/trace <parent>` still showed only the
                         dispatch event
  After this commit:     8 rows under the parent conv_id, 2 distinct
                         agent_ids ({forge, sage}), every sage row
                         has parent_agent_id=forge, `/trace` renders
                         the child indented under the Task dispatch

Implementation:

1. `Services` trait: add `trajectory_repo()` default-`None` method.
   Overridden on `ForgeServices<F>` to return `self.infra.clone()`
   when `F: TrajectoryRepo`. This is the per-call hook the blanket
   AgentService impl needs to thread the repo into the registry
   it builds.

2. Blanket `AgentService::call` (`crate::agent`): chain
   `.with_trajectory_repo(self.trajectory_repo())` onto the
   per-call ToolRegistry. Now *every* tool dispatch — including the
   orchestrator's `services.call(...)` path — flows through a
   registry whose AgentExecutor can build child recorders.

3. `ToolCallContext` (`forge_domain`): add
   `parent_conversation_id: Option<ConversationId>`. Set by the
   orchestrator from its trajectory recorder's bound conv_id
   (falling back to its own conversation.id when recording is
   disabled). `AgentExecutor::execute` reads this via
   `parent_conversation_id_ref()` and uses it as the recorder's
   conversation_id, so child events land in the parent's
   trajectory bucket — and grandchildren land in the *root's* bucket
   because the propagation is recorder-id → ctx → next executor's
   recorder-id, not orchestrator's own conv_id.

4. `TrajectoryRecorder::conversation_id()`: small accessor so the
   orchestrator can read its own recorder's bound conv_id to set
   on the tool context (step 3).

All workspace tests pass (cargo test --workspace, 0 failures).
Recorder-layer regression test from #111 still green.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: blackfloofie-a codegraff agent <265516171+blackfloofie@users.noreply.github.com>
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@justrach justrach merged commit 1ef44d4 into release/0.1.9 May 20, 2026
@justrach justrach deleted the backport/0.1.9/subagent-trajectory-conv-scope branch May 20, 2026 20:42
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.

1 participant