Agents
Agents are large language models (LLMs) that use tools in a loop to accomplish tasks.
These components work together:
- LLMs process input and decide the next action
- Tools extend capabilities beyond text generation (reading files, calling APIs, writing to databases)
- Loop orchestrates execution through:
- Context management - Maintaining conversation history and deciding what the model sees (input) at each step
- Stopping conditions - Determining when the loop (task) is complete
ToolLoopAgent
The ToolLoopAgent handles these three components. Here's an agent that uses multiple tools in a loop to accomplish a task:
package main
import (
"context"
"fmt"
"log"
"math/rand"
"os"
"github.com/digitallysavvy/go-ai/pkg/agent"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
ctx := context.Background()
// Set up provider and model
provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := provider.LanguageModel("gpt-4")
// Create agent with tools
weatherAgent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
Tools: []types.Tool{
{
Name: "weather",
Description: "Get the weather in a location (in Fahrenheit)",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "The location to get the weather for",
},
},
"required": []string{"location"},
},
Execute: func(ctx context.Context, input map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
location := input["location"].(string)
return map[string]interface{}{
"location": location,
"temperature": 72 + rand.Intn(21) - 10,
}, nil
},
},
{
Name: "convertFahrenheitToCelsius",
Description: "Convert temperature from Fahrenheit to Celsius",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"temperature": map[string]interface{}{
"type": "number",
"description": "Temperature in Fahrenheit",
},
},
"required": []string{"temperature"},
},
Execute: func(ctx context.Context, input map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
temp := input["temperature"].(float64)
celsius := int((temp - 32) * (5.0 / 9.0))
return map[string]interface{}{
"celsius": celsius,
}, nil
},
},
},
StopWhen: []ai.StopCondition{ai.IsStepCount(20)}, // Agent stops after maximum of 20 steps
})
// Execute agent
result, err := weatherAgent.Execute(ctx, "What is the weather in San Francisco in celsius?")
if err != nil {
log.Fatal(err)
}
fmt.Println("Final Answer:", result.Text)
fmt.Printf("Steps taken: %d\n", len(result.Steps))
}
The agent automatically:
- Calls the
weathertool to get the temperature in Fahrenheit - Calls
convertFahrenheitToCelsiusto convert it - Generates a final text response with the result
The ToolLoopAgent handles the loop, context management, and stopping conditions.
Why Use Agents?
Using agents provides several benefits:
- Reduces boilerplate - Manages loops and message arrays automatically
- Improves reusability - Define once, use throughout your application
- Simplifies maintenance - Single place to update agent configuration
- Handles complexity - Manages tool execution, context, and termination logic
For most use cases, start with agents. Use core functions (ai.GenerateText, ai.StreamText) when you need explicit control over each step for complex structured workflows.
Basic Agent Structure
Every agent needs:
1. Model
The language model that powers the agent:
provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := provider.LanguageModel("gpt-4")
2. Tools
Functions the agent can call to perform actions:
tools := []types.Tool{
{
Name: "search",
Description: "Search the web for information",
Parameters: /* JSON schema */,
Execute: func(ctx context.Context, input map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
// Implementation
},
},
}
3. Configuration
Settings that control agent behavior:
config := agent.AgentConfig{
Model: model,
Tools: tools,
MaxSteps: 15, // Maximum iterations
System: "You are a helpful assistant", // System prompt
}
Agent Execution Flow
When you call agent.Execute(), the following happens:
- Initial Call: Agent receives user prompt and converts it to messages
- Generation: Model generates response (text and/or tool calls)
- Tool Execution: If tool calls are present, execute them
- Context Update: Add assistant response and tool results to conversation
- Loop: Repeat steps 2-4 until stopping condition is met
- Return: Final result with text, steps, and metadata
┌─────────────┐
│ User Prompt │
└──────┬──────┘
│
▼
┌─────────────────┐
│ Model Generate │◄────┐
└────────┬────────┘ │
│ │
▼ │
┌────────┐ │
│ Tools? │──No───►Stop
└───┬────┘ │
│Yes │
▼ │
┌──────────────┐ │
│ Execute Tools│ │
└──────┬───────┘ │
│ │
└────────────────┘
Stopping Conditions
Agents stop when:
- StopWhen reached: Agent hits the configured step limit (default:
ai.IsStepCount(20)) - No tool calls: Model returns text without requesting tool calls
- Error occurs: Tool execution or generation fails
- Context canceled: Context deadline or cancellation
config := agent.AgentConfig{
Model: model,
Tools: tools,
StopWhen: []ai.StopCondition{ai.IsStepCount(20)}, // Stop after 20 iterations
}
Callbacks
Monitor agent execution with callbacks:
config := agent.AgentConfig{
Model: model,
Tools: tools,
OnStepStart: func(stepNum int) {
fmt.Printf("Starting step %d\n", stepNum)
},
OnStepFinish: func(step types.StepResult) {
fmt.Printf("Step %d complete: %s\n", step.StepNumber, step.Text)
},
OnToolCall: func(toolCall types.ToolCall) {
fmt.Printf("Calling tool: %s\n", toolCall.ToolName)
},
OnToolResult: func(toolResult types.ToolResult) {
fmt.Printf("Tool %s returned: %v\n", toolResult.ToolName, toolResult.Result)
},
OnFinish: func(result *agent.AgentResult) {
fmt.Printf("Agent complete after %d steps\n", len(result.Steps))
},
}
Agent vs Core Functions
When to use Agents
Use agents when:
- Task requires multiple tool calls
- You want automatic loop management
- You need reusable agent configurations
- You want simplified context management
// Agent handles everything automatically
agent := agent.NewToolLoopAgent(config)
result, _ := agent.Execute(ctx, "Complex multi-step task")
When to use Core Functions
Use core functions when:
- You need explicit control over each step
- Building complex structured workflows
- Implementing custom loop logic
- Requiring specific error handling
// Manual control over each step
for step := 0; step < maxSteps; step++ {
result, _ := ai.GenerateText(ctx, options)
// Custom logic for each step
if shouldStop(result) {
break
}
}
Structured Workflows
Agents are flexible and powerful, but non-deterministic. When you need reliable, repeatable outcomes with explicit control flow, use core functions with structured workflow patterns combining:
- Conditional statements for explicit branching
- Standard functions for reusable logic
- Error handling for robustness
- Explicit control flow for predictability
Explore workflow patterns to learn more about building structured, reliable systems.
Real-World Example
Here's a practical agent that can search the web and summarize results:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/digitallysavvy/go-ai/pkg/agent"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
"github.com/digitallysavvy/go-ai/pkg/providers/anthropic"
)
func main() {
ctx := context.Background()
provider := anthropic.New(anthropic.Config{APIKey: os.Getenv("ANTHROPIC_API_KEY")})
model, _ := provider.LanguageModel("claude-sonnet-4-5")
// Create research agent
researchAgent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
System: "You are a research assistant. Use the available tools to find and summarize information.",
Tools: []types.Tool{
{
Name: "search",
Description: "Search the web for information",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
"description": "The search query",
},
},
"required": []string{"query"},
},
Execute: func(ctx context.Context, input map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
query := input["query"].(string)
// Actual implementation would call a search API
return fmt.Sprintf("Search results for: %s", query), nil
},
},
{
Name: "fetchWebpage",
Description: "Fetch and return the content of a webpage",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"url": map[string]interface{}{
"type": "string",
"description": "The URL to fetch",
},
},
"required": []string{"url"},
},
Execute: func(ctx context.Context, input map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
url := input["url"].(string)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Actual implementation would parse and clean HTML
return fmt.Sprintf("Content from %s", url), nil
},
},
},
MaxSteps: 15,
OnStepFinish: func(step types.StepResult) {
fmt.Printf("[Step %d] %s\n", step.StepNumber, step.Text)
},
})
result, err := researchAgent.Execute(ctx, "Research the latest developments in quantum computing and provide a summary")
if err != nil {
log.Fatal(err)
}
fmt.Println("\n=== Final Answer ===")
fmt.Println(result.Text)
fmt.Printf("\nCompleted in %d steps\n", len(result.Steps))
}
Next Steps
- Building Agents - Detailed guide to creating agents
- Workflow Patterns - Structured patterns using core functions
- Loop Control - Advanced execution control
- Configuring Call Options - Fine-tune agent behavior