Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

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.
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

```bash
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
go get github.com/katasec/forge/provider/xai # optional: xAI Responses API with web search
```

## Quick Start
Expand Down Expand Up @@ -92,7 +92,7 @@ type Provider interface {

### Tools

Define tools with `Func[T]` — the JSON schema for parameters is derived from the Go struct at construction time using [invopop/jsonschema](https://github.com/invopop/jsonschema):
Define tools with `Func[T]`. The JSON schema for parameters is derived from the Go struct at construction time using [invopop/jsonschema](https://github.com/invopop/jsonschema):

```go
type SearchInput struct {
Expand Down Expand Up @@ -123,9 +123,9 @@ type Tool interface {

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`
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`:
Expand All @@ -147,8 +147,8 @@ Use `Run` when you need full control over message roles, multiple messages, or a

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
- `ErrorPolicyStop` (default): terminate the loop immediately
- `ErrorPolicyContinue`: feed the error back to the LLM so it can adapt

### Middleware

Expand All @@ -172,7 +172,7 @@ agent, _ := forge.NewAgent(forge.Config{
})
```

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

### Memory

Expand Down Expand Up @@ -243,8 +243,9 @@ if meta, ok := forge.MetadataFromContext(ctx); ok {

See the [`_examples`](./_examples) directory for runnable demos:

- **[hello-world](./_examples/hello-world)** — Simplest possible example: call Claude or xAI with one flag swap
- **[calculator](./_examples/calculator)** — Agent with math tools, middleware, and a mock provider
- **[hello-world](./_examples/hello-world)**: simplest possible example: call Claude or xAI with one flag swap
- **[calculator](./_examples/calculator)**: agent with math tools, middleware, and a mock provider
- **[chat-console](./_examples/chat-console)**: interactive console app showing `forge.Config`, explicit memory, `Ask`, and `LastText`

## License

Expand Down
6 changes: 4 additions & 2 deletions _examples/calculator/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Calculator

Demonstrates tool use with a mock provider — no API key needed.
Demonstrates tool use with a mock provider. No API key needed.

## Run

Expand Down Expand Up @@ -31,6 +31,8 @@ Everything is in `main.go`:
- **Tools**: `add` and `multiply` using `forge.Func[T]` with typed inputs
- **Mock provider**: Simulates an LLM that decides to call the `add` tool, then formulates a response from the result
- **Logging middleware**: Prints each provider call and its finish reason
- **Agent loop**: Shows the full cycle — provider call → tool execution → provider call → stop
- **Agent setup**: Uses `forge.Config` to wire provider, tools, middleware, and loop policy
- **Agent loop**: Shows the full cycle: provider call, tool execution, provider call, stop
- **Common API**: Uses `agent.Ask` and `resp.LastText` rather than manual message wiring

This example is useful for understanding how the agent loop works without needing API credentials.
6 changes: 3 additions & 3 deletions _examples/calculator/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Calculator example — demonstrates forge with math tools and a mock provider.
// Calculator demonstrates forge with math tools and a mock provider.
//
// This uses a mock provider that simulates an LLM deciding to call tools.
// Replace MockProvider with a real provider (Anthropic, OpenAI, etc.) to
Expand Down Expand Up @@ -103,7 +103,7 @@ func main() {
}
})

// Build the agent.
// Build the agent runtime once: provider, tools, middleware, and loop policy.
agent, err := forge.NewAgent(forge.Config{
Provider: &MockProvider{},
Tools: []forge.Tool{addTool, mulTool},
Expand All @@ -116,7 +116,7 @@ func main() {
log.Fatal(err)
}

// Run the agent.
// Ask drives the full provider -> tool -> provider loop.
fmt.Println("User: What is 12 + 30?")
fmt.Println(strings.Repeat("-", 40))

Expand Down
1 change: 1 addition & 0 deletions _examples/chat-console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
An interactive console app that shows the intended forge developer experience:

- configure an agent once with `forge.Config`
- plug in an explicit memory implementation with `memory/inmem`
- talk to it with `Ask`
- read the latest answer with `LastText`
- keep conversation context in memory across turns
Expand Down
38 changes: 14 additions & 24 deletions _examples/hello-world/README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
# Hello World

The simplest possible forge example — call an LLM and get a response.
The smallest forge example: pick a provider, build an agent with `forge.Config`, ask one question, and print the latest assistant text.

## Run with Claude (Anthropic)
## Run with Claude

```bash
export ANTHROPIC_API_KEY=sk-ant-...
go run .
```

## Run with Grok (xAI)
## Run with Grok

```bash
export XAI_API_KEY=xai-...
go run . -provider xai
```

## What's in here
## Run with xAI Search

| File | What it does |
|------|-------------|
| `main.go` | Picks a provider, builds an agent, sends "Hello!", prints the response |
| `anthropic.go` | `AnthropicProvider` — implements `forge.Provider` using the Anthropic Messages API |
| `openai_compat.go` | `OpenAIProvider` — implements `forge.Provider` for any OpenAI-compatible API (xAI, OpenAI, Together, Groq, etc.) |

## Swapping providers

The only thing that changes is one line:

```go
// Claude
provider := NewAnthropicProvider(os.Getenv("ANTHROPIC_API_KEY"), "claude-sonnet-4-20250514")

// xAI Grok
provider := NewOpenAIProvider("https://api.x.ai/v1", os.Getenv("XAI_API_KEY"), "grok-3-mini")

// OpenAI
provider := NewOpenAIProvider("https://api.openai.com/v1", os.Getenv("OPENAI_API_KEY"), "gpt-4o")
```bash
export XAI_API_KEY=xai-...
go run . -provider xai-search
```

Everything else — agent config, tools, middleware, memory — stays the same regardless of provider.
## What this shows

- `forge.Config` as the agent setup point
- provider swapping without changing app code
- `agent.Ask(ctx, prompt)` for the common path
- `resp.LastText()` for the latest assistant answer
- provider-specific access to xAI citations when search is enabled
14 changes: 7 additions & 7 deletions _examples/hello-world/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Hello World the simplest possible forge example.
// Hello World is the simplest possible forge example.
//
// Shows how to call Claude with your Anthropic API key, and how to
// swap to xAI's Grok by changing one line.
Expand All @@ -8,7 +8,7 @@
// export ANTHROPIC_API_KEY=sk-ant-...
// go run .
//
// # Or use xAI (OpenAI-compatible) instead:
// # Or use xAI's OpenAI-compatible endpoint instead:
// export XAI_API_KEY=xai-...
// go run . -provider xai
//
Expand All @@ -34,7 +34,7 @@ func main() {
providerFlag := flag.String("provider", "anthropic", "Provider to use: anthropic, xai, or xai-search")
flag.Parse()

// Pick your provider — this is the only thing that changes.
// Pick your provider. The agent setup below stays the same.
var provider forge.Provider
var xaiProvider *xai.Provider // for citation access
switch *providerFlag {
Expand All @@ -61,7 +61,7 @@ func main() {
log.Fatalf("Unknown provider: %s (use 'anthropic', 'xai', or 'xai-search')", *providerFlag)
}

// Build the agent — same code regardless of provider.
// Build the agent: provider, prompt, and runtime behavior live in Config.
agent, err := forge.NewAgent(forge.Config{
Provider: provider,
SystemPrompt: "You are a helpful assistant. Keep responses brief.",
Expand All @@ -70,7 +70,7 @@ func main() {
log.Fatal(err)
}

// Ask preserves conversation history on this agent by default.
// Ask is the common path: user text in, AgentResponse out.
resp, err := agent.Ask(context.Background(), "Hello! What are you?")
if err != nil {
log.Fatal(err)
Expand All @@ -79,12 +79,12 @@ func main() {
fmt.Println(resp.LastText())
fmt.Printf("\n[%s | tokens: %d in, %d out]\n", *providerFlag, resp.Usage.InputTokens, resp.Usage.OutputTokens)

// Show citations if using xai-search.
// xAI search exposes provider-specific citations after the run.
if xaiProvider != nil {
if citations := xaiProvider.LastCitations(); len(citations) > 0 {
fmt.Println("\nSources:")
for i, c := range citations {
fmt.Printf(" [%d] %s %s\n", i+1, c.Title, c.URL)
fmt.Printf(" [%d] %s - %s\n", i+1, c.Title, c.URL)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (r *AgentResponse) LastText() string {
return ""
}

// Agent orchestrates the LLM call tool execution response loop.
// Agent orchestrates the LLM call -> tool execution -> response loop.
type Agent struct {
provider Provider
registry *ToolRegistry
Expand Down Expand Up @@ -156,7 +156,7 @@ func (a *Agent) Run(ctx context.Context, req AgentRequest) (*AgentResponse, erro
break
}

// FinishReason is tool_use execute the tool calls.
// FinishReason is tool_use - execute the tool calls.
toolResults := a.executor.Execute(ctx, resp.Message.ToolCalls)

// Check for tool errors.
Expand Down
31 changes: 15 additions & 16 deletions docs/design/design.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Forge Type Specification & Design
# Forge - Type Specification & Design

> This is the authoritative specification for the forge Go agent framework.
> All implementation should conform to this document.
Expand All @@ -7,7 +7,7 @@

```
module github.com/katasec/forge
go 1.23
go 1.25.6
```

Root package: `package forge` — the primary user-facing facade: `Config`, `Agent`, `Ask`, `AskIn`, core interfaces, and type aliases for common concepts.
Expand Down Expand Up @@ -121,10 +121,10 @@ const (
```

**Semantics:**
- `stop` the provider returned a response with no tool calls. The agent loop terminates normally.
- `tool_use` the provider requested tool calls. The loop continues to execute them. This value appears in `ProviderResponse` but never as a final `AgentResponse.FinishReason` (the loop always processes tool calls before returning).
- `iter_limit` the loop hit `Config.MaxIterations`. The agent returns whatever content the last assistant message contained.
- `error` a provider error occurred, or a tool error occurred with `ErrorPolicyStop`.
- `stop` - the provider returned a response with no tool calls. The agent loop terminates normally.
- `tool_use` - the provider requested tool calls. The loop continues to execute them. This value appears in `ProviderResponse` but never as a final `AgentResponse.FinishReason` (the loop always processes tool calls before returning).
- `iter_limit` - the loop hit `Config.MaxIterations`. The agent returns whatever content the last assistant message contained.
- `error` - a provider error occurred, or a tool error occurred with `ErrorPolicyStop`.

### TokenUsage

Expand All @@ -149,8 +149,8 @@ const (
```

Controls behavior when a tool invocation returns an error (`ToolResult.IsError == true`):
- `stop` the agent loop terminates immediately with `FinishReasonError`.
- `continue` the error is fed back to the LLM as a tool result so it can recover or try a different approach.
- `stop` - the agent loop terminates immediately with `FinishReasonError`.
- `continue` - the error is fed back to the LLM as a tool result so it can recover or try a different approach.

### ToolError

Expand Down Expand Up @@ -180,9 +180,9 @@ func WithMetadata(ctx context.Context, m Metadata) context.Context
func MetadataFromContext(ctx context.Context) (Metadata, bool)
```

- `WithMetadata` stores a `Metadata` in the context using the unexported `metadataKey`.
- `WithMetadata` stores a `Metadata` in the context using the unexported metadata key.
- `MetadataFromContext` retrieves it; returns `false` if not present.
- `Metadata.Values` is never nil after construction `WithMetadata` should initialize the map if nil.
- `Metadata.Values` is never nil after construction. `WithMetadata` should initialize the map if nil.

---

Expand Down Expand Up @@ -338,7 +338,7 @@ Middleware wraps `RunFunc` in the standard decorator pattern.
**Composition order:** middlewares are applied innermost-last. Given `[A, B, C]`:

```
request A B C provider.Generate C B A response
request -> A -> B -> C -> provider.Generate -> C -> B -> A -> response
```

Applied as:
Expand Down Expand Up @@ -421,7 +421,7 @@ func (a *Agent) AskIn(ctx context.Context, conversationID, prompt string) (*Agen
- Both return the full `AgentResponse`, preserving access to conversation ID, token usage, finish reason, errors, and message history.
- `LastText` returns the latest assistant text content in the response.

### Agent Loop `Agent.Run(ctx, req)`
### Agent Loop - `Agent.Run(ctx, req)`

```
func (a *Agent) Run(ctx context.Context, req AgentRequest) (*AgentResponse, error)
Expand Down Expand Up @@ -463,7 +463,7 @@ Pseudocode:
31. finishReason = FinishReasonStop
32. break LOOP
33.
34. // FinishReason is tool_use execute the tool calls
34. // FinishReason is tool_use - execute the tool calls
35. toolResults = executor.Execute(ctx, providerResp.Message.ToolCalls)
36.
37. // Check for tool errors
Expand Down Expand Up @@ -495,10 +495,10 @@ Pseudocode:
```

**Key behaviors:**
- Provider errors (line 23) are always fatal — they return an error, not an `AgentResponse`.
- Provider errors (line 23) are always fatal. They return an error, not an `AgentResponse`.
- Tool errors with `ErrorPolicyContinue` are collected in `errors` but the loop continues, letting the LLM see the error and adapt.
- Tool errors with `ErrorPolicyStop` break the loop immediately but still include the tool results in the message history.
- `FinishReasonToolUse` never appears in the final `AgentResponse` — the loop always processes tool calls.
- `FinishReasonToolUse` never appears in the final `AgentResponse`. The loop always processes tool calls.
- Memory is enabled by default with an in-memory store. It is saved once at the end, with the complete conversation.
- File-backed or database-backed memory must be explicitly configured because persistence changes privacy and lifecycle expectations.
- Context cancellation is respected: `composedRunFunc` and `tool.Invoke` should check `ctx`.
Expand All @@ -515,4 +515,3 @@ Tracked separately in `docs/design/future-refinements.md`. Not in scope for the
- Persistent memory stores (SQLite, Redis)
- Structured output / response format constraints
- Token budget management (auto-truncate history)
- Agent-to-agent delegation
1 change: 0 additions & 1 deletion docs/design/future-refinements.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@
| Persistent memory stores | SQLite, Redis, or file-backed `MemoryStore` implementations |
| Structured output | Response format constraints passed to providers |
| Token budget management | Auto-truncate or summarize history when approaching limits |
| Agent-to-agent delegation | One agent invoking another as a tool |
2 changes: 1 addition & 1 deletion provider/xai/xai.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func parseResponse(resp *response) (*forge.ProviderResponse, []Citation) {
}
}
// Server-side tool calls (web_search_call, x_search_call, etc.)
// are auto-executed by xAI we don't surface them.
// are auto-executed by xAI, so we don't surface them.
}
}

Expand Down
Loading