Skip to main content
Tools let the LLM take actions. chainforge dispatches all tool calls from a single LLM response concurrently — each runs in its own goroutine with an independent timeout.

Built-in tools

import "github.com/lioarce01/chainforge/pkg/tools/calculator"
import "github.com/lioarce01/chainforge/pkg/tools/websearch"

chainforge.WithTools(
    calculator.New(),
    websearch.New(backend),
)
ToolPackageDescription
Calculatorpkg/tools/calculatorSafe AST-based math evaluation
WebSearchpkg/tools/websearchPluggable search backend

Custom tools

TypedFunc — one-line tool definition

Use tools.TypedFunc[TInput] to define a tool from a typed struct. Schema generation, JSON marshalling, and unmarshalling are handled automatically — no boilerplate required.
import "github.com/lioarce01/chainforge/pkg/tools"

type WeatherInput struct {
    City  string `json:"city"  cf:"required,description=City name"`
    Units string `json:"units" cf:"enum=celsius|fahrenheit"`
}

weatherTool, err := tools.TypedFunc[WeatherInput](
    "get_weather",
    "Get the current weather for a city",
    func(ctx context.Context, in WeatherInput) (string, error) {
        return fetchWeather(in.City, in.Units)
    },
)
Use MustTypedFunc in package-level declarations or test setup:
var weatherTool = tools.MustTypedFunc[WeatherInput]("get_weather", "...", fn)

Func — raw JSON handler

For full control over input parsing, use tools.Func directly:
schema := tools.NewSchema().
    Add("city", tools.Property{Type: tools.TypeString, Description: "City name"}, true).
    MustBuild()

weatherTool, _ := tools.Func(
    "get_weather",
    "Get the current weather for a city",
    schema,
    func(ctx context.Context, input string) (string, error) {
        var args struct{ City string }
        json.Unmarshal([]byte(input), &args)
        return fetchWeather(args.City)
    },
)

Schema shorthand methods

The Schema builder now has typed shorthand methods so you don’t need to construct Property literals manually:
schema := tools.NewSchema().
    AddString("city",      "City name",          true).
    AddInt("limit",        "Max results",         false).
    AddNumber("threshold", "Min confidence score", false).
    AddBool("verbose",     "Enable verbose output", false).
    MustBuild()
Equivalent to the explicit form:
schema := tools.NewSchema().
    Add("city", tools.Property{Type: tools.TypeString, Description: "City name"}, true).
    MustBuild()
MethodJSON schema type
AddString(name, desc, required)"string"
AddInt(name, desc, required)"integer"
AddNumber(name, desc, required)"number"
AddBool(name, desc, required)"boolean"

Struct-based schema

Generate a JSON schema directly from a Go struct using field tags — no NewSchema() calls needed:
type SearchInput struct {
    Query  string `json:"query"  cf:"required,description=The search query"`
    Limit  int    `json:"limit"  cf:"description=Max results to return"`
    Format string `json:"format" cf:"enum=json|text|markdown"`
}

schema, err := tools.SchemaFromStruct[SearchInput]()
// or panic on error:
schema = tools.MustSchemaFromStruct[SearchInput]()
Supported tags:
TagEffect
json:"fieldName"Property name (falls back to Go field name)
cf:"required"Marks the field as required
cf:"description=My desc"Sets the property description
cf:"enum=a|b|c"Restricts to listed values
Multiple cf directives can be combined: cf:"required,description=The query,enum=json|text". Go type → JSON schema type:
Go typeSchema type
string"string"
int, int8int64, uint"integer"
float32, float64"number"
bool"boolean"
[]T, [N]T"array"
everything else"object"
Pointer types are dereferenced one level. Unexported fields are skipped.

Parsing tool input

Inside a tool handler, use tools.ParseInput[T] to unmarshal the JSON input string into a typed struct:
type WeatherInput struct {
    City  string `json:"city"`
    Units string `json:"units"`
}

func(ctx context.Context, input string) (string, error) {
    args, err := tools.ParseInput[WeatherInput](input)
    if err != nil {
        return "", err
    }
    return fetchWeather(args.City, args.Units)
}
ParseInput returns a tools: invalid input: ... error on malformed JSON.

Cached tools

Wrap any tool with CachedTool to memoize results by input JSON string. Useful for expensive or rate-limited tools that may be called repeatedly with the same arguments.
import "github.com/lioarce01/chainforge/pkg/tools"

cached := tools.NewCachedTool(expensiveTool)

chainforge.WithTools(cached)
Behaviour:
  • Results (including errors) are stored by exact input JSON string.
  • Concurrent calls with the same input call the inner tool exactly once — subsequent calls return the cached value.
  • Call cached.InvalidateAll() to flush the cache on demand.
cached.InvalidateAll()  // force re-execution on next call
CachedTool delegates Definition() to the wrapped tool, so the LLM sees the same schema.

TTL-based expiry

Use NewCachedToolWithTTL to automatically expire cache entries after a duration:
import "time"

// Re-execute if the cached result is older than 5 minutes.
cached := tools.NewCachedToolWithTTL(expensiveTool, 5*time.Minute)
A zero TTL behaves identically to NewCachedTool — entries never expire. Cache expiry uses a sliding window: the timestamp is recorded on first call; the next call after TTL elapses re-invokes the inner tool and refreshes the entry.

Tool errors

Tool execution errors are non-fatal. The error string is returned to the LLM as the tool result so the model can observe and handle it — the agent loop continues.

RetrieverTool — LLM-driven RAG

Wrap any rag.Retriever as a core.Tool so the LLM can call retrieval explicitly:
import "github.com/lioarce01/chainforge/pkg/rag"

retriever := rag.NewQdrantRetriever(store, embedder, "kb")

agent, _ := chainforge.NewAgent(
    chainforge.WithAnthropic(key, "claude-sonnet-4-6"),
    chainforge.WithTools(
        rag.NewRetrieverTool(retriever, rag.WithTopK(5)),
        // ... other tools
    ),
)
The LLM can call retriever_tool with {"query": "..."} to fetch relevant context on demand. For automatic injection on every turn, use chainforge.WithRetriever instead. See the RAG guide.

MCP servers

Connect any MCP server and its tools are discovered automatically. See the MCP guide.