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?
- Core Concepts - Deep dive into the architecture
- State Machine - Understand the canonical states
- Tools - Advanced tool patterns and annotations
- Policies - Configure budgets and approvals