a3s-code-core 8.5.4

A3S Code Core - Embeddable AI agent library with tool execution
Documentation
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;

use crate::tools::{Tool, ToolContext, ToolOutput};

use super::{
    code_intelligence_error, format, invalid_argument, provider, query_capabilities,
    structured_success, unavailable,
};

const DEFAULT_SEARCH_LIMIT: usize = 100;
const MAX_SEARCH_LIMIT: usize = 500;

pub(super) struct CodeSymbolsTool;

fn code_symbols_properties() -> Value {
    serde_json::json!({
        "operation": {
            "type": "string",
            "enum": ["outline", "search"]
        },
        "path": {
            "type": "string",
            "minLength": 1,
            "description": "Saved workspace document path. Required for outline."
        },
        "query": {
            "type": "string",
            "minLength": 1,
            "description": "Semantic symbol query. Required for search."
        },
        "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": MAX_SEARCH_LIMIT,
            "description": "Maximum workspace symbols for search. Default: 100; maximum: 500."
        }
    })
}

fn code_symbols_examples() -> Value {
    serde_json::json!([
        {"operation": "outline", "path": "src/lib.rs"},
        {"operation": "search", "query": "WorkspaceClient", "limit": 50}
    ])
}

/// Model-facing schema without top-level `oneOf`.
///
/// OpenAI-compatible and Anthropic tool validators reject union keywords at the
/// parameters root. Operation-specific required fields remain in
/// [`CodeSymbolsTool::parameters`] for gateway validation and in `execute`.
fn code_symbols_model_parameters() -> Value {
    serde_json::json!({
        "type": "object",
        "additionalProperties": false,
        "properties": code_symbols_properties(),
        "required": ["operation"],
        "examples": code_symbols_examples()
    })
}

#[async_trait]
impl Tool for CodeSymbolsTool {
    fn name(&self) -> &str {
        "code_symbols"
    }

    fn description(&self) -> &str {
        "Query semantic symbols from saved workspace files. Use operation='outline' for a document hierarchy or operation='search' for workspace symbols. Returns symbol metadata and locations, not source code; use read when source text is needed."
    }

    fn parameters(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "additionalProperties": false,
            "properties": code_symbols_properties(),
            "required": ["operation"],
            "oneOf": [
                {
                    "properties": {"operation": {"const": "outline"}},
                    "required": ["path"]
                },
                {
                    "properties": {"operation": {"const": "search"}},
                    "required": ["query"]
                }
            ],
            "examples": code_symbols_examples()
        })
    }

    fn definition(&self) -> crate::llm::ToolDefinition {
        crate::llm::ToolDefinition {
            name: self.name().to_string(),
            description: self.description().to_string(),
            parameters: code_symbols_model_parameters(),
        }
    }

    fn capabilities(&self, _args: &Value) -> crate::tools::ToolCapabilities {
        query_capabilities()
    }

    async fn execute(&self, args: &Value, ctx: &ToolContext) -> Result<ToolOutput> {
        let operation = match args.get("operation").and_then(Value::as_str) {
            Some(operation @ ("outline" | "search")) => operation,
            Some(_) => return Ok(invalid_argument("operation must be 'outline' or 'search'")),
            None => return Ok(invalid_argument("operation parameter is required")),
        };
        let Some(provider) = provider(ctx) else {
            return Ok(unavailable(operation));
        };

        if operation == "outline" {
            let Some(path) = args.get("path").and_then(Value::as_str) else {
                return Ok(invalid_argument("path is required for outline"));
            };
            if path.trim().is_empty() {
                return Ok(invalid_argument("path must not be empty"));
            }
            let path = match ctx.resolve_workspace_path(path) {
                Ok(path) => path,
                Err(error) => {
                    return Ok(invalid_argument(format!(
                        "failed to resolve workspace path: {error}"
                    )))
                }
            };
            match provider
                .document_symbols(&path, ctx.cancellation_token())
                .await
            {
                Ok(result) => Ok(structured_success(format::document_symbols(result))),
                Err(error) => Ok(code_intelligence_error(operation, error)),
            }
        } else {
            let Some(query) = args.get("query").and_then(Value::as_str) else {
                return Ok(invalid_argument("query is required for search"));
            };
            let query = query.trim();
            if query.is_empty() {
                return Ok(invalid_argument("query must not be empty"));
            }
            let limit = match args.get("limit") {
                Some(limit) => match limit.as_u64().and_then(|limit| usize::try_from(limit).ok()) {
                    Some(limit) if (1..=MAX_SEARCH_LIMIT).contains(&limit) => limit,
                    _ => {
                        return Ok(invalid_argument(format!(
                            "limit must be an integer between 1 and {MAX_SEARCH_LIMIT}"
                        )))
                    }
                },
                None => DEFAULT_SEARCH_LIMIT,
            };
            match provider
                .search_symbols(query, limit, ctx.cancellation_token())
                .await
            {
                Ok(result) => Ok(structured_success(format::workspace_symbols(result))),
                Err(error) => Ok(code_intelligence_error(operation, error)),
            }
        }
    }
}