docs(comments): rewrite comments across all crates to the guidelines

Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
This commit is contained in:
2026-07-23 16:55:39 -04:00
parent ff0fb56c67
commit a02b555e66
1458 changed files with 10729 additions and 21750 deletions
-10
View File
@@ -39,14 +39,12 @@ impl Extensions {
Self::default()
}
/// Retrieve a reference to a stored value by type.
pub fn get<T: Any + Send + Sync + 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|e| e.data.downcast_ref())
}
/// Retrieve a mutable reference to a stored value by type.
pub fn get_mut<T: Any + Send + Sync + 'static>(&mut self) -> Option<&mut T> {
self.map
.get_mut(&TypeId::of::<T>())
@@ -64,7 +62,6 @@ impl Extensions {
);
}
/// Remove and return a value by type.
pub fn remove<T: Any + Send + Sync + 'static>(&mut self) -> Option<T> {
self.map
.remove(&TypeId::of::<T>())
@@ -72,17 +69,14 @@ impl Extensions {
.map(|b| *b)
}
/// Check if a value of the given type is stored.
pub fn contains<T: Any + Send + Sync + 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>())
}
/// Number of stored entries.
pub fn len(&self) -> usize {
self.map.len()
}
/// Returns true if no entries are stored.
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
@@ -105,10 +99,6 @@ impl fmt::Debug for Extensions {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
@@ -143,9 +143,7 @@ pub fn parse_arguments_from_schema_lossy(schema: &serde_json::Value) -> Vec<Tool
.collect()
}
// ---------------------------------------------------------------------------
// $ref / $defs / anyOf / oneOf resolution
// ---------------------------------------------------------------------------
/// Resolve type info from a property, following `$ref` → `$defs` and
/// `anyOf` patterns that schemars generates for Rust enums and
@@ -263,7 +261,6 @@ fn extract_enum_from_def(
(Some(arg_type), Some(values), first_value)
}
/// Infer the [`ArgumentType`] from a sample enum value.
fn infer_arg_type(sample: &Option<Value>) -> ArgumentType {
match sample {
Some(Value::String(_)) => ArgumentType::String,
@@ -274,10 +271,6 @@ fn infer_arg_type(sample: &Option<Value>) -> ArgumentType {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
@@ -464,8 +457,6 @@ mod tests {
assert!(args[0].arg_type.contains(ArgumentType::Integer));
}
// -- $ref / $defs / anyOf resolution ----------------------------------------
#[test]
fn parse_schema_any_of_ref_resolves_enum() {
// schemars pattern for `Option<MyEnum>` with oneOf-style defs.
@@ -593,7 +584,7 @@ mod tests {
#[test]
fn parse_schema_no_defs_still_works() {
// Properties with no $ref/$defs should work exactly as before.
// Properties with no $ref/$defs still parse normally.
let schema = serde_json::json!({
"type": "object",
"properties": {
@@ -611,8 +602,6 @@ mod tests {
assert_eq!(args[0].default, Some(serde_json::json!("x")));
}
// -- numeric constraints --------------------------------------------------
#[test]
fn parse_schema_numeric_constraints() {
let schema = serde_json::json!({
@@ -1,18 +1,14 @@
//! Lenient deserializers for tool-argument booleans: a boolean may arrive as a
//! JSON string (`"true"`) or number (`1`) when a client doesn't coerce args
//! against the tool schema. Accepted forms (strings case-insensitive, trimmed;
//! `null` is `false`):
//!
//! | Truthy | Falsy |
//! |---------------------------------------|------------------------------------------------|
//! | `true`, `"true"`, `"yes"`, `"1"`, `1` | `false`, `"false"`, `"no"`, `"0"`, `0`, `null` |
//! against the tool schema. Strings are trimmed and matched case-insensitively,
//! and `null` reads as `false`.
use serde::Deserialize;
const TRUE_LITERALS: [&str; 3] = ["true", "yes", "1"];
const FALSE_LITERALS: [&str; 3] = ["false", "no", "0"];
/// Parse a JSON value into a `bool` per the accepted forms; `None` otherwise.
/// `None` when the value matches none of the accepted forms.
pub fn lenient_bool_from_json(value: &serde_json::Value) -> Option<bool> {
match value {
serde_json::Value::Bool(b) => Some(*b),
@@ -48,8 +44,8 @@ fn invalid_bool_message(value: &serde_json::Value) -> String {
)
}
/// Deserialize a required `bool`; pair with `#[serde(default)]` so an absent key
/// uses the field default.
/// Pair with `#[serde(default)]` so an absent key falls back to the field
/// default instead of failing.
pub fn deserialize_lenient_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
@@ -59,8 +55,8 @@ where
.ok_or_else(|| serde::de::Error::custom(invalid_bool_message(&value)))
}
/// Deserialize `Option<bool>`: absent key `None` (via `#[serde(default)]`),
/// explicit `null` `Some(false)`.
/// With `#[serde(default)]` an absent key yields `None`, while an explicit
/// `null` yields `Some(false)`.
pub fn deserialize_lenient_option_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: serde::Deserializer<'de>,
+6 -67
View File
@@ -4,15 +4,12 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// ───────────────────────────────────────────────────────────────────────────
// `task` (spawn) tool — Input
// ───────────────────────────────────────────────────────────────────────────
/// Input for the `task` tool — launches a subagent to handle a task
/// autonomously.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TaskToolInput {
/// The full task prompt for the subagent to execute.
#[schemars(description = "The full task prompt for the subagent to execute.")]
pub prompt: String,
@@ -28,10 +25,8 @@ pub struct TaskToolInput {
#[serde(default = "default_subagent_type")]
pub subagent_type: String,
/// Whether to run the subagent in the background.
///
/// Returns immediately with a subagent_id. Use the task output tool to
/// retrieve results. This is set to true by default.
/// retrieve results. Defaults to true.
#[schemars(
description = "Returns immediately with a subagent_id. Use the task output tool to \
retrieve results. This is set to true by default."
@@ -42,7 +37,6 @@ pub struct TaskToolInput {
)]
pub run_in_background: bool,
/// Capability mode controlling the child's tool access.
#[schemars(
description = "Capability mode: \"read-only\", \"read-write\", \"execute\", or \"all\". \
Controls which tool classes the child can use. Default is determined by the role."
@@ -50,7 +44,6 @@ pub struct TaskToolInput {
#[serde(default)]
pub capability_mode: Option<SubagentCapabilityMode>,
/// Isolation mode for the child's execution environment.
#[schemars(
description = "Isolation mode: \"none\" (default, shared workspace) or \"worktree\" \
(isolated git worktree). Worktree mode prevents the child's edits from \
@@ -93,7 +86,6 @@ pub struct TaskToolInput {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
/// Optional model slug for this subagent.
#[schemars(
description = "Optional model slug for this agent. If provided, it must resolve to one \
of the available model slugs. If omitted, the subagent uses the same model as the \
@@ -109,7 +101,6 @@ pub struct TaskToolInput {
pub task_id: Option<String>,
}
/// Default `subagent_type` for [`TaskToolInput`] when the caller omits it.
pub fn default_subagent_type() -> String {
"general-purpose".to_string()
}
@@ -180,7 +171,6 @@ impl SubagentCapabilityMode {
}
}
/// Isolation mode for subagent execution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum SubagentIsolationMode {
@@ -201,9 +191,7 @@ impl SubagentIsolationMode {
}
}
// ───────────────────────────────────────────────────────────────────────────
// `task` (spawn) tool — Output
// ───────────────────────────────────────────────────────────────────────────
/// Structured completion output from a subagent (`task` tool).
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -215,7 +203,6 @@ pub struct SubagentCompletedOutput {
pub turns: u32,
pub duration_ms: u64,
pub worktree_path: Option<String>,
/// Persona used by this subagent, if any.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persona: Option<String>,
/// The `subagent_id` to pass as `resume_from` to continue this subagent.
@@ -228,7 +215,6 @@ pub struct SubagentCompletedOutput {
}
impl SubagentCompletedOutput {
/// Render the resume footer showing the subagent ID and resume hint.
pub fn resume_footer(&self) -> String {
format_resume_footer(
&self.subagent_id,
@@ -317,10 +303,8 @@ pub fn format_resume_footer(
/// fan-out, and the toolbox wait path so the cap cannot drift.
pub const MAX_MULTI_WAIT_IDS: usize = 20;
/// Input for the `get_task_output` tool.
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
pub struct TaskOutputToolInput {
/// Task IDs to query. Pass one or more; a single task is a one-element list.
#[schemars(
description = "Task IDs to get output from. Pass one or more; for a single task use a one-element array. With a positive timeout_ms, multiple ids wait until all complete. Omit timeout_ms or pass 0 for a non-blocking snapshot."
)]
@@ -351,12 +335,10 @@ pub fn resolve_task_ids(ids: &[String]) -> Vec<String> {
}
impl TaskOutputToolInput {
/// Resolved, de-duplicated task IDs preserving first-seen order.
pub fn resolved_task_ids(&self) -> Vec<String> {
resolve_task_ids(&self.task_ids)
}
/// True only when `timeout_ms` is set and greater than zero.
pub fn waits(&self) -> bool {
task_output_waits(self.timeout_ms)
}
@@ -378,7 +360,6 @@ pub fn task_output_waits_from_json(args: &serde_json::Value) -> bool {
task_output_waits(timeout_ms)
}
/// Output from the `get_task_output` tool.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum TaskOutputOutput {
Result(TaskOutputResult),
@@ -386,18 +367,16 @@ pub enum TaskOutputOutput {
MultiResult(MultiTaskOutputResult),
}
/// Successful result from the `get_task_output` tool.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct TaskOutputResult {
pub task_id: String,
pub command: String,
pub status: String,
pub exit_code: Option<i32>,
/// Wall-clock start time (ISO 8601 format)
/// ISO 8601 wall-clock start time.
pub started: String,
/// Wall-clock end time if completed (ISO 8601 format)
/// ISO 8601 wall-clock end time when completed.
pub ended: Option<String>,
/// Duration in seconds
pub duration_secs: f64,
pub output: String,
pub output_file: String,
@@ -428,7 +407,6 @@ impl TaskOutputOutput {
}
}
/// Result from a multi-wait `get_task_output` / `wait_tasks` call.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MultiTaskOutputResult {
pub mode: String,
@@ -468,11 +446,8 @@ impl TaskOutputResult {
}
}
// ───────────────────────────────────────────────────────────────────────────
// `wait_tasks` tool — Input
// ───────────────────────────────────────────────────────────────────────────
/// How a multi-wait (`wait_tasks`) request should resolve.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WaitMode {
@@ -497,9 +472,7 @@ pub struct WaitTasksToolInput {
pub timeout_ms: Option<u64>,
}
// ───────────────────────────────────────────────────────────────────────────
// `kill_task` (cancel) tool — Input / Output
// ───────────────────────────────────────────────────────────────────────────
// `kill_task` tool — Input / Output
/// Input for the `kill_task` tool — terminates a running background task,
/// monitor, or subagent by id.
@@ -509,14 +482,12 @@ pub struct KillTaskToolInput {
pub task_id: String,
}
/// Output from the `kill_task` tool.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum KillTaskOutput {
Result(KillTaskResult),
TaskNotFound(String),
}
/// Successful result from the `kill_task` tool.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct KillTaskResult {
pub task_id: String,
@@ -538,7 +509,6 @@ impl KillTaskOutput {
pub struct SubagentDescriptor {
/// `subagent_type` value the model passes to the `task` tool.
pub name: String,
/// One-line summary of what this subagent does.
pub description: String,
/// Optional fragment summarizing the tools the subagent can use. Appended
/// verbatim after the description; may itself contain product-specific
@@ -572,8 +542,6 @@ impl BuiltinSubagent {
})
}
/// Build a [`SubagentDescriptor`], rendering the tool-access fragment via
/// [`Self::render_tools`] with the supplied `naming`.
pub fn to_descriptor(&self, naming: &SubagentToolNaming) -> SubagentDescriptor {
SubagentDescriptor {
name: self.name.to_owned(),
@@ -679,9 +647,6 @@ fn substitute_tool_placeholders(
}
/// Prompt body for the **general-purpose** subagent.
///
/// This agent has access to all tools and is used for complex search,
/// code exploration, and multi-step research tasks.
pub const GENERAL_PURPOSE_PROMPT: &str = "\
Complete the assigned task directly. Do what was asked; nothing more, nothing less. \
Respond with a detailed writeup when done.
@@ -708,8 +673,6 @@ Workspace boundary:
- Do not run whole-filesystem searches unless the user clearly requires it.";
/// Prompt body for the **explore** subagent.
///
/// A fast, read-only agent specialized for codebase exploration.
pub const EXPLORE_PROMPT: &str = "\
You are a fast, read-only codebase exploration agent.
@@ -737,9 +700,6 @@ Workspace boundary:
- If not found in the workspace, report that rather than broadening scope.";
/// Prompt body for the **plan** subagent.
///
/// A read-only architect agent that explores the codebase and produces
/// implementation plans.
pub const PLAN_PROMPT: &str = "\
You are a read-only software architect. Explore the codebase and design implementation plans.
@@ -771,7 +731,6 @@ Workspace boundary:
- Your default analysis scope is the workspace in <user_info>. Stay within it unless asked otherwise.
- Note explicitly if the design requires understanding external dependencies.";
/// The **general-purpose** built-in subagent.
pub const GENERAL_PURPOSE_SUBAGENT: BuiltinSubagent = BuiltinSubagent {
name: "general-purpose",
description: "General purpose agent for multi-step tasks.",
@@ -782,7 +741,6 @@ pub const GENERAL_PURPOSE_SUBAGENT: BuiltinSubagent = BuiltinSubagent {
prompt_template: GENERAL_PURPOSE_PROMPT,
};
/// The **explore** built-in subagent.
pub const EXPLORE_SUBAGENT: BuiltinSubagent = BuiltinSubagent {
name: "explore",
description: "Fast, read-only agent specialized for codebase exploration.",
@@ -792,7 +750,6 @@ pub const EXPLORE_SUBAGENT: BuiltinSubagent = BuiltinSubagent {
prompt_template: EXPLORE_PROMPT,
};
/// The **plan** built-in subagent.
pub const PLAN_SUBAGENT: BuiltinSubagent = BuiltinSubagent {
name: "plan",
description: "Software architect for planning implementation strategies.",
@@ -817,18 +774,11 @@ pub fn builtin_subagent_by_name(name: &str) -> Option<&'static BuiltinSubagent>
/// rendering the shared `task` tool description.
#[derive(Clone, Copy, Debug)]
pub struct TaskToolNaming<'a> {
/// Name of the spawn tool (canonical: `task`).
pub task_tool: &'a str,
/// Name of the `subagent_type` parameter.
pub subagent_type_param: &'a str,
/// Name of the `run_in_background` parameter.
pub run_in_background_param: &'a str,
/// Name of the `resume_from` parameter.
pub resume_from_param: &'a str,
/// Name of the task result retrieval tool.
pub background_retrieval_tool: &'a str,
/// Name of the `isolation` parameter, used in the isolation/worktree
/// paragraph.
pub isolation_param: &'a str,
}
@@ -888,7 +838,6 @@ fn lifecycle_target_suffix(monitor_present: bool, subagent_present: bool) -> &'s
}
}
/// Optional "(a monitor's task_id is returned by {monitor})" clause.
fn monitor_task_id_note(monitor_tool: Option<&str>) -> String {
match monitor_tool {
Some(m) => format!(" (a monitor's task_id is returned by {m})"),
@@ -909,7 +858,6 @@ pub struct KillTaskToolNaming<'a> {
pub is_windows: bool,
}
/// Build the shared `kill_task` tool description.
pub fn build_kill_task_description(naming: &KillTaskToolNaming) -> String {
let KillTaskToolNaming {
monitor_tool,
@@ -966,7 +914,6 @@ pub struct TaskOutputToolNaming<'a> {
pub subagent_background_param: Option<&'a str>,
}
/// Build the shared `get_task_output` tool description.
pub fn build_task_output_description(naming: &TaskOutputToolNaming) -> String {
let TaskOutputToolNaming {
monitor_tool,
@@ -1014,7 +961,6 @@ pub struct WaitTasksToolNaming<'a> {
pub subagent_background_param: Option<&'a str>,
}
/// Build the shared `wait_tasks` tool description.
pub fn build_wait_tasks_description(naming: &WaitTasksToolNaming) -> String {
let WaitTasksToolNaming {
background_retrieval_tool,
@@ -1174,7 +1120,6 @@ mod tests {
assert!(
desc.contains("- **general-purpose**: General-purpose agent. Has access to all tools.")
);
// User-defined entries (tools = None) get no trailing fragment.
assert!(desc.contains("- **code-reviewer**: Reviews code."));
assert!(desc.contains("## Usage notes"));
assert!(desc.contains(
@@ -1317,7 +1262,6 @@ mod tests {
#[test]
fn render_tools_substitutes_naming_with_bare_kind_fallback() {
// Bare-kind naming reproduces the placeholder kinds verbatim.
assert_eq!(
GENERAL_PURPOSE_SUBAGENT.render_tools(&plain_tool_naming()),
"Has access to all tools: execute, read, edit, list, search, web_search, and plan."
@@ -1328,7 +1272,6 @@ mod tests {
read, list, search, web_search, and plan."
);
// Real tool names are substituted per kind.
let naming = SubagentToolNaming {
execute: "run_terminal_cmd",
read: "read_file",
@@ -1372,12 +1315,8 @@ mod tests {
assert!(desc.contains("Use ${{ params.task.isolation }} to control"));
}
// ── Lifecycle tool descriptions ──────────────────────────────────────
//
// These lock the exact model-facing text. The "cli_default" cases must
// match what the kigi-shell MiniJinja templates render for the default
// kigi toolset (monitor + task + bash + read present, POSIX). The
// "toolbox" cases lock the subagent-only rendering used by the backend toolbox.
// Lifecycle tool descriptions — lock model-facing text against kigi-shell
// MiniJinja defaults (cli_default) and backend toolbox (subagent-only).
#[test]
fn kill_task_matches_cli_default_posix() {
+6 -50
View File
@@ -8,39 +8,28 @@ use crate::ext::Extensions;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolDescription {
/// Tool name (e.g. "web_search", "read_file") that is called by
/// the model.
pub name: String,
/// Optional namespace grouping (e.g. "github", "slack").
/// None for xAI native tools.
/// Namespace grouping (e.g. "github", "slack"). `None` for native tools.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
/// Display name (e.g. "Web Search") can be shown to the model.
/// If absent, derive the title from 'name'.
/// Human display title; when absent, derive from `name`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Description of the tool.
pub description: String,
/// Raw JSON Schema describing the tool's arguments.
/// JSON Schema for tool arguments.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments_schema: Option<Value>,
/// High-level tool kind (stable snake_case, e.g. "read"), set by the tool
/// server so consumers can group tools by kind. `None` if undeclared.
/// Stable snake_case kind (e.g. "read") for grouping; `None` if undeclared.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
/// Metadata attached by downstream libraries to support
/// custom tool behavior. NOT serialized and NOT sent over
/// the wire.
///
/// Note: 'Extensions' always compares as equal (it carries opaque
/// runtime data), so 'ToolDescription's derived 'PartialEq' ignores
/// this field. See 'Extensions' for details.
/// Opaque runtime metadata — not serialized, not on the wire.
/// `Extensions` always compares equal, so derived `PartialEq` ignores this.
#[serde(skip)]
pub extra: Extensions,
}
@@ -63,7 +52,6 @@ impl ToolDescription {
self
}
/// Set the high-level tool kind (snake_case string, e.g. "read").
pub fn with_kind(mut self, kind: impl Into<String>) -> Self {
self.kind = Some(kind.into());
self
@@ -74,7 +62,6 @@ impl ToolDescription {
self
}
/// Attach the raw JSON Schema for this tool's arguments.
pub fn with_arguments_schema(mut self, schema: impl Into<Value>) -> Self {
self.arguments_schema = Some(schema.into());
self
@@ -94,8 +81,6 @@ impl ToolDescription {
.unwrap_or_default()
}
/// Returns the raw JSON Schema for the tool's arguments if one was
/// attached via [`Self::with_arguments_schema`].
pub fn arguments_schema(&self) -> Option<&Value> {
self.arguments_schema.as_ref()
}
@@ -160,14 +145,11 @@ impl fmt::Display for ToolDescription {
}
}
/// A single argument for a tool.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolArgument {
/// Argument name (e.g. "file_path").
pub name: String,
/// Human-readable description of the argument.
pub description: String,
/// Type of the argument. Accepts both a single JSON Schema type
@@ -179,12 +161,10 @@ pub struct ToolArgument {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<serde_json::Value>,
/// Whether the argument is required.
/// Defaults to true. Omitted from JSON when true.
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub required: bool,
/// Default value for the argument.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<Value>,
@@ -241,7 +221,6 @@ impl ToolArgument {
self
}
/// Set a default value for this argument.
pub fn with_default(mut self, default: impl Into<Value>) -> Self {
self.default = Some(default.into());
self
@@ -276,7 +255,6 @@ impl ToolArgument {
}
}
/// Type of a tool argument.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ArgumentType {
@@ -347,15 +325,11 @@ impl fmt::Display for ArgumentType {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SchemaType {
/// A single type, e.g. "string".
Single(ArgumentType),
/// Multiple types, e.g. ["string", "null"].
Multiple(Vec<ArgumentType>),
}
impl SchemaType {
/// Parse a JSON Schema "type" value (string or array) into a
/// `SchemaType`.
pub fn from_value(v: &serde_json::Value) -> Self {
if let Some(s) = v.as_str() {
return ArgumentType::from_schema_type(s)
@@ -404,7 +378,6 @@ impl SchemaType {
}
}
/// Whether the type list contains a specific `ArgumentType`.
pub fn contains(&self, ty: ArgumentType) -> bool {
match self {
Self::Single(t) => *t == ty,
@@ -442,7 +415,6 @@ impl SchemaType {
}
}
/// Return the JSON Schema `"type"` representation.
pub fn to_schema_value(&self) -> serde_json::Value {
match self {
Self::Single(t) => serde_json::Value::String(t.as_str().to_owned()),
@@ -500,7 +472,6 @@ fn is_true(v: &bool) -> bool {
*v
}
// -- Helpers
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub field: String,
@@ -515,22 +486,18 @@ impl fmt::Display for ValidationError {
impl std::error::Error for ValidationError {}
/// Wrapper around multiple ValidationError.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationErrors(pub Vec<ValidationError>);
impl ValidationErrors {
/// Iterate over the individual errors.
pub fn iter(&self) -> std::slice::Iter<'_, ValidationError> {
self.0.iter()
}
/// Number of validation errors.
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns true if there are no errors.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
@@ -588,10 +555,6 @@ fn validate_identifier(field: &str, value: &str, errors: &mut Vec<ValidationErro
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
@@ -644,8 +607,6 @@ mod tests {
assert!(!ArgumentType::Null.is_composite());
}
// -- SchemaType -----------------------------------------------------------
#[test]
fn schema_type_single_serde_roundtrip() {
let st = SchemaType::Single(ArgumentType::String);
@@ -752,17 +713,14 @@ mod tests {
#[test]
fn schema_type_primitive_composite_multiple() {
// All primitive → is_primitive=true, is_composite=false
let nullable_string = SchemaType::Multiple(vec![ArgumentType::String, ArgumentType::Null]);
assert!(nullable_string.is_primitive());
assert!(!nullable_string.is_composite());
// Any composite → is_primitive=false, is_composite=true
let nullable_array = SchemaType::Multiple(vec![ArgumentType::Array, ArgumentType::Null]);
assert!(!nullable_array.is_primitive());
assert!(nullable_array.is_composite());
// Mixed primitive + composite
let mixed = SchemaType::Multiple(vec![ArgumentType::String, ArgumentType::Object]);
assert!(!mixed.is_primitive());
assert!(mixed.is_composite());
@@ -915,7 +873,6 @@ mod tests {
assert_eq!(tool.to_input_schema(), raw);
}
/// Without a raw schema, `to_input_schema` returns an empty object schema.
#[test]
fn description_to_input_schema_empty_when_no_raw() {
let tool = ToolDescription::new("echo", "Echo a string");
@@ -1042,7 +999,6 @@ mod tests {
tool.namespace = Some(String::new());
let errors = tool.validate().unwrap_err();
// empty tool name + empty namespace = 2
assert!(errors.len() >= 2);
}
}