Skip to content

Studio Code evals: Report where a run's time and context go - #5197

Merged
chubes4 merged 1 commit into
trunkfrom
studio-code/eval-runner-request-timing
Oct 20, 2026
Merged

chubes4 merged 1 commit into
trunkfrom
studio-code/eval-runner-request-timing

Conversation

@chubes4

@chubes4 chubes4 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Related issues

How AI was used

Claude (via the Pi coding agent in a Roadie session) wrote the change and its tests, and ran the eval below. I reviewed the diff and the results.

Proposed Changes

Additions to the eval runner's result (apps/cli/ai/eval-runner.ts), with the summary logic in a new apps/cli/ai/eval-timing.ts:

  • requests: one record per model request:
    • start, end and duration, measured from the timestamp pi sets just before it sends the request;
    • context tokens sent (input + cache read + cache write) and output tokens;
    • stopReason, plus errorMessage when the request failed;
    • whether the reply carried text, and how many tool calls.
  • timing: a summary of the run:
    • model time;
    • tool wall time, counting parallel calls once;
    • number of requests and peak context;
    • emptyStops: replies that end with stop but carry no text and no tool call, so the turn ends and the user sees nothing;
    • per tool: calls, total and max time, and output size, slowest first.
  • outputChars on each toolEvents entry: the length of the result's text, which is what the tool adds to the context.

Before, a run only had turnDurationsMs, which mixes model time with the previous tools' time. Context growth, failed requests that were retried, and silent empty stops were visible only by reading the session JSONL by hand. Existing fields are unchanged.

The README documents the new fields.

Testing Instructions

  1. npx vitest run apps/cli/ai/tests/eval-timing.test.ts
  2. cd apps/cli && npx vite build --config ./vite.config.dev.ts
  3. Run any building case, for example node dist/cli/eval-runner.mjs "Build me a website for my neighborhood bakery." "" '{"vars":{"timeoutMs":1200000}}'
  4. In the result file, expect timing (model time, tool wall time, peak context, per-tool entries) and one requests entry per assistant message.

Example from one real run, a single-message bakery build on the default fast tier that took 181s:

Model time 149s over 31 requests
Tool wall time 27s
Context 12k → 101k tokens
Slowest tools inspect_design 10.4s / 5 calls (108k chars of output), site_start 5.8s, validate_blocks 4.9s
Largest outputs inspect_design 108k chars, Skill 79k, validate_blocks 22k
Hidden failure request 2 hit Request timed out. after 10.5s and was retried

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@chubes4
chubes4 marked this pull request as ready for review October 20, 2026 03:30
@wpmobilebot

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 1bbd1aa vs trunk

app-size

Metric trunk 1bbd1aa Diff Change
App Size (Mac) 927.54 MB 927.54 MB 0.00 MB ⚪ 0.0%

site-editor

Metric trunk 1bbd1aa Diff Change
load 1137 ms 1192 ms +55 ms 🔴 4.8%

site-startup

Metric trunk 1bbd1aa Diff Change
siteCreation 8144 ms 8583 ms +439 ms 🔴 5.4%
siteStartup 5900 ms 5914 ms +14 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@chubes4
chubes4 merged commit aad80b3 into trunk Oct 20, 2026
13 checks passed
@chubes4
chubes4 deleted the studio-code/eval-runner-request-timing branch October 20, 2026 04:10
chubes4 added a commit that referenced this pull request Oct 20, 2026
## Related issues

- Follows #4858, which noted that "the fast tier's upstream stops after
4,096 reasoning tokens and returns an empty completion" and added
`STUDIO_EVAL_AUTO_CONTINUE` to the eval runner. The app had no
equivalent, so users hit it directly.

## How AI was used

Claude (via the Pi coding agent in a Roadie session) measured the issue,
wrote the change and its tests, and ran the evals below. I reviewed the
diff and the results.

## Proposed Changes

Any model can end a turn with an empty reply; this handles it for every
tier, built from two pi primitives (a hidden custom message, and
`agent_end`'s `willRetry`). The case that prompted it: on the `fast`
tier the model's reasoning is capped at about 4,096 tokens. When it uses
all of that before answering, the reply ends with `stop` and has no text
and no tool call. The turn ends, nothing shows up, and nothing reports
an error (the turn counts as a success). Sending a reasoning budget from
Studio doesn't lift the cap; I tried the usual fields through the eval
runner.

- **The runtime continues such a turn**
(`apps/cli/ai/runtimes/pi/index.ts`) with a hidden pi custom message
(`display: false`, `triggerTurn: true`) that stays out of the
transcript.
- **The message carries the cut-off reasoning:** "Your reasoning was cut
off before you replied. Continue from where it stopped:" followed by
that reasoning. pi's Chat Completions conversion leaves out an assistant
message that has neither content nor tool calls (some providers reject
one), so the reasoning of an empty reply never reaches the next request.
With a bare `continue`, the model started the same reasoning over and
ran out again.
- **Limit:** two continues per turn, so a turn can't keep itself going.
If a later turn needs more, the user can send another message.
- **No visible change for consumers:** the interim `agent_end` goes out
with `willRetry: true`, the contract pi's own error retry already uses.
The terminal UI, the app and the CLI's turn status all wait for the real
end of the turn.
- **Interrupting:** if the user interrupts before the continue, the turn
ends as interrupted.
- **`isEmptyStop`** in `@studio/common/ai/session-events` defines an
empty reply in one place.
- **Eval runner:** `STUDIO_EVAL_AUTO_CONTINUE` and `autoContinues` are
removed, because the runtime now does this for every caller, evals
included. Empty stops still show up in `timing.emptyStops` (#5197).

## Testing Instructions

1. `npx vitest run apps/cli/ai/tests/pi-runtime.test.ts`. It covers: the
hidden continue carrying the reasoning, the interim `willRetry`, the
limit of two per turn, replies with text or a tool call being left
alone, and interrupting before the continue.
2. **End to end:** `cd apps/cli && npx vite build --config
./vite.config.dev.ts`, then repeat a reasoning-heavy prompt on the
`fast` tier until a run hits the cap. For example: `node
dist/cli/eval-runner.mjs "Without using any tools, solve this by
reasoning only: how many 6-digit numbers have digits that are all
distinct, contain no 0, are divisible by 11, and have digits in neither
strictly increasing nor strictly decreasing order? Verify with a second
method. Reply with just the number." ""
'{"vars":{"timeoutMs":600000}}'`. In the session JSONL, a 4,098-token
reply with only thinking is followed by a `custom_message` entry
(`studio.empty_stop_continue`) and the next request, which sends about
4k more tokens of context.

Evidence from runs of that prompt on the `fast` tier. The prompt is
chosen to push reasoning to the cap; most real requests stay far below
it, and most runs never reach it.

| Version | Runs | Turns that hit the cap | Recovered |
|---|---|---|---|
| Bare `continue` | 14 | 2 | 1 |
| Continue carrying the reasoning (this PR) | 14 | 1 | 1 |

- **Bare `continue`, did not recover:** three empty replies in a row.
Each continue added 1 token of context (just `continue`), and each
reply's reasoning began the enumeration again from the instructions.
- **Carrying the reasoning, recovered:** also two empty replies in a
row, but each continue added about 4k tokens of context. The model then
picked up from that reasoning, made two tool calls, and gave the correct
answer.

The samples are small. The mechanism is the evidence here: the dropped
reasoning is visible in the context sizes, and carrying it is what
changed.

Four real build runs with a follow-up edit (bakery site + menu page,
photography portfolio + homepage changes; two on trunk, two on this
branch) hit no empty replies, so the continue didn't fire. Both runs on
this branch finished their builds and follow-ups as usual.

**Production context:** about 500 replies a week come back empty this
way, and about two-thirds of them end the turn. They show up in 19% of
`fast` sessions, every day since at least Oct 5.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?
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.

2 participants