Repository navigation
E.3: ToolResult split by source; core marks provider results providerExecuted:true (phase 1E; stacked on #27) - #28
Open
cunninghamcard-bit wants to merge 9 commits into
Open
cunninghamcard-bit wants to merge 9 commits into
cunninghamcard-bit wants to merge 9 commits into
Conversation
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.
ToolResult.source: ToolResultSource = Client { dynamic: bool, .. } |
Provider { dynamic: Option<bool>, marked }. JSON goes through the private
ToolResultWire (the E.3a struct) with serde(from/into) and
#[ts(as = "ToolResultWire")]; ToolResult.ts unchanged. Client results
always write dynamic (execute-tool-call.ts:169,199) and never gain
providerExecuted; provider results pass dynamic through. Provider converters
build unmarked V4 results (no providerExecuted, as @ai-sdk/provider 4.0.21);
core sets providerExecuted:true at generate.rs (step content) and
stream_part.rs (into_text_stream_part) via ToolResult::into_core, as
convert-language-model-content.ts:113 / stream-language-model-call.ts:772.
…data; per-provider core-conversion tests
- Provider { dynamic, mark: ProviderExecutedMark::{Unmarked, Marked} }
replaces the bool; documented as transitional until E.3b moves the
provider side to #16's V4 tool-result type.
- Client { dynamic, provider_executed: Option<bool> }: plain data, no
hidden 'input had false' marker. Test: absent / false client samples
round-trip byte for byte.
- provider_executed_core_conversion_test: anthropic, google, vertex,
openai responses, xai, huggingface each yield providerExecuted:true
through generate_text and stream_text (cassettes / upstream-port bodies).
JSON and TS unchanged.
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.
Phase 1E unit E.3 (A7: "E.3 建模定稿" plus "E.3 补充: core 转换时置 providerExecuted:true"). Stacked on #27 (E.3a) → #26 → #25 → #24. The E.3 commits are 132c301 and the follow-up 0f4aa9e.
Model (
aimux-core/src/tool.rs)ToolResultkeeps its common fields as public fields:toolCallId,toolName,result,isError,preliminary,providerMetadata.isErrorandpreliminarystay independent, as A7 requires. The source becomespub source: ToolResultSource:Client { dynamic: bool, provider_executed: Option<bool> }:dynamicis always written as a boolean (AI SDKexecute-tool-call.ts:169,199).provider_executedis plain data, written as given. There is no hidden "input hadfalse" marker. Client results built by core never set it.Provider { dynamic: Option<bool>, mark: ProviderExecutedMark }:dynamicis passed through (stream-language-model-call.ts:774,789).ProviderExecutedMarkhas two variants:Unmarked: V4 provider-layer output;providerExecutedis not written.Marked: core output;"providerExecuted": trueis written.The marker is transitional. Provider converters and core share
ToolResulttoday, so this enum is what tells the two layers apart. Follow-up unit E.3b will switch the provider side to feat(provider): V4 type crate aimux-provider (phase 1 unit 1.6) #16's V4 tool-result type once feat(provider): V4 type crate aimux-provider (phase 1 unit 1.6) #16 merges (the 3.3 split), and the marker goes away. feat(provider): V4 type crate aimux-provider (phase 1 unit 1.6) #16 (b837aac3) is not an ancestor of this branch (git merge-base --is-ancestorexits 1), so this PR is not stacked on it.ToolResultWireis the E.3a struct, unchanged, used through#[serde(from/into)]and#[ts(export, as = "ToolResultWire")].ToolResult.tshas zero diff andgen_ts_types.py --checkpasses.rg 'pub dynamic:' aimux-core/src/tool.rshits onlyRawToolCall, the V4-style DTO that A7 leaves alone.providerExecuted: truebecomesProvider { mark: Marked }.falsebecomesClient { provider_executed: None | Some(false) }.ToolResultSource::provider(dynamic), which is unmarked. Their JSON is identical to before: noproviderExecuted, anddynamicas given. This matches@ai-sdk/provider@4.0.21, whoselanguage-model-v4-tool-result.tshas no providerExecuted field.providerExecuted: truethroughToolResult::into_core(), which passesdynamicthrough:aimux-core/src/generate.rs:794: provider content becomes step content (AI SDKconvert-language-model-content.ts:113,129,149,165).aimux-core/src/stream_part.rs:396:into_text_stream_part(AI SDKstream-language-model-call.ts:772,788).is_dynamic(),dynamic()(the value as written on the wire),is_provider_executed(),provider_executed(),into_core().Changed assertions
providerExecuted-related changes: none. No existing assertion about a provider-executed result had to change.
openai_upstream_e2e_test::responses_web_search_streampasses with its assertions untouched.D25 normalization changes (two, unrelated to providerExecuted): client rows now gain
"dynamic":false, as A7 D25 requires (execute-tool-call.ts:169,199).aimux-core/tests/enum_state_wire_test.rstool_result_samples_match_baseline, nowtool_result_samples_match_normalized. Samples are compared withnormalized/tool_result.jsonl, which was committed in E.3a and is unchanged here. The client rows withoutdynamicgain"dynamic":false. Provider-executed rows are byte-identical, including the one withoutdynamic.aimux-core/tests/enum_state_wire_test.rstool_result_provider_executed_is_passed_through. The expected output of the two client inputs (absent, and"providerExecuted":false) gains"dynamic":false. The provider rows are unchanged.Not an assertion change:
aimux-providers/tests/data_loss_regression_test.rs:266destructures throughtool_result.dynamic()instead of the removed field. The asserted values are the same.New tests
enum_state_wire_test:tool_result_absent_and_false_dynamic_are_the_same_variantprovider_layer_result_is_marked_by_core_conversion: V4 JSON is unmarked, andinto_coreadds"providerExecuted":true.aimux-core/tests/provider_executed_marking_test.rs(new):generate_marks_provider_tool_result: a mock model returns a V4 tool result, andgenerate_textstep content hasproviderExecuted: truewithdynamicabsent.text_stream_marks_provider_tool_result:into_text_stream_partmarks the result, while the StreamPart's own JSON stays unmarked.Per-provider evidence (new
aimux-providers/tests/provider_executed_core_conversion_test.rs, 6 tests)Each test runs the provider's real model through
generate_text(step content,generate.rs:794) andstream_text(into_text_stream_part,stream_part.rs:396). It asserts that everytool-result/tool-errorpart has"providerExecuted": true, and that at least one exists.anthropic/test_pause_turn_web_search_vcr_1.json(web_search)anthropic/test_anthropic_code_execution_tool_stream.jsonandanthropic/test_anthropic_web_fetch_tool_stream.jsonstream.rsparse_anthropic_contentandstream_parts_for_result_blockgemini/test_code_execution.jsongemini/test_code_execution_stream.jsongoogle/model.rsgenerate + stream code-execution resultsvertex/model.rsgenerate + streamopenai/test_openai_responses_model_web_search_tool_1.jsonresponses_web_search_streamupstream events (the recorded*_stream_1cassette holds a non-streaming body)provider_events.rsweb_search pair; streaming web_search resulttool_results_preserve_actions_and_sourcesbody (web_search)file_search_callSSExai/responses/mod.rsgenerate web_search + stream file_searchshould_handle_mcp_tools_with_annotationsbody (mcp_call)should_stream_tool_callsevents (function_call with output)huggingface/responses.rs:1042(generate) and the stream output siteNot covered here: the image_generation result paths (
openai responses_convert.rspartial image;xai image_generation_result) and the remaining anthropic result kinds (tool_search, advisor, mcp, bash/text-editor). They go through the same two core conversion points but have no direct test.Also new:
enum_state_wire_test::client_tool_result_provider_executed_is_plain_data. Client samples withproviderExecutedabsent andfalse(rows already intool_result.jsonl) round-trip byte for byte asClient { provider_executed: None | Some(false) }.Fixtures / cassettes with a diff
None.
git diff --stat exp/e3a-toolresult-provider-executed -- fixtures/ contract-tests/ aimux-providers/tests/cassettes/ bindings/is empty, and no test rewrote a fixture. That includesfixtures/v4/enum-state/, because E.3a already committed the E.3 normalized form. The only intended JSON change,"providerExecuted":trueon provider results after core conversion, appears in no committed fixture or cassette.Results (local linux x64; new test names confirmed in the log)
New code has no
untaggedorflatten. Hand-written bindings are untouched. Go/Java/Kotlin/Swift/Flutter were not run.