Repository navigation
Description
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:
GET /v1/sessions/{id}/timelinereturnstools[]withstart,end,previewand the owning agent (Feat: Serve a session's agents, tools and hosts as timeline spans #1516, Feat: Agent-neutral timeline turns and tools (#1559) #1600). A tool runs from the response that requested it to the agent's first request carrying its result.- The Timeline pane (
T) draws tools as amber bars and adds up tool time and the tool-call count per agent in the gutter.
So there is per-tool latency, but the events table doesn't show it and there is no per-call list.
Request
- 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.Enteropens the response that requested the tool and the request that returned its result. - Sorting and filtering by duration. The same sorting the table has for model calls, to answer questions like "which tool calls took longest".
- Totals per tool name (optional): count, total and p50/p95 duration per tool for the session, e.g.
Bash,Read, an MCP tool.
Notes
- Cortex sees tool time from outside. The span also includes the client's own delay before it sends the next request, so it's an upper bound on how long the tool itself ran. The UI should say so.
- Replayed history: every request re-sends all earlier tool results (see feature: three archive fields for offline session analysis — distinct tool-result counts, interleaved-completion marker, stream mismatch #1740 §1), so build the rows from the timeline's de-duplicated
tools[]and not from rawtoolResults. - Clients: the timeline already matches results for Anthropic
tool_result, Responses APIfunction_call_outputand chat-completionstool_call_id. Codex rows would work, even though Feat: Show tool calls and results under the message that sent them #1658's per-message detail doesn't cover the Responses API.
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:
Enteron 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:
- agentop loads events with
view=summary, which setsToolCalls,ToolResultsandMessagesto nil (core/sessionapi/summary.go). So tool rows must come from the timeline endpoint, not from agentop's events. timeline.Tool.Seqis the seq of the response that asked for the tool.exchangeResp(row).Seqgives the same value for an events-table row. This is the join key.timeline.Agent.SpawnToolnames theAgenttool call that started the subagent.SpawnedAtandReturnedAtgive the subagent's run.- A background job (
timeline.Job) belongs to the tool wherejob.Agent == tool.Agent,job.Start == tool.Endandtool.Name == "Bash". Without the name check, 2 of 24 jobs matched two parallel tools. - 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.handleTimelineTickreturns early whenm.pane != paneTimeline. tui/tablerenders a one-cell row across all columns (renderRow→renderSectionRow). Subagent block headers use this. A rendered row with an emptyeventRow{}invisibleRowsis already handled (block headers, the older-events row).- 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. - 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:
- 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
hideInactivehide it with its exchange. #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.- DIR cell. Empty. Inside an expanded subagent block it gets the same
│gutter that member rows get. - TIME cell. The tool's
start, in the same format as exchange rows. - ACTION, PLUGIN cells. Empty.
- METHOD cell. The tool name.
- STATUS cell. One of:
running:endis absent.error:erroris true. Use the error style that exchange rows use.lost:lostis true and the tool is not an end-of-run tool (rule 9).ok: all other tools with anend.- empty: end-of-run tools (rule 9).
- DURATION cell.
end − start, formatted withformatDuration.- Prefix
∥when another tool of the same response has the sameend. running:⋯ <now − start>. It updates on each rebuild.lostand end-of-run tools: empty.
- Prefix
- End-of-run tools. A tool with
losttrue whose name isStructuredOutputorSubagentHandbackends the run by design. It is not a failure. Showends runin the trailing cell and nothing in STATUS or DURATION. These two names are the onescore/session/timelinealready treats this way. Do not add others. - 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 atimeline.Agentnames as itsSpawnTool. IfReturnedAtis absent, showrun ⋯ <now − SpawnedAt>.bg <job.End − job.Start>for a Bash tool with a matching job (fact 4). If the job has noEnd, showbg ⋯ <now − job.Start>.ends run(rule 9).- The
preview.
- Sort. Tool rows take part in the sort with their own keys:
- DURATION: the tool's duration.
runningusesnow − start.lostand end-of-run tools use 0. - TIME:
start. - METHOD: the tool name.
- All other columns: the zero
sortValue. - The
exchangetie-break is the parent exchange's arrival index, so a tool ties next to its exchange.
- DURATION: the tool's duration.
- Search.
/matches the tool name and the full trailing cell. - Enter. Does nothing on a tool row.
selectedEventreturns nil for it, as for a block header. - Cursor. A tool row has its own pin key (the tool
idand its agent), so the cursor stays on the same tool row when a rebuild or a sort changes the order. - 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.
- 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
- Timeline data on the events pane. Let the existing timeline refresh chain run while the events pane shows a session. Set
m.timeline.sessionto 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. - Tool index. From one
*timeline.Timeline, build a map from response seq to its tools (timeline order), with therunandbgfigures 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. - 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. - Tool rows in
rebuildEventsTable. Addtool *timeline.Tool(and the parent exchange index) toeventRow. Render tool rows by rules 1-12. Add the pin key (rule 14). Make sure that every reader ofvisibleRows[i].eventaccepts 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 Bashbg. - Tools inside an expanded block, and hidden in a collapsed block.
hideInactivehides 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.
- Help text (rule 16).
- Benchmark. Add tool rows to the fixture of
BenchmarkRebuildEventsTable. Report the result againstmainin 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,∥,runningupdates, Agentrun,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
Type
Projects
- StatusShow more project fieldsIn progress
Activity