Quickstart

Build your first agent in minutes with a step-by-step guide.

Overview

In this guide, we'll build a simple file operations agent that can read and analyze files. You'll learn how to define tools, configure policies, and run an agent.

Step 1: Define a Tool

Tools are the capabilities your agent can use. Each tool has annotations that influence how it's executed.

package main

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

    api "github.com/felixgeelhaar/agent-go/interfaces/api"
    "github.com/felixgeelhaar/agent-go/domain/tool"
)

type ReadFileInput struct {
    Path string `json:"path"`
}

var readFileTool = api.NewToolBuilder("read_file").
    WithDescription("Reads the contents of a file").
    WithAnnotations(api.Annotations{
        ReadOnly: true,        // No side effects
        Cacheable: true,       // Results can be cached
        RiskLevel: 1,          // Low risk
    }).
    WithExecutor(func(ctx context.Context, input json.RawMessage) (tool.Result, error) {
        var req ReadFileInput
        if err := json.Unmarshal(input, &req); err != nil {
            return tool.Result{}, err
        }

        data, err := os.ReadFile(req.Path)
        if err != nil {
            return tool.Result{}, err
        }

        return tool.Result{
            Output: json.RawMessage(`{"content": "` + string(data) + `"}`),
        }, nil
    }).
    Build()

Step 2: Create a Registry and Policies

Register your tools and configure which states they can be used in.

// Create tool registry
registry := api.NewToolRegistry()
registry.Register(readFileTool)

// Configure tool eligibility per state
eligibility := api.NewToolEligibility()
eligibility.Allow(api.StateExplore, "read_file")  // Only in explore state

// Set up budgets to limit operations
budgets := map[string]int{
    "tool_calls": 50,   // Max 50 tool calls per run
    "tokens":     10000, // Max 10k tokens
}

Step 3: Create an Engine

The engine orchestrates agent execution according to your configuration.

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

    // Build the engine with all configuration
    engine, err := api.New(
        api.WithRegistry(registry),
        api.WithPlanner(myPlanner),              // Your planner implementation
        api.WithToolEligibility(eligibility),
        api.WithTransitions(api.DefaultTransitions()),
        api.WithBudgets(budgets),
        api.WithMaxSteps(100),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Run the agent with a goal
    run, err := engine.Run(ctx, "Read and summarize the config.yaml file")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Final state: %s\n", run.State)
    fmt.Printf("Result: %s\n", run.Result)
}

Step 4: Test Without LLMs

Use the ScriptedPlanner for deterministic tests that don't require API calls.

func TestFileOpsAgent(t *testing.T) {
    // Define expected behavior
    planner := api.NewScriptedPlanner(
        api.ScriptStep{
            ExpectState: api.StateIntake,
            Decision:    api.NewTransitionDecision(api.StateExplore, "begin exploration"),
        },
        api.ScriptStep{
            ExpectState: api.StateExplore,
            Decision:    api.NewCallToolDecision("read_file",
                json.RawMessage(`{"path": "config.yaml"}`),
                "reading config"),
        },
        api.ScriptStep{
            ExpectState: api.StateExplore,
            Decision:    api.NewFinishDecision("analysis complete",
                json.RawMessage(`{"summary": "config loaded"}`)),
        },
    )

    engine, _ := api.New(
        api.WithPlanner(planner),
        api.WithRegistry(registry),
        api.WithToolEligibility(eligibility),
        api.WithTransitions(api.DefaultTransitions()),
    )

    run, err := engine.Run(context.Background(), "Analyze config")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if run.State != api.StateDone {
        t.Errorf("expected done, got %s", run.State)
    }
}

What's Next?