Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/GuaiZai233/FrostAgent/llms.txt

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

Any Go function can become a tool the FrostAgent engine hands to the LLM. At each reasoning iteration, the engine passes all registered tool definitions to the model, which decides autonomously whether and when to invoke one. The result is fed back into the conversation and the loop continues until the model produces a final text response or the iteration limit is reached.

Tool anatomy

Every tool is a value of the tools.Tool struct defined in internal/tools/tools.go:
type Tool struct {
    name        string
    description string
    parameter   any                      // JSON Schema passed to the LLM
    execute     func(args string) (string, error)
}
FieldPurpose
nameUnique identifier the LLM uses when requesting a call
descriptionNatural-language explanation of what the tool does — the LLM reads this to decide when to call it
parameterA map[string]any encoding a JSON Schema object that describes the expected arguments
executeThe Go function that runs when the LLM calls the tool; receives JSON-encoded arguments, returns a plain-text result
The struct exposes four public methods — Name(), Description(), Parameters(), and Execute() — which satisfy the llm.ToolExecutor interface used by the engine’s run loop.

Step-by-step: building a custom tool

1

Define the execute function

Write a function that accepts a JSON string of arguments and returns a string result. Parse the JSON into a typed struct and validate the inputs before doing any work.The built-in weather tool (internal/tools/weather.go) follows exactly this pattern:
execute: func(args string) (string, error) {
    var params struct {
        City string `json:"city"`
    }
    if err := json.Unmarshal([]byte(args), &params); err != nil {
        return "", fmt.Errorf("failed to parse arguments: %w", err)
    }
    if params.City == "" {
        return "", fmt.Errorf("city cannot be empty")
    }
    // perform the actual work
    return fmt.Sprintf("Weather in %s: sunny, 25°C", params.City), nil
}
2

Return a Tool from a constructor function

Wrap the execute closure inside a Tool struct and return it from a constructor. This is the pattern used across all built-in tools (GetWeatherTool, GetGameVersionTool, SendMsgTool).
package tools

import (
    "encoding/json"
    "fmt"
    "time"
)

func GetCurrentTimeTool() Tool {
    return Tool{
        name:        "get_current_time",
        description: "Returns the current server time in RFC3339 format for the given timezone.",
        parameter: map[string]any{
            "type": "object",
            "properties": map[string]any{
                "timezone": map[string]any{
                    "type":        "string",
                    "description": "IANA timezone name, e.g. America/New_York or UTC.",
                },
            },
            "required": []string{"timezone"},
        },
        execute: func(args string) (string, error) {
            var params struct {
                Timezone string `json:"timezone"`
            }
            if err := json.Unmarshal([]byte(args), &params); err != nil {
                return "", fmt.Errorf("failed to parse arguments: %w", err)
            }
            if params.Timezone == "" {
                return "", fmt.Errorf("timezone cannot be empty")
            }
            loc, err := time.LoadLocation(params.Timezone)
            if err != nil {
                return "", fmt.Errorf("unknown timezone %q: %w", params.Timezone, err)
            }
            return time.Now().In(loc).Format(time.RFC3339), nil
        },
    }
}
3

Write the JSON Schema parameters

The parameter field must be a map[string]any that follows JSON Schema conventions. The LLM reads this schema to understand what arguments to pass. A minimal single-string parameter looks like:
parameter: map[string]any{
    "type": "object",
    "properties": map[string]any{
        "timezone": map[string]any{
            "type":        "string",
            "description": "IANA timezone name, e.g. UTC or Europe/Paris.",
        },
    },
    "required": []string{"timezone"},
},
The top-level object is always type: object. Individual properties each have their own type and description. List every mandatory field in required.
4

Register the tool in main.go

Open cmd/app/main.go and add your constructor call alongside the existing tool registrations inside the init() function. The engine uses two maps: registry stores the full Tool metadata (for listing to the LLM), and executorMap stores the same values cast to the llm.ToolExecutor interface (for execution).
// existing registrations
weatherTool := tools.GetWeatherTool()
registry[weatherTool.Name()] = weatherTool

// add your new tool
timeTool := tools.GetCurrentTimeTool()
registry[timeTool.Name()] = timeTool

// build the executor map after all tools are registered
executorMap := make(map[string]llm.ToolExecutor)
for name, tool := range registry {
    executorMap[name] = tool
}
Because tools.Tool implements every method required by llm.ToolExecutor (Name(), Description(), Parameters(), Execute()), no adapter code is needed.
5

Test the tool

Start FrostAgent and send a message that should trigger your tool. The engine logs each tool invocation and its result — look for log lines prefixed with 【智能体调用工具】 and 【工具执行结果】 to confirm the LLM called your tool with the expected arguments and that the execute function returned the right output.The LLM decides when to call a tool based on the conversation context, the tool description, and the parameter schema. If the tool is not being invoked, revisit the description to make the trigger conditions clearer.

Complete example: get_current_time

Below is the full file ready to drop into internal/tools/time.go:
package tools

import (
    "encoding/json"
    "fmt"
    "time"
)

// GetCurrentTimeTool returns a tool that reports the current server time
// in any IANA timezone the LLM requests.
func GetCurrentTimeTool() Tool {
    return Tool{
        name:        "get_current_time",
        description: "Returns the current server time in RFC3339 format. Use this when the user asks what time it is or needs a timestamp in a specific timezone.",
        parameter: map[string]any{
            "type": "object",
            "properties": map[string]any{
                "timezone": map[string]any{
                    "type":        "string",
                    "description": "IANA timezone name, e.g. UTC, America/New_York, Asia/Shanghai.",
                },
            },
            "required": []string{"timezone"},
        },
        execute: func(args string) (string, error) {
            var params struct {
                Timezone string `json:"timezone"`
            }
            if err := json.Unmarshal([]byte(args), &params); err != nil {
                return "", fmt.Errorf("failed to parse arguments: %w", err)
            }
            if params.Timezone == "" {
                return "", fmt.Errorf("timezone cannot be empty")
            }
            loc, err := time.LoadLocation(params.Timezone)
            if err != nil {
                return "", fmt.Errorf("unknown timezone %q: %w", params.Timezone, err)
            }
            return time.Now().In(loc).Format(time.RFC3339), nil
        },
    }
}
Register it in main.go:
timeTool := tools.GetCurrentTimeTool()
registry[timeTool.Name()] = timeTool

JSON Schema tips

The parameter map is serialised and forwarded to the LLM exactly as written. Use standard JSON Schema conventions: String parameter
{
  "type": "string",
  "description": "The city name to look up, e.g. Tokyo."
}
Number parameter
{
  "type": "number",
  "description": "Timeout in seconds. Must be greater than zero."
}
Enum parameter (restrict to a fixed set of values)
{
  "type": "string",
  "enum": ["celsius", "fahrenheit", "kelvin"],
  "description": "The temperature unit for the result."
}
Array parameter
{
  "type": "array",
  "items": { "type": "string" },
  "description": "A list of city names to query in a single call."
}
Always set the top-level type to "object", wrap all parameters under properties, and list every required argument in the required array.

Error handling

Execute returns (string, error). When the execute function returns a non-nil error, the engine formats it as "工具执行失败: <err>" and sends that string back to the LLM as the tool result. The LLM can then decide to retry with different arguments, call a different tool, or report the failure to the user. Good practice:
  • Validate all required fields immediately after unmarshalling and return a descriptive error if any are missing or invalid.
  • Wrap external errors with fmt.Errorf("...: %w", err) so context is preserved in logs.
  • Return structured text on success — for example, JSON or a short sentence — so the LLM can parse or relay it accurately.
execute: func(args string) (string, error) {
    var params struct {
        Query string `json:"query"`
    }
    if err := json.Unmarshal([]byte(args), &params); err != nil {
        // Engine will report: 工具执行失败: failed to parse arguments: ...
        return "", fmt.Errorf("failed to parse arguments: %w", err)
    }
    if params.Query == "" {
        return "", fmt.Errorf("query cannot be empty")
    }
    result, err := callExternalAPI(params.Query)
    if err != nil {
        return "", fmt.Errorf("external API call failed: %w", err)
    }
    return result, nil
},
Write the description field as if you are explaining the tool to a developer who has never seen your code, not to a computer. The LLM uses it to decide when to call your tool. Be specific about the trigger condition — e.g., "Use this when the user asks what time it is or needs a timestamp" — rather than a vague label like "time tool". Overly broad descriptions cause the model to call the tool in situations where it is not appropriate; descriptions that are too narrow cause it to be ignored.
For the complete tool system reference, see Tool System.

Build docs developers (and LLMs) love