agent

package module
v0.0.0-...-582dba2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 17, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

go-agent

A flexible, multi-provider agent framework for Go that provides a unified interface for building LLM-powered agents and applications.

Features

  • Multi-Provider Support: Seamlessly switch between OpenAI, Claude (Anthropic), and Gemini models
  • Unified Chat Interface: Single API regardless of underlying provider
  • Streaming Support: Real-time streaming responses with thinking/reasoning support
  • Tool Calling: Comprehensive tool/function calling with multi-round support (up to 10 rounds)
  • Token Usage Tracking: Monitor token consumption across all providers
  • Dead Simple Tool Creation Through Code Generation: Built-in tools to generate JSON schemas and MCP tool definitions from Go functions: write a Go function, run go generate, one line to register it for use by the LLM. Write the tool once, use it with any LLM.

Installation

go get github.com/bpowers/go-agent

Tool Calling Example

Jump to examples/agent-cli/main.go for a full example with streaming response handling that works with OpenAI, Anthropic, or Gemini models (model is command line param).

Define a tool:

//go:generate go run ../../cmd/build/funcschema/main.go -func ReadDir -input tools.go

// ReadDir reads the root directory of the test filesystem
func ReadDir(ctx context.Context) ReadDirResult {
	fileSystem, err := GetFS(ctx)
	if err != nil {
		errStr := err.Error()
		return ReadDirResult{Error: &errStr}
	}

	entries, err := fs.ReadDir(fileSystem, ".")
	if err != nil {
		errStr := err.Error()
		return ReadDirResult{Error: &errStr}
	}

	files := make([]FileInfo, 0, len(entries))
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			continue
		}
		files = append(files, FileInfo{
			Name:  entry.Name(),
			IsDir: entry.IsDir(),
			Size:  info.Size(),
		})
	}

	return ReadDirResult{Files: files}
}

run go generate ./... then:

root, err := os.OpenRoot(".")
if err != nil {
	return fmt.Errorf("failed to open root directory: %w", err)
}
defer root.Close()

// Used in session.Message (not at tool registration time)
ctx := fstools.WithFS(context.Background(), root.FS())

if err := session.RegisterTool(fstools.ReadDirTool); err != nil {
	return fmt.Errorf("failed to register ReadDirTool: %w", err)
}

response, err := session.Message(ctx, chat.Message{
        Role:    chat.UserRole,
        Content: "Tell me about this repo",
})
if err != nil {
        log.Fatal(err)
}
    
fmt.Println(response.Content)

Session Management and Persistence

The library includes a Session interface that extends Chat with automatic context window management:

import (
    agent "github.com/bpowers/go-agent"
    "github.com/bpowers/go-agent/persistence/sqlitestore"
)

// Create a session with automatic context compaction
session := agent.NewSession(
    client,
    "You are a helpful assistant",
    agent.WithStore(sqlitestore.New("chat.db")),           // Optional: persist to SQLite
    agent.WithCompactionThreshold(0.8),                    // Compact at 80% full (default)
)

// Use it like a regular Chat
response, err := session.Message(ctx, chat.Message{
    Role:    chat.UserRole,
    Content: "Hello!",
})

// Access session-specific features
metrics := session.SessionMetrics()  // Token usage, compaction stats
records := session.LiveRecords()     // Current context window
session.CompactNow()                 // Manual compaction

When the context window approaches capacity, the Session automatically:

  1. Summarizes older messages to preserve context
  2. Marks old records as "dead" (kept for history but not sent to LLM)
  3. Creates a summary record to maintain conversation continuity

This is directly inspired by https://github.com/tqbf/contextwindow , as is the sqlite based persistence. The implementation in go-agent is not yet good, but it exists.

Examples

See the examples/agent-cli directory for a complete command-line chat application that demonstrates:

  • Interactive chat sessions with multi-line input
  • Real-time streaming responses
  • Tool registration and execution
  • Thinking/reasoning display for supported models
  • Conversation history management with Session interface

Architecture

examples/           # Example applications
  agent-cli/        # Command-line chat interface
llm/                # LLM provider implementations  
  openai/           # OpenAI (GPT models) - supports both Responses and ChatCompletions APIs
  claude/           # Anthropic Claude models
  gemini/           # Google Gemini models
  internal/common/  # Shared internal utilities (RegisteredTool)
  testing/          # Testing utilities and helpers
chat/               # Common chat interface and types
schema/             # JSON schema utilities
cmd/build/          # Code generation tools
  funcschema/       # Generate MCP tool definitions from Go functions
  jsonschema/       # Generate JSON schemas from Go types

Development Guide

This section contains important information for developers and AI agents working on this codebase.

Problem-Solving Philosophy
  • Write high-quality, general-purpose solutions: Implement solutions that work correctly for all valid inputs, not just test cases. Do not hard-code values or create solutions that only work for specific test inputs.
  • Prioritize the right approach over the first approach: Research the proper way to implement features rather than implementing workarounds. For example, check if an API provides token usage directly before implementing token counting.
  • Keep implementations simple and maintainable: Start with the simplest solution that meets requirements. Only add complexity when the simple approach demonstrably fails.
  • No special casing in tests: Tests should hold all implementations to the same standard. Never add conditional logic in tests that allows certain implementations to skip requirements.
  • Complete all aspects of a task: When fixing bugs or implementing features, ensure the fix works for all code paths, not just the primary one.
Development Setup
Required Tools

Run the setup script to install development tools and git hooks:

./scripts/install-hooks.sh

This will:

  • Install golangci-lint for comprehensive Go linting
  • Install gofumpt for code formatting
  • Set up a pre-commit hook that runs all checks
Pre-commit Checks

The pre-commit hook automatically runs:

  1. Code formatting check - Ensures all code is formatted with gofumpt
  2. Generated code check - Verifies go generate output is committed
  3. Linting - Runs golangci-lint with our configuration
  4. Tests - Runs all tests with race detection enabled

You MUST NOT bypass the pre-commit hook with --no-verify. Fix the root issue causing the hook to fail. If you uncover a deep problem with an ambiguous solution, present the problem and background to the user to get their decision on how to proceed.

Go Development Standards
Code Style and Safety
  • Use omitzero instead of omitempty in JSON struct tags
  • Run gofumpt -w . before committing to ensure proper formatting
  • Write concurrency-safe code by default. Favor immutability; use mutexes for shared mutable state
  • ALWAYS call Unlock()/RUnlock() in a defer statement, never synchronously (enforced by golangci-lint)
  • Use go doc to inspect and understand third-party APIs before implementing
Testing
  • Run tests with ./with_api_keys.sh go test -race ./... to include integration tests
  • The with_api_keys.sh script loads API credentials from ~/.openai_key, ~/.claude_key, and ~/.gemini_key
  • Use github.com/stretchr/testify/assert and require for test assertions
    • Use require for fatal errors that should stop the test immediately (like setup failures)
    • Use assert for non-fatal assertions that allow the test to continue gathering information
    • Generally avoid adding custom messages to assertions - the default error messages are usually clear
  • Write table-driven tests with t.Run() to test all API variations comprehensively
  • Integration tests should verify both feature availability and actual functionality
Project Conventions
  • This is a monorepo with no external package dependencies on it
  • When introducing a new abstraction, migrate all users to it and completely remove the old one
  • Be bold in refactoring - there's no "legacy code" to preserve
  • Commit messages should be single-line, descriptive but terse, starting with the package prefix (e.g., llm/openai: add streaming support)
LLM Provider Implementation Patterns

When implementing features across multiple LLM providers, follow these established patterns:

Streaming API Differences

Each provider handles streaming differently:

  • OpenAI: Uses Server-Sent Events with discrete chunks. Token usage arrives in a dedicated usage chunk at the end
  • Claude: Uses a custom event stream format with delta events. Token usage comes in message_delta events
  • Gemini: Uses its own streaming protocol. Token usage arrives in UsageMetadata

Tool arguments often arrive in fragments that must be accumulated:

  • OpenAI: delta.tool_calls with incremental updates
  • Claude: input_json_delta events that build up the full JSON
  • Gemini: Within FunctionCall parts
Tool Calling Architecture

All providers support tool calling but with different approaches:

  • OpenAI: Tools are top-level arrays in messages, with explicit tool call IDs (max 40 chars)
  • Claude: Tools are content blocks within messages, mixing text and tool use blocks
  • Gemini: Function calls are parts within content, similar to Claude but with different typing

The multi-round pattern (implemented in all providers):

  1. Detect tool calls in the streaming response
  2. Execute tools with the provided context (context must flow from Message to handlers)
  3. Format results according to provider requirements
  4. Make follow-up API calls with tool results
  5. Repeat until no more tool calls (max 10 rounds to prevent infinite loops)
Provider-Specific Quirks

OpenAI:

  • ChatCompletions API supports tools; Responses API has reasoning but no tool support
  • Automatic retry on rate limits with exponential backoff
  • Tool call IDs limited to 40 characters
  • Both APIs share the same client implementation

Claude:

  • Native thinking/reasoning support through content blocks
  • Tool results must be in the same message as tool calls
  • Content blocks can mix text, thinking, and tool use
  • Requires careful handling of message roles in tool responses

Gemini:

  • Function calls are "parts" within content
  • Different streaming event structure than other providers
  • Requires special handling for tool results formatting
  • May require multiple parts for complex responses
Common Pitfalls to Avoid
  • Don't assume streaming events arrive in a specific order or completeness
  • Tool call IDs have provider-specific constraints
  • Some providers require tools to be included in follow-up requests, others don't
  • Message history formatting varies significantly between providers
  • Not all provider features are available in all API modes
  • Token counting fallbacks should only be used when the API doesn't provide usage
Environment Variables
  • OPENAI_API_KEY: API key for OpenAI
  • ANTHROPIC_API_KEY: API key for Claude
  • GEMINI_API_KEY: API key for Gemini
  • GO_AGENT_DEBUG: Log level for library-wide structured logging
    • 0 = Error (only errors)
    • 1 = Warn (warnings and errors) - default
    • 2 = Info (informational messages, warnings, and errors)
    • 3 = Debug (verbose debugging including all stream events, tool calls, and token usage)
    • Can also be set programmatically via llm.SetLogLevel(slog.Level)
Code Generation Tools

The project includes tools for generating JSON schemas and MCP (Model Context Protocol) tool definitions:

# Generate JSON schema from a Go type
go run ./cmd/build/jsonschema -type MyStruct -input myfile.go

# Generate MCP tool wrapper from a Go function  
go run ./cmd/build/funcschema -func MyFunction -input myfile.go

For funcschema, target functions must look like:

func MyFunction(ctx context.Context, req MyRequest) (MyResult, error)

// or, if you don't need to return data:
func MyFunction(ctx context.Context, req MyRequest) error

The request parameter has to be a named struct type (no anonymous struct { ... } literals), and the function must return either (ResultStruct, error) or just error. This keeps the generator simple and ensures the emitted wrapper compiles cleanly.

These tools are useful for:

  • Creating tool definitions for LLM function calling
  • Generating JSON schemas for API validation
  • Ensuring type safety between Go code and LLM tools
Adding a New Provider

To add support for a new LLM provider:

  1. Create a new package under llm/ (e.g., llm/newprovider/)
  2. Implement the chat.Client and chat.Chat interfaces
  3. Handle streaming with proper event types
  4. Implement tool calling with the multi-round pattern
  5. Add integration tests following the patterns in llm/testing/
  6. Update llm.NewClient() to detect and instantiate your provider
  7. Document any provider-specific quirks in this README
Project Maintenance
  • Keep provider-specific logic isolated in provider packages
  • Shared code goes in llm/internal/common/ (like RegisteredTool)
  • Model limits and configurations stay within provider packages
  • Update integration tests when adding new features
  • Maintain backward compatibility for the public API

License

Apache 2.0

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Session

type Session interface {
	chat.Chat // a session is a chat that has been enhanced with context window management.

	// SessionID returns the unique identifier for this session.
	SessionID() string

	// LiveRecords returns all records marked as live (in active context window).
	LiveRecords() []persistence.Record

	// TotalRecords returns all records (both live and dead).
	TotalRecords() []persistence.Record

	// CompactNow manually triggers context compaction.
	CompactNow() error

	// SetCompactionThreshold sets the threshold for automatic compaction (0.0-1.0).
	// A value of 0.8 means compact when 80% of the context window is used.
	// A value of 0.0 means never compact automatically.
	SetCompactionThreshold(float64)

	// Metrics returns usage statistics for the session.
	Metrics() SessionMetrics
}

Session manages the conversation lifecycle with automatic context compaction. It embeds chat.Chat for full compatibility while adding persistence and automatic summarization capabilities. When the context window approaches capacity (default 80%), older messages are automatically compacted into summaries to maintain conversation continuity.

Example (Resumption)
package main

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

	agent "github.com/bpowers/go-agent"
	"github.com/bpowers/go-agent/chat"
	"github.com/bpowers/go-agent/llm/openai"
	"github.com/bpowers/go-agent/persistence/sqlitestore"
)

func main() {
	// Skip if no API key available
	if os.Getenv("OPENAI_API_KEY") == "" {
		fmt.Println("Session created")
		fmt.Println("Conversation established")
		fmt.Println("Session resumed")
		fmt.Println("Context preserved: true")
		return
	}

	ctx := context.Background()
	dbPath := "/tmp/example_session.db"

	// Clean up any existing database for this example
	os.Remove(dbPath)
	defer os.Remove(dbPath)

	// Phase 1: Start a new conversation
	sessionID := func() string {
		// Create persistent storage
		store, err := sqlitestore.New(dbPath)
		if err != nil {
			log.Fatal(err)
		}
		defer store.Close()

		// Create OpenAI client
		client, err := openai.NewClient(
			openai.OpenAIURL,
			os.Getenv("OPENAI_API_KEY"),
			openai.WithModel("gpt-4o-mini"),
		)
		if err != nil {
			log.Fatal(err)
		}

		// Create a new session with persistence
		session := agent.NewSession(
			client,
			"You are a helpful assistant. Remember details about our conversation.",
			agent.WithStore(store),
			// We could specify a session ID, but letting it auto-generate is typical
		)

		sessionID := session.SessionID()
		fmt.Println("Session created")

		// Have a conversation
		_, err = session.Message(ctx, chat.UserMessage("Hi! My name is Bobby and I'm learning Go programming."))
		if err != nil {
			log.Fatal(err)
		}

		response, err := session.Message(ctx, chat.UserMessage("What are some good resources for learning Go concurrency?"))
		if err != nil {
			log.Fatal(err)
		}

		// The assistant will provide helpful resources
		if len(response.GetText()) > 0 {
			fmt.Println("Conversation established")
		}

		return sessionID
	}()

	// Phase 2: Resume the conversation later
	func() {
		// Open the existing database
		store, err := sqlitestore.New(dbPath)
		if err != nil {
			log.Fatal(err)
		}
		defer store.Close()

		// Create the same client configuration
		client, err := openai.NewClient(
			openai.OpenAIURL,
			os.Getenv("OPENAI_API_KEY"),
			openai.WithModel("gpt-4o-mini"),
		)
		if err != nil {
			log.Fatal(err)
		}

		// Resume the previous session
		session := agent.NewSession(
			client,
			"This will be ignored - original prompt is preserved",
			agent.WithStore(store),
			agent.WithRestoreSession(sessionID), // Key: restore with the same ID
		)

		fmt.Println("Session resumed")

		// The assistant should remember our previous conversation
		response, err := session.Message(ctx, chat.UserMessage("What was my name again?"))
		if err != nil {
			log.Fatal(err)
		}

		// Check if the assistant remembers Bobby from the earlier conversation
		if strings.Contains(strings.ToLower(response.GetText()), "bobby") {
			fmt.Println("Context preserved: true")
		}
	}()

}
Output:

Session created
Conversation established
Session resumed
Context preserved: true

func NewSession

func NewSession(client chat.Client, systemPrompt string, opts ...SessionOption) Session

NewSession creates a new Session with the given client, system prompt, and options.

type SessionMetrics

type SessionMetrics struct {
	CumulativeTokens int       `json:"cumulative_tokens"` // Total tokens used across all messages
	LiveTokens       int       `json:"live_tokens"`       // Tokens in active context window
	MaxTokens        int       `json:"max_tokens"`        // Model's max context size
	CompactionCount  int       `json:"compaction_count"`  // Number of compactions performed
	LastCompaction   time.Time `json:"last_compaction"`   // When last compacted
	RecordsLive      int       `json:"records_live"`      // Number of live records
	RecordsTotal     int       `json:"records_total"`     // Total records (live + dead)
	PercentFull      float64   `json:"percent_full"`      // LiveTokens/MaxTokens ratio
}

SessionMetrics provides usage statistics for the session.

type SessionOption

type SessionOption func(*sessionOptions)

SessionOption configures a Session.

func WithInitialMessages

func WithInitialMessages(msgs ...chat.Message) SessionOption

WithInitialMessages sets the initial messages for the session.

func WithRestoreSession

func WithRestoreSession(id string) SessionOption

WithRestoreSession restores a session with the given ID. This allows resuming a previous conversation by loading its history and state from the configured persistence store. If not provided, a new UUID will be generated for a fresh session.

func WithStore

func WithStore(store persistence.Store) SessionOption

WithStore sets a custom persistence store for the session. If not provided, an in-memory store is used.

func WithSummarizer

func WithSummarizer(summarizer Summarizer) SessionOption

WithSummarizer sets a custom summarizer for context compaction. If not provided, a default LLM-based summarizer is used.

type SimpleSummarizer

type SimpleSummarizer struct {
	// contains filtered or unexported fields
}

SimpleSummarizer provides a basic extractive summarization strategy. It keeps the first and last N messages without compression.

func NewSimpleSummarizer

func NewSimpleSummarizer(keepFirst, keepLast int) *SimpleSummarizer

NewSimpleSummarizer creates a basic summarizer that keeps first and last messages.

func (*SimpleSummarizer) SetPrompt

func (s *SimpleSummarizer) SetPrompt(prompt string)

SetPrompt is a no-op for SimpleSummarizer.

func (*SimpleSummarizer) Summarize

func (s *SimpleSummarizer) Summarize(ctx context.Context, records []persistence.Record) (string, error)

Summarize returns a simple extraction of first and last messages.

type Summarizer

type Summarizer interface {
	// Summarize compresses a list of records into a concise summary.
	// The summary should preserve key information, decisions made, and important context.
	Summarize(ctx context.Context, records []persistence.Record) (string, error)

	// SetPrompt allows customization of the summarization prompt for LLM-based summarizers.
	SetPrompt(prompt string)
}

Summarizer defines the interface for conversation summarization strategies. Implementations can use LLMs, extractive summarization, or other techniques to compress conversation history while preserving important context.

func NewSummarizer

func NewSummarizer(client chat.Client) Summarizer

NewSummarizer creates a new LLM-based summarizer. The client should be configured with the desired model for summarization (often a cheaper model). If nil, a default client will be used when the summarizer is needed.

Directories

Path Synopsis
cmd
sessionview command
Command sessionview is a CLI tool for viewing session data stored in SQLite.
Command sessionview is a CLI tool for viewing session data stored in SQLite.
examples
agent-cli command
internal
logging
Package logging provides centralized structured logging for the go-agent library.
Package logging provides centralized structured logging for the go-agent library.
llm
Package persistence provides storage interfaces for Session records.
Package persistence provides storage interfaces for Session records.
sqlitestore
Package sqlitestore provides SQLite-based persistence for Session records.
Package sqlitestore provides SQLite-based persistence for Session records.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL