Skip to content

[release/0.1.9] fix(trajectory): record subagent runs under their own agent_id (#33)#112

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

[release/0.1.9] fix(trajectory): record subagent runs under their own agent_id (#33)#112
justrach merged 1 commit into
release/0.1.9from
backport/0.1.9/subagent-trajectory-recording

Conversation

@justrach

Copy link
Copy Markdown
Owner

Summary

Backports #111 (closes #33) to release/0.1.9. Clean cherry-pick of
51f1897, no merge conflicts.

Fixes the case where subagent runs dispatched via the Task tool
silently bypassed the TrajectoryRecorder, leaving /trace blind
to anything below the root agent. After this, every dispatched
(conversation_id, child_agent_id) pair gets its own recorder and
child rows carry parent_agent_id linking back to the dispatcher.

See #111 for the full rationale and design notes.

Test plan

  • cherry-pick applies cleanly to release/0.1.9
  • cargo test -p forge_app trajectory_recorder — all 5 tests pass,
    including the new
    parent_and_child_recorders_share_conversation_trajectory
    regression test

Generated with Devin

Closes #33.

Before this change, the `/trace` command (and any tooling that queries
`trajectory_events`) saw only the root agent's tool calls. Every Task
dispatch that ran inside `AgentExecutor::execute` constructed a fresh
`ForgeApp` *without* a recorder, so the child orchestrator silently
bypassed the trajectory pipeline. The acceptance criterion was:

  SELECT DISTINCT agent_id
  FROM trajectory_events
  WHERE conversation_id = ?

…should return parent + every dispatched child, with child rows linked
back via `parent_agent_id`. It returned only the parent.

Fix: thread the raw `Arc<dyn TrajectoryRepo>` from `ForgeAPI::chat`
through `ForgeApp -> ToolRegistry -> AgentExecutor` via a constructor
injection (`with_trajectory_repo`), then have `AgentExecutor::execute`
build a *child* `TrajectoryRecorder` scoped to
`(conversation_id, child_agent_id, Some(parent_agent_id))` before
spinning up the child's `ForgeApp`. Recursive dispatches inherit the
repo, so grandchild runs build their own recorders against the same
backend and the `/trace` tree walks naturally.

Plumbing notes:
  - Added `AgentExecutor::with_trajectory_repo(...)` and a new
    `parent_agent_id` parameter on `AgentExecutor::execute(...)`
  - Updated both Task-tool + agent-delegation callsites in
    `ToolRegistry` to capture `agent.id` and pass it through
  - Added `ForgeApp::with_trajectory_repo(...)` mirroring the existing
    `with_trajectory_recorder(...)` chain; both are wired together in
    `ForgeAPI::chat`
  - Recording stays best-effort: every recorder error is swallowed
    with a `tracing::warn!`; a telemetry failure never aborts an
    agent run

Tests: added `parent_and_child_recorders_share_conversation_trajectory`
to `trajectory_recorder::tests` which asserts the exact acceptance
criterion at the recorder layer (distinct agent_ids in the
conversation, all child rows reference the parent, root rows have no
parent_agent_id). Full `cargo test -p forge_app` is green (736
tests).

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>
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