Skip to main content

Documentation Index

Fetch the complete documentation index at: https://cognis.vasanth.xyz/llms.txt

Use this file to discover all available pages before exploring further.

Tools are how an agent reaches outside the chat — searching the web, querying a database, running calculations, calling your domain functions. In Cognis, every tool implements the Tool trait. There are three idiomatic ways to write one, each more convenient than the last when you can use it.

What a tool is

#[async_trait::async_trait]
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn args_schema(&self) -> Option<serde_json::Value>;
    fn return_direct(&self) -> bool { false }
    async fn _run(&self, input: ToolInput) -> Result<ToolOutput>;
}
Four facts and a method:
  • name — what the model will type to call you.
  • description — what the model reads to decide when to call you. Make it good.
  • args_schema — JSON Schema for the arguments. The provider sends this to the model.
  • return_direct — when true, the tool’s output becomes the final response (skips the next model turn).
  • _run — the body. Receives ToolInput, returns ToolOutput.

When to reach for which option

You have…Use
A simple async function with typed inputs#[cognis::tool] macro
A struct with config (HTTP client, DB pool) and a typed inputSchemaBasedTool trait
Anything more customHand-written impl Tool

Quick example — typed SchemaBasedTool

The most common shape: typed Params, derive JsonSchema, implement one method.
use async_trait::async_trait;
use cognis::prelude::*;
use cognis_core::schemars::{self, JsonSchema};
use cognis_llm::tools::{SchemaBasedTool, Tool};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct WeatherArgs {
    /// City to look up.
    city: String,
    /// Either "c" or "f" — defaults to "c".
    #[serde(default)]
    unit: Option<String>,
}

struct WeatherTool;

#[async_trait]
impl SchemaBasedTool for WeatherTool {
    type Params = WeatherArgs;
    type Output = Value;

    fn name(&self) -> &str { "get_weather" }
    fn description(&self) -> &str { "Fetch current weather for a city." }

    async fn execute_typed(&self, args: WeatherArgs) -> Result<Value> {
        let unit = args.unit.unwrap_or_else(|| "c".into());
        Ok(json!({"city": args.city, "temp": 21, "unit": unit}))
    }
}
SchemaBasedTool provides a blanket Tool impl that handles serde, validation, and the JSON Schema for you.

Quick example — hand-written Tool

When you need control over the schema or want to handle raw ToolInput:
use std::sync::Arc;
use async_trait::async_trait;
use cognis::prelude::*;
use cognis_core::schemars::{self, schema_for, JsonSchema};
use cognis_llm::tools::{Tool, ToolInput, ToolOutput};
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchArgs {
    /// The search query.
    query: String,
    /// Max results (default 5).
    #[serde(default)]
    limit: Option<u32>,
}

struct SearchTool;

#[async_trait]
impl Tool for SearchTool {
    fn name(&self) -> &str { "search" }
    fn description(&self) -> &str { "Search a knowledge base." }
    fn args_schema(&self) -> Option<serde_json::Value> {
        Some(serde_json::to_value(schema_for!(SearchArgs)).unwrap())
    }
    async fn _run(&self, input: ToolInput) -> Result<ToolOutput> {
        let args: SearchArgs = serde_json::from_value(input.into_json())?;
        Ok(ToolOutput::Content(json!({
            "results": [{"title": format!("Result for {}", args.query)}]
        })))
    }
}
Source: examples/tools/derive_tool.rs.

Wiring tools into an agent

Tools are Arc<dyn Tool>. The builder accepts one or many.
use std::sync::Arc;
use cognis::{AgentBuilder, Calculator};

let mut agent = AgentBuilder::new()
    .with_llm(client)
    .with_tool(Arc::new(Calculator::new()))
    .with_tool(Arc::new(SearchTool))
    .with_system_prompt("Use tools when helpful.")
    .build()?;
You can also gate which tools the model is allowed to call at runtime via middleware (ToolAllowList, ToolDenyList).

Validators

When using #[cognis::tool] or SchemaBasedTool, you can declare per-field validators that emit both runtime checks and JSON Schema constraints:
AttributeEffect
#[schema(range(min = 0, max = 100))]Numeric bounds.
#[schema(length(min = 1, max = 200))]String / Vec length.
#[schema(pattern("^[A-Z][a-z]+$"))]Regex.
#[schema(enum_values("low", "medium", "high"))]Enum.
#[schema(format("email"))]JSON Schema format.
These ride on the args struct fields — the model sees them in the schema, and your tool receives validated input.

Built-in tools

Cognis ships a few generally useful tools under cognis::tools::*:
  • Calculator — pure-Rust expression evaluator (+ - * / % ^, parens, unary -). No code execution.
  • FilesystemTool family — read/write/edit/list a workspace, gated by a Backend.
  • HttpRequest (feature tools-http) — make HTTP calls with allow-listed hosts.
  • JsonQuery — JMESPath-like query over JSON.
  • PythonRepl — sandboxed Python execution (where the runtime is available).
  • OpenApiTool — generate tools from an OpenAPI spec.
  • HumanTool — ask a human a question, return their reply.

ToolOrchestrator (no-loop DAG)

When you need to call a fixed set of tools in a known DAG — not an agent loop — use ToolOrchestrator:
use cognis::{ExecutionPlan, ToolOrchestrator, ToolStep};
use cognis_llm::tools::ToolInput;

let orch = ToolOrchestrator::new()
    .register(fetch_a)
    .register(fetch_b)
    .register(merge)
    .with_max_concurrency(4);

let plan = ExecutionPlan::new()
    .step(ToolStep::new("a", "fetch_a", ToolInput::Text("doc-1".into())))
    .step(ToolStep::new("b", "fetch_b", ToolInput::Text("doc-2".into())))
    .step(ToolStep::new("m", "merge", ToolInput::Text("combine".into())).after(["a", "b"]));

let result = orch.run(plan).await?;
Topo-sorts the DAG, runs independent steps concurrently, and skips downstream steps whose ancestors errored.

How it works

  • The model never sees Rust types directly. It sees the JSON Schema you produce. Make descriptions and field docs clear — that’s the model’s only signal.
  • Tool dispatch is a node in the agent graph. When the model emits tool calls, the dispatcher node parses arguments, runs the tool, and threads the result back as a Message::tool entry tied to the call id.
  • Errors are tool-shaped. Returning an error from _run becomes a ToolMessage with the error text — the agent sees it and can recover or apologize.

See also

Models and providers

The other half of the agent — the LLM that picks the tool.

Human-in-the-loop

Require approval before sensitive tools run.

Middleware

ToolAllowList, ToolDenyList, ToolRetry, ToolEmulator.

Patterns → HITL approval

A worked tool-approval flow.