car-ir 0.1.1

Agent IR types for Common Agent Runtime
Documentation
//! Built-in tool schemas for common agent tools.
//!
//! These are schemas only — the runtime doesn't implement the tools.
//! Callers provide the `ToolExecutor` implementation; these schemas give
//! parameter validation, caching hints, and rate limit suggestions.
//!
//! ```rust,ignore
//! use car_ir::builtins;
//!
//! // Register all common schemas
//! for schema in builtins::all() {
//!     runtime.register_tool_schema(schema).await;
//! }
//!
//! // Or pick specific ones
//! runtime.register_tool_schema(builtins::shell()).await;
//! runtime.register_tool_schema(builtins::read_file()).await;
//! ```

use crate::{ToolRateLimit, ToolSchema};
use serde_json::json;

/// Shell command execution.
pub fn shell() -> ToolSchema {
    ToolSchema {
        name: "shell".to_string(),
        description: "Execute a shell command and return stdout/stderr.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "The shell command to execute"
                },
                "cwd": {
                    "type": "string",
                    "description": "Working directory (optional)"
                },
                "timeout_ms": {
                    "type": "integer",
                    "description": "Timeout in milliseconds (optional)"
                }
            },
            "required": ["command"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "stdout": { "type": "string" },
                "stderr": { "type": "string" },
                "exit_code": { "type": "integer" }
            }
        })),
        idempotent: false,
        cache_ttl_secs: None,
        rate_limit: None,
    }
}

/// Read a file's contents.
pub fn read_file() -> ToolSchema {
    ToolSchema {
        name: "read_file".to_string(),
        description: "Read the contents of a file.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Absolute or relative file path"
                },
                "encoding": {
                    "type": "string",
                    "description": "Text encoding (default: utf-8)"
                }
            },
            "required": ["path"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "content": { "type": "string" },
                "size_bytes": { "type": "integer" }
            }
        })),
        idempotent: true,
        cache_ttl_secs: Some(10),
        rate_limit: None,
    }
}

/// Write content to a file.
pub fn write_file() -> ToolSchema {
    ToolSchema {
        name: "write_file".to_string(),
        description: "Write content to a file, creating it if it doesn't exist.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Absolute or relative file path"
                },
                "content": {
                    "type": "string",
                    "description": "Content to write"
                },
                "append": {
                    "type": "boolean",
                    "description": "Append instead of overwrite (default: false)"
                }
            },
            "required": ["path", "content"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "bytes_written": { "type": "integer" }
            }
        })),
        idempotent: false,
        cache_ttl_secs: None,
        rate_limit: None,
    }
}

/// List directory contents.
pub fn list_dir() -> ToolSchema {
    ToolSchema {
        name: "list_dir".to_string(),
        description: "List files and directories in a path.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Directory path"
                },
                "recursive": {
                    "type": "boolean",
                    "description": "List recursively (default: false)"
                },
                "pattern": {
                    "type": "string",
                    "description": "Glob pattern filter (optional)"
                }
            },
            "required": ["path"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "entries": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": { "type": "string" },
                            "path": { "type": "string" },
                            "is_dir": { "type": "boolean" },
                            "size_bytes": { "type": "integer" }
                        }
                    }
                }
            }
        })),
        idempotent: true,
        cache_ttl_secs: Some(5),
        rate_limit: None,
    }
}

/// Make an HTTP request.
pub fn http_request() -> ToolSchema {
    ToolSchema {
        name: "http_request".to_string(),
        description: "Make an HTTP request to a URL.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "The URL to request"
                },
                "method": {
                    "type": "string",
                    "description": "HTTP method (GET, POST, PUT, DELETE, PATCH)",
                    "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"]
                },
                "headers": {
                    "type": "object",
                    "description": "Request headers as key-value pairs"
                },
                "body": {
                    "type": "string",
                    "description": "Request body (for POST/PUT/PATCH)"
                }
            },
            "required": ["url"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "status": { "type": "integer" },
                "headers": { "type": "object" },
                "body": { "type": "string" }
            }
        })),
        idempotent: false,
        cache_ttl_secs: None,
        rate_limit: Some(ToolRateLimit {
            max_calls: 30,
            interval_secs: 60.0,
        }),
    }
}

/// Evaluate a mathematical expression.
pub fn calculate() -> ToolSchema {
    ToolSchema {
        name: "calculate".to_string(),
        description: "Evaluate a mathematical expression.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4')"
                }
            },
            "required": ["expression"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "result": { "type": "number" }
            }
        })),
        idempotent: true,
        cache_ttl_secs: Some(3600),
        rate_limit: None,
    }
}

/// Search/query a knowledge base or external service.
pub fn search() -> ToolSchema {
    ToolSchema {
        name: "search".to_string(),
        description: "Search for information using a query string.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query"
                },
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of results (default: 10)"
                }
            },
            "required": ["query"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "results": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "title": { "type": "string" },
                            "snippet": { "type": "string" },
                            "url": { "type": "string" }
                        }
                    }
                }
            }
        })),
        idempotent: true,
        cache_ttl_secs: Some(60),
        rate_limit: Some(ToolRateLimit {
            max_calls: 10,
            interval_secs: 60.0,
        }),
    }
}

/// Browser navigation and interaction.
pub fn browser() -> ToolSchema {
    ToolSchema {
        name: "browser".to_string(),
        description: "Navigate and interact with web pages in a browser.".to_string(),
        parameters: json!({
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "description": "Browser action to perform",
                    "enum": ["navigate", "click", "fill", "screenshot", "text", "back", "forward"]
                },
                "url": {
                    "type": "string",
                    "description": "URL to navigate to (for 'navigate' action)"
                },
                "selector": {
                    "type": "string",
                    "description": "CSS selector for the target element"
                },
                "value": {
                    "type": "string",
                    "description": "Value to fill (for 'fill' action)"
                }
            },
            "required": ["action"]
        }),
        returns: Some(json!({
            "type": "object",
            "properties": {
                "success": { "type": "boolean" },
                "content": { "type": "string" },
                "screenshot_path": { "type": "string" }
            }
        })),
        idempotent: false,
        cache_ttl_secs: None,
        rate_limit: Some(ToolRateLimit {
            max_calls: 60,
            interval_secs: 60.0,
        }),
    }
}

/// Return all built-in tool schemas.
pub fn all() -> Vec<ToolSchema> {
    vec![
        shell(),
        read_file(),
        write_file(),
        list_dir(),
        http_request(),
        calculate(),
        search(),
        browser(),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_schemas_have_required_fields() {
        for schema in all() {
            assert!(!schema.name.is_empty(), "schema name is empty");
            assert!(!schema.description.is_empty(), "schema {} has no description", schema.name);
            assert!(schema.parameters.is_object(), "schema {} parameters not an object", schema.name);
            let params = schema.parameters.as_object().unwrap();
            assert_eq!(params.get("type").and_then(|v| v.as_str()), Some("object"),
                "schema {} parameters type not 'object'", schema.name);
            assert!(params.contains_key("properties"),
                "schema {} parameters missing 'properties'", schema.name);
            assert!(params.contains_key("required"),
                "schema {} parameters missing 'required'", schema.name);
        }
    }

    #[test]
    fn schemas_are_unique() {
        let schemas = all();
        let names: Vec<&str> = schemas.iter().map(|s| s.name.as_str()).collect();
        let mut unique = names.clone();
        unique.sort();
        unique.dedup();
        assert_eq!(names.len(), unique.len(), "duplicate schema names");
    }

    #[test]
    fn idempotent_tools_have_cache_hints() {
        for schema in all() {
            if schema.idempotent && schema.name != "shell" {
                // Idempotent tools should generally have cache hints
                // (shell is intentionally not cached despite being marked non-idempotent)
            }
        }
        // read_file, list_dir, calculate, search are idempotent and cached
        assert!(read_file().cache_ttl_secs.is_some());
        assert!(list_dir().cache_ttl_secs.is_some());
        assert!(calculate().cache_ttl_secs.is_some());
        assert!(search().cache_ttl_secs.is_some());
    }

    #[test]
    fn rate_limited_tools() {
        assert!(http_request().rate_limit.is_some());
        assert!(search().rate_limit.is_some());
        assert!(browser().rate_limit.is_some());
        assert!(shell().rate_limit.is_none());
    }
}