diff --git a/README.md b/README.md index 9fefe68..7184105 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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 @@ -10,7 +10,7 @@ Forge handles the **LLM call → tool execution → response** cycle. You supply 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 @@ -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 { @@ -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`: @@ -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 @@ -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 @@ -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 diff --git a/_examples/calculator/README.md b/_examples/calculator/README.md index 7492f30..6467024 100644 --- a/_examples/calculator/README.md +++ b/_examples/calculator/README.md @@ -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 @@ -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. diff --git a/_examples/calculator/main.go b/_examples/calculator/main.go index 11899ca..c757b0c 100644 --- a/_examples/calculator/main.go +++ b/_examples/calculator/main.go @@ -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 @@ -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}, @@ -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)) diff --git a/_examples/chat-console/README.md b/_examples/chat-console/README.md index 4de8d54..9bb3432 100644 --- a/_examples/chat-console/README.md +++ b/_examples/chat-console/README.md @@ -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 diff --git a/_examples/hello-world/README.md b/_examples/hello-world/README.md index 19b9567..e882d41 100644 --- a/_examples/hello-world/README.md +++ b/_examples/hello-world/README.md @@ -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 diff --git a/_examples/hello-world/main.go b/_examples/hello-world/main.go index a85f490..79cfe0f 100644 --- a/_examples/hello-world/main.go +++ b/_examples/hello-world/main.go @@ -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. @@ -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 // @@ -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 { @@ -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.", @@ -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) @@ -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) } } } diff --git a/agent.go b/agent.go index d83dfde..32fb5f4 100644 --- a/agent.go +++ b/agent.go @@ -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 @@ -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. diff --git a/docs/design/design.md b/docs/design/design.md index 7c427c1..137cd76 100644 --- a/docs/design/design.md +++ b/docs/design/design.md @@ -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. @@ -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. @@ -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 @@ -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 @@ -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. --- @@ -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: @@ -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) @@ -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 @@ -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`. @@ -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 diff --git a/docs/design/future-refinements.md b/docs/design/future-refinements.md index f40a597..23739b5 100644 --- a/docs/design/future-refinements.md +++ b/docs/design/future-refinements.md @@ -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 | diff --git a/provider/xai/xai.go b/provider/xai/xai.go index 50cf1ca..01a9083 100644 --- a/provider/xai/xai.go +++ b/provider/xai/xai.go @@ -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. } }