Skip to content

fix(py): record subagent end_time at SubagentStop, not at conversation end#2921

Open
James Won (jwon) wants to merge 1 commit into
langchain-ai:mainfrom
jwon:jwon/fix/claude-agent-subagent-end-time
Open

fix(py): record subagent end_time at SubagentStop, not at conversation end#2921
James Won (jwon) wants to merge 1 commit into
langchain-ai:mainfrom
jwon:jwon/fix/claude-agent-subagent-end-time

Conversation

@jwon
Copy link
Copy Markdown

@jwon James Won (jwon) commented May 22, 2026

Problem

When a session spawns multiple subagents in parallel (e.g. the assistant fires several Agent tool calls in one turn), the resulting subagent chain runs are recorded with end_time equal to conversation termination, not actual subagent completion. In trace UIs this manifests as sibling subagent chain spans visually outlasting their parent Agent tool spans — sometimes by many minutes — and all closing at the same instant.

Root cause

subagent_stop_hook (python/langsmith/integrations/claude_agent_sdk/_hooks.py) intentionally defers both .end() and .patch() until clear_active_tool_runs() runs, so that PostToolUse for the parent Agent tool can attach outputs to the subagent run first.

Deferring .patch() is required (outputs aren't known yet at SubagentStop). Deferring .end() is not — it just records end_time. Because both were deferred together, RunTree.end_time was set to datetime.now() inside clear_active_tool_runs(), which only runs once at the end of receive_response().

This deferred-end behavior dates back to #2670, which introduced subagent tracing — .end() and .patch() were paired in the same code path, but only .patch() actually needs to wait.

Fix

Record end_time immediately when SubagentStop fires by calling subagent_run.end() in the hook. .patch() remains deferred until clear_active_tool_runs() so outputs added later by PostToolUse are still flushed. clear_active_tool_runs() no longer re-calls .end() for already-ended subagents (which would overwrite the correct timestamp with now()).

Orphan paths (SubagentStop never fired, or no matching Agent tool) are unchanged.

Testing

  • Updated test_subagent_stop_and_post_tool_use_set_outputs — previously asserted end_time is None after SubagentStop / PostToolUse (locking in the deferred behavior). Now asserts end_time is set at SubagentStop and is unchanged by subsequent PostToolUse and clear_active_tool_runs calls.
  • Added test_subagent_end_time_recorded_at_stop_not_conversation_end — regression test with synthetic delays between hooks, asserting the recorded end_time matches subagent stop time, not cleanup time.
  • make format && make lint && make tests pass locally (Python SDK only).

…n end

When multiple `Agent` (subagent) tool calls run in parallel within a session,
the subagent chain runs had `end_time` stamped at conversation-termination time
rather than at actual subagent completion. In trace visualizations this made
sibling `general-purpose` (chain) spans visually outlast their parent `Agent`
tool spans, with all siblings closing at the same instant.

`subagent_stop_hook` deferred both `.end()` and `.patch()` to
`clear_active_tool_runs()` so that `PostToolUse` for the parent `Agent` tool
could still attach outputs to the subagent run. `.patch()` must be deferred,
but `.end()` (which only records `end_time`) can run immediately.

Call `subagent_run.end()` in `subagent_stop_hook` to record `end_time` at the
correct moment, and drop the redundant `.end()` call from
`clear_active_tool_runs()` so the stamped time is preserved through cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jwon James Won (jwon) marked this pull request as ready for review May 22, 2026 00:20
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