[release/0.1.9] fix(trajectory): record subagent runs under parent's conversation_id (follow-up to #112)#114
Merged
justrach merged 1 commit intoMay 20, 2026
Conversation
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>
This was referenced May 21, 2026
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.
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 pathgoes via
services.call()which constructs a freshToolRegistryper call — so subagent runs still bypassed recording. This PR
finishes the fix:
Services::trajectory_repo()so the blanketAgentService::callimpl can thread the repo into the per-callregistry, and
parent_conversation_idthroughToolCallContextso thechild 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 dispatchedchild, with child rows carrying
parent_agent_idlinked back tothe dispatcher.
See #113 for the full rationale, before/after table, and
implementation notes.
Test plan
release/0.1.9cargo test -p forge_app trajectory_recorder— all 5 testspass, including the
parent_and_child_recorders_share_conversation_trajectoryregression test from fix(trajectory): record subagent runs under their own agent_id (#33) #111/[release/0.1.9] fix(trajectory): record subagent runs under their own agent_id (#33) #112
Generated with Devin