Examples

Complete, runnable examples showing GoGrid patterns in action.

Single Agent with Tools

A calculator agent that uses tool calling to evaluate math expressions. Demonstrates agent creation, tool definition, and the iterative tool-use loop.

examples/calculator/main.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"
    "strconv"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/memory"
    "github.com/lonestarx1/gogrid/pkg/tool"
    "github.com/lonestarx1/gogrid/pkg/trace"
)

// CalcTool evaluates simple math expressions.
type CalcTool struct{}

func (c *CalcTool) Name() string        { return "calculate" }
func (c *CalcTool) Description() string { return "Evaluate a math expression" }
func (c *CalcTool) Schema() tool.Schema {
    return tool.Schema{
        Type: "object",
        Properties: map[string]*tool.Schema{
            "a":  {Type: "number", Description: "First operand"},
            "b":  {Type: "number", Description: "Second operand"},
            "op": {Type: "string", Description: "Operator: +, -, *, /"},
        },
        Required: []string{"a", "b", "op"},
    }
}

func (c *CalcTool) Execute(_ context.Context, input json.RawMessage) (string, error) {
    var args struct {
        A  float64 `json:"a"`
        B  float64 `json:"b"`
        Op string  `json:"op"`
    }
    if err := json.Unmarshal(input, &args); err != nil {
        return "", err
    }
    var result float64
    switch args.Op {
    case "+":
        result = args.A + args.B
    case "-":
        result = args.A - args.B
    case "*":
        result = args.A * args.B
    case "/":
        if args.B == 0 {
            return "error: division by zero", nil
        }
        result = args.A / args.B
    default:
        return "error: unknown operator " + args.Op, nil
    }
    return strconv.FormatFloat(result, 'f', -1, 64), nil
}

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))

    a := agent.New("calculator",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a calculator. Use the calculate tool for math."),
        agent.WithTools(&CalcTool{}),
        agent.WithMemory(memory.NewInMemory()),
        agent.WithTracer(trace.NewStdout(os.Stdout)),
        agent.WithConfig(agent.Config{MaxTurns: 5}),
    )

    result, err := a.Run(ctx, "What is 42 * 17 + 3?")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Answer: %s\n", result.Message.Content)
    fmt.Printf("Turns: %d | Cost: $%.6f | Tokens: %d\n",
        result.Turns, result.Cost, result.Usage.TotalTokens)
}

Multi-Provider Swap

The same agent logic works with any LLM provider. Swap the provider and model — everything else stays the same.

examples/multi-provider/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm"
    "github.com/lonestarx1/gogrid/pkg/llm/anthropic"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
)

func runWith(ctx context.Context, name string, provider llm.Provider, model string) {
    a := agent.New(name,
        agent.WithProvider(provider),
        agent.WithModel(model),
        agent.WithInstructions("Answer concisely in one sentence."),
    )

    result, err := a.Run(ctx, "What is the capital of France?")
    if err != nil {
        log.Printf("[%s] error: %v", name, err)
        return
    }
    fmt.Printf("[%s] %s (cost: $%.6f)\n", name, result.Message.Content, result.Cost)
}

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

    // OpenAI
    oai := openai.New(os.Getenv("OPENAI_API_KEY"))
    runWith(ctx, "openai", oai, "gpt-4o")

    // Anthropic
    ant := anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))
    runWith(ctx, "anthropic", ant, "claude-sonnet-4-5-20250929")
}

Persistent Memory

File-backed memory that persists across process restarts. The agent remembers previous conversations.

examples/persistent-memory/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/memory/file"
)

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

    // File memory persists to disk — survives restarts
    mem, err := file.New("./agent-data")
    if err != nil {
        log.Fatal(err)
    }

    provider := openai.New(os.Getenv("OPENAI_API_KEY"))

    a := agent.New("assistant",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a helpful assistant with persistent memory."),
        agent.WithMemory(mem),
    )

    // First run: tell it something
    result, _ := a.Run(ctx, "My name is Alice and I like Go programming.")
    fmt.Println(result.Message.Content)

    // Second run (even after restart): it remembers
    result, _ = a.Run(ctx, "What's my name and what do I like?")
    fmt.Println(result.Message.Content)

    // Check memory stats
    stats, _ := mem.Stats(ctx)
    fmt.Printf("Memory: %d keys, %d entries, %d bytes\n",
        stats.Keys, stats.TotalEntries, stats.TotalSize)
}

Code Review Team

Three agents review code concurrently — one for correctness, one for security, one for performance. All responses are combined using the Unanimous strategy.

examples/code-review/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/team"
    "github.com/lonestarx1/gogrid/pkg/trace"
)

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))
    tracer := trace.NewStdout(os.Stdout)

    reviewer := agent.New("reviewer",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Review the code for correctness, readability, and Go idioms."),
        agent.WithTracer(tracer),
    )

    security := agent.New("security",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Review the code for security vulnerabilities (injection, auth, data exposure)."),
        agent.WithTracer(tracer),
    )

    perf := agent.New("performance",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Review the code for performance issues (allocations, complexity, concurrency)."),
        agent.WithTracer(tracer),
    )

    t := team.New("code-review",
        team.WithMembers(
            team.Member{Agent: reviewer, Role: "correctness"},
            team.Member{Agent: security, Role: "security"},
            team.Member{Agent: perf, Role: "performance"},
        ),
        team.WithStrategy(team.Unanimous{}),
        team.WithTracer(tracer),
        team.WithConfig(team.Config{CostBudget: 2.00}),
    )

    code := `func handleLogin(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")
    query := fmt.Sprintf("SELECT * FROM users WHERE name='%s' AND pass='%s'", username, password)
    rows, _ := db.Query(query)
    // ...
}`

    result, err := t.Run(ctx, "Review this Go code:\n\n"+code)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("=== Team Decision ===")
    fmt.Println(result.Decision.Content)
    fmt.Printf("\nCost: $%.4f | Rounds: %d\n", result.TotalCost, result.Rounds)

    // Individual agent responses
    for name, r := range result.Responses {
        fmt.Printf("\n--- %s ---\n%s\n", name, r.Message.Content)
    }
}

Multi-Round Debate

Two agents debate a topic over multiple rounds. Each round, agents see the other's previous response and refine their position.

examples/debate/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/team"
)

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))

    advocate := agent.New("advocate",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You argue IN FAVOR of the topic. Be concise but persuasive."),
    )

    skeptic := agent.New("skeptic",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You argue AGAINST the topic. Be concise but rigorous."),
    )

    // Subscribe to the bus to watch the debate in real time
    bus := team.NewBus()
    ch, unsub := bus.Subscribe("team.response", 20)
    defer unsub()

    go func() {
        for msg := range ch {
            fmt.Printf("  [Round %s] %s: %s\n\n",
                msg.Metadata["round"], msg.From, msg.Content[:min(len(msg.Content), 100)]+"...")
        }
    }()

    t := team.New("debate",
        team.WithMembers(
            team.Member{Agent: advocate, Role: "advocate"},
            team.Member{Agent: skeptic, Role: "skeptic"},
        ),
        team.WithBus(bus),
        team.WithStrategy(team.Unanimous{}),
        team.WithConfig(team.Config{MaxRounds: 3}),
    )

    result, err := t.Run(ctx, "Should companies adopt AI agents in production?")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("\n=== Final Decision ===")
    fmt.Println(result.Decision.Content)
    fmt.Printf("Rounds: %d | Cost: $%.4f\n", result.Rounds, result.TotalCost)
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

Coordinated Team

A coordinator agent listens to all team members and synthesizes a single, coherent decision — instead of concatenating responses.

examples/coordinated-team/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/team"
)

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))

    // Domain experts
    frontend := agent.New("frontend",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a frontend engineer. Evaluate proposals from a UI/UX perspective."),
    )
    backend := agent.New("backend",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a backend engineer. Evaluate proposals from an architecture perspective."),
    )
    security := agent.New("security",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a security engineer. Evaluate proposals for security implications."),
    )

    // Team lead synthesizes all perspectives
    lead := agent.New("tech-lead",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are the tech lead. Synthesize all perspectives into a clear, actionable decision."),
    )

    t := team.New("design-review",
        team.WithMembers(
            team.Member{Agent: frontend, Role: "frontend"},
            team.Member{Agent: backend, Role: "backend"},
            team.Member{Agent: security, Role: "security"},
        ),
        team.WithCoordinator(lead),
        team.WithStrategy(team.Unanimous{}),
        team.WithConfig(team.Config{MaxRounds: 2}),
    )

    result, err := t.Run(ctx, "Should we add WebSocket support for real-time notifications?")
    if err != nil {
        log.Fatal(err)
    }

    // The decision comes from the coordinator, not concatenated responses
    fmt.Println("=== Tech Lead Decision ===")
    fmt.Println(result.Decision.Content)

    fmt.Printf("\nRounds: %d | Cost: $%.4f\n", result.Rounds, result.TotalCost)

    // Individual perspectives are still available
    for name, r := range result.Responses {
        fmt.Printf("\n--- %s ---\n%s\n", name, r.Message.Content[:min(len(r.Message.Content), 100)])
    }
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

Review Loop Graph

A graph with conditional edges: draft is reviewed, and if it needs revision, it loops back through revise → review until approved, then publishes.

examples/review-graph/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strings"
    "time"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/graph"
    "github.com/lonestarx1/gogrid/pkg/trace"
)

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))
    tracer := trace.NewStdout(os.Stdout)

    draft := agent.New("draft",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Write a short blog post about the given topic."),
        agent.WithTracer(tracer),
    )
    review := agent.New("review",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Review this draft. If it needs work, say 'needs revision' and explain. If good, say 'approved'."),
        agent.WithTracer(tracer),
    )
    revise := agent.New("revise",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Revise the draft based on the review feedback."),
        agent.WithTracer(tracer),
    )
    publish := agent.New("publish",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Format the approved draft for publication. Add a title and summary."),
        agent.WithTracer(tracer),
    )

    g, err := graph.NewBuilder("review-pipeline").
        AddNode("draft", draft).
        AddNode("review", review).
        AddNode("revise", revise).
        AddNode("publish", publish).
        AddEdge("draft", "review").
        AddEdge("review", "revise", graph.When(func(out string) bool {
            return strings.Contains(strings.ToLower(out), "needs revision")
        })).
        AddEdge("review", "publish", graph.When(func(out string) bool {
            return strings.Contains(strings.ToLower(out), "approved")
        })).
        AddEdge("revise", "review").
        Options(
            graph.WithConfig(graph.Config{
                MaxIterations: 3,
                Timeout:       2 * time.Minute,
                CostBudget:    2.00,
            }),
            graph.WithTracer(tracer),
        ).
        Build()
    if err != nil {
        log.Fatal(err)
    }

    // Export DOT for visualization
    fmt.Println("=== Graph Structure (DOT) ===")
    fmt.Println(g.DOT())

    result, err := g.Run(ctx, "The future of AI agents in production systems")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("=== Published Output ===")
    fmt.Println(result.Output)
    fmt.Printf("\nCost: $%.4f | Tokens: %d\n", result.TotalCost, result.TotalUsage.TotalTokens)

    // Show iteration counts
    for name, results := range result.NodeResults {
        fmt.Printf("  %s: %d iteration(s)\n", name, len(results))
    }
}

Research Pipeline

A three-stage pipeline with retry, input transforms, and progress reporting. Each stage processes output from the previous one.

examples/research-pipeline/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/pipeline"
    "github.com/lonestarx1/gogrid/pkg/trace"
)

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))
    tracer := trace.NewStdout(os.Stdout)

    researcher := agent.New("researcher",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a research assistant. Find key facts about the topic."),
        agent.WithTracer(tracer),
    )

    analyst := agent.New("analyst",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a data analyst. Identify patterns and insights."),
        agent.WithTracer(tracer),
    )

    writer := agent.New("writer",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a technical writer. Produce a clear, concise summary."),
        agent.WithTracer(tracer),
    )

    p := pipeline.New("research",
        pipeline.WithStages(
            pipeline.Stage{
                Name:  "research",
                Agent: researcher,
                Retry: pipeline.RetryPolicy{MaxAttempts: 2, Delay: time.Second},
            },
            pipeline.Stage{
                Name:  "analyze",
                Agent: analyst,
                InputTransform: func(input string) string {
                    return "Analyze the following research:\n\n" + input
                },
            },
            pipeline.Stage{
                Name:  "summarize",
                Agent: writer,
                InputTransform: func(input string) string {
                    return "Summarize these findings in 3 bullet points:\n\n" + input
                },
            },
        ),
        pipeline.WithTracer(tracer),
        pipeline.WithConfig(pipeline.Config{
            Timeout:    2 * time.Minute,
            CostBudget: 1.00,
        }),
        pipeline.WithProgress(func(idx, total int, sr pipeline.StageResult) {
            fmt.Printf("[%d/%d] Stage %q completed\n", idx+1, total, sr.Name)
        }),
    )

    result, err := p.Run(ctx, "Impact of large language models on software development")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("\n=== Final Output ===")
    fmt.Println(result.Output)
    fmt.Printf("\nStages: %d | Cost: $%.4f | Tokens: %d\n",
        len(result.Stages), result.TotalCost, result.TotalUsage.TotalTokens)

    fmt.Println("\n=== State Transfer Log ===")
    for _, entry := range result.TransferLog {
        fmt.Printf("  %s -> %s (generation %d)\n",
            entry.From, entry.To, entry.Generation)
    }
}

Pipeline State Transfer

Demonstrates ownership enforcement for pipeline stages. Each stage processes data and transfers state to the next — previous stages can no longer modify the data.

examples/pipeline-state/main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/lonestarx1/gogrid/pkg/llm"
    "github.com/lonestarx1/gogrid/pkg/memory"
    "github.com/lonestarx1/gogrid/pkg/memory/transfer"
)

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

    // Create transferable state wrapping an in-memory store
    state := transfer.NewState(memory.NewInMemory())

    // Register a validation hook
    state.OnTransfer(func(from, to string) error {
        fmt.Printf("  Transfer: %s -> %s\n", from, to)
        return nil
    })

    // Stage 1: Data collection
    h1, err := state.Acquire("collector")
    if err != nil {
        log.Fatal(err)
    }
    _ = h1.Save(ctx, "pipeline", []llm.Message{
        llm.NewUserMessage("raw data from source"),
    })
    fmt.Println("[collector] Saved raw data")

    // Stage 2: Data processing
    h2, _ := state.Transfer("collector", "processor")
    msgs, _ := h2.Load(ctx, "pipeline")
    msgs = append(msgs, llm.NewAssistantMessage("processed: "+msgs[0].Content))
    _ = h2.Save(ctx, "pipeline", msgs)
    fmt.Println("[processor] Processed data")

    // Stage 1 can no longer access the data
    _, err = h1.Load(ctx, "pipeline")
    fmt.Printf("[collector] Tried to read: %v\n", err)
    // Output: state has been transferred to a new owner

    // Stage 3: Data output
    h3, _ := state.Transfer("processor", "output")
    final, _ := h3.Load(ctx, "pipeline")
    for _, m := range final {
        fmt.Printf("[output] %s: %s\n", m.Role, m.Content)
    }

    // Print audit trail
    fmt.Println("\n=== Audit Trail ===")
    for _, entry := range state.AuditLog() {
        fmt.Printf("  %s -> %s (generation %d)\n",
            entry.From, entry.To, entry.Generation)
    }
}

Dynamic Research Coordinator

A coordinator agent dynamically spawns sub-agents and a pipeline at runtime based on the task. Demonstrates resource governance, async futures, and aggregate metrics.

examples/dynamic-research/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/dynamic"
    "github.com/lonestarx1/gogrid/pkg/orchestrator/pipeline"
    "github.com/lonestarx1/gogrid/pkg/trace"
)

func main() {
    ctx := context.Background()
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))
    tracer := trace.NewStdout(os.Stdout)

    // Create a runtime with resource governance
    rt := dynamic.New("coordinator",
        dynamic.WithConfig(dynamic.Config{
            MaxConcurrent: 3,
            MaxDepth:      2,
            CostBudget:    5.00,
        }),
        dynamic.WithTracer(tracer),
    )
    ctx = rt.Context(ctx)

    // Define child agents
    researcher := agent.New("researcher",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Find key facts and data about the given topic."),
    )
    analyst := agent.New("analyst",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Analyze data and identify trends."),
    )
    writer := agent.New("writer",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("Write a clear summary from the analysis."),
    )

    topic := "Impact of AI agents on enterprise software"

    // Phase 1: Spawn two researchers concurrently using Go
    fmt.Println("Phase 1: Research (parallel)")
    f1 := rt.Go(ctx, "research-papers", func(ctx context.Context) (string, error) {
        r, err := rt.SpawnAgent(ctx, researcher, "Find academic papers about: "+topic)
        if err != nil {
            return "", err
        }
        return r.Message.Content, nil
    })
    f2 := rt.Go(ctx, "research-industry", func(ctx context.Context) (string, error) {
        r, err := rt.SpawnAgent(ctx, analyst, "Find industry reports about: "+topic)
        if err != nil {
            return "", err
        }
        return r.Message.Content, nil
    })

    papers, err := f1.Wait(ctx)
    if err != nil {
        log.Fatal(err)
    }
    industry, err := f2.Wait(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // Phase 2: Feed into a summarization pipeline
    fmt.Println("\nPhase 2: Summarize (pipeline)")
    combined := "Academic Research:\n" + papers + "\n\nIndustry Analysis:\n" + industry

    summarizer := pipeline.New("summarize",
        pipeline.WithStages(
            pipeline.Stage{
                Name:  "synthesize",
                Agent: analyst,
                InputTransform: func(in string) string {
                    return "Synthesize these findings into key insights:\n\n" + in
                },
            },
            pipeline.Stage{
                Name:  "write",
                Agent: writer,
                InputTransform: func(in string) string {
                    return "Write an executive summary from these insights:\n\n" + in
                },
            },
        ),
        pipeline.WithConfig(pipeline.Config{Timeout: time.Minute}),
    )

    pResult, err := rt.SpawnPipeline(ctx, summarizer, combined)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("\n=== Executive Summary ===")
    fmt.Println(pResult.Output)

    // Aggregate metrics from all children
    res := rt.Result()
    fmt.Printf("\nChildren: %d | Total Cost: $%.4f | Tokens: %d\n",
        len(res.Children), res.TotalCost, res.TotalUsage.TotalTokens)
    fmt.Printf("Remaining Budget: $%.4f\n", rt.RemainingBudget())

    for _, child := range res.Children {
        fmt.Printf("  [%s] %s — $%.4f\n", child.Type, child.Name, child.Cost)
    }
}

CLI Multi-Agent Project

Define multiple agents with different providers and models in gogrid.yaml, then run them from the command line. No Go code required — just YAML and API keys.

gogrid.yaml
version: "1"

agents:
  # Research agent using Anthropic's Claude
  researcher:
    model: ${ANTHROPIC_MODEL:-claude-sonnet-4-5-20250929}
    provider: anthropic
    instructions: |
      You are a technical researcher. When given a topic:
      1. Explain the core concepts clearly
      2. Provide concrete examples
      3. Discuss trade-offs and alternatives
      4. Mention common pitfalls
      Be thorough but structured. Use headings and bullet points.
    config:
      max_turns: 10
      max_tokens: 4096
      temperature: 0.7
      timeout: 2m
      cost_budget: 0.50

  # Code review agent using OpenAI's GPT-4o-mini
  code-reviewer:
    model: ${OPENAI_MODEL:-gpt-4o-mini}
    provider: openai
    instructions: |
      You are a senior Go code reviewer. When given code:
      1. Check for correctness and edge cases
      2. Evaluate error handling
      3. Assess naming and readability
      4. Suggest improvements with code examples
      Be constructive. Follow Go conventions and idioms.
    config:
      max_turns: 5
      max_tokens: 2048
      timeout: 60s
      cost_budget: 0.10

  # Summarization agent — fast and cheap
  summarizer:
    model: ${OPENAI_MODEL:-gpt-4o-mini}
    provider: openai
    instructions: |
      Condense the input into 3-5 bullet points. Under 200 words.
    config:
      max_turns: 3
      max_tokens: 1024
      timeout: 30s
      cost_budget: 0.05
terminal
# Set API keys for the providers you use
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-proj-...

# List all defined agents
$ gogrid list
NAME             PROVIDER    MODEL
code-reviewer    openai      gpt-4o-mini
researcher       anthropic   claude-sonnet-4-5-20250929
summarizer       openai      gpt-4o-mini

# Run the researcher agent
$ gogrid run researcher -input "Explain how garbage collection works in Go"

# Run the code reviewer
$ gogrid run code-reviewer -input "Review this Go function:

func fetchUser(id string) (*User, error) {
    resp, err := http.Get(\"https://api.example.com/users/\" + id)
    if err != nil {
        return nil, err
    }
    var user User
    json.NewDecoder(resp.Body).Decode(&user)
    return &user, nil
}"

# Run the summarizer
$ gogrid run summarizer -input "Go is a statically typed, compiled language..."

# Override the model at runtime without editing the config
$ ANTHROPIC_MODEL=claude-haiku-4-5-20251001 gogrid run researcher -input "What is a goroutine?"

CLI Trace & Cost Inspection

After running agents, inspect what happened under the hood. View execution span trees to understand timing, and cost breakdowns to track spend per model.

terminal
# List recent runs
$ gogrid trace
Recent runs:
  019479a3c4e80001  researcher     claude-sonnet-4-5-20250929  4.2s
  019479a1b2c70002  code-reviewer  gpt-4o-mini                1.1s
  019479a0a1b60003  summarizer     gpt-4o-mini                0.8s

# View the span tree for a specific run
$ gogrid trace 019479a3c4e80001
Run: 019479a3c4e80001
Agent: researcher | Model: claude-sonnet-4-5-20250929 | Duration: 4.2s

agent.run (4.2s)
├── memory.load (1ms)
├── llm.complete (2.1s) [prompt: 150, completion: 89]
├── llm.complete (1.8s) [prompt: 280, completion: 145]
└── memory.save (2ms)

# Export spans as JSON for scripts or other tools
$ gogrid trace 019479a3c4e80001 -json | jq '.[].name'
"agent.run"
"memory.load"
"llm.complete"
"llm.complete"
"memory.save"
terminal
# View cost overview of all runs
$ gogrid cost
RUN ID              AGENT           MODEL                         COST
019479a3c4e80001    researcher      claude-sonnet-4-5-20250929    $0.003280
019479a1b2c70002    code-reviewer   gpt-4o-mini                   $0.000150
019479a0a1b60003    summarizer      gpt-4o-mini                   $0.000090

# Detailed cost breakdown for a specific run
$ gogrid cost 019479a3c4e80001
Run: 019479a3c4e80001

MODEL                         CALLS  PROMPT  COMPLETION  COST
claude-sonnet-4-5-20250929    2      430     234         $0.003280
────────────────────────────────────────────────────────────────
TOTAL                         2      430     234         $0.003280

# Export costs as JSON
$ gogrid cost -json | jq '.[] | {agent, cost}'

# Inspect raw run records directly
$ cat .gogrid/runs/019479a3c4e80001.json | jq '{agent, model, turns, cost}'
{
  "agent": "researcher",
  "model": "claude-sonnet-4-5-20250929",
  "turns": 2,
  "cost": 0.00328
}

Project Scaffolding

Use gogrid init to scaffold a complete project from a template. Each template generates a working project with gogrid.yaml, main.go, Makefile, and README — ready to run.

terminal
# Scaffold a single-agent project
$ gogrid init --template single my-agent
Created GoGrid project in my-agent/
  gogrid.yaml   Agent configuration
  main.go       Programmatic entry point
  Makefile      Build targets
  README.md     Setup instructions

$ cd my-agent && cat gogrid.yaml
my-agent/gogrid.yaml (generated)
version: "1"

agents:
  assistant:
    model: gpt-4o-mini
    provider: openai
    instructions: |
      You are a helpful assistant for the my-agent project.
      Provide clear, concise, and accurate responses.
    config:
      max_turns: 10
      max_tokens: 4096
      timeout: 60s
terminal
# Scaffold a team project
$ gogrid init --template team my-research-team
$ cat my-research-team/gogrid.yaml
my-research-team/gogrid.yaml (generated)
version: "1"

agents:
  researcher:
    model: gpt-4o
    provider: openai
    instructions: |
      You are a research specialist. Investigate the given topic
      thoroughly and provide detailed findings with evidence.
    config:
      max_turns: 10
      max_tokens: 4096
      timeout: 2m
      cost_budget: 1.00

  reviewer:
    model: gpt-4o
    provider: openai
    instructions: |
      You are a critical reviewer. Evaluate the research for accuracy,
      completeness, and potential biases. Suggest improvements.
    config:
      max_turns: 5
      max_tokens: 2048
      timeout: 60s
      cost_budget: 0.50
terminal
# Scaffold a pipeline project
$ gogrid init --template pipeline my-content-pipeline

# Set up and run any scaffolded project
$ cd my-content-pipeline
$ go mod init github.com/example/my-content-pipeline
$ go mod tidy
$ export OPENAI_API_KEY=sk-proj-...
$ gogrid list
$ gogrid run drafter -input "Write about AI agents in production"

Full Observability Stack

Wire up OTLP trace export, structured JSON logging with trace correlation, Prometheus-compatible metrics, and cost governance with budget alerts — all using the Go standard library.

examples/observability/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/cost"
    "github.com/lonestarx1/gogrid/pkg/llm/openai"
    tracelog "github.com/lonestarx1/gogrid/pkg/trace/log"
    "github.com/lonestarx1/gogrid/pkg/trace/metrics"
    "github.com/lonestarx1/gogrid/pkg/trace/otel"
)

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

    // 1. OTLP Exporter — sends spans to Jaeger/Tempo/etc.
    exporter := otel.NewExporter(
        otel.WithEndpoint("http://localhost:4318/v1/traces"),
        otel.WithServiceName("my-agent-service"),
        otel.WithServiceVersion("1.0.0"),
        otel.WithBatchSize(100),
        otel.WithFlushInterval(5 * time.Second),
    )
    defer exporter.Shutdown()

    // 2. Metrics Collector — wraps the exporter, auto-populates metrics
    reg := metrics.NewRegistry()
    collector := metrics.NewCollector(exporter, reg)

    // 3. Structured Logger — JSON logging with trace correlation
    logger := tracelog.New(os.Stdout, tracelog.Info)

    // 4. Cost Governance — budget with threshold alerts
    tracker := cost.NewTracker()
    tracker.SetBudget(5.00)
    tracker.OnBudgetThreshold(func(threshold, current float64) {
        logger.Warn("cost alert",
            "threshold", fmt.Sprintf("%.0f%%", threshold*100),
            "current", fmt.Sprintf("$%.4f", current),
        )
    }, 0.5, 0.8, 1.0)

    // 5. Prometheus metrics endpoint
    go func() {
        http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "text/plain; version=0.0.4")
            fmt.Fprint(w, reg.Export())
        })
        _ = http.ListenAndServe(":9090", nil)
    }()

    // 6. Create agent with the full observability stack
    provider := openai.New(os.Getenv("OPENAI_API_KEY"))
    a := agent.New("assistant",
        agent.WithProvider(provider),
        agent.WithModel("gpt-4o"),
        agent.WithInstructions("You are a helpful assistant."),
        agent.WithTracer(collector), // OTLP export + auto-metrics
    )

    // Run the agent
    logger.Info("starting agent run", "agent", "assistant")
    result, err := a.Run(ctx, "What are the key benefits of observability?")
    if err != nil {
        log.Fatal(err)
    }

    // Log the result with trace correlation
    logger.InfoCtx(ctx, "agent run complete",
        "turns", fmt.Sprintf("%d", result.Turns),
        "cost", fmt.Sprintf("$%.6f", result.Cost),
        "tokens", fmt.Sprintf("%d", result.Usage.TotalTokens),
    )

    // Record cost for governance
    tracker.AddForEntity("gpt-4o", "assistant", result.Usage)

    // Generate cost report
    report := tracker.Report()
    fmt.Printf("\nCost Report: $%.4f across %d calls\n", report.TotalCost, report.RecordCount)
    for model, mr := range report.ByModel {
        fmt.Printf("  %s: %d calls, $%.4f\n", model, mr.Calls, mr.Cost)
    }

    fmt.Printf("\nMetrics available at http://localhost:9090/metrics\n")
    fmt.Println(result.Message.Content)
}

Evaluation Suite

Score agent outputs with composable evaluators — exact match, cost budgets, and custom checks.

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/eval"
    "github.com/lonestarx1/gogrid/pkg/llm"
    "github.com/lonestarx1/gogrid/pkg/llm/mock"
)

func main() {
    provider := mock.New(mock.WithFallback(&llm.Response{
        Message: llm.NewAssistantMessage("Go is a statically typed, compiled language designed for simplicity."),
        Usage:   llm.Usage{PromptTokens: 15, CompletionTokens: 14, TotalTokens: 29},
        Model:   "mock",
    }))

    a := agent.New("eval-agent",
        agent.WithProvider(provider),
        agent.WithModel("mock"),
    )

    result, err := a.Run(context.Background(), "Describe Go in one sentence.")
    if err != nil {
        log.Fatal(err)
    }

    // Compose evaluators into a suite.
    suite := eval.NewSuite(
        eval.NewContains("Go", "compiled", "simplicity"),
        eval.NewCostWithin(0.01),
        eval.NewFunc("min_length", func(_ context.Context, r *agent.Result) (eval.Score, error) {
            if len(r.Message.Content) >= 20 {
                return eval.Score{Pass: true, Value: 1.0, Reason: "sufficient length"}, nil
            }
            return eval.Score{Pass: false, Value: 0.0, Reason: "too short"}, nil
        }),
    )

    sr, _ := suite.Run(context.Background(), result)
    fmt.Printf("Suite passed: %v\n", sr.Pass)
    for name, score := range sr.Scores {
        fmt.Printf("  %s: pass=%v value=%.2f reason=%s\n",
            name, score.Pass, score.Value, score.Reason)
    }
}

Mock Provider Testing

Test agents with sequential responses, error injection, and call recording — no API keys needed.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    "github.com/lonestarx1/gogrid/pkg/agent"
    "github.com/lonestarx1/gogrid/pkg/llm"
    "github.com/lonestarx1/gogrid/pkg/llm/mock"
)

func main() {
    // Queue a tool call response followed by a final answer.
    toolCallResp := &llm.Response{
        Message: llm.Message{
            Role: llm.RoleAssistant,
            ToolCalls: []llm.ToolCall{
                {ID: "tc-1", Function: "search", Arguments: json.RawMessage(`{"q":"Go"}`)},
            },
        },
        Usage: llm.Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15},
        Model: "mock",
    }
    finalResp := &llm.Response{
        Message: llm.NewAssistantMessage("Go was created at Google in 2007."),
        Usage:   llm.Usage{PromptTokens: 20, CompletionTokens: 10, TotalTokens: 30},
        Model:   "mock",
    }

    provider := mock.New(
        mock.WithResponses(toolCallResp, finalResp),
        mock.WithFallback(finalResp),
    )

    a := agent.New("test-agent",
        agent.WithProvider(provider),
        agent.WithModel("mock"),
    )

    result, err := a.Run(context.Background(), "When was Go created?")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Response: %s\n", result.Message.Content)
    fmt.Printf("Calls: %d\n", provider.Calls())
    fmt.Printf("History: %d params recorded\n", len(provider.History()))
}