Repository navigation
Studio Code evals: Report where a run's time and context go - #5197
Merged
Merged
Conversation
chubes4
marked this pull request as ready for review
October 20, 2026 03:30
Collaborator
📊 Performance Test ResultsComparing 1bbd1aa vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
1 task done
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?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 newapps/cli/ai/eval-timing.ts:requests: one record per model request:stopReason, pluserrorMessagewhen the request failed;timing: a summary of the run:emptyStops: replies that end withstopbut carry no text and no tool call, so the turn ends and the user sees nothing;outputCharson eachtoolEventsentry: 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
npx vitest run apps/cli/ai/tests/eval-timing.test.tscd apps/cli && npx vite build --config ./vite.config.dev.tsnode dist/cli/eval-runner.mjs "Build me a website for my neighborhood bakery." "" '{"vars":{"timeoutMs":1200000}}'timing(model time, tool wall time, peak context, per-tool entries) and onerequestsentry per assistant message.Example from one real run, a single-message bakery build on the default
fasttier that took 181s:inspect_design10.4s / 5 calls (108k chars of output),site_start5.8s,validate_blocks4.9sinspect_design108k chars,Skill79k,validate_blocks22kRequest timed out.after 10.5s and was retriedPre-merge Checklist