Skip to content

feature: agentop — list tool calls in the events table with their duration #1776

Open

Description

@clawgenti

Summary

Show tool calls as their own rows in agentop's events table, each with a measured duration, so you can see and sort a session's tool calls next to its model calls.

Today

Since #1658 (on main, not yet released), tool calls and results show in the detail of the event that carries them (toolCalls under the assistant message, toolResults under the user message). The events table still has one row per request and response, so a tool call can't be listed, filtered or sorted from there.

Tool timing is already computed, but only for the Timeline pane:

So there is per-tool latency, but the events table doesn't show it and there is no per-call list.

Request

  1. Tool rows in the events table. One row per tool call: tool name, preview (command or path), agent, start, duration, and whether the result was an error. A row is still "running" until its result arrives. Enter opens the response that requested the tool and the request that returned its result.
  2. Sorting and filtering by duration. The same sorting the table has for model calls, to answer questions like "which tool calls took longest".
  3. Totals per tool name (optional): count, total and p50/p95 duration per tool for the session, e.g. Bash, Read, an MCP tool.

Notes

Requested in Slack by Gosia Steinder, with @huang195 agreeing there's a use case.


Spec (v1)

Agreed scope for the first version. /sdd-fast 1776 executes this section.

Scope

In v1:

  • One row per tool call in the events table, under the exchange whose response asked for it.
  • Per row: tool name, preview, start time, duration, result status.
  • Sort and search on tool rows with the existing table controls.

Not in v1:

  • Enter on a tool row. The row's position already shows which exchange asked for the tool and which exchange carries its result.
  • Totals per tool name (count, p50, p95). Request item 3 is out.
  • A new API field. All data is already in GET /v1/sessions/{id}/timeline.
  • A "waiting for you" label (for example on AskUserQuestion). It needs a list of agent-specific tool names.

Facts the design depends on

Measured on 40 local Claude Code sessions (3,364 tool calls, 3,150 steps):

Fact Result
All tools of one response have the same start (the response's end) 3,150 / 3,150 steps
Parallel tools of one response have the same end 165 / 165 parallel steps
A tool's end is the start of the next request of the same agent 3,181 / 3,181 ended tools
Agent / Task tool duration max 2.9s. The subagents run in the background for 12s to 647s
lost tools that are StructuredOutput / SubagentHandback 175 / 178. These end the run by design (core/session/timeline/agents.go)
Steps with one tool 95%

From the code:

  1. agentop loads events with view=summary, which sets ToolCalls, ToolResults and Messages to nil (core/sessionapi/summary.go). So tool rows must come from the timeline endpoint, not from agentop's events.
  2. timeline.Tool.Seq is the seq of the response that asked for the tool. exchangeResp(row).Seq gives the same value for an events-table row. This is the join key.
  3. timeline.Agent.SpawnTool names the Agent tool call that started the subagent. SpawnedAt and ReturnedAt give the subagent's run.
  4. A background job (timeline.Job) belongs to the tool where job.Agent == tool.Agent, job.Start == tool.End and tool.Name == "Bash". Without the name check, 2 of 24 jobs matched two parallel tools.
  5. The Timeline pane already gets and refreshes the timeline (cmd/agentop/tui/timeline_pane.go): stale-driven at most once a second, idle refresh every 10s, backoff on error. handleTimelineTick returns early when m.pane != paneTimeline.
  6. tui/table renders a one-cell row across all columns (renderRow → renderSectionRow). Subagent block headers use this. A rendered row with an empty eventRow{} in visibleRows is already handled (block headers, the older-events row).
  7. Subagent blocks render their member rows together under the header (rebuildEventsTable). So a tool row placed directly after its exchange never lands between another agent's rows.
  8. Column widths: STATUS 7, DURATION 9, METHOD 18 (methodColWidth). The extras below do not fit in these columns.

UI

Chronological order (no sort):

 #    TIME         DIR   METHOD           STATUS  DURATION
 12   15:04:05.12  out   claude-opus-5-5     200     4.21s   70.6k→312  $0.2546  api.anthropic.com
 ┣⚒   15:04:09.33        Bash                 ok   ∥13.21s   git -C /tmp/wt status
 ┗⚒   15:04:09.33        Read                 ok   ∥13.21s   tui/events_columns.go
 13   15:04:22.54  out   claude-opus-5-5     200     3.02s   71.9k→188  $0.0911  api.anthropic.com
 ┗⚒   15:04:25.56        Agent                ok     1.7s    run 5m48s · explore the table code
 14   15:04:27.27  out   claude-opus-5-5     200     2.75s   72.4k→96   $0.0402  api.anthropic.com
 ┣⚒   15:04:30.02        Bash                 ok     0.30s   bg 4m02s · go test ./... > /tmp/t.log
 ┗⚒   15:04:30.02        Bash            running    ⋯ 38s    go vet ./...

Sorted (for example DURATION▼):

 #     TIME         METHOD        STATUS  DURATION▼
 ↳14   15:04:30.02  Bash         running     ⋯ 38s   go vet ./...
 ↳12   15:04:09.33  Bash              ok   ∥13.21s   git -C /tmp/wt status
 ↳12   15:04:09.33  Read              ok   ∥13.21s   tui/events_columns.go
 9     15:02:40.11  claude-opus-5-5  200     4.40s   ...

Rules:

  1. Placement. While the table is in time order, the tool rows of an exchange render directly after that exchange's row, in timeline order. This applies to main rows and to rows inside an expanded subagent block. A tool row shows only when its exchange shows: a collapsed block and hideInactive hide it with its exchange.
  2. # cell. ┣⚒ on each tool row of the exchange except the last, and ┗⚒ on the last. When a sort is active, show ↳<n> instead, where <n> is the parent exchange's number in the # column.
  3. DIR cell. Empty. Inside an expanded subagent block it gets the same │ gutter that member rows get.
  4. TIME cell. The tool's start, in the same format as exchange rows.
  5. ACTION, PLUGIN cells. Empty.
  6. METHOD cell. The tool name.
  7. STATUS cell. One of:
    • running: end is absent.
    • error: error is true. Use the error style that exchange rows use.
    • lost: lost is true and the tool is not an end-of-run tool (rule 9).
    • ok: all other tools with an end.
    • empty: end-of-run tools (rule 9).
  8. DURATION cell. end − start, formatted with formatDuration.
    • Prefix ∥ when another tool of the same response has the same end.
    • running: ⋯ <now − start>. It updates on each rebuild.
    • lost and end-of-run tools: empty.
  9. End-of-run tools. A tool with lost true whose name is StructuredOutput or SubagentHandback ends the run by design. It is not a failure. Show ends run in the trailing cell and nothing in STATUS or DURATION. These two names are the ones core/session/timeline already treats this way. Do not add others.
  10. Trailing cell. All columns after DURATION become one cell that spans to the right edge of the table. It holds, in this order and joined with ·:
    • run <ReturnedAt − SpawnedAt> for a tool that a timeline.Agent names as its SpawnTool. If ReturnedAt is absent, show run ⋯ <now − SpawnedAt>.
    • bg <job.End − job.Start> for a Bash tool with a matching job (fact 4). If the job has no End, show bg ⋯ <now − job.Start>.
    • ends run (rule 9).
    • The preview.
  11. Sort. Tool rows take part in the sort with their own keys:
    • DURATION: the tool's duration. running uses now − start. lost and end-of-run tools use 0.
    • TIME: start.
    • METHOD: the tool name.
    • All other columns: the zero sortValue.
    • The exchange tie-break is the parent exchange's arrival index, so a tool ties next to its exchange.
  12. Search. / matches the tool name and the full trailing cell.
  13. Enter. Does nothing on a tool row. selectedEvent returns nil for it, as for a block header.
  14. Cursor. A tool row has its own pin key (the tool id and its agent), so the cursor stays on the same tool row when a rebuild or a sort changes the order.
  15. No timeline. If the timeline fetch fails, or the proxy has no timeline endpoint (404), the table shows no tool rows and no extra error. The Timeline pane already reports these errors.
  16. Help. The DURATION column's picker description and the help overlay say: "for a tool: from the reply to the next request of the same agent". Cortex sees tool time from outside. It includes the client's own delay, and it can miss a tool that started while the reply still streamed.

Tasks

  1. Timeline data on the events pane. Let the existing timeline refresh chain run while the events pane shows a session. Set m.timeline.session to the selected session when the events pane opens it. Keep the Timeline pane's behavior the same. Tests: the chain starts on the events pane, a stream event for the session marks it stale, and a 404 gives no tool rows.
  2. Tool index. From one *timeline.Timeline, build a map from response seq to its tools (timeline order), with the run and bg figures from facts 3 and 4. Build it once per fetched timeline, not once per rebuild. Unit tests: parallel tools, ∥ detection, Agent run (ended and still running), bg job (ended and still running, two parallel tools with one Bash), end-of-run tools.
  3. Spanning last cell in tui/table. A row with more than one cell and fewer cells than columns: its last cell spans the remaining columns, the same way a one-cell row spans all of them. First check that no current caller renders a row of that shape. Test: the cell spans, truncates at the table width, and keeps cursor and search styles.
  4. Tool rows in rebuildEventsTable. Add tool *timeline.Tool (and the parent exchange index) to eventRow. Render tool rows by rules 1-12. Add the pin key (rule 14). Make sure that every reader of visibleRows[i].event accepts a nil event on a tool row: detail, yank, Enter, tail-follow, findByKey. Tests, one for each of these:
    • One tool.
    • Parallel tools with ∥.
    • running, error, lost, end-of-run.
    • Agent run, and Bash bg.
    • Tools inside an expanded block, and hidden in a collapsed block.
    • hideInactive hides the tools with their exchange.
    • Sort by DURATION shows ↳<n> and ranks a tool among exchanges.
    • Search finds a preview.
    • Enter on a tool row does nothing.
    • The cursor stays on a tool row across a rebuild.
  5. Help text (rule 16).
  6. Benchmark. Add tool rows to the fixture of BenchmarkRebuildEventsTable. Report the result against main in the PR body.

Gate: from cmd/agentop, gofmt -l ., GOWORK=off go vet ./..., scripts/dev/go-test-shards.sh -race cmd/agentop (from the repo root), and go test -race $(go list ./... | grep -v 'agentop$').

For a human after review

  • Open 2 or 3 large live sessions on the laptop service (:47601) in agentop. Check the tool rows on screen: placement under each exchange, ∥, running updates, Agent run, bg, and sorting by DURATION.
  • Watch the timeline fetch size on a large live session (up to ~470 KB per fetch, up to once a second, as the Timeline pane does now). Decide if a smaller projection is necessary.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions