Skip to main content

Tools & Function Calling

Tools let agents call external functions during conversation. Chronos integrates with OpenAI-style function calling: the model requests tool invocations, the agent executes them, and results are sent back for the model to incorporate into its response.

tool.Definition

Each tool is defined by a tool.Definition:

FieldTypeDescription
NamestringUnique tool identifier (used by the model)
DescriptionstringDescription for the model to decide when to call
Parametersmap[string]anyJSON Schema for arguments
PermissionPermissionExecution policy: allow, require_approval, or deny
HandlerHandlerfunc(ctx context.Context, args map[string]any) (any, error)

Example:

&tool.Definition{
Name: "get_weather",
Description: "Get the current weather for a location",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name, e.g. San Francisco",
},
"unit": map[string]any{
"type": "string",
"enum": []string{"celsius", "fahrenheit"},
"description": "Temperature unit",
},
},
"required": []string{"location"},
},
Permission: tool.PermAllow,
Handler: func(ctx context.Context, args map[string]any) (any, error) {
location, _ := args["location"].(string)
unit, _ := args["unit"].(string)
if unit == "" {
unit = "celsius"
}
// Call weather API, return result
return map[string]any{
"location": location,
"temp": 22,
"unit": unit,
}, nil
},
}

Permissions

PermissionValueBehavior
PermAllow"allow"Auto-approved; executes immediately
PermRequireApproval"require_approval"Blocks until ApprovalFunc returns true
PermDeny"deny"Always blocked

YAML permissions

YAML-defined tools can override a built-in's default permission:

agents:
- id: developer
name: Developer
permission_mode: prompt
tools:
- name: file_read
permission: allow
- name: file_write
permission: require_approval
- name: shell
permission: deny

permission_mode controls approval-gated tools for that agent:

ModeBehavior
promptInvoke the approval handler (default)
auto_approveSkip approval/confirmation prompts, while still respecting explicit deny
denyReject approval-gated tools without prompting

The optional requires_confirmation and requires_user_input fields override those flags on a tool definition. If a tool both requires approval and confirmation, one successful approval satisfies the confirmation for that call—Chronos does not prompt twice.

tool.Registry

The registry manages tool definitions, permissions, and execution:

MethodDescription
Register(def *Definition)Add a tool
List()Return all registered tools
Execute(ctx, name, args)Run a tool by name (enforces permissions)
SetApprovalHandler(fn ApprovalFunc)Set handler for PermRequireApproval tools
SetPermissionMode(mode)Set prompt, auto_approve, or deny handling for approval-gated tools
PermissionMode()Return the current registry permission mode

Adding Tools via Builder

Register tools when building an agent:

a, err := agent.New("tool-agent", "Tool Agent").
WithModel(provider).
AddTool(&tool.Definition{
Name: "get_weather",
Description: "Get weather for a location",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name",
},
},
"required": []string{"location"},
},
Permission: tool.PermAllow,
Handler: func(ctx context.Context, args map[string]any) (any, error) {
loc, _ := args["location"].(string)
return map[string]any{"location": loc, "temp": 20}, nil
},
}).
Build()

Automatic Tool-Call Loop

In Chat and ChatWithSession, when the model returns StopReason == StopReasonToolCall and ToolCalls is non-empty, the agent:

  1. Appends the assistant message (with tool calls) to the conversation
  2. For each tool call: fires tool_call.before hook, executes the tool, fires tool_call.after hook
  3. Appends each tool result as a tool-role message
  4. Sends the updated conversation back to the model
  5. Repeats until the model returns a final text response

No extra code is required; the loop runs automatically.

Hook Events

Tool execution fires these hook events:

EventWhen
tool_call.beforeBefore each tool execution
tool_call.afterAfter each tool execution

The event Name is the tool name; Input holds the arguments; Output holds the result; Error holds any execution error.

type Event struct {
Type EventType
Name string // tool name
Input any // args
Output any // result
Error error
Metadata map[string]any
}

Approval Flow

For tools with PermRequireApproval, set an approval handler on the registry. The handler blocks until the user approves or denies.

a.Tools.SetApprovalHandler(func(ctx context.Context, toolName string, args map[string]any) (bool, error) {
// In production: show UI, wait for user decision
log.Printf("Approval requested for %s with args %v", toolName, args)
return true, nil // true = approved
})

If no handler is set and a tool requires approval, execution returns an error.

In the Chronos CLI, enter a at a prompt to approve the current call and auto-approve later approval-gated tools for the rest of the session:

Approve? [y/N/a=all for session]: a

You can also choose a process-wide mode:

chronos --permission-mode auto_approve repl
chronos --dangerously-skip-permissions run "perform the trusted task"
chronos --permission-mode deny pipe

The dangerous shortcut never overrides a tool declared with permission: deny. See CLI Runtime Controls for precedence and non-interactive behavior.

Complete Example: Weather Tool

package main

import (
"context"
"encoding/json"
"fmt"
"log"
"os"

"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/engine/tool"
"github.com/spawn08/chronos/sdk/agent"
)

func main() {
ctx := context.Background()

weatherTool := &tool.Definition{
Name: "get_weather",
Description: "Get the current weather for a city. Use this when the user asks about weather.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name, e.g. San Francisco, London",
},
"unit": map[string]any{
"type": "string",
"enum": []string{"celsius", "fahrenheit"},
"description": "Temperature unit (default: celsius)",
},
},
"required": []string{"location"},
},
Permission: tool.PermAllow,
Handler: func(ctx context.Context, args map[string]any) (any, error) {
location, _ := args["location"].(string)
unit, _ := args["unit"].(string)
if unit == "" {
unit = "celsius"
}
// Simulated response
return map[string]any{
"location": location,
"temperature": 22,
"unit": unit,
"condition": "Partly cloudy",
}, nil
},
}

a, err := agent.New("weather-agent", "Weather Agent").
WithModel(model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))).
WithSystemPrompt("You help users with weather information. Use the get_weather tool when asked about weather.").
AddTool(weatherTool).
Build()
if err != nil {
log.Fatal(err)
}

resp, err := a.Chat(ctx, "What's the weather like in Tokyo?")
if err != nil {
log.Fatal(err)
}

fmt.Println(resp.Content)
}

JSON Schema for Parameters

Parameters must follow JSON Schema. The model uses this to generate valid arguments. Minimal schema:

Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"param_name": map[string]any{
"type": "string",
"description": "What this parameter does",
},
},
"required": []string{"param_name"},
}

Supported types: string, number, integer, boolean, array, object. Use enum for fixed choices.