Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }

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

# Proc-macro support
proc-macro2 = "1"
Expand Down
1 change: 1 addition & 0 deletions aimux-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ tracing = { workspace = true }
ts-rs = { workspace = true }
tokio-util = "0.7"
base64 = "0.22"
jsonschema = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
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
35 changes: 33 additions & 2 deletions aimux-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,32 @@ 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>>,
},

/// 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 optional repair callback failed while handling an invalid call.
#[error("Error repairing tool call: {cause}")]
ToolCallRepair {
original_error: Box<AiMuxError>,
cause: Box<AiMuxError>,
},

#[error("invalid argument: {0}")]
InvalidArgument(String),
Expand Down Expand Up @@ -149,6 +173,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