Skip to content

E.3a: ToolResult.providerExecuted passthrough (phase 1E; stacked on #26) - #27

Open
cunninghamcard-bit wants to merge 7 commits into
exp/restartfrom
exp/e3a-toolresult-provider-executed
Open

cunninghamcard-bit wants to merge 7 commits into
exp/restartfrom
exp/e3a-toolresult-provider-executed

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Owner

Phase 1E unit E.3a (PHASES.md, added 2026-10-11): adds providerExecuted? to ToolResult only. No enum change. Stacked on #26 (E.2) → #25 → #24. The E.3a-only change is commit 5f427aa.

Change

  • aimux-core/src/tool.rs: ToolResult gets pub provider_executed: Option<bool> with #[ts(optional)] and serde(default, skip_serializing_if = "Option::is_none"). It sits after dynamic. Sources: docs: full-chain AI SDK alignment design and impact map arcships/aimux#200:2292 and AI SDK generate-text/tool-result.ts:17,32.
  • bindings/node/src/types/ToolResult.ts: +6 lines only, which is the field's doc comment plus providerExecuted?: boolean, (git diff --stat: 6 insertions, 0 deletions). gen_ts_types.py --check passes. Hand-written bindings are untouched.

Where ToolResults are produced (all 29 struct literals; the commit adds provider_executed at each)

Every literal is in a provider converter that builds the V4 provider tool-result. In @ai-sdk/provider@4.0.21, language-model-v4-tool-result.ts has no providerExecuted field, so the source carries nothing and each literal writes None. This keeps provider output identical to AI SDK provider output.

I first tried mirroring the paired call's Some(true). That broke openai_upstream_e2e_test::responses_web_search_stream, a port of the AI SDK provider test, because its expected tool-result has no providerExecuted. So I reverted to None.

Everywhere else in core and bindings, ToolResult values move whole. These paths carry the field through unchanged:

  • stream_part.rs:357, :395
  • generate.rs:472, :792
  • response_messages.rs:174, :257
  • serde (recordings, bindings, replay)

Literal sites (file:line of the inserted field):

  • aimux-providers/src/anthropic/stream.rs:380
  • aimux-providers/src/anthropic/stream.rs:400
  • aimux-providers/src/anthropic/stream.rs:502
  • aimux-providers/src/anthropic/stream.rs:800
  • aimux-providers/src/anthropic/stream.rs:835
  • aimux-providers/src/anthropic/stream.rs:860
  • aimux-providers/src/anthropic/stream.rs:876
  • aimux-providers/src/anthropic/stream.rs:897
  • aimux-providers/src/anthropic/stream.rs:917
  • aimux-providers/src/anthropic/stream.rs:933
  • aimux-providers/src/anthropic/stream.rs:952
  • aimux-providers/src/google/model.rs:481
  • aimux-providers/src/google/model.rs:536
  • aimux-providers/src/google/model.rs:839
  • aimux-providers/src/google/model.rs:971
  • aimux-providers/src/huggingface/responses.rs:455
  • aimux-providers/src/huggingface/responses.rs:1044
  • aimux-providers/src/huggingface/responses.rs:1070
  • aimux-providers/src/openai/responses/provider_events.rs:44
  • aimux-providers/src/openai/responses/responses_convert.rs:913
  • aimux-providers/src/vertex/model.rs:649
  • aimux-providers/src/vertex/model.rs:710
  • aimux-providers/src/vertex/model.rs:928
  • aimux-providers/src/vertex/model.rs:1076
  • aimux-providers/src/xai/responses/mod.rs:162
  • aimux-providers/src/xai/responses/mod.rs:280
  • aimux-providers/src/xai/responses/mod.rs:319
  • aimux-providers/src/xai/responses/mod.rs:1063
  • aimux-providers/src/xai/responses/mod.rs:1131

Open for E.3 (needs a decision): AI SDK core sets providerExecuted: true itself when it turns a provider tool-result into step content or a stream part:

  • convert-language-model-content.ts:113,129,149,165
  • stream-language-model-call.ts:772,788

The matching aimux points are generate.rs:792 and stream_part.rs:395. E.3a does not do this, because it would change core output that existing tests assert. It belongs with E.3's provider-executed variant.

Samples (fixtures/v4/enum-state/)

  • tool_result.jsonl adds 6 rows: providerExecuted false/true crossed with dynamic absent/false/true. That includes provider-executed without dynamic. Rows without providerExecuted are the absent case.
  • normalized/tool_result.jsonl is now the A7 D25 form plus the A7 correction:
    • client-executed results (providerExecuted not true) always write dynamic as a boolean;
    • provider-executed results pass dynamic through;
    • providerExecuted itself is always passed through.
  • Tests:
    • tool_result_samples_match_baseline: all 14 rows round-trip byte for byte (passthrough). The 8 rows that existed at 8455db9 still equal baseline/. The normalized lines are a fixed point. serialize(x) == normalize(x) is E.3's job (the enum).
    • New: tool_result_provider_executed_is_passed_through covers absent/false/true and provider-executed without dynamic, compared byte for byte.
    • baseline_equals_samples no longer lists tool_result, because its new rows cannot exist at 8455db9. The old rows are still checked against baseline/ in the test above.

Acceptance

  • (a) Absent/false/true are byte-identical, and old samples are unchanged.
  • (b) ToolResult.ts only gains providerExecuted?: boolean (plus its doc comment).
  • (c) git diff --stat -- fixtures/ contract-tests/fixtures/ aimux-providers/tests/cassettes/ outside fixtures/v4/enum-state/ is empty.
  • (d) No untagged or flatten.

Results (local linux x64)

R.0 E.2 (#26) E.3a
cargo test --workspace 2871 / 0 / 7 2882 / 0 / 7 2883 / 0 / 7
fmt / clippy -D warnings — ok ok
boundary script 0 — 0
gen_ts_types --check — ok, empty diff ok, ToolResult.ts +6
Node ava (node 22) 87 pass, 1 fail 87 / same 1 87 / same 1
Python pytest 93 pass, 5 fail 93 / same 5 not re-run

fixtures/v4/enum-state/: JSONL samples for ToolCall, ToolResult, RawToolCall
and StreamPart/TextStreamPart tool-input-start (absent / explicit false /
true, providerExecuted true/false/absent, invalid+error, D26 rejected inputs,
RFC-0042:84-91 vectors as RawToolCall input), normalized/ per A7 D25, and
baseline/ recorded at 8455db9. aimux-core/tests/enum_state_wire_test.rs
compares serialize(deserialize(line)) byte for byte with the baseline and
checks normalized/ is a round-trip fixed point. A7 copied verbatim into
docs/amendments/.
New aimux_core::tool::RawArguments newtype, serde(transparent) and
#[ts(as = "String")] on the field, so the wire and RawToolCall.ts are
unchanged. Not an enum (arcships#200:2724). Provides new/as_str/into_string/push_str,
From<String|&str>, Into<String>, Deref<str>, AsRef<str>, Display and
PartialEq with str/String/serde_json::Value (parity with String). Call sites
in core, provider-utils, providers and tests converted with .into() /
.to_string(); no behaviour change. Enum-state samples still match the
8455db9 baseline byte for byte.
ToolCall = Static(StaticToolCall) | Dynamic(DynamicToolCall { validity:
Valid | Invalid { error } }), serialized through the private ToolCallWire
(the old struct) with serde(try_from/into) and #[ts(as = "ToolCallWire")];
ToolCall.ts is unchanged. D25: static writes no dynamic, dynamic writes
true, invalid only as true with its error, providerExecuted passed through.
D26: invalid without dynamic:true or without error fails conversion.
Accessors (tool_call_id/tool_name/input/provider_executed/provider_metadata/
is_dynamic/is_invalid/error); parsed_tool_call_arguments and
response_tool_call_input take &ToolCall and match. Consumers converted.
tool_call.jsonl gains the RFC-0042:84-93 core values (string "Tokyo", string
"{\"x\":1}" kept a string, number, bool, null, last-wins {"a":2}, 100.0)
and empty text / {"a": as invalid dynamic calls with an error (never {}).
tool_call_semantic.jsonl holds the non-canonical spellings (duplicate key,
1e2), compared by JSON semantics. check_normalized asserts line counts before
zipping; baseline_equals_samples is skipped in bless mode. Baseline re-blessed
at 8455db9 (existing lines unchanged; new lines appended).
Adds providerExecuted?: boolean to ToolResult only (no enum change), per
arcships#200:2292 / AI SDK generate-text/tool-result.ts:17,32. Absent/false/true
round-trip byte for byte. Provider converters construct V4 tool results,
which carry no providerExecuted in @ai-sdk/provider 4.0.21
(language-model-v4-tool-result.ts), so they write None; every other path
moves the whole ToolResult and passes the field through. Samples: the
providerExecuted x dynamic rows (incl. provider-executed without dynamic)
and the D25 normalized form for E.3.
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