Skip to content

E.4: ToolInputStart dynamic as ToolKind (core normalized, provider layer tri-state) (phase 1E; on #28) - #31

Open
cunninghamcard-bit wants to merge 1 commit into
exp/e3-toolresult-enumfrom
exp/e4-tool-input-start-kind
Open

cunninghamcard-bit wants to merge 1 commit into
exp/e3-toolresult-enumfrom
exp/e4-tool-input-start-kind

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Owner

Phase 1E unit E.4 (PHASES 1E, A7 §D25). Stacked on #28 (exp/e3-toolresult-enum @ 0f4aa9e), and the PR base is that branch. Commit a498558.

Change

  • aimux-core/src/tool.rs: new shared pub enum ToolKind { Static, Dynamic }, re-exported, with is_dynamic() and from_dynamic(). Two crate-private serde modules handle the wire boolean:
    • tool_kind_bool always writes the boolean, and treats an absent field or false as Static.
    • tool_kind_option keeps all three states (absent / false / true).
  • aimux-core/src/stream_part.rs:
    • Provider-layer StreamPart::ToolInputStart.dynamic: Option<ToolKind>: keeps the three states. This is V4 provider output, and AI SDK language-model-v4-stream-part.ts:58 passes it through.
    • Core TextStreamPart::ToolInputStart.dynamic: ToolKind: always writes the boolean, and absent equals false (AI SDK core stream-language-model-call.ts:809 writes dynamic: chunk.dynamic ?? tool?.type === 'dynamic').
    • into_text_stream_part maps absent to Static. Core has no tool set at this point, so the SDK's tool?.type === 'dynamic' fallback is not modelled.
    • provider_executed is unchanged (Option<bool>, passed through).
  • TS: #[ts(optional, as = "Option<bool>")], so StreamPart.ts and TextStreamPart.ts have zero diff. gen_ts_types.py --check passes and bindings/ has no diff.
  • Provider construction sites: one change, anthropic/stream.rs:1385 (dynamic.map(ToolKind::from_dynamic)). All other sites write dynamic: None and are unchanged.

Why StreamPart is not normalized

A7's D25 table lists "StreamPart/TextStreamPart → always write". However:

  • StreamPart is the provider-layer (V4) stream type, and A7's own row for V4 DTOs says dynamic is passed through.
  • AI SDK writes the boolean only in core.
  • Normalizing StreamPart would add "dynamic":false to every provider stream part, which would break openai_upstream_e2e_test::responses_web_search_stream and the other upstream ports.

So this PR normalizes only the core TextStreamPart and keeps the three states on the provider-layer StreamPart. That is the same layer split as E.3. Please confirm, and A7's D25 row should be reworded to name TextStreamPart only.

Samples / tests

  • fixtures/v4/enum-state/normalized/stream_tool_input_start.jsonl is now the identity, because the provider layer passes values through. gen_samples.py was updated. The 4 rows that changed are the absent/false rows that E.0 had normalized. stream_tool_input_start.jsonl still matches the 8455db9 baseline byte for byte.
  • text_stream_tool_input_start_samples_match_baseline became text_stream_tool_input_start_samples_match_normalized: samples serialize to normalized/ (D25), and the normalized lines are a fixed point. baseline_equals_samples no longer lists it.
  • New tool_input_start_kind:
    • in core, absent and false give the same JSON;
    • in the provider layer, absent round-trips as absent (None);
    • into_text_stream_part maps true to ToolKind::Dynamic.
  • No other assertion changed. responses_web_search_stream passes unchanged.
  • Fixture, cassette and contract-tests diff: none outside fixtures/v4/enum-state/ (the two files above).
  • No untagged or flatten added.

Results (local linux x64)

E.3 (#28) E.4
cargo test --workspace 2894 / 0 / 7 2895 / 0 / 7
fmt / clippy -D warnings ok ok
boundary script 0 0
gen_ts_types --check ok ok, zero .ts diff
Node ava (node 22) 87 / 1 baseline fail 87 passed / same 1 baseline fail

Python was not run (no venv). The final clippy fix, which made the serde modules pub(crate), was made after the full test run; it only changes visibility, and clippy passes on it.

Shared ToolKind { Static, Dynamic }. Provider-layer StreamPart keeps the V4
tri-state (Option<ToolKind>, stream-part.ts:58); core TextStreamPart always
writes the boolean (stream-language-model-call.ts:809), absent = Static.
into_text_stream_part maps absent to Static. serde(with) modules keep the
wire; #[ts(optional, as = "Option<bool>")] keeps StreamPart.ts /
TextStreamPart.ts unchanged.
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