a3s-code-core 5.2.7

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;

#[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": {
                "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."
                }
            },
            "required": ["operation"],
            "oneOf": [
                {
                    "properties": {"operation": {"const": "outline"}},
                    "required": ["path"]
                },
                {
                    "properties": {"operation": {"const": "search"}},
                    "required": ["query"]
                }
            ],
            "examples": [
                {"operation": "outline", "path": "src/lib.rs"},
                {"operation": "search", "query": "WorkspaceClient", "limit": 50}
            ]
        })
    }

    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)),
            }
        }
    }
}