Skip to content

katasec/forge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

forge

A provider-agnostic Go library for building AI agent loops with pluggable tools, memory, and middleware.

Forge handles the LLM call -> tool execution -> response cycle. You supply a provider (Anthropic, OpenAI, etc.), register tools, and forge runs the loop, including error handling, iteration limits, and conversation memory.

Install

go get github.com/katasec/forge
go get github.com/katasec/forge/provider/anthropic  # optional
go get github.com/katasec/forge/provider/openai      # optional
go get github.com/katasec/forge/provider/xai         # optional: xAI Responses API with web search

Quick Start

export ANTHROPIC_API_KEY=sk-ant-...
package main

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

    "github.com/katasec/forge"
    "github.com/katasec/forge/provider/anthropic"
)

func main() {
    provider := anthropic.New(os.Getenv("ANTHROPIC_API_KEY"), "claude-sonnet-4-20250514")

    agent, err := forge.NewAgent(forge.Config{
        Provider:     provider,
        SystemPrompt: "You are a helpful assistant. Keep responses brief.",
    })
    if err != nil {
        log.Fatal(err)
    }

    resp, err := agent.Ask(context.Background(), "Hello! What are you?")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(resp.LastText())
}

Swap to OpenAI by changing one import:

import "github.com/katasec/forge/provider/openai"

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

The openai package uses the OpenAI Responses API, including text and image content.

Or use the xAI Responses API with built-in web search:

import "github.com/katasec/forge/provider/xai"

provider := xai.New(os.Getenv("XAI_API_KEY"), xai.ModelGrok4FastNonReasoning, xai.WithWebSearch())

// After running the agent, access citations:
citations := provider.LastCitations()
for _, c := range citations {
    fmt.Printf("[%s] %s\n", c.Title, c.URL)
}

See _examples/hello-world for the full runnable code.

Core Concepts

Provider

The Provider interface makes a single LLM call. Forge ships with three built-in providers, or you can implement your own:

type Provider interface {
    Generate(ctx context.Context, req ProviderRequest) (*ProviderResponse, error)
}

Tools

Define tools with Func[T]. The JSON schema for parameters is derived from the Go struct at construction time using invopop/jsonschema:

type SearchInput struct {
    Query string `json:"query" jsonschema:"description=Search query"`
    Limit int    `json:"limit" jsonschema:"description=Max results"`
}

searchTool := forge.Func[SearchInput]("search", "Search the database", func(ctx context.Context, in SearchInput) (string, error) {
    // ... your search logic
    return results, nil
})

Or implement the Tool interface directly for full control:

type Tool interface {
    Name() string
    Description() string
    Schema() ToolSchema
    Invoke(ctx context.Context, args json.RawMessage) (string, error)
}

Agent Loop

Agent.Run executes this loop:

  1. Load conversation history from memory
  2. Call the provider with messages + tool definitions
  3. If the provider says stop, return the response
  4. If the provider requests tool use, execute tools, feed results back, go to 2
  5. If iteration limit hit, return with FinishReasonIterLimit
  6. Save conversation to memory

For the common case, use Ask:

resp, err := agent.Ask(ctx, "Hello")
fmt.Println(resp.LastText())

For multimodal input, use AskContent:

resp, err := agent.AskContent(ctx,
    forge.Text("Describe this image."),
    forge.ImageURL("https://example.com/cat.png"),
)
fmt.Println(resp.LastText())

Use AskIn when you want to manage multiple named conversations:

resp, err := agent.AskIn(ctx, "support-ticket-123", "What happened last?")

Use Run when you need full control over message roles, multiple messages, or advanced conversation wiring.

Error Policy

Controls what happens when a tool returns an error:

  • ErrorPolicyStop (default): terminate the loop immediately
  • ErrorPolicyContinue: feed the error back to the LLM so it can adapt

Middleware

Intercept provider calls for logging, retries, rate limiting, etc:

logging := forge.Middleware(func(next forge.RunFunc) forge.RunFunc {
    return func(ctx context.Context, req forge.ProviderRequest) (*forge.ProviderResponse, error) {
        log.Printf("calling provider with %d messages", len(req.Messages))
        resp, err := next(ctx, req)
        if err == nil {
            log.Printf("provider returned: %s", resp.FinishReason)
        }
        return resp, err
    }
})

agent, _ := forge.NewAgent(forge.Config{
    Provider:   myProvider,
    Middleware: []forge.Middleware{logging},
})

Middleware composes as decorators: given [A, B, C], request flows A -> B -> C -> provider -> C -> B -> A.

Memory

Forge uses in-memory conversation history by default. Repeated Ask calls on the same agent continue the same default conversation:

agent, _ := forge.NewAgent(forge.Config{
    Provider: myProvider,
})

resp, _ := agent.Ask(ctx, "My name is Ameer.")
resp, _ = agent.Ask(ctx, "What is my name?")

For named conversations:

resp, _ := agent.AskIn(ctx, "conv-1", "Hi")
resp, _ = agent.AskIn(ctx, "conv-1", "What did I just say?")

Disable memory explicitly for stateless agents:

agent, _ := forge.NewAgent(forge.Config{
    Provider:      myProvider,
    DisableMemory: true,
})

Implement MemoryStore for persistent storage (SQLite, Redis, etc.):

type MemoryStore interface {
    Load(ctx context.Context, conversationID string) ([]Message, error)
    Save(ctx context.Context, conversationID string, messages []Message) error
    Clear(ctx context.Context, conversationID string) error
}

Or opt into a supplied memory implementation explicitly:

import "github.com/katasec/forge/memory/inmem"

agent, _ := forge.NewAgent(forge.Config{
    Provider: myProvider,
    Memory:   inmem.New(),
})

Metadata

Attach arbitrary key-value data to the context, accessible by tools and middleware:

ctx := forge.WithMetadata(context.Background(), forge.Metadata{
    Values: map[string]string{"user_id": "123", "tenant": "acme"},
})

// Inside a tool:
if meta, ok := forge.MetadataFromContext(ctx); ok {
    userID := meta.Values["user_id"]
}

Examples

See the _examples directory for runnable demos:

  • hello-world: simplest possible example: call Claude or xAI with one flag swap
  • calculator: agent with math tools, middleware, and a mock provider
  • chat-console: interactive console app showing forge.Config, explicit memory, Ask, and LastText

License

MIT

About

Go agent framework

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages