Skip to content

Parse each LLM candidate once in ai.Agent - #5118

Merged
aaronvg merged 2 commits into
canaryfrom
aaron/runner-parse-once
Oct 3, 2026
Merged

aaronvg merged 2 commits into
canaryfrom
aaron/runner-parse-once

Conversation

@aaronvg

@aaronvg aaronvg commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Issue Reference

No issue filed. Found while profiling LLM-call memory (see #5111 and #5113, which this PR does not depend on).

Changes

ai.Agent parsed each accepted candidate twice:

  1. _attempt_turn decided whether to accept a turn with _parses, which ran baml.sap.parse<Out>(candidate) and discarded the value.
  2. run then ran baml.sap.parse<Out>(candidate) again on the same text to get the result.

On canary every parse builds a parse model, so the second parse doubled that cost. This PR keeps the accepting parse:

  • _parses splits into _fits_without_text<Out>(turn) -> bool? (the provider-decoded output or the turn's media decides; null when only the text can tell) and _parse_text<Out>(candidate) -> ParsedOutput? (the parsed value, wrapped so a parsed null is distinct from no parse).
  • _attempt_turn returns _AcceptedTurn { turn, text_output }.
  • run takes the value from turn.parsed_output ?? accepted.text_output, or builds it from the turn's media. Exactly one of the three accepted the turn, so the duplicated three-way logic in run goes away.

_media_value still runs in run, after the turn is on the record, as its doc requires.

Measure (debug build, canary) before after
Bytes allocated per small LLM call 13.5 MB 8.5 MB
Extra bytes per call from 400 unrelated classes 6.3 MB 3.2 MB

(Measured with llm_call_memory.rs from #5111. With #5111 the parse model is small, so the saving there is the cost of one parse instead of most of the call.)

Testing

  • BAML corpus (baml-cli test --from crates/baml_tests/baml_src): 5082 passed, 2 tolerated (the deliberate test-runner tests). This covers the runner's repair re-ask, provider-decoded outputs, and the image? / image[] media cases.
  • CI snapshot job (cargo insta test --test-runner nextest --dnd -p baml_tests -p baml_cli -p baml_pack_host --all-features --unreferenced=reject): 1615 passed, no snapshot changes.
  • CI general job: 4971 passed.
  • runner.baml is baml fmt-clean.

Additional Notes

No new test asserts the parse count: nothing in the test harness observes how many times SAP runs, and the memory numbers above show the change.

🤖 Generated with Claude Code


Note

Medium Risk
Touches the default agent loop and final output resolution (SAP, provider decode, media), but behavior is covered by the existing BAML corpus and snapshot tests.

Overview
ai.Agent now parses each accepted terminal reply once instead of running baml.sap.parse in _attempt_turn for acceptance and again in run for the final value.

_parses is replaced by _fits_without_text (provider-decoded output or turn media vs. needs text; null when only text can decide) and _parse_text (returns a retained ParsedOutput?). _attempt_turn returns _AcceptedTurn with optional text_output from that single parse. run builds the result from turn.parsed_output ?? accepted.text_output, or _media_value when media satisfied acceptance—removing duplicated three-way branching and the second SAP pass.

_fits_without_text keeps media-shaped outputs that have no blocks on the turn on the text-parse path so optional/absent media (image?, image[]) still work instead of failing early.

Reviewed by Cursor Bugbot for commit 080e786. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of AI responses that contain decoded results, text, or media. Results now use available decoded output first, retain text parsing when needed, and otherwise return the media value. This helps ensure the final result matches the accepted response and avoids repeating text parsing when a parsed value is already available.

_attempt_turn accepted a turn by parsing its terminal text with baml.sap.parse and threw the value away; run then parsed the same text again for the result. The accepting parse now travels with the turn (_AcceptedTurn.text_output, a ParsedOutput), and run uses it, the provider-decoded output, or the turn's media, whichever accepted the turn.

On canary a small LLM call allocates 8.5 MB instead of 13.5 MB, and each unrelated class in the program costs half as much per call, because each call builds one parse model instead of two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
developer-docs Ready Ready Preview Oct 3, 2026 6:17am UTC

Request Review

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: facfdd7f-0b2f-47e4-a8a0-5357b8b1ee55
📥 Commits

Reviewing files that changed from the base of the PR and between 41fc7a1 and df51f8b.

📒 Files selected for processing (1)
  • baml_language/crates/baml_builtins2/baml_std/ai/runner.baml

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The runner now returns an accepted model turn together with any terminal-text parse result. It uses that retained parse during final result construction when provider-decoded output is unavailable.

Changes

Turn output acceptance and construction

Layer / File(s) Summary
Determine and retain accepted output
baml_language/crates/baml_builtins2/baml_std/ai/runner.baml
The runner checks provider-decoded or media output first. If neither determines acceptance, it parses terminal text and retains the parsed value with the accepted model turn.
Construct the final result
baml_language/crates/baml_builtins2/baml_std/ai/runner.baml
The caller extracts the model turn for existing handling. Final result construction uses provider-decoded output, the retained text parse, or a media value.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 080e7

This change avoids parsing the same LLM output twice and lowers allocation per call. No merge-blocking risk was identified.

Architecture Summary

Architecture risk: 🔵 Low · up to df51f

The change affects 1 system.

Changed systems: baml_language

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — baml_language (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in baml_language/crates/baml_builtins2/baml_std/ai/runner.baml: Adds internal _AcceptedTurn, pairing a ModelTurn with an optional ParsedOutput from terminal text.
  • observed — Modified behavior in baml_language/crates/baml_builtins2/baml_std/ai/runner.baml: Renames _parses to _fits_without_text and changes its contract to return whether provider-decoded or media output fits, or null when terminal-text parsing is needed.
  • observed — Modified behavior in baml_language/crates/baml_builtins2/baml_std/ai/runner.baml: _fits_without_text now returns null when neither provider-decoded nor media output determines the result. Replaces the boolean _parses check with _parse_text, which returns the parsed value or null when parsing fails.
  • observed — Modified behavior in baml_language/crates/baml_builtins2/baml_std/ai/runner.baml: Changes _attempt_turn’s return type from ModelTurn to _AcceptedTurn.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: reuse each accepted LLM candidate’s parsed value instead of parsing it again.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit watched the turn arrive,
A text parse stayed safely alive.
Decoded output took its place,
Or media filled the final space.
The runner packed each result with care,
Then hopped away with ears in air.

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Binary size checks failed

❌ 2 violations · ✅ 1 passed

⚠️ Please fix the size gate issues or acknowledge them by updating baselines.

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 59.9 MB 25.4 MB file 73.1 MB -13.2 MB (-18.1%) OK
❌ packed-program Linux 🔒 38.5 MB 15.6 MB file 29.2 MB +9.3 MB (+31.9%) FAIL
❌ bridge_wasm WASM 26.1 MB 🔒 7.4 MB gzip 5.7 MB +1.7 MB (+30.6%) FAIL

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.

Details & how to fix

Violations:

  • packed-program (Linux) file_bytes: 38.5 MB exceeds limit of 30.1 MB (exceeded by +8.4 MB, policy: max_file_bytes)
  • packed-program (Linux) file_delta_pct: +31.9% exceeds limit of 3.0% (exceeded by +28.9pp, policy: max_delta_pct)
  • bridge_wasm (WASM) gzip_bytes: 7.4 MB exceeds limit of 5.9 MB (exceeded by +1.5 MB, policy: max_gzip_bytes)
  • bridge_wasm (WASM) gzip_delta_pct: +30.6% exceeds limit of 3.0% (exceeded by +27.6pp, policy: max_delta_pct)

Add/update baselines:

.ci/size-gate/wasm32-unknown-unknown.toml:

[artifacts.bridge_wasm]
file_bytes = 26057520
gzip_bytes = 7394778

.ci/size-gate/x86_64-unknown-linux-gnu.toml:

[artifacts.packed-program]
file_bytes = 38454256
gzip_bytes = 15619183

Generated by cargo size-gate · workflow run

@aaronvg
aaronvg added this pull request to the merge queue Oct 3, 2026
Merged via the queue into canary with commit 66a4a18 Oct 3, 2026
97 of 98 checks passed
@aaronvg
aaronvg deleted the aaron/runner-parse-once branch October 3, 2026 07:17

This branch was successfully deployed

1 active deployment
Preview – developer-docs — 080e786e Deployed Oct 3, 2026 by vercel[bot]
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