Skip to content

E.2: ToolCall -> Static/Dynamic enum via private wire (D25/D26; phase 1E; stacked on #25) - #26

Open
cunninghamcard-bit wants to merge 6 commits into
exp/restartfrom
exp/e2-toolcall-enum
Open

cunninghamcard-bit wants to merge 6 commits into
exp/restartfrom
exp/e2-toolcall-enum

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Owner

Phase 1E unit E.2 (A7 row ToolCall; D2, D25, D26). Stacked on #25 (E.1) and #24 (E.0), so this diff includes them until they merge. The E.2-only change is commit 55699c0; the merge commit after it only brings in the E.0 review fixes (684c8b9). It is independent of the 1R stack (#19–#23).

Change (aimux-core/src/tool.rs)

  • pub enum ToolCall { Static(StaticToolCall), Dynamic(DynamicToolCall) }. DynamicToolCall has validity: ToolCallValidity = Valid | Invalid { error: AiMuxError }, so an invalid call cannot exist without its error. StaticToolCall cannot be invalid at all.
  • Wire. The old struct, unchanged, becomes the private ToolCallWire. ToolCall serializes through it with #[serde(try_from = "ToolCallWire", into = "ToolCallWire")] and #[ts(export, as = "ToolCallWire")]. The container-level as in ts-rs 12 keeps the TS name ToolCall and inlines the wire shape, so ToolCall.ts has zero diff. The extended explanation is written as // comments, because the /// line is the generated TS doc.
  • D25 (AI SDK parse-tool-call.ts:126-128,276,280-287): static calls write no dynamic, so an input dynamic:false is dropped. Dynamic calls write dynamic:true. invalid is written only as true, together with error. providerExecuted stays Option<bool> and is passed through.
  • D26: try_from returns an error for invalid:true without dynamic:true, and for invalid:true without error.
  • Public accessors: tool_call_id(), tool_name(), input() / input_mut(), provider_executed(), provider_metadata() / provider_metadata_mut(), is_dynamic(), is_invalid(), error(). DynamicToolCall, StaticToolCall and ToolCallValidity are re-exported.
  • openai_output::parsed_tool_call_arguments and response_messages::response_tool_call_input now take &ToolCall and match on it, as A7 specifies. parse_tool_call builds the variants directly.
  • Consumers in core, providers and tests were migrated to the accessors (.dynamic / .invalid in tests became is_dynamic() / is_invalid()). No behaviour change beyond D25/D26.

Tests (aimux-core/tests/enum_state_wire_test.rs)

  • tool_call_samples_match_normalized: every tool_call.jsonl sample serializes to its normalized/ line byte for byte, and the normalized lines are a fixed point. This includes the RFC-0042 core-value rows from E.0: enum-state shared wire samples + 8455db9a baseline; add A7 (phase 1E) #24.
  • tool_call_absent_and_false_are_the_same_variant: absent vs dynamic:false, absent vs invalid:false, and dynamic:true vs dynamic:true, invalid:false give the same variant and the same JSON.
  • tool_call_provider_executed_is_passed_through: providerExecuted true / false / absent round-trips byte for byte on static, dynamic and invalid calls.
  • tool_call_rejected_samples_fail_conversion: all 3 D26 rows from E.0's tool_call_rejected.jsonl fail conversion.
  • invalid_tool_call_carries_its_error.
  • tool_call_semantic_samples_match_by_json_value (from E.0: enum-state shared wire samples + 8455db9a baseline; add A7 (phase 1E) #24) still passes on the enum.
  • The tool_call baselines stay as the 8455db9 record. ToolCall is now compared with normalized/, as A7 specifies.

Shared acceptance

  • (a) Normalized samples are byte-identical. git diff --stat -- fixtures/ contract-tests/fixtures/ aimux-providers/tests/cassettes/ against E.1 is empty.
  • (b) gen_ts_types.py --check: up to date, and bindings/ has zero diff.
  • (c) No new untagged, flatten or tag field.
  • (d) Hand-written bindings are untouched.
  • (f) grep -nE 'pub (dynamic|invalid|error):' aimux-core/src/tool.rs hits only RawToolCall.dynamic and ToolResult.dynamic; nothing on ToolCall (the wire struct is private).

Results vs R.0 (local linux x64; test names confirmed in the log)

R.0 (8455db9) E.1 E.2
cargo test --workspace 2871 / 0 / 7 2878 / 0 / 7 2882 / 0 / 7
fmt / clippy -D warnings — ok / ok ok / ok
gen_ts_types --check, .ts diff — ok, empty ok, empty
Python pytest 93 pass, 5 fail 93 / same 5 93 / same 5 (same KeyErrors)
Node ava (node 22) 87 pass, 1 fail 87 / same 1 87 / same 1

Go/Java/Kotlin/Swift/Flutter were not run locally.

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).
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