Skip to content

feat(ai): builtin ai/provider packages + @spec desugar for LLM functions - #4352

Merged
aaronvg merged 60 commits into
canaryfrom
aaron/custom-llm-providers-v5
Aug 11, 2026
Merged

feat(ai): builtin ai/provider packages + @spec desugar for LLM functions#4352
aaronvg merged 60 commits into
canaryfrom
aaron/custom-llm-providers-v5

Conversation

@aaronvg

@aaronvg aaronvg commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What

Ships the BEP phase-1 AI runtime as builtin stdlib packages and implements the @spec desugar for LLM functions.

Builtin packages

The _planv2/baml_src reference implementation moves into crates/baml_builtins2/baml_std/ as six root packages, reachable from any project with no root. prefix and no files to copy:

  • ai — the public surface: FunctionSpec, Prompt, Tool/Toolbox, Journal + events, ModelTurn/Client, the Agent runner, Retry/Fallback wrappers, ai.errors
  • openai, anthropic, google — provider clients (Responses / Messages / generateContent)
  • claude_code — the Claude Code CLI as a harness client
  • mcp — MCP servers as ordinary journaled tools

Two intentional changes landed during the move: FunctionSpec.default_client is now a lazy thunk (() -> Client throws unknown) so building a spec never touches credentials, and Agent.run selects the client via match on the nullable override.

@spec desugar

An LLM function with a backtick prompt and a "provider/model" client string gets a compiler-synthesized <Fn>$spec companion; new postfix sugar Fn@spec(args) references it:

function PlanTrip(trip_request: string) -> Itinerary {
    client "openai/gpt-4o-mini"
    tools [search_flights, search_hotels]
    prompt `
        You are a travel agent. The brief: ${trip_request}
        ${ctx.output_format}
    `
}

let spec = PlanTrip@spec(trip_request = "2 weeks in Japan");   // ai.FunctionSpec<Itinerary>
let result = ai.Agent<Itinerary>.new(client = scripted).run(spec);

The new tools: field (list of function references, each normalized through ai.tool(...)) switches the function to spec mode: its direct call desugars to ai.Agent<Out>.new(client = client).run(Fn@spec(...)).value, with client: ai.Client? = null as a compiler-injected override parameter, and the legacy baml.llm companions ($render_prompt, $build_request*, $stream, $parse*) are not generated. Functions without tools: keep the legacy direct-call path unchanged and additionally get the $spec companion.

A tools: field on a function that is not spec-eligible (Jinja prompt or unknown provider prefix) is a compile error.

Compiler details

  • Parser: tools field in LLM bodies (optional colon, arbitrary expression), postfix @spec (new SPEC_EXPR node), plus tighter LLM-body detection (prompt(/prompt. and friends no longer misclassify expression bodies).
  • Lowering: the $spec body is built while the CST backtick is in hand; the prompt template re-lowers as a plain template string inside a closure with ctx bound to ai.internal.SpecCtx, so ${ctx.output_format} substitutes the render-time argument and interp diagnostics keep real spans.
  • MIR lower_lambda now disambiguates same-span lambda scopes by owner (mirrors build_tagged_body_closure), fixing capture resolution for companions that share the parent's source ranges.
  • Formatter: tools field support; @spec prints verbatim.

_planv2/baml_src

Keeps only fixtures, tests, how-tos, and live smokes, rewritten against the builtins; plan_trip_spec(...) is replaced by the real PlanTrip LLM function + PlanTrip@spec(...).

Testing

  • _planv2 offline: 25/25 pass (includes a new direct-call desugar test).
  • Live smokes (via infisical run --env=test): live_openai() (exercises the lazy default-client path), live_anthropic(), live_google(), live_claude_code(), live_mcp_tools(), live_claude_code_dynamic_mcp() — all complete a real tool loop and return a typed Itinerary / echo reply.
  • cargo test -p baml_tests (all test_06_codegen snapshot churn is the stdlib listing growing by the new packages; reviewed and accepted), plus parser/AST/MIR/HIR/TIR/emit/engine/CLI/surface suites.

🤖 Generated with Claude Code


Note

High Risk
Large stdlib and execution-model swap (journal-based runner vs legacy LLM orchestration) affects all spec-mode LLM calls, streaming FFI, and binary size; provider clients and tool/MCP paths are security- and reliability-sensitive.

Overview
Replaces the monolithic baml.llm stdlib (orchestrating Client / PrimitiveClient, Jinja path, legacy $stream) with a BEP-style ai package and separate provider builtins (openai, anthropic, google, claude_code), all embedded in baml_builtins2 and registered in ALL.

The new surface centers on FunctionSpec, structural ai.Prompt, Journal + events, ModelTurn / Client, Agent runner (tool loop, repair, budgets), typed failures, Retry / Fallback / RoundRobin, one-turn streaming (TurnStream, ai.stream.Stream, from_spec), MCP over stdio, and client wrappers in BAML rather than Rust orchestration. Built-in HTTP clients lower prompts and journals to each provider’s wire format (plus Claude Code CLI and Anthropic/OpenAI/Google streaming decoders).

baml.prompt holds prompt/output-format helpers; legacy provider option schemas move to sys_llm_types for Rust-only compatibility. SAP parsing cache and stream parse hooks move from baml.llm into baml.sap. baml.sys gains start_process, line-oriented stdout, and stdin control for MCP and harness clients.

Shared AI_STREAM_* constants are added for host/SDK identity. Packed-program size gates and CI baselines rise ~3% for the new bytecode. CLI baml describe snapshots switch from baml.llm to baml.prompt.

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

Summary by CodeRabbit

  • New Features

    • Added typed AI agents with tool execution, structured outputs, journals, sessions, retries, budgets, approvals, and subagents.
    • Added OpenAI, Anthropic, Google, Claude Code, and MCP integrations.
    • Added live process management with streaming output, stdin controls, timeouts, and termination.
    • Added @spec syntax for reusable AI function specifications.
  • Documentation

    • Added comprehensive guides, examples, API references, and implementation notes.
  • Bug Fixes

    • Improved reflective argument conversion for numeric values and nested arrays.
    • Improved generated Java method naming for reserved platform methods.
  • Tests

    • Added extensive offline, integration, reliability, provider, MCP, process, and Go SDK coverage.

aaronvg and others added 15 commits August 3, 2026 13:55
Flue-style user-facing BEP in _plan/pages (introduction, guides,
examples, advanced, appendix), a compiling+tested reference
implementation in _plan/ai_agents (typed Session<T, X>, policies,
journal, 30 offline tests, live OpenAI loop), and reference_notes.md
recording toolchain bugs and spec issues found while building it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the BEP phase-1 reference implementation from _planv2/baml_src into
builtin stdlib packages (ai, openai, anthropic, google, claude_code, mcp),
reachable from any project without a root. prefix.

Compiler: an LLM function with a backtick prompt and a "provider/model"
client string gets a synthesized <Fn>$spec companion; `Fn@spec(args)` is
new postfix sugar for it and builds the bound, unrun ai.FunctionSpec<Out>.
A new `tools: [fn, ...]` field switches the function's direct call to the
ai runner (ai.Agent<Out>.new(client = client).run(Fn@spec(...)).value with
a compiler-injected ai.Client? override param) and opts out of the legacy
baml.llm companions. FunctionSpec.default_client is a lazy thunk so spec
construction never touches credentials.

MIR lower_lambda now disambiguates same-span lambda scopes by owner
(mirrors build_tagged_body_closure), fixing capture resolution for
companions that share the parent's source ranges.

_planv2/baml_src keeps fixtures/tests/how-tos/live smokes, rewritten
against the builtins and PlanTrip@spec. Offline: 25/25 pass. Live smokes
pass for openai (default-client path), anthropic, google, claude_code,
mcp_tools, and dynamic MCP attach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whole-program codegen snapshots (test_06_codegen, baml_src bytecode)
list every stdlib function, so the six new packages churn all of them;
the diffs are the new ai/provider/mcp listings plus block reordering,
with no user-code changes. New per-namespace snapshots appear for
ai.errors / ai.internal, and prompt_tag_runtime gains the PtRenderPerson
$spec companion. One diagnostics snapshot picks up the new
"'client', 'tools' and 'prompt'" wording.

baml_fmt learns the LLM-body `tools` field (any field order, canonical
client/tools/prompt output); `@spec` prints verbatim via the existing
unknown-expression fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The accessor truncated `client openai/gpt-4o-mini` to its first WORD, so
the unquoted form resolved the provider prefix alone — previously a
guaranteed E0003, and with the openai builtin package now a silent
misresolution. Concatenating the value tokens makes the unquoted
shorthand behave exactly like the quoted form; the shorthand_clients LSP
fixture now expects no diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview Aug 11, 2026 5:55am
promptfiddle2 Ready Ready Preview Aug 11, 2026 5:55am

Request Review

@github-actions

github-actions Bot commented Aug 9, 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.

Comment thread baml_language/_plan/ai_agents/baml_src/ns_ai/policy.baml Outdated
Comment thread baml_language/_plan/ai_agents/baml_src/ns_ai/policy.baml Outdated
Comment thread baml_language/_plan/ai_agents/baml_src/ns_ai/policy.baml Outdated
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds typed AI runtimes, provider clients, MCP support, compiler support for @spec, process APIs, Plan V2 fixtures, tests, and BEP documentation.

Changes

AI agent runtime

Layer / File(s) Summary
Agent runtime and session model
baml_language/_plan/ai_agents/...
Adds typed journals, sessions, policies, runners, tools, provider clients, custom events, retries, budgets, subagents, and scenario coverage.
AI standard library and providers
baml_language/crates/baml_builtins2/baml_std/ai/..., .../anthropic/..., .../google/..., .../openai/..., .../claude_code/..., .../mcp/...
Adds typed AI contracts, runners, tools, failures, wire helpers, retry and fallback wrappers, provider clients, and MCP connections.
Compiler lowering and package wiring
baml_language/crates/baml_compiler2_*/..., baml_language/crates/baml_compiler_parser/..., baml_language/crates/baml_compiler_syntax/..., baml_language/crates/baml_fmt/...
Adds tools fields, postfix @spec syntax, spec companions, Agent-based lowering, provider resolution, dependency wiring, and formatter support.
Plan V2 fixtures and documentation
baml_language/_planv2/...
Adds Plan V2 examples, provider resolution, MCP and reliability tests, live smoke tests, Go integration, and API documentation.
BEP documentation and examples
baml_language/_plan/...
Adds guides, examples, design appendices, reference notes, outlines, and publishing support.
Process runtime and platform support
baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/..., baml_language/crates/sys_native/..., baml_language/crates/sys_ops/..., baml_language/crates/bridge_wasm/...
Adds asynchronous process startup, streamed stdout, stdin control, lifecycle operations, native implementation, and unsupported-platform behavior.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

Poem

A rabbit checks the journal trail,
While typed agents start and sail.
Tools and clients share the load,
Specs guide models on the road.
MCP hops through every gate,
Tests record each changing state.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: builtin AI/provider packages and @spec desugaring for LLM functions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aaron/custom-llm-providers-v5

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
baml_language/crates/baml_compiler_parser/src/parser.rs (1)

4241-4253: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not classify client(...) as an LLM directive.

client lexes as TokenKind::Client. Line 4244 accepts client(...) as an LLM directive because the excluded-token set does not include TokenKind::LParen. A regular function body that calls a parameter named client then enters LLM-body parsing and fails.

Add TokenKind::LParen to this excluded-token set. Add a parser unit test for a regular body containing client(...).

Proposed fix
                             TokenKind::Dot
                                 | TokenKind::Equals
                                 | TokenKind::Comma
                                 | TokenKind::RParen
+                                | TokenKind::LParen

As per coding guidelines, “**/*.rs: Prefer writing Rust unit tests over integration tests where possible”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler_parser/src/parser.rs` around lines 4241 -
4253, The LLM-directive detection around TokenKind::Client incorrectly
classifies regular client(...) calls because LParen is not excluded. Add
TokenKind::LParen to the matches exclusion in the relevant parser logic, and add
a Rust parser unit test covering a regular body that invokes client(...).

Source: Coding guidelines

baml_language/crates/baml_fmt/src/ast/declarations.rs (1)

739-745: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

The formatter drops the tools field.

LlmFunctionBody::print emits client, prompt, and type_builder, but never self.tools. Formatting a function that declares tools deletes the field from the source. The deletion is not cosmetic: lower_function keys spec mode on the presence of tools, so the formatted file compiles down the legacy baml.llm path instead of the ai.Agent path. The struct doc on Line 656 states the canonical order is client, tools, prompt, so print must include it.

Add a formatter snapshot for an LLM function with tools so the round trip is covered.

🐛 Proposed fix to print the `tools` field
+        if let Some(tools) = &self.tools {
+            printer.print_standalone_with_trivia(tools, inner_indent);
+            printer.print_newline();
+        }
+
         printer.print_standalone_with_trivia(&self.prompt, inner_indent);
         printer.print_newline();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_fmt/src/ast/declarations.rs` around lines 739 -
745, Update LlmFunctionBody::print to emit self.tools between self.client and
self.prompt, preserving the documented canonical order and existing
trivia/newline handling. Add a formatter snapshot covering an LLM function
declaring tools to verify the field survives formatting and round trips
correctly.
🟠 Major comments (32)
baml_language/crates/sys_ops/src/lib.rs-2168-2180 (1)

2168-2180: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Wire the start_process operation and process classes through with_sys_instance.

with_sys_instance rewires only baml_sys_exec, baml_sys_shell, and baml_sys_sleep. A custom IoNamespaceSys installed this way still uses DefaultIoOps::baml_sys_exec, and the new IoClassSysProcess / IoClassSysProcessLineStream classes are missing from sys glue and SysOps fields, so baml.sys.start_process(...) stays unreachable/unsupported under the builder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/sys_ops/src/lib.rs` around lines 2168 - 2180, Update
with_sys_instance and the related sys glue to route start_process through the
configured IoNamespaceSys instead of DefaultIoOps::start_process. Add
IoClassSysProcess and IoClassSysProcessLineStream to the sys glue and
corresponding SysOps fields, then wire their operations so
baml.sys.start_process(...) works with custom instances.
baml_language/_planv2/pages/02_guides/01_functions/03_calling_functions.md-18-31 (1)

18-31: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the stale “not implemented” caveat.

The PR objective states that direct-call lowering through ai.Agent is implemented. Lines 18-27 document that lowering, but Lines 30-31 tell users to expand calls manually. Delete or update the caveat so the guide matches the compiler behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/01_functions/03_calling_functions.md`
around lines 18 - 31, The calling-functions guide still says direct-call
lowering is not implemented; remove or revise the caveat following the
`PlanTrip` desugared example so it states that plain calls are lowered
automatically and no manual expansion is required.
baml_language/_plan/pages/02_guides/02_sessions.md-114-120 (1)

114-120: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not send the first message twice.

In the no-snapshot branch, msg becomes trip_request and is then sent again at Line 120. The initial prompt and the transcript therefore contain the same input. Use either the function argument or send() for the first turn.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/02_sessions.md` around lines 114 - 120,
Update handle_turn so the no-snapshot branch does not pass msg as trip_request
and then send it again; initialize the session without the first-turn message in
that branch, while preserving the existing send(msg) call and snapshot-resume
behavior.
baml_language/_planv2/pages/02_guides/01_functions/02_tools.md-61-100 (1)

61-100: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Qualify tool-error behavior by on_error mode.

The guide says validation failures and thrown tool errors are never application exceptions, but later says Raise makes a tool failure throw ToolFailedError. State explicitly that Report converts failures to tool results, while Raise propagates them after journaling. Clarify whether invalid arguments follow the same mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/01_functions/02_tools.md` around lines
61 - 100, Update the “Argument validation,” “Tool errors are data,” and “Tool
failure policy” sections to qualify behavior by on_error mode: Report converts
validation and tool failures into tool results for the model, while Raise
journals the failure and propagates ToolFailedError. Explicitly state whether
invalid arguments follow this same Report/Raise behavior, without changing the
documented default.
baml_language/_plan/pages/02_guides/02_sessions.md-31-46 (1)

31-46: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use one send() and journal-timing contract.

Session.send queues data, while Session.run appends the corresponding event to the journal. The two guides currently describe queue admission and journal append as immediate and policy-controlled.

  • baml_language/_plan/pages/02_guides/02_sessions.md#L31-L46: document that send() queues and run() journals before policy processing.
  • baml_language/_plan/pages/02_guides/04_steering.md#L27-L35: distinguish queue admission, journal append, and model injection.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/02_sessions.md` around lines 31 - 46, The
session documentation must consistently describe the timing contract: in
baml_language/_plan/pages/02_guides/02_sessions.md lines 31-46, update the send
flow to state that Session.send queues the message and Session.run appends the
corresponding event to the journal before policy processing, rather than
implying immediate or policy-controlled journaling; in
baml_language/_plan/pages/04_steering.md lines 27-35, explicitly distinguish
queue admission, journal append, and model injection while preserving their
respective timing and policy behavior.
baml_language/_plan/pages/02_guides/02_sessions.md-95-109 (1)

95-109: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define one snapshot and resume-policy contract.

The sessions guide presents $resume as sufficient by itself, while the configuration guide requires the caller to provide $policy for resumed sessions.

  • baml_language/_plan/pages/02_guides/02_sessions.md#L95-L109: show $policy when required, or qualify the claim that the snapshot contains everything needed.
  • baml_language/_plan/pages/02_guides/03_configuration.md#L75-L81: state whether the default policy is restored automatically and whether custom policies require $policy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/02_sessions.md` around lines 95 - 109,
Define one consistent snapshot/resume policy contract across both guides: in
baml_language/_plan/pages/02_guides/02_sessions.md lines 95-109, either include
$policy in the resume example when required or qualify the statement that
snapshots contain everything needed; in
baml_language/_plan/pages/02_guides/03_configuration.md lines 75-81, explicitly
state whether the default policy is restored automatically and when resumed
sessions require $policy, especially for custom policies.
baml_language/_plan/pages/02_guides/06_tools.md-52-58 (1)

52-58: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Respect ToolErrorMode in the error guarantee.

At Lines 52-58, the page says a tool throw never crashes the agent and always becomes a tool result. baml_language/_planv2/pages/02_guides/02_specs_and_runners/02_the_default_runner.md states that Raise mode throws ToolFailedError after recording ToolFailed. Qualify this recipe as Report mode or document both modes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/06_tools.md` around lines 52 - 58, Update
the tool-error guidance in the page’s “throw inside a tool” section to qualify
the behavior by ToolErrorMode: document that Report mode converts failures into
tool results, while Raise mode propagates ToolFailedError after recording the
failure. Keep the recommendation to handle total failure at the agent call site.
baml_language/_planv2/pages/02_guides/02_specs_and_runners/01_specs.md-28-31 (1)

28-31: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Document lazy default-client resolution consistently.

The pages describe eager resolution, but the PR objective states that FunctionSpec.default_client is lazy.

  • baml_language/_planv2/pages/02_guides/02_specs_and_runners/01_specs.md#L28-L31: describe the default client as lazy rather than resolved during spec creation.
  • baml_language/_planv2/pages/02_guides/02_specs_and_runners/01_specs.md#L44-L52: document when default_client resolves and when credential errors occur.
  • baml_language/_planv2/pages/02_guides/03_clients/01_choosing_a_model.md#L41-L42: remove the eager-validation claim.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/02_specs_and_runners/01_specs.md`
around lines 28 - 31, Update
baml_language/_planv2/pages/02_guides/02_specs_and_runners/01_specs.md lines
28-31 to describe FunctionSpec.default_client as lazy rather than resolved
during spec creation; update lines 44-52 to state when it resolves and when
credential errors occur; update
baml_language/_planv2/pages/02_guides/03_clients/01_choosing_a_model.md lines
41-42 to remove the claim that client validation is eager.
baml_language/_plan/pages/02_guides/05_models.md-39-50 (1)

39-50: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The documentation trees define incompatible Client interfaces.

Choose the phase-1 public contract and update every affected statement.

  • baml_language/_plan/pages/02_guides/05_models.md#L39-L50: align the interface declaration with the selected contract.
  • baml_language/_plan/pages/02_guides/05_models.md#L100-L105: align the implementation instructions with the selected contract.
  • baml_language/_planv2/pages/02_guides/03_clients/02_the_client_interface.md#L5-L10: align the public interface declaration with the selected contract.
  • baml_language/_planv2/pages/02_guides/03_clients/02_the_client_interface.md#L26-L30: align the rendering and parsing visibility statement with the selected contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/05_models.md` around lines 39 - 50, Adopt
one phase-1 public Client contract consistently across all affected
documentation. In baml_language/_plan/pages/02_guides/05_models.md lines 39-50,
update the Client interface; in lines 100-105, align the implementation
instructions. In
baml_language/_planv2/pages/02_guides/03_clients/02_the_client_interface.md
lines 5-10, update the public interface declaration, and in lines 26-30, make
the rendering and parsing visibility statement match that same contract.
baml_language/_plan/pages/02_guides/10_policies.md-119-135 (1)

119-135: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep refolded policy state in SessionState or derive it from the journal.

The refold contract rebuilds SessionState, but these examples keep replay-sensitive state on the policy object. A resume or policy replacement can retain stale values or fail to restore them.

  • baml_language/_plan/pages/02_guides/10_policies.md#L119-L135: Store the accumulated cost in SessionState, or derive it from Journal, instead of mutating self.spent.
  • baml_language/_plan/pages/02_guides/10_policies.md#L145-L170: Store held tool calls in SessionState, or rebuild them deterministically from journal entries, instead of mutating self.held.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/10_policies.md` around lines 119 - 135,
The policy examples must keep replay-sensitive state in SessionState or derive
it deterministically from Journal. In
baml_language/_plan/pages/02_guides/10_policies.md lines 119-135, update
WithBudget so accumulated cost is not mutated in self.spent; in lines 145-170,
update the held-tool-call policy so state is not mutated in self.held. Ensure
both values are restored correctly during refolding, resume, and policy
replacement.
baml_language/_plan/pages/02_guides/10_policies.md-155-158 (1)

155-158: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Consume a held tool call when its approval is processed.

Line 156 returns the held RunTool without removing it. A second PermissionGranted event with the same call_id emits the same command again. The runner then executes the tool again.

Remove the held entry for both approval outcomes before returning. Ignore later approval events for that call_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/pages/02_guides/10_policies.md` around lines 155 - 158,
Update the PermissionGranted and PermissionDenied handling to consume and remove
the held entry for the event’s call_id before producing the command. Ensure a
later approval for the same call_id finds no held tool and emits no duplicate
execution, while preserving the existing denied failure command behavior.
baml_language/_planv2/baml_src/tests/loop.baml-101-126 (1)

101-126: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Provide a third tool-use turn before expecting StepBudgetExceeded.

The initial model call is uncounted. After the second tool turn, the loop has steps == 2 and issues a third model call. ScriptedClient then throws its out-of-turns InvalidArgument error instead of the expected budget error.

Keep max_steps = 2 and add a third tool-use turn. The next call attempt will then exceed the budget.

Proposed test fix
             ai.ModelTurn {
                 content: [ai.ToolUse { id: "c2", name: "search_flights", args: call_args }],
                 stop_reason: ai.StopReason.ToolUse,
                 usage: null,
             },
+            ai.ModelTurn {
+                content: [ai.ToolUse { id: "c3", name: "search_flights", args: call_args }],
+                stop_reason: ai.StopReason.ToolUse,
+                usage: null,
+            },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/baml_src/tests/loop.baml` around lines 101 - 126, Add a
third ai.ModelTurn with ToolUse to the scripted client in the “step budget
throws typed” test, keeping max_steps = 2 and the existing tool-use pattern.
This ensures the third model call is available and the following call attempt
produces ai.errors.StepBudgetExceeded instead of ScriptedClient’s out-of-turns
error.
baml_language/_planv2/pages/04_reference/01_api.md-138-173 (1)

138-173: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep phase-2 media APIs out of the phase-1 reference.

The implemented ContentBlock union has Text, Reasoning, and ToolUse only. Prompt provides render_text; it does not provide render or InstructionPart[]. The future-phases page also assigns media outputs to phase 2.

  • baml_language/_planv2/pages/04_reference/01_api.md#L138-L173: Remove Media, InstructionPart, and Prompt.render from the phase-1 API reference.
  • baml_language/_planv2/pages/04_reference/02_events.md#L44-L44: Remove Media lowering from the AssistantMessage rendering rule.
  • baml_language/_planv2/readme.md#L80-L86: Remove media instruction parts and Media from the phase-1 API tree.

Based on the upstream ai/journal.baml and ai/spec.baml contracts, these APIs are not implemented in phase 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/04_reference/01_api.md` around lines 138 - 173,
Remove phase-2 media APIs from the phase-1 reference: in
baml_language/_planv2/pages/04_reference/01_api.md lines 138-173, remove Media,
InstructionPart, and Prompt.render while retaining the implemented Text,
Reasoning, ToolUse, and Prompt.render_text contracts; in
baml_language/_planv2/pages/04_reference/02_events.md line 44, remove Media
lowering from the AssistantMessage rendering rule; and in
baml_language/_planv2/readme.md lines 80-86, remove media instruction parts and
Media from the phase-1 API tree.
baml_language/_planv2/publish.py-139-140 (1)

139-140: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Set a timeout for the publish request.

urlopen(req) can block indefinitely when the server or network stalls. Pass a finite timeout so the publishing workflow fails predictably.

Proposed fix
-    with urllib.request.urlopen(req) as resp:
+    with urllib.request.urlopen(req, timeout=30) as resp:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/publish.py` around lines 139 - 140, Update the
urllib.request.urlopen call in the publish request flow to pass a finite timeout
value, ensuring stalled network operations fail predictably while preserving the
existing response decoding and printing behavior.
baml_language/_planv2/pages/04_reference/01_api.md-30-44 (1)

30-44: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Document default_client as a lazy resolver.

FunctionSpec.default_client is a () -> Client throws unknown thunk. It is not an eagerly resolved Client.

Update the field type and state that spec creation does not access credentials or environment variables. The current reference tells callers to expect failures at spec creation, but the implementation defers them until the runner resolves the client.

Based on the upstream baml_language/crates/baml_builtins2/baml_std/ai/spec.baml contract, default_client is lazy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/04_reference/01_api.md` around lines 30 - 44,
Update the FunctionSpec.default_client documentation to describe it as a lazy ()
-> Client throws unknown resolver rather than an eagerly resolved Client. State
that spec creation does not access credentials or environment variables, and
that unknown-prefix or missing-credential failures occur when the runner
resolves the client.
baml_language/_planv2/pages/02_guides/03_clients/04_reliability.md-133-134 (1)

133-134: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact provider bodies before logging.

The example logs raw_body directly. A provider error body can contain prompt data, user data, or credentials. Log a bounded, redacted diagnostic, or require explicit opt-in for raw response logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/03_clients/04_reliability.md` around
lines 133 - 134, Update the error handling example around the provider rejection
log to avoid emitting i.raw_body directly. Log only a bounded, redacted
diagnostic by default, or gate raw response logging behind explicit opt-in,
while preserving the existing status and throw behavior.
baml_language/_planv2/pages/03_how_to/01_retry_a_failed_parse_with_feedback.md-31-38 (1)

31-38: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Commit the complete event set for failed attempts.

The journal contract records a Usage event for each model turn. This branch appends only AssistantMessage and UserMessage, so failed attempts are missing from usage totals and event callbacks. Append the same events as the default runner, or narrow the helper's documented contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/_planv2/pages/03_how_to/01_retry_a_failed_parse_with_feedback.md`
around lines 31 - 38, Update the failed-attempt branch in turn_with_feedback to
append the complete event set for the model turn, including its Usage event,
matching the default runner’s journal and callback behavior. Preserve the
existing AssistantMessage, correction UserMessage, and retry flow while ensuring
usage totals include failed attempts.
baml_language/_planv2/pages/02_guides/04_the_journal.md-28-35 (1)

28-35: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve the UserMessage persistence contradiction.

Lines [16-22] say correction requests are committed events, and baml_language/_planv2/pages/03_how_to/01_retry_a_failed_parse_with_feedback.md appends the correction request to the journal. The table says parse repair uses UserMessage ephemerally. For the documented implementation, state that parse repair appends UserMessage so replay and observers see the correction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/04_the_journal.md` around lines 28 -
35, Update the UserMessage entry in the journal event table to state that
parse-repair correction requests are appended as committed journal events, while
preserving the existing custom-runner behavior. Ensure the description reflects
that replay and observers can see these correction messages rather than treating
them as ephemeral.
baml_language/_planv2/pages/03_how_to/04_observe_a_run_with_on_event.md-29-35 (1)

29-35: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log raw tool outputs by default.

The callback logs ToolCompleted.output directly at Line [33]. Tool results can contain user data, secrets, or large payloads. Redact or truncate the output, or mark raw logging as an explicit opt-in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/03_how_to/04_observe_a_run_with_on_event.md`
around lines 29 - 35, Update log_event’s ToolCompleted branch so it does not log
done.output raw by default; apply the project’s existing redaction or truncation
utility, or require an explicit opt-in before including raw output, while
preserving the completion event and tool ID in the log.
baml_language/_planv2/pages/03_how_to/01_retry_a_failed_parse_with_feedback.md-21-28 (1)

21-28: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Handle tool turns before parsing terminal text.

c.invoke can return a ModelTurn with StopReason.ToolUse. In that case, terminal_text() is null, so this helper parses an empty string and sends a correction request instead of executing the tools. Add the tool-dispatch loop, or reject specs with tools before invoking the client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/_planv2/pages/03_how_to/01_retry_a_failed_parse_with_feedback.md`
around lines 21 - 28, Handle tool-use ModelTurn results from c.invoke before
calling baml.sap.parse<Out>; either dispatch the returned tools and continue the
invocation loop until a terminal response is available, or reject specs with
configured tools before invoking the client. Ensure terminal_text() is parsed
only after tool turns are handled, rather than treating null as an empty
candidate.
baml_language/_planv2/pages/02_guides/03_clients/04_reliability.md-49-53 (1)

49-53: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the constructors shown by the runtime fixtures.

The supplied reliability fixture uses ai.Retry.new(...), ai.Backoff.new(...), and ai.Fallback { ... }. This page uses ai.clients.Retry, an unqualified Backoff, and a different constructor shape. A copied example will not match the current public API. Align Lines [49-53] and [91-96] with the fixture syntax, or update both the fixture and runtime together.

Also applies to: 91-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/03_clients/04_reliability.md` around
lines 49 - 53, Update the reliability guide examples around the Retry and
fallback declarations to use the runtime fixture’s public constructors:
ai.Retry.new(...), ai.Backoff.new(...), and the ai.Fallback { ... } shape. Apply
the same syntax consistently to both referenced examples, or update the
corresponding fixture and runtime API together if the guide is intended to
define a new API.
baml_language/_planv2/pages/02_guides/03_clients/05_the_built_in_clients.md-556-560 (1)

556-560: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep retry safety consistent for state-changing Claude Code and MCP runs.

The documentation states both that all failures are Safe and that state-changing tools can complete external effects before a failure while Unknown classification is not implemented. This permits Retry or Fallback to replay non-idempotent actions.

  • baml_language/_planv2/pages/02_guides/03_clients/05_the_built_in_clients.md#L556-L560: implement Unknown classification or disable retries for clients with state-changing harness or MCP tools.
  • baml_language/_planv2/pages/02_guides/03_clients/04_reliability.md#L40-L42: qualify the Safe statement so it excludes clients that can perform external side effects.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/02_guides/03_clients/05_the_built_in_clients.md`
around lines 556 - 560, Update
baml_language/_planv2/pages/02_guides/03_clients/05_the_built_in_clients.md
lines 556-560 to implement Unknown failure classification for clients with
state-changing harness_tools or MCP tools, or explicitly disable Retry and
Fallback for those clients; remove the contradictory “not yet implemented”
behavior. Update
baml_language/_planv2/pages/02_guides/03_clients/04_reliability.md lines 40-42
to qualify that failures are Safe only when no external side effects can occur.
baml_language/_planv2/pages/03_how_to/06_use_mcp_tools_with_any_client.md-21-38 (1)

21-38: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not document pending call desugaring as current executable syntax.

Both examples use a tools: surface that the supplied context says the reference implementation does not yet support.

  • baml_language/_planv2/pages/03_how_to/06_use_mcp_tools_with_any_client.md#L21-L38: show the manual Toolbox.new(conn.tools()) form first, or label the expression form as planned.
  • baml_language/_planv2/pages/03_how_to/05_attach_mcp_servers_to_claude_code.md#L76-L96: bind attach_mcp_tool(client) in the manual spec, or label [attach_mcp] as future syntax.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_planv2/pages/03_how_to/06_use_mcp_tools_with_any_client.md`
around lines 21 - 38, Update
baml_language/_planv2/pages/03_how_to/06_use_mcp_tools_with_any_client.md lines
21-38 to show the manual Toolbox.new(conn.tools()) form first, or explicitly
label the tools: expression as planned syntax; update
baml_language/_planv2/pages/03_how_to/05_attach_mcp_servers_to_claude_code.md
lines 76-96 to bind attach_mcp_tool(client) in the manual spec, or label
[attach_mcp] as future syntax. Ensure neither example presents unsupported
pending syntax as current executable code.
baml_language/crates/baml_builtins2/baml_std/anthropic/ns_internal/messages.baml-192-202 (1)

192-202: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

base_url redirects the API key to an arbitrary host.

base_url overrides the host, and the x-api-key header is attached unconditionally. A caller that sets base_url to any other origin sends the Anthropic credential to that origin. The override is a legitimate feature for gateways and proxies, so the risk is a posture gap rather than an exploit. Two mitigations are worth considering: require https for a non-default base_url, and document that base_url is a trusted-input field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/crates/baml_builtins2/baml_std/anthropic/ns_internal/messages.baml`
around lines 192 - 202, Update the request construction around base_url so
non-default overrides are accepted only when they use HTTPS, while preserving
the default Anthropic URL and existing gateway/proxy support. Validate the URL
scheme before attaching the unconditional x-api-key header, and document
base_url as requiring trusted input.
baml_language/crates/baml_builtins2/baml_std/ai/tools.baml-15-28 (1)

15-28: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The comment promises schema validation that call does not perform.

Lines 15-17 state that the raw handler receives "the validated-by-schema argument map". The raw_handler branch at Lines 26-28 returns rh(args) directly. Nothing validates args against self.input_schema. The handler branch below does validate, because reflect.call_any checks the signature.

Raw tools are the MCP and OpenAPI path, so the arguments come from model output and reach an external server unchecked. Either validate args against input_schema before dispatch, or correct the comment to state that the raw handler owns validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_builtins2/baml_std/ai/tools.baml` around lines 15 -
28, Update the raw_handler path in Tool.call so args is validated against
self.input_schema before dispatching to rh, preserving the documented
validated-by-schema contract and rejecting invalid model-generated arguments
before reaching external servers.
baml_language/_plan/ai_agents/baml_src/ns_ai/session.baml-95-167 (1)

95-167: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Nothing bounds the number of model calls in the reference session. The runner delegates the entire step budget to the policy, and one shipped middleware emits CallModel without charging a step. The two sites together allow an unbounded sequence of paid model calls.

  • baml_language/_plan/ai_agents/baml_src/ns_ai/session.baml#L95-L167: add a runner-side cap on CallModel executions inside run(), so the loop terminates even when a policy never charges a step.
  • baml_language/_plan/ai_agents/baml_src/ns_ai/policy.baml#L184-L194: route the steering flush through the inner policy instead of emitting a bare CallModel, so ToolLoop.next_call increments st.steps and enforces max_steps.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/_plan/ai_agents/baml_src/ns_ai/session.baml` around lines 95 -
167, The session runner in run() needs an independent cap on CallModel
executions; add runner-side counting and termination when the configured
model-call budget is exhausted, while preserving normal command processing. In
baml_language/_plan/ai_agents/baml_src/ns_ai/session.baml lines 95-167, update
the CallModel branch to enforce this bound. In
baml_language/_plan/ai_agents/baml_src/ns_ai/policy.baml lines 184-194, route
the steering flush through the inner policy’s next_call path instead of emitting
a bare CallModel so ToolLoop.next_call increments st.steps and enforces
max_steps.
baml_language/crates/baml_builtins2/baml_std/ai/wrappers.baml-26-46 (1)

26-46: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clamp the retry_after_ms hint to max_ms.

The hint path returns h unchanged. max_ms bounds only the computed backoff. A provider that returns a large retry_after_ms therefore blocks the caller for that full duration, and Backoff.max_ms gives no protection. The value comes from an external response, so it must be bounded. Also reject negative values.

🐛 Proposed fix
         if let h: int = hinted {
-            h
+            if (h < 0) {
+                0
+            } else if (h > self.max_ms) {
+                self.max_ms
+            } else {
+                h
+            }
         } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_builtins2/baml_std/ai/wrappers.baml` around lines
26 - 46, Update Backoff.delay_ms so the retry_after_ms hint is bounded by
self.max_ms and never returns a negative delay. Preserve the computed backoff
behavior for cases without a hint, and apply the same non-negative,
maximum-bound contract to the hinted value.
baml_language/crates/baml_builtins2/baml_std/ai/journal.baml-85-99 (1)

85-99: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

entries() returns the mutable backing list and breaks the append-only invariant.

The comment at Line 94 states that the driving runner is the only writer. entries() returns self.log itself, so any caller can push events directly and bypass append_all. The same aliasing also means a caller that holds the result sees later appends, which makes length-based scans (such as the futility scan in ai/runner.baml) depend on mutation timing. Return a copy.

🛡️ Proposed fix
     function entries(self) -> Event[] {
-        self.log
+        self.log.map((e) -> { e })
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_builtins2/baml_std/ai/journal.baml` around lines 85
- 99, Update Journal.entries() to return a copy of self.log rather than the
mutable backing list. Preserve append-only ownership through append_all() and
ensure callers receive a snapshot that does not reflect later appends.
baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml-277-296 (1)

277-296: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Undefined stdin contract when keep_stdin_open is unset. The new ProcessOptions.keep_stdin_open option decides whether a writable stdin pipe exists, but neither the declaration nor the call site defines the behavior of write_stdin and close_stdin when the option is absent. The Claude Code client depends on that undefined behavior.

  • baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml#L277-L296: set keep_stdin_open: true if the intent is to deliver EOF through close_stdin, or remove the close_stdin call and its catch_all.
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml#L89-L97: document whether write_stdin and close_stdin throw root.errors.Io or act as no-ops when keep_stdin_open is absent or false, and state whether the child inherits the parent's stdin in that case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml`
around lines 277 - 296, The stdin contract is undefined when
ProcessOptions.keep_stdin_open is absent or false. In
baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml
lines 277-296, set keep_stdin_open to true if the Claude Code client should
deliver EOF via close_stdin; otherwise remove the close_stdin call and its
catch_all. In baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml
lines 89-97, document whether write_stdin and close_stdin throw root.errors.Io
or no-op when stdin is not kept open, and whether the child inherits the parent
stdin.
baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml-99-104 (1)

99-104: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Escape tool names and descriptions before embedding them in JSON text.

catalog.push builds a JSON object by string interpolation. t.name and t.description are inserted directly between quotes. A description that contains ", \, or a newline produces invalid JSON in the prompt. The model then cannot parse the catalog reliably. Build the object as a map and serialize it with baml.json.stringify, as the code already does for t.input_schema.

The same pattern appears at Lines 20-22, where c.id and c.name are interpolated into a JSON tool-call line.

🛠️ Proposed fix for the catalog entry
     for (let t in tb.list()) {
-        catalog.push(
-            `{"name": "${t.name}", "description": "${t.description}", "parameters": ${baml.json.stringify(baml.json.to_json(t.input_schema))}}`,
-        );
+        let entry: map<string, unknown> = {};
+        let _ = entry.set("name", t.name);
+        let _ = entry.set("description", t.description);
+        let _ = entry.set("parameters", t.input_schema);
+        catalog.push(baml.json.stringify(baml.json.to_json(entry)));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml`
around lines 99 - 104, Update the catalog construction in the loop over
tb.list() to build each entry as a map and serialize the complete object with
baml.json.stringify, rather than interpolating t.name and t.description into
JSON text; preserve the existing input_schema serialization. Apply the same
change to the tool-call line using c.id and c.name so those values are safely
escaped before embedding in JSON.
baml_language/crates/baml_builtins2/baml_std/openai/ns_internal/responses.baml-148-154 (1)

148-154: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the tool-argument decode.

item.arguments is model-generated text. baml.json.from_string is called without catch_all, so malformed arguments throw an untyped decode error out of openai_parse and out of invoke. The Gemini and Claude Code clients both guard the same decode and fall back to an empty map. Align the OpenAI client, or throw ai.errors.ParseFailed so the runner sees a typed failure.

🛠️ Proposed fix
             let args: map<string, unknown> = {};
             if let s: string = item.arguments {
-                args = baml.json.from_string<map<string, unknown>>(s);
+                args = baml.json.from_string<map<string, unknown>>(s) catch_all (e) {
+                    _ => {
+                        let empty: map<string, unknown> = {};
+                        empty
+                    },
+                };
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/crates/baml_builtins2/baml_std/openai/ns_internal/responses.baml`
around lines 148 - 154, Guard the baml.json.from_string call in the openai_parse
tool-argument handling for item.arguments, matching the Gemini and Claude Code
clients by using catch_all and falling back to an empty map for malformed
model-generated JSON, or convert failures into ai.errors.ParseFailed so invoke
receives a typed error.
baml_language/crates/baml_builtins2/baml_std/mcp/mcp.baml-71-83 (1)

71-83: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the child process if the initialize handshake fails.

start_process succeeds, then conn._request("initialize", init) runs. _request throws on a read failure, on stream EOF, or on a JSON-RPC error object. _send also throws on a write failure. In every one of those paths the spawned server process is never closed, so the child leaks for the lifetime of the program. Wrap the handshake and close the process before rethrowing.

🛠️ Proposed fix
         let conn = McpConnection { server_name: name, p: p, next_id: 1 };
         let init: map<string, unknown> = {};
         let _ = init.set("protocolVersion", "2025-06-18");
         let caps: map<string, unknown> = {};
         let _ = init.set("capabilities", caps);
         let info: map<string, unknown> = {};
         let _ = info.set("name", "baml-mcp");
         let _ = info.set("version", "0.1");
         let _ = init.set("clientInfo", info);
-        let _ = conn._request("initialize", init);
-        conn._send(rpc_line(null, "notifications/initialized", null));
+        let _ = conn._request("initialize", init) catch_all (e) {
+            _ => {
+                conn.close();
+                throw e
+            },
+        };
+        conn._send(rpc_line(null, "notifications/initialized", null)) catch_all (e) {
+            _ => {
+                conn.close();
+                throw e
+            },
+        };
         conn
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_builtins2/baml_std/mcp/mcp.baml` around lines 71 -
83, Update the handshake flow in the McpConnection constructor/startup method
around conn._request("initialize", init) and conn._send(...) to catch any
failure from initialization or the initialized notification, close the spawned
child process, then rethrow the original error; return conn unchanged only after
both operations succeed.

Comment thread baml_language/_planv2/baml_src/howto/attach_mcp.baml Outdated
Comment thread baml_language/_planv2/pages/03_how_to/05_attach_mcp_servers_to_claude_code.md Outdated
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 26.4 MB 11.2 MB file 27.4 MB -956.7 KB (-3.5%) OK
packed-program Linux 🔒 17.0 MB 6.9 MB file 18.6 MB -1.6 MB (-8.7%) OK
baml-cli macOS 🔒 20.6 MB 9.8 MB file 21.3 MB -698.4 KB (-3.3%) OK
packed-program macOS 🔒 13.3 MB 6.1 MB file 14.5 MB -1.1 MB (-7.8%) OK
baml-cli Windows 🔒 22.1 MB 10.0 MB file 23.0 MB -849.0 KB (-3.7%) OK
packed-program Windows 🔒 14.1 MB 6.1 MB file 15.5 MB -1.4 MB (-8.9%) OK
bridge_wasm WASM 15.8 MB 🔒 4.3 MB gzip 4.6 MB -344.4 KB (-7.5%) OK

🔒 = 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.


Generated by cargo size-gate · workflow run

- sdkgen_java: escape method names that collide with java.lang.Object's
  final methods (wait/notify/notifyAll/getClass) with the same `$` suffix
  as keywords — `baml.sys.Process.wait` generated `wait()` which cannot
  override the final `Object.wait()`. Runtime binding keeps the BAML name.
- markdown whitelist: allow the _plan/ and _planv2/ BEP planning trees
  (the validate-markdown hook checks all files once any .md changes).
- size gate: update packed-program baselines (+~700 KB, the six new
  builtin stdlib packages' bytecode).
- accept bytecode_format display snapshots (stdlib listing grew).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread baml_language/.ci/size-gate/aarch64-apple-darwin.toml Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/sdks/java/sdkgen_java/src/emit.rs`:
- Line 786: Update the accessor-name generation in render_class to use
java_method_identifier(...) for generated getter/setter method names, while
retaining java_identifier(...) for field and reference names. Add an emitter
unit test covering a property named wait (and the java.lang.Object final-method
names) to verify generated SDK code compiles without accessor conflicts.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 883daee3-aed9-441f-b2b4-17019850b7e6

📥 Commits

Reviewing files that changed from the base of the PR and between 2f64f97 and d6c9f89.

⛔ Files ignored due to path filters (3)
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_textual.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • baml_language/.ci/size-gate/aarch64-apple-darwin.toml
  • baml_language/.ci/size-gate/x86_64-pc-windows-msvc.toml
  • baml_language/.ci/size-gate/x86_64-unknown-linux-gnu.toml
  • baml_language/.markdown-whitelist
  • baml_language/sdks/java/sdkgen_java/src/emit.rs
  • baml_language/sdks/java/sdkgen_java/src/routing.rs

Comment thread baml_language/sdks/java/sdkgen_java/src/emit.rs
@aaronvg
aaronvg enabled auto-merge August 11, 2026 05:51
@aaronvg
aaronvg added this pull request to the merge queue Aug 11, 2026
Merged via the queue into canary with commit e8ad36f Aug 11, 2026
86 checks passed
@aaronvg
aaronvg deleted the aaron/custom-llm-providers-v5 branch August 11, 2026 06:06
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.

1 participant