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
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

Expand All @@ -22,9 +21,23 @@ jobs:
- name: Vet
run: go vet ./...

- name: Vet examples
run: |
cd _examples/hello-world
go vet ./...
cd ../calculator
go vet ./...

- name: Test
run: gotestsum --junitfile test-results.xml -- ./... -v

- name: Test examples
run: |
cd _examples/hello-world
go test ./...
cd ../calculator
go test ./...

- name: Test Summary
uses: test-summary/action@v2
if: always()
Expand Down
55 changes: 35 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,12 @@ func main() {
log.Fatal(err)
}

resp, err := agent.Run(context.Background(), forge.AgentRequest{
Messages: []forge.Message{
{Role: forge.RoleUser, Content: "Hello! What are you?"},
},
})
resp, err := agent.Ask(context.Background(), "Hello! What are you?")
if err != nil {
log.Fatal(err)
}

fmt.Println(resp.Messages[len(resp.Messages)-1].Content)
fmt.Println(resp.LastText())
}
```

Expand Down Expand Up @@ -125,13 +121,28 @@ type Tool interface {

`Agent.Run` executes this loop:

1. Load conversation history from memory (if configured)
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`:

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

Use `AskIn` when you want to manage multiple named conversations:

```go
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:
Expand Down Expand Up @@ -165,26 +176,30 @@ Middleware composes as decorators: given `[A, B, C]`, request flows `A → B →

### Memory

Persist conversations across `Agent.Run` calls:
Forge uses in-memory conversation history by default. Repeated `Ask` calls on the same agent continue the same default conversation:

```go
store := forge.NewInMemoryStore()

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

// First call — starts a conversation.
resp, _ := agent.Run(ctx, forge.AgentRequest{
ConversationID: "conv-1",
Messages: []forge.Message{{Role: forge.RoleUser, Content: "Hi"}},
})
resp, _ := agent.Ask(ctx, "My name is Ameer.")
resp, _ = agent.Ask(ctx, "What is my name?")
```

For named conversations:

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

Disable memory explicitly for stateless agents:

// Second call — continues the same conversation.
resp, _ = agent.Run(ctx, forge.AgentRequest{
ConversationID: "conv-1",
Messages: []forge.Message{{Role: forge.RoleUser, Content: "What did I just say?"}},
```go
agent, _ := forge.NewAgent(forge.Config{
Provider: myProvider,
DisableMemory: true,
})
```

Expand Down
10 changes: 3 additions & 7 deletions _examples/calculator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func main() {
agent, err := forge.NewAgent(forge.Config{
Provider: &MockProvider{},
Tools: []forge.Tool{addTool, mulTool},
Middleware: []forge.Middleware{logging},
Middleware: []forge.Middleware{logging},
SystemPrompt: "You are a helpful calculator assistant.",
MaxIterations: 5,
ErrorPolicy: forge.ErrorPolicyContinue,
Expand All @@ -120,17 +120,13 @@ func main() {
fmt.Println("User: What is 12 + 30?")
fmt.Println(strings.Repeat("-", 40))

resp, err := agent.Run(context.Background(), forge.AgentRequest{
Messages: []forge.Message{
{Role: forge.RoleUser, Content: "What is 12 + 30?"},
},
})
resp, err := agent.Ask(context.Background(), "What is 12 + 30?")
if err != nil {
log.Fatal(err)
}

fmt.Println(strings.Repeat("-", 40))
fmt.Printf("Assistant: %s\n", resp.Messages[len(resp.Messages)-1].Content)
fmt.Printf("Assistant: %s\n", resp.LastText())
fmt.Printf("Finish reason: %s\n", resp.FinishReason)
fmt.Printf("Tokens: %d in, %d out\n", resp.Usage.InputTokens, resp.Usage.OutputTokens)
fmt.Printf("Conversation: %d messages\n", len(resp.Messages))
Expand Down
10 changes: 3 additions & 7 deletions _examples/hello-world/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,13 @@ func main() {
log.Fatal(err)
}

// Run it.
resp, err := agent.Run(context.Background(), forge.AgentRequest{
Messages: []forge.Message{
{Role: forge.RoleUser, Content: "Hello! What are you?"},
},
})
// Ask preserves conversation history on this agent by default.
resp, err := agent.Ask(context.Background(), "Hello! What are you?")
if err != nil {
log.Fatal(err)
}

fmt.Println(resp.Messages[len(resp.Messages)-1].Content)
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.
Expand Down
63 changes: 47 additions & 16 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,28 @@ type AgentResponse struct {
Errors []ToolError `json:"errors,omitempty"`
}

// LastText returns the latest assistant text in the response.
func (r *AgentResponse) LastText() string {
for i := len(r.Messages) - 1; i >= 0; i-- {
msg := r.Messages[i]
if msg.Role == RoleAssistant && msg.Content != "" {
return msg.Content
}
}
return ""
}

// Agent orchestrates the LLM call → tool execution → response loop.
type Agent struct {
provider Provider
registry *ToolRegistry
executor ToolExecutor
run RunFunc
memory MemoryStore
systemPrompt string
maxIterations int
errorPolicy ErrorPolicy
provider Provider
registry *ToolRegistry
executor ToolExecutor
run RunFunc
memory MemoryStore
defaultConversationID string
systemPrompt string
maxIterations int
errorPolicy ErrorPolicy
}

// NewAgent creates an Agent from the given Config.
Expand All @@ -58,18 +70,37 @@ func NewAgent(cfg Config) (*Agent, error) {
errorPolicy = ErrorPolicyStop
}

memory := cfg.Memory
if memory == nil && !cfg.DisableMemory {
memory = NewInMemoryStore()
}

return &Agent{
provider: cfg.Provider,
registry: registry,
executor: executor,
run: run,
memory: cfg.Memory,
systemPrompt: cfg.SystemPrompt,
maxIterations: cfg.MaxIterations,
errorPolicy: errorPolicy,
provider: cfg.Provider,
registry: registry,
executor: executor,
run: run,
memory: memory,
defaultConversationID: uuid.New().String(),
systemPrompt: cfg.SystemPrompt,
maxIterations: cfg.MaxIterations,
errorPolicy: errorPolicy,
}, nil
}

// Ask sends a user prompt in the agent's default conversation.
func (a *Agent) Ask(ctx context.Context, prompt string) (*AgentResponse, error) {
return a.AskIn(ctx, a.defaultConversationID, prompt)
}

// AskIn sends a user prompt in the named conversation.
func (a *Agent) AskIn(ctx context.Context, conversationID, prompt string) (*AgentResponse, error) {
return a.Run(ctx, AgentRequest{
ConversationID: conversationID,
Messages: []Message{UserMessage(prompt)},
})
}

// Run executes the agent loop per the design spec pseudocode.
func (a *Agent) Run(ctx context.Context, req AgentRequest) (*AgentResponse, error) {
conversationID := req.ConversationID
Expand Down
Loading
Loading