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:
@@ -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>,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user