Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b28c685
feat(core)!: parse and validate tool inputs at the Core boundary
cunninghamcard-bit Aug 24, 2026
c3d98fb
test(core): pin the fifteen-variant error contract
cunninghamcard-bit Aug 24, 2026
7ed1dd8
feat: replay server-tool calls and repair streamed tool inputs end to…
cunninghamcard-bit Aug 25, 2026
97491f9
refactor(core): extract response-message assembly into its own module
cunninghamcard-bit Aug 25, 2026
4935ffc
refactor(core): split the tool-parse contract out of the tool types
cunninghamcard-bit Aug 25, 2026
b265847
refactor(core)!: type the provider tool-input boundary as String
cunninghamcard-bit Aug 25, 2026
b2aa752
fix: repair two defects the master merge introduced
cunninghamcard-bit Aug 25, 2026
7231686
fix(core): stop overloading Value::String for raw tool-call arguments
cunninghamcard-bit Sep 3, 2026
c6da09c
fix(providers): stop Cohere streaming from parsing tool-call input it…
cunninghamcard-bit Sep 3, 2026
edcaa40
fix(core): accept the legacy object-shaped GenerateContent::ToolCall.…
cunninghamcard-bit Sep 3, 2026
6b5e6cf
fix(core): recover raw tool-call text for NoSuchTool and failed-repai…
cunninghamcard-bit Sep 3, 2026
f22be33
fix(core): parse an unknown tool's arguments back into the transcript
cunninghamcard-bit Sep 3, 2026
41591d3
fix(core): render blank invalid tool arguments as an empty object
cunninghamcard-bit Sep 3, 2026
1cad118
docs(api): correct and trim the tool-call input wire-shape note
cunninghamcard-bit Sep 3, 2026
65b3eb7
test(core): collapse the invalid-tool-argument renderer tests
cunninghamcard-bit Sep 3, 2026
676e658
test(core): keep one legacy-shape tool-call input test
cunninghamcard-bit Sep 3, 2026
d4e8454
test: drop duplicate tool-error test cases
cunninghamcard-bit Sep 3, 2026
a667bb4
fix(core): preserve original arguments when tool calls remain invalid
cunninghamcard-bit Sep 7, 2026
eb0a93f
fix: reconcile the rebase onto #164 with the Retry error code
cunninghamcard-bit Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ url = "2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Schema validation
jsonschema = { version = "0.50.1", default-features = false }

# Internal crates
aimux-core = { path = "aimux-core", version = "0.3.0" }
aimux-providers = { path = "aimux-providers", version = "0.3.0" }
Expand Down
1 change: 1 addition & 0 deletions aimux-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ tokio-util = "0.7"
httpdate = { workspace = true }
rand = { workspace = true }
base64 = "0.22"
jsonschema = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "test-util"] }
Expand Down
6 changes: 6 additions & 0 deletions aimux-core/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ pub enum ContentPart {
tool_name: String,
/// Arguments as a JSON value (usually an object).
input: Value,
/// Whether the tool call is executed by the provider rather than by
/// the client. This is part of the prompt contract because providers
/// need it to replay server tool calls on a later turn.
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_executed: Option<bool>,
/// Provider-assigned thought signature (e.g. Google Gemini
/// `thoughtSignature`). Must be echoed back verbatim on the follow-up
/// turn when the tool result is sent.
Expand Down Expand Up @@ -161,6 +166,7 @@ impl ContentPart {
tool_call_id: tool_call_id.into(),
tool_name: tool_name.into(),
input,
provider_executed: None,
thought_signature: None,
provider_options: None,
}
Expand Down
39 changes: 37 additions & 2 deletions aimux-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,36 @@ pub enum AiMuxError {
#[error("invalid response data: {0}")]
InvalidResponseData(String),

#[error("tool error: {0}")]
Tool(String),
/// A model requested a tool that was not present in the call's tool set.
/// Message templates for the three tool variants match the AI SDK verbatim
/// (`NoSuchToolError` / `InvalidToolInputError` / `ToolCallRepairError`):
/// the AI SDK feeds these strings back to the model as tool-error content,
/// so the wording — including the available-tools list — is contract.
#[error("Model tried to call unavailable tool '{tool_name}'. {}", no_such_tool_availability(.available_tools))]
NoSuchTool {
tool_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
available_tools: Option<Vec<String>>,
/// Original argument text, when supplied by the provider. Absent in
/// older serialized errors and errors constructed without a call.
#[serde(default, skip_serializing_if = "Option::is_none")]
tool_input: Option<String>,
},

/// A tool call could not be parsed or did not satisfy its input schema.
#[error("Invalid input for tool {tool_name}: {cause}")]
InvalidToolInput {
tool_name: String,
tool_input: String,
cause: String,
},

/// The repair callback failed or returned a call that was still invalid.
#[error("Error repairing tool call: {cause}")]
ToolCallRepair {
original_error: Box<AiMuxError>,
cause: Box<AiMuxError>,
},

#[error("invalid argument: {0}")]
InvalidArgument(String),
Expand Down Expand Up @@ -228,6 +256,13 @@ pub enum AiMuxError {
Other(String),
}

fn no_such_tool_availability(available_tools: &Option<Vec<String>>) -> String {
match available_tools {
Some(tools) => format!("Available tools: {}.", tools.join(", ")),
None => "No tools are available.".to_string(),
}
}

/// Canonical serde classification: syntactically broken/truncated JSON is a
/// parse failure; well-formed JSON that violates the expected shape is invalid
/// response data. `Io` has no transport context here, so it stays `JsonParse`;
Expand Down
Loading
Loading