Skip to content

feat(coding-agent): report in-flight tool-call progress in agent_observe - #891

Open
Hotragn wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Hotragn:fix/822-agent-observe-tool-elapsed
Open

feat(coding-agent): report in-flight tool-call progress in agent_observe#891
Hotragn wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Hotragn:fix/822-agent-observe-tool-elapsed

Conversation

@Hotragn

@Hotragn Hotragn commented Aug 7, 2026

Copy link
Copy Markdown

Fixes #822.

agent_observe told an orchestrator that a child was executing a tool call, never how long. AgentObserveAgentSummary collapsed the whole in-flight set into status: "tool" — no count, no start time, no duration — so a child three seconds into a fast bash call and a child forty minutes into one blocked on a subprocess that never exits produced byte-identical summaries. Since bash's timeout is optional with no default, an indefinitely blocking call is ordinary behaviour rather than an error path, and there was no way to write "treat a child stuck in one tool call for more than N minutes as wedged" against the API.

The time dimension did not exist

AgentState tracked pendingToolCalls as a bare Set<string> of ids; nothing anywhere recorded when a call started, so this could not be fixed in the coding-agent alone.

packages/agent now keeps AgentState.pendingToolCallStartedAt: ReadonlyMap<string, number> alongside it — populated in the tool_execution_start reducer, removed in tool_execution_end, and cleared by reset() and finishRun() exactly where pendingToolCalls is. It is replaced rather than mutated in place, matching how the existing set is updated, so snapshot readers keep seeing a stable value. It joins the Omit lists that keep runtime-owned state out of initialState, so it cannot be seeded from the outside.

What summaries now carry

Every AgentObserveAgentSummary gains:

  • pendingToolCallCount — tool calls executing right now. Required, so a caller can always branch on it. A passive (non-resident) subagent has no live agent and reports 0.
  • oldestPendingToolCallStartedAt — epoch ms of the longest-running in-flight call. Absent when nothing is in flight.
  • pendingToolCallElapsedMs — how long that call has been running, measured when the summary was built.

Both the timestamp and the duration are reported deliberately: elapsed is what a policy reads directly, while the timestamp lets a caller recompute against its own clock or diff across two polls without trusting the daemon's wall clock. summarizeInFlightToolCalls is a small exported pure function so all three fields have one definition and one test surface, and it clamps elapsed at 0 rather than reporting a negative duration if the clock moves backwards.

The Python agent_observe skill passes host payloads through unchanged, so the fields are available from the kernel with no skill change. SKILL.md documents them with the policy this issue asks for:

snapshot = await agent_observe.get_agent(handle.name)
agent = snapshot["agent"]
stuck_for = agent.get("pendingToolCallElapsedMs") or 0
if agent["status"] == "tool" and stuck_for > 10 * 60 * 1000:
    await agent_message.send(
        "You have been in one tool call for over ten minutes. Report what it is waiting on.",
        receiver_role="child",
        receiver_name=handle.name,
    )

Scope

status was already correct for a wedged child — it reports "tool", and latestMessage.toolCalls names the tools — so nothing about status classification changed. This adds only the missing time and count.

No daemon schema revision: AgentObserveAgentSummary is carried inside agent_observe.* host-bridge results, not the daemon command/event wire types the schema id covers.

Tests

packages/coding-agent/test/suite/regressions/822-agent-observe-tool-elapsed.test.ts (4 cases): the empty case reports a zero count and no timestamps; a three-call map picks the oldest start rather than the most recent and derives elapsed from it; a backwards clock yields 0 rather than a negative; and a live-session case runs two tools that block until released, asserting the tracked ids match pendingToolCalls exactly, that the recorded starts are not earlier than the moment the turn began, that both calls are counted concurrently, and that the map is empty again once the run finishes.

Three existing controller fixtures construct summaries by hand and were updated for the now-required count.

Verified: npm run check clean; packages/agent full suite 69/69; agent-session-observe, agent-session-services, and the new suite 9/9.

Note

Report in-flight tool-call count and elapsed time in agent_observe summaries

  • Adds pendingToolCallStartedAt: Map<string, number> to AgentState in agent.ts, recording epoch ms start times keyed by tool call ID on tool_execution_start and cleared on tool_execution_end.
  • Introduces summarizeInFlightToolCalls in agent-observe.ts to compute pendingToolCallCount, oldestPendingToolCallStartedAt, and pendingToolCallElapsedMs from the state map.
  • Spreads these fields into live agent summaries in listAgents/getAgent responses; passive subagents without a live session report pendingToolCallCount: 0.
  • Updates SKILL.md with documentation and example policy code for detecting wedged child agents using the new timing fields.

Macroscope summarized 3e1ac80.

AgentObserveAgentSummary collapsed a child's entire set of in-flight tool
calls into status: "tool" with no count and no time. A child three seconds
into a fast bash call and a child forty minutes into one blocked on a
subprocess that never exits produced byte-identical summaries, so an
orchestrator had no way to express "treat a child stuck in one tool call for
more than N minutes as wedged". The bash tool's timeout is optional with no
default, so an indefinitely blocking call is ordinary behavior, not an error
path.

The agent runtime only tracked pending tool call ids, never when they started.
Add AgentState.pendingToolCallStartedAt, keyed by the same ids as
pendingToolCalls and cleared on tool_execution_end, reset, and run finish.

Summaries now carry pendingToolCallCount plus, when something is in flight,
oldestPendingToolCallStartedAt and pendingToolCallElapsedMs. Both the
timestamp and the duration are reported: elapsed is what a policy reads
directly, and the timestamp lets a caller recompute against its own clock or
across polls. A passive subagent has no live agent, so it reports a count of
zero.

fixes PrimeIntellect-ai#822
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.

[coding-agent] agent_observe reports that a child is in a tool call but never for how long — no elapsed time, no in-flight tool-call count

1 participant