agent

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Apr 30, 2026 License: MIT Imports: 20 Imported by: 0

README

🤖 react-agent

Go Reference Go Report Card

Build AI agents that think before they act.

Most LLM integrations are one-shot: send a prompt, get an answer. But complex questions require reasoning — looking things up, checking results, adjusting the plan. react-agent implements the ReAct pattern (Reason + Act), a technique where the model alternates between thinking out loud and using tools until it's confident enough to answer.

📄 Based on "ReAct: Synergizing Reasoning and Acting in Language Models" — Yao et al., 2022


🧠 What is the ReAct pattern?

Think of it like a detective 🕵️ who never guesses. Instead of jumping to a conclusion, they follow a strict method: form a hypothesis → gather evidence → revise → repeat until the case is solved.

flowchart TD
    Q(["❓ User Question"])
    THINK["🧠 Think\nWhat do I need to find out?"]
    DECIDE{{"🤔 Need\na tool?"}}
    ACT["🔧 Act\nCall a tool"]
    OBSERVE["👁️ Observe\nRead tool output"]
    ANSWER["✅ Answer\nReturn final response"]
    LIMIT{{"🚧 Max steps\nreached?"}}
    ERR(["❌ ErrMaxStepsReached"])

    Q --> THINK
    THINK --> DECIDE
    DECIDE -->|"Yes 🛠️"| ACT
    ACT --> OBSERVE
    OBSERVE --> LIMIT
    LIMIT -->|"No"| THINK
    LIMIT -->|"Yes"| ERR
    DECIDE -->|"No, I know enough ✨"| ANSWER

🔍 A Concrete Example

"What was Apple's stock price the day the iPhone was announced?"

A one-shot model will guess. A ReAct agent will reason:

sequenceDiagram
    participant U as 👤 User
    participant A as 🤖 Agent
    participant L as 🧠 LLM
    participant T as 🔧 search_web

    U->>A: "What was Apple's stock price the day the iPhone was announced?"

    A->>L: Think 💭
    L-->>A: I need the announcement date first
    A->>T: search_web("first iPhone announcement date")
    T-->>A: "January 9, 2007" 📅

    A->>L: Think 💭 (now I have the date)
    L-->>A: Now I need the stock price on that date
    A->>T: search_web("AAPL stock price January 9 2007")
    T-->>A: "$11.74" 📈

    A->>L: Think 💭 (I have everything I need)
    L-->>A: ✅ Final answer

    A-->>U: "Apple's stock was $11.74 on Jan 9, 2007 — the day Steve Jobs unveiled the iPhone."

💡 Notice how the agent builds on previous observations — each step's result is fed back into the next Think. The LLM never loses context.


📦 Installation

go get github.com/v8tix/react-agent

🚀 Quick Start

Here's a complete example: a research assistant 🔬 that can search the web and do math to answer complex questions.

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/openai/openai-go"
    agent "github.com/v8tix/react-agent"
)

func main() {
    ctx := context.Background()

    // 1️⃣ Wrap your LLM (any OpenAI-compatible endpoint, including LiteLLM)
    openaiClient := openai.NewClient() // reads OPENAI_API_KEY from env
    client := agent.NewLiteLLMClient(openaiClient, "gpt-4o-mini")

    // 2️⃣ Declare the tools the agent can use (OpenAI JSON-schema format)
    defs := []agent.ToolDefinition{
        {
            Name:        "search_web",
            Description: "Search the web for up-to-date information",
            Parameters: map[string]any{
                "type": "object",
                "properties": map[string]any{
                    "query": map[string]any{"type": "string", "description": "The search query"},
                },
                "required": []string{"query"},
            },
        },
        {
            Name:        "calculator",
            Description: "Evaluate a simple arithmetic expression",
            Parameters: map[string]any{
                "type": "object",
                "properties": map[string]any{
                    "expression": map[string]any{"type": "string", "description": "e.g. '42 * 1.2'"},
                },
                "required": []string{"expression"},
            },
        },
    }

    // 3️⃣ Wire up the executor — your code that actually runs the tools
    executor := &myExecutor{}

    // 4️⃣ Build the agent with a fluent builder chain
    a := agent.New(client, defs, executor).
        WithInstructions("You are a precise research assistant. Always verify facts before answering.").
        WithMaxSteps(10)

    // 5️⃣ Run! Returns result, a replayable event stream, and any error.
    result, events, err := a.Run(ctx, "How many seconds did it take the Voyager 1 spacecraft to travel 1 AU?")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("🏁 Answer:", result.Output)
    _ = events // see "Observability" section below
}

🏗️ Architecture

graph TB
    subgraph "Your Code"
        U(["👤 Caller"])
        EX["🔧 ToolExecutor\nimpl"]
        B["🪝 BeforeToolCallback"]
        AFT["🪝 AfterToolCallback"]
        HITL["✅ External approver / UI"]
    end

    subgraph "react-agent"
        AG["🤖 Agent\norchestrator"]
        EC["📋 ExecutionContext\nmessage history"]
        LP["🔁 ReAct Loop\nstep controller"]
        IR["⏸️ InteractionRequest /\nSuspendedRun"]
    end

    subgraph "External"
        LLM["🧠 LLM\n(OpenAI / LiteLLM)"]
        TOOLS["🛠️ Tools\n(search, DB, APIs...)"]
    end

    U -->|"New(...).WithX().Run()"| AG
    U -->|"Resume(...)"| AG
    AG --> EC
    AG --> LP
    SR["🧵 SessionRunner"]
    SM["💾 SessionManager"]
    MUT["🧠 MutatingLLMClient /\nRequestMutators"]
    MEM["📚 TaskMemoryManager /\nMemoryInjector"]
    OPT["🪶 ContextOptimizer /\nSummarization"]
    SR -->|"Run/Resume"| AG
    SR -->|"load + save"| SM
    LP -->|"prepare request"| MUT
    MUT -->|"inject memories"| MEM
    MUT -->|"shrink / summarize"| OPT
    MUT -->|"Generate(mutated request)"| LLM
    LLM -->|"ToolCall or Answer"| LP
    LP -->|"BeforeTool(call)"| B
    B -->|"override / continue"| LP
    B -.->|"Suspend(req)"| IR
    IR -.->|"emit + return"| U
    U -.->|"approve / deny"| HITL
    HITL -.->|"InteractionResponse"| AG
    LP -->|"Execute(calls)"| EX
    EX -->|"dispatch"| TOOLS
    TOOLS -->|"results"| EX
    EX -->|"ToolResult[]"| LP
    LP -->|"AfterTool(result)"| AFT
    AFT -->|"replace / keep"| LP
    LP -->|"*Result"| AG
    AG -->|"*Result, Observable, error"| U

🔧 Implementing ToolExecutor

The only interface you must implement:

type ToolExecutor interface {
    Execute(ctx context.Context, calls []model.ToolCall) ([]model.ToolResult, error)
}

A typical implementation dispatches by tool name:

type myExecutor struct {
    searcher WebSearcher
    calc     Calculator
}

func (e *myExecutor) Execute(ctx context.Context, calls []model.ToolCall) ([]model.ToolResult, error) {
    results := make([]model.ToolResult, len(calls))
    for i, call := range calls {
        output, err := e.dispatch(ctx, call.Name, call.Arguments)
        if err != nil {
            results[i] = model.ToolResult{
                ID: call.ID, Name: call.Name,
                Status: "error", Content: []string{err.Error()},
            }
            continue
        }
        results[i] = model.ToolResult{
            ID: call.ID, Name: call.Name,
            Status: "success", Content: []string{output},
        }
    }
    return results, nil
}

func (e *myExecutor) dispatch(ctx context.Context, name, args string) (string, error) {
    switch name {
    case "search_web":
        return e.searcher.Search(ctx, args)
    case "calculator":
        return e.calc.Eval(args)
    default:
        return "", fmt.Errorf("unknown tool: %s", name)
    }
}

💡 ToolExecutor is the integration seam — plug in MCP, LangChain tools, a local SQLite, a REST API, anything. The agent doesn't care what's behind it.


🎓 Learning Flows

💡 Study by scenario, not by type. Pick the flow that looks like your app, copy the pattern, and then mix flows together as your agent grows up.

❓ Where should I start?
flowchart TD
    START{{"What are you building?"}}

    START -->|"One-shot assistant"| BASIC["🏃 Flow 1 + 🔧 Flow 2"]
    START -->|"Production chatbot"| CHAT["🧵 Flow 5 + 🪶 Flow 6"]
    START -->|"Agent automation"| AUTO["🔧 Flow 2 + 📚 Flow 7"]
    START -->|"Human-supervised agent"| SAFE["⏸️ Flow 4 + 🧵 Flow 5"]
    START -->|"Need observability"| OBS["📡 Flow 3 + 📊 Flow 8"]
Flow Best for Complexity Core pieces
Flow 1 — 🏃 Basic Run direct answers, no tools Agent, LLMClient
Flow 2 — 🔧 Tool Use search, APIs, calculators ⭐⭐ ToolExecutor, ToolDefinition
Flow 3 — 📡 Event Stream logs, tracing, dashboards ⭐⭐ Observable, AgentEvent
Flow 4 — ⏸️ Approval Loop risky or destructive tools ⭐⭐⭐ Suspend, Resume, callbacks
Flow 5 — 🧵 Session Continuity chat, multi-turn memory ⭐⭐⭐ SessionRunner, SessionManager
Flow 6 — 🪶 Context Optimization long conversations ⭐⭐⭐⭐ MutatingLLMClient, strategies
Flow 7 — 📚 Long-Term Memory learning from past tasks ⭐⭐⭐⭐ TaskMemoryManager, MemoryInjector
Flow 8 — 📊 Request Logging debugging prompt pipelines ⭐⭐⭐ WithLogger, WithMutatorLogger

Flow 1 — 🏃 Basic Run: no tools, just reasoning

Study this flow when: you want the cleanest possible setup — one prompt in, one answer out.

sequenceDiagram
    participant U as 👤 User
    participant A as 🤖 Agent
    participant L as 🧠 LLM

    U->>A: "What is the capital of France?"
    A->>L: Generate(request)
    L-->>A: "Paris."
    A-->>U: Result{Output: "Paris."}
a := agent.New(client, nil, nil).
    WithInstructions("You are a helpful assistant.")

result, _, err := a.Run(ctx, "What is the capital of France?")
if err != nil {
    log.Fatal(err)
}

fmt.Println(result.Output)
fmt.Println("tool called:", result.ToolCalled)

🙂 Friendly rule of thumb: start here first. If this solves your use case, you probably do not need sessions, approvals, or memory yet.


Flow 2 — 🔧 Tool Use: multi-step reasoning with evidence

Study this flow when: your model needs to look something up instead of guessing.

sequenceDiagram
    participant U as 👤 User
    participant A as 🤖 Agent
    participant L as 🧠 LLM
    participant X as 🔧 ToolExecutor
    participant T as 🛠️ Tool

    U->>A: Ask a question that needs external data
    A->>L: Think
    L-->>A: ToolCall(search_web)
    A->>X: Execute(tool calls)
    X->>T: dispatch
    T-->>X: result
    X-->>A: ToolResult[]
    A->>L: Observe and think again
    L-->>A: Final answer
    A-->>U: Result
defs := []model.ToolDefinition{{
    Name:        "search_web",
    Description: "Search the web for current information",
    Parameters: map[string]any{
        "type": "object",
        "properties": map[string]any{
            "query": map[string]any{"type": "string"},
        },
        "required": []string{"query"},
    },
}}

a := agent.New(client, defs, executor).
    WithInstructions("Verify facts before answering.")

result, _, err := a.Run(ctx, "What was Apple's stock price the day the iPhone was announced?")
if err != nil {
    log.Fatal(err)
}

fmt.Println(result.Output)

What to study here:

  1. The agent writes ToolCall items into history before execution.
  2. Your ToolExecutor decides how each tool name is actually dispatched.
  3. Tool failures are still useful — the model can read them and recover on the next step.

Flow 3 — 📡 Event Stream: observability without extra plumbing

Study this flow when: you want to see what the agent is doing while keeping the core orchestration code unchanged.

Run() returns three values:

result, events, err := a.Run(ctx, question)
//        ^^^^^^
//        rxgo.Observable — a replayable stream of everything the agent did
timeline
    title Events emitted during one agent run
    RunStart    : 🚀 RunStartEvent
    Step 1      : 📍 StepStartEvent
                : 🧠 LLMCallEvent
                : 🪝 CallbackEvent
                : 🔧 ToolExecEvent
                : 📍 StepEndEvent
    Step 2      : 📍 StepStartEvent
                : 🧠 LLMCallEvent
                : ⏸️ InteractionRequestedEvent
                : ▶️ InteractionResumedEvent
                : 🔧 ToolExecEvent
                : 📍 StepEndEvent
    RunEnd      : 🏁 RunEndEvent
Event Why you care
RunStartEvent identify a run and the original user question
StepStartEvent / StepEndEvent measure loop progress
LLMCallEvent track model latency and failures
CallbackEvent observe approval or rewrite hooks
ToolExecEvent track tool batches and latency
InteractionRequestedEvent surface human input requests
InteractionResumedEvent confirm resume handoff
RunEndEvent capture final result or terminal error
result, events, err := a.Run(ctx, question)
if err != nil {
    log.Fatal(err)
}

for item := range events.Observe() {
    switch e := item.V.(type) {
    case agent.RunStartEvent:
        slog.Info("run started", "run_id", e.RunID, "question", e.UserMessage)
    case agent.ToolExecEvent:
        slog.Info("tool exec", "tools", e.ToolNames, "latency_ms", e.Latency.Milliseconds())
    case agent.RunEndEvent:
        slog.Info("run ended", "err", e.Err)
    }
}

fmt.Println(result.Output)

🧊 Cold & replayable — every Observe() call replays the full run from the beginning, so one subscriber can log while another records metrics.


Flow 4 — ⏸️ Approval Loop: pause, ask a human, continue safely

Study this flow when: a tool can delete, charge, publish, or otherwise do something you want a human to approve.

sequenceDiagram
    participant U as 👤 User
    participant A as 🤖 Agent
    participant C as 🪝 BeforeToolCallback
    participant UI as ✅ External approver
    participant X as 🔧 ToolExecutor

    U->>A: "Delete danger.txt"
    A->>C: BeforeTool(delete_file)
    C-->>A: Suspend(InteractionRequest)
    A-->>UI: InteractionRequestedError
    UI-->>A: Resume(approved=true/false)
    A->>C: BeforeTool(delete_file) again
    alt approved
        C-->>A: continue
        A->>X: Execute(delete_file)
        X-->>A: ToolResult(success)
    else denied
        C-->>A: ToolResult(error)
    end
    A-->>U: final answer
approval := agent.NewConfirmationCallback(agent.StaticApprovalPolicy{
    "delete_file": {
        MessageTemplate: "Approve deleting this file?",
        DeniedMessage:   "Deletion cancelled by user.",
    },
})

a := agent.New(client, defs, executor).
    WithInstructions("Ask for approval before destructive tools.").
    WithBeforeToolCallbacks(approval).
    WithMaxSteps(8)
result, _, err := a.Run(ctx, "Delete danger.txt")
if err != nil {
    var suspended *agent.InteractionRequestedError
    if errors.As(err, &suspended) {
        approved := true
        result, _, err = a.Resume(ctx, suspended.Suspended, agent.InteractionResponse{
            RequestID: suspended.Suspended.Interaction.ID,
            Approved:  &approved,
        })
    }
}

Nice built-in detail: ConfirmationCallback automatically redacts sensitive-looking tool arguments before they land in InteractionRequest.Payload.

Payload pattern How it is handled
api_key, token, password, secret replaced with [redacted]
path-like fields sanitized before being surfaced
normal strings truncated and cleaned for safe display

Flow 5 — 🧵 Session Continuity: multi-turn conversations without losing context

Study this flow when: your caller is not a single CLI invocation — it's a chat UI, API, or worker that talks to the same user over time.

sequenceDiagram
    participant App as 👤 App / API
    participant SR as 🧵 SessionRunner
    participant SM as 💾 SessionManager
    participant AG as 🤖 Agent

    App->>SR: Run(sessionID, userID, "My name is Alice")
    SR->>SM: GetOrCreate(sessionID, userID)
    SM-->>SR: session(events=[], state={})
    SR->>AG: replay events + run agent
    AG-->>SR: RunResult{Status: complete}
    SR->>SM: Save(updated events)
    SR-->>App: "Nice to meet you, Alice!"

    App->>SR: Run(sessionID, userID, "What's my name?")
    SR->>SM: Get(sessionID)
    SM-->>SR: session(previous events)
    SR->>AG: replay events + run agent
    AG-->>SR: RunResult{Status: complete, Output: "Your name is Alice."}
    SR->>SM: Save(updated events)
    SR-->>App: "Your name is Alice."
sessions := agent.NewInMemorySessionManager()

runner := agent.NewSessionRunner(
    agent.New(client, defs, executor).WithMaxSteps(8),
    sessions,
    8,
)

first, err := runner.Run(ctx, "chat-42", "user-7", "My name is Alice")
if err != nil {
    log.Fatal(err)
}

second, err := runner.Run(ctx, "chat-42", "user-7", "What's my name?")
if err != nil {
    log.Fatal(err)
}

fmt.Println(first.Status)
fmt.Println(second.Output)

Study notes:

  1. SessionRunner replays stored model.Event values into a fresh execution context.
  2. The session store is pluggable — InMemorySessionManager is just the starter version.
  3. If a session suspends, you get StatusPending and can continue later with runner.Resume(...).

Flow 6 — 🪶 Context Optimization: managing token budgets without going blind

Study this flow when: your conversation gets long, tool outputs get noisy, or the model starts forgetting important recent context.

flowchart LR
    MSG["📝 Current request"] --> MUT["MutatingLLMClient"]
    MUT --> INJ["MemoryInjector (optional)"]
    INJ --> OPT["ContextOptimizer"]
    OPT --> SW["SlidingWindowStrategy"]
    OPT --> CP["CompactionStrategy"]
    OPT --> SUM["SummarizationStrategy"]
    SUM --> LLM["🧠 Underlying LLMClient"]
counter, err := agent.NewRequestTokenCounter("gpt-4o-mini")
if err != nil {
    log.Fatal(err)
}

optimizedClient := agent.NewMutatingLLMClient(
    client,
    agent.WithMutatorLogger(
        agent.NewContextOptimizer(
            counter,
            8_000,
            agent.NewSlidingWindowStrategy(8),
            agent.NewCompactionStrategy(),
        ),
        slog.Default(),
    ),
)

a := agent.New(optimizedClient, defs, executor).WithMaxSteps(12)
Strategy What it does Best when
SlidingWindowStrategy keeps the last user turn plus a recent tail you only need fresh context
CompactionStrategy shrinks bulky tool payloads into short summaries tools return huge blobs
SummarizationStrategy moves older history into a generated summary conversations are long but earlier turns still matter

🧠 Think of this as luggage management for prompts: keep the passport, fold the clothes, and summarize the travel diary.


Flow 7 — 📚 Long-Term Task Memory: learning from similar problems

Study this flow when: your agent solves recurring task shapes and you want it to reuse successful approaches next time.

sequenceDiagram
    participant App as 👤 App
    participant MM as 📚 TaskMemoryManager
    participant E as 🧠 Embedder
    participant VS as 🗂️ VectorStore
    participant MI as 🪄 MemoryInjector

    App->>MM: Save(TaskMemory)
    MM->>E: Embed("Task: ...")
    E-->>MM: vector
    MM->>VS: Search(vector, 3) for duplicates
    VS-->>MM: nearest memories
    MM->>VS: Add(VectorDocument)

    App->>MI: Mutate(request)
    MI->>MM: Search(last user message, topK)
    MM->>E: Embed(query)
    E-->>MM: vector
    MM->>VS: Search(vector, topK)
    VS-->>MM: relevant memories
    MM-->>MI: TaskMemory[]
    MI-->>App: instructions + <PAST_EXPERIENCES>
memories := agent.NewTaskMemoryManager(
    embedder,
    agent.NewInMemoryVectorStore(),
    agent.SimpleDuplicateChecker{},
)

if _, saved, err := memories.Save(ctx, agent.TaskMemory{
    TaskSummary: "find capitals",
    Approach:    "used search",
    FinalAnswer: "Paris",
    IsCorrect:   true,
}); err != nil {
    log.Fatal(err)
} else if saved {
    fmt.Println("memory stored")
}

clientWithMemory := agent.NewMutatingLLMClient(
    client,
    agent.NewMemoryInjector(memories, 3),
)
Field What to put there
TaskSummary short description of the problem
Approach how the agent solved it
FinalAnswer the final answer or resolution
IsCorrect whether the solution should be reused confidently
ErrorAnalysis what went wrong when a result was bad

Stored memories and injected text are sanitized before reuse, so prompt-injection strings and sensitive-looking paths do not get copied back into the model context verbatim.


Flow 8 — 📊 Request Logging: see the mutator pipeline in action

Study this flow when: you want to debug why a prompt got shorter, why memories were injected, or why an approval flow suspended.

sequenceDiagram
    participant App as 👤 App
    participant M1 as 🪄 WithMutatorLogger
    participant MI as 📚 MemoryInjector
    participant M2 as 🪄 WithMutatorLogger
    participant CO as 🪶 ContextOptimizer
    participant L as 🧠 LLM

    App->>M1: Mutate(request)
    M1->>MI: start
    MI-->>M1: finish
    App->>M2: Mutate(request)
    M2->>CO: start
    CO-->>M2: finish
    App->>L: Generate(mutated request)
logger := slog.Default()

memories := agent.NewTaskMemoryManager(embedder, agent.NewInMemoryVectorStore(), agent.SimpleDuplicateChecker{}).
    WithLogger(logger)

counter, err := agent.NewRequestTokenCounter("gpt-4o-mini")
if err != nil {
    log.Fatal(err)
}

clientWithPipelineLogs := agent.NewMutatingLLMClient(
    client,
    agent.WithMutatorLogger(agent.NewMemoryInjector(memories, 3), logger),
    agent.WithMutatorLogger(
        agent.NewContextOptimizer(
            counter,
            8_000,
            agent.NewSlidingWindowStrategy(8),
            agent.NewCompactionStrategy(),
        ).WithLogger(logger),
        logger,
    ),
)

Typical things you'll see in logs:

  1. memory_search_start / memory_search_end
  2. mutator_start / mutator_finish
  3. context_optimize_start
  4. context_strategy_apply / context_strategy_applied
  5. session_run_start / session_run_end
  6. approval_requested

🔗 Flow Combinations

Real systems usually mix more than one flow:

Your use case Combine these flows Why
Production chatbot Flow 3 + Flow 5 + Flow 6 observe runs, persist turns, keep prompts small
Risky automation Flow 2 + Flow 4 + Flow 8 use tools, gate them, log every decision
Research assistant Flow 2 + Flow 3 + Flow 7 fetch evidence, inspect reasoning, reuse good prior work
Support agent Flow 5 + Flow 6 + Flow 7 remember the conversation, manage token budget, learn from resolved cases

🧩 The library is intentionally composable: sessions, approvals, mutators, memory, and event streams are designed to stack cleanly instead of forcing one giant framework.


🕹️ Manual Step Control

Step is still available when you want to drive the loop yourself — useful for streaming UI updates, custom checkpoints, or research/debug flows where you want to inspect each step manually:

execCtx := agent.NewExecutionContextForTest()
execCtx.AddEvent("user", model.Message{Role: "user", Content: "Plan a 3-day trip to Kyoto"})

for execCtx.CurrentStep() < 15 {
    if err := a.Step(ctx, execCtx); err != nil {
        break
    }
    if execCtx.Done() {
        break
    }

    latest := execCtx.Events()[len(execCtx.Events())-1]
    fmt.Printf("step %d: %s emitted %d item(s)\n", execCtx.CurrentStep(), latest.Author, len(latest.Content))

    execCtx.IncrementStep()
}

🔍 Inspecting the Reasoning Trail

Every run keeps a full, ordered history of messages, tool calls, and tool results in result.Context:

for _, event := range result.Context.Events() {
        fmt.Printf("[%s] at %s\n", event.Author, event.Timestamp.Format(time.RFC3339))
    for _, item := range event.Content {
        switch v := item.(type) {
        case model.Message:
            fmt.Printf("  💬 message: %s\n", v.Content)
        case model.ToolCall:
            fmt.Printf("  🔧 tool_call: %s(%s)\n", v.Name, v.Arguments)
        case model.ToolResult:
            fmt.Printf("  📊 tool_result: [%s] %v\n", v.Status, v.Content)
        }
    }
}

💡 Use this for debugging, audit logs, or displaying the agent's "chain of thought" to end users.


🏛️ Design Reference

classDiagram
    class Agent {
        +Run(ctx, question) Result, Observable, error
        +Resume(ctx, suspended, response) Result, Observable, error
        +Step(ctx, execCtx) error
        +Think(ctx, execCtx) error
        +Act(ctx, execCtx) error
        +WithInstructions(string) Agent
        +WithMaxSteps(int) Agent
        +WithBeforeToolCallbacks(...BeforeToolCallback) Agent
        +WithAfterToolCallbacks(...AfterToolCallback) Agent
    }

    class LLMClient {
        <<interface>>
        +Generate(ctx, req) Response, error
    }

    class ToolExecutor {
        <<interface>>
        +Execute(ctx, calls) ToolResults, error
    }

    class BeforeToolCallback {
        <<interface>>
        +BeforeTool(ctx, execCtx, call) ToolResult, error
    }

    class AfterToolCallback {
        <<interface>>
        +AfterTool(ctx, execCtx, result) ToolResult, error
    }

    class ExecutionContext {
        +Events() []Event
        +AddEvent(author, items)
        +CurrentStep() int
        +IncrementStep()
        +PendingInteraction() *InteractionRequest, bool
        +InteractionResponse(requestID) InteractionResponse, bool
    }

    class SuspendedRun {
        +Context ExecutionContext
        +Interaction InteractionRequest
    }

    class InteractionRequest {
        +ID string
        +Kind string
        +Prompt string
        +ToolCallID string
        +ToolName string
    }

    class InteractionResponse {
        +RequestID string
        +Approved *bool
        +Value string
    }

    class AgentEvent {
        <<interface sealed>>
        RunStartEvent
        StepStartEvent
        LLMCallEvent
        CallbackEvent
        ToolExecEvent
        InteractionRequestedEvent
        InteractionResumedEvent
        StepEndEvent
        RunEndEvent
    }

    Agent --> LLMClient : uses
    Agent --> ToolExecutor : uses
    Agent --> BeforeToolCallback : uses
    Agent --> AfterToolCallback : uses
    Agent --> ExecutionContext : owns
    Agent --> SuspendedRun : resumes
    Agent ..> AgentEvent : emits via Observable
    InteractionRequestedEvent --> InteractionRequest : carries
    InteractionResumedEvent --> InteractionResponse : carries
Type Role
Agent 🤖 Orchestrator — owns the loop
ExecutionContext 📋 Mutable run state — thread-safe event log
Event 📝 Timestamped history entry (author + content items)
LLMClient 🧠 Interface — swap any provider
ToolExecutor 🔧 Interface — bring your own dispatch strategy
LiteLLMClient 🔌 Concrete adapter for openai-go / LiteLLM proxy
MutatingLLMClient 🧠 Request pipeline — inject memory, optimize, sanitize
SessionRunner 🧵 Conversation wrapper — replay, persist, resume
TaskMemoryManager 📚 Long-term memory store — save and search past tasks
ConfirmationCallback ✅ Human approval gate for selected tools
AgentEvent 📡 Sealed sum type — emitted on the observable stream

License

MIT

Documentation

Overview

Package agent implements the ReAct (Reason + Act) pattern for AI agents.

Overview

A ReAct agent runs a bounded Think → Act → Observe loop: the model thinks (generates a response), acts (calls tools), and observes (results are appended to the history), repeating until it produces a final answer or exhausts the step limit.

The pattern is based on "ReAct: Synergizing Reasoning and Acting in Language Models" (Yao et al., 2022 — https://arxiv.org/abs/2210.03629).

Building an agent

Use the fluent builder to compose an agent from an LLM client, tool definitions, and a tool executor:

a := agent.New(client, toolDefs, executor).
         WithInstructions("You are a precise research assistant.").
         WithMaxSteps(15)

Running an agent

Agent.Run executes the full loop for a single user question. It returns a Result, a replayable rxgo.Observable of AgentEvent values, and any error:

result, events, err := a.Run(ctx, "Who won the 2025 Nobel Prize in Physics?")
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.Output)

Observable event stream

The returned observable is a cold, replayable stream of everything that happened during the run. Subscribe by calling Observe():

for item := range events.Observe() {
    switch e := item.V.(type) {
    case agent.LLMCallEvent:
        slog.Info("llm call", "step", e.Step, "latency_ms", e.Latency.Milliseconds())
    case agent.ToolExecEvent:
        slog.Info("tool exec", "tools", e.ToolNames)
    case agent.RunEndEvent:
        slog.Info("run end", "err", e.Err)
    }
}

Calling Observe() again replays all events from the beginning — safe for multiple independent subscribers (loggers, metrics, tracing).

Execution history

The full conversation is available via result.Context.Events(). Each model.Event has an Author ("user", "agent", or "tools"), a timestamp, and typed model.ContentItem values (model.Message, model.ToolCall, model.ToolResult).

Bringing your own tools

Implement model.ToolExecutor to connect any tool-running backend:

type myExecutor struct{ /* your registry, MCP session, etc. */ }

func (e *myExecutor) Execute(ctx context.Context, calls []model.ToolCall) ([]model.ToolResult, error) {
    results := make([]model.ToolResult, len(calls))
    for i, call := range calls {
        out, err := e.dispatch(ctx, call.Name, call.Arguments)
        if err != nil {
            results[i] = model.ToolResult{ID: call.ID, Name: call.Name, Status: "error", Content: []string{err.Error()}}
            continue
        }
        results[i] = model.ToolResult{ID: call.ID, Name: call.Name, Status: "success", Content: []string{out}}
    }
    return results, nil
}

For MCP-based tools (github.com/v8tix/mcp-toolkit/v2), use the ready-made adapter in the [mcpadapter] sub-package.

Stateful sessions and approvals

Use SessionRunner when a conversation must persist across multiple user-facing turns. It replays prior model.Event values from a SessionManager, runs the agent, and saves the updated state after each call. If a callback suspends the run, SessionRunner.Run returns StatusPending plus a pending interaction payload that your app can surface in a UI or API before resuming:

sessions := agent.NewInMemorySessionManager()
runner := agent.NewSessionRunner(
    agent.New(client, defs, executor).
        WithBeforeToolCallbacks(agent.NewConfirmationCallback(agent.StaticApprovalPolicy{
            "delete_file": {MessageTemplate: "Approve file deletion?"},
        })),
    sessions,
    8,
)

first, _ := runner.Run(ctx, "chat-1", "user-7", "My name is Alice")
next, _ := runner.Run(ctx, "chat-1", "user-7", "What's my name?")
_, _ = first, next

Approval callbacks use Suspend under the hood and can be resumed with Agent.Resume or SessionRunner.Resume. The built-in ConfirmationCallback also redacts sensitive tool arguments from the interaction payload.

Request mutation and context memory

MutatingLLMClient lets you rewrite a request immediately before it is sent to the underlying LLMClient. This is the extension point for prompt hygiene, context-window management, and memory injection.

Common building blocks:

Long-term task memory

TaskMemoryManager stores solved tasks in a pluggable VectorStore so future requests can retrieve similar work. Pair it with MemoryInjector to inject the most relevant prior records into the prompt before each LLM call:

memories := agent.NewTaskMemoryManager(embedder, agent.NewInMemoryVectorStore(), agent.SimpleDuplicateChecker{})
clientWithMemory := agent.NewMutatingLLMClient(
    client,
    agent.NewMemoryInjector(memories, 3),
)

_, _, _ = memories, clientWithMemory, agent.New(clientWithMemory, defs, executor)

Manual step control

Agent.Step is exported so callers can drive the loop themselves — useful for streaming, checkpointing, or human-in-the-loop interrupts:

execCtx := agent.NewExecutionContextForTest()
execCtx.AddEvent("user", model.Message{Role: "user", Content: question})

for execCtx.CurrentStep() < 20 {
    if err := a.Step(ctx, execCtx); err != nil {
        break
    }
    if execCtx.Done() {
        break
    }
    execCtx.IncrementStep()
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInteractionRequested = errors.New("agent: interaction requested")

ErrInteractionRequested signals that the agent suspended awaiting external input.

View Source
var ErrMaxStepsReached = errors.New("agent: max steps reached without final answer")

ErrMaxStepsReached is returned when Run exhausts maxSteps without a final answer.

Functions

func Suspend added in v1.0.1

func Suspend(req InteractionRequest) error

Suspend requests external interaction from inside a callback.

Types

type AfterToolCallback added in v1.0.1

type AfterToolCallback interface {
	AfterTool(ctx context.Context, execCtx *ExecutionContext, result model.ToolResult) (*model.ToolResult, error)
}

AfterToolCallback can replace a tool result after the executor (or a before-tool callback) produced it. Returning a non-nil ToolResult replaces the current result for that call.

type Agent

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

Agent is the ReAct orchestrator. It runs a Think → Act → Observe loop until the LLM produces a final answer or maxSteps is exhausted.

func New

func New(client LLMClient, defs []model.ToolDefinition, executor model.ToolExecutor) *Agent

New creates an Agent with sensible defaults (maxSteps=10).

  • defs: tool definitions the LLM can call (pass nil or empty for no tools)
  • executor: executes tool calls concurrently (pass nil when defs is empty)

Use the builder methods to customise the agent:

agent.New(client, defs, executor).
    WithInstructions("You are helpful.").
    WithMaxSteps(15)
Example

ExampleNew shows how to construct an agent with the fluent builder. Replace demoLLM with agent.NewLiteLLMClient(openaiClient, model) to target a real LLM.

package main

import (
	"context"
	"fmt"

	agent "github.com/v8tix/react-agent"
	"github.com/v8tix/react-agent/model"
)

// demoLLM is a scripted LLM stub: it returns responses[0], responses[1], …
// in sequence. Use it to write deterministic, self-contained documentation
// examples that produce a known output without any network calls.
type demoLLM struct {
	responses []model.Response
	n         int
}

func (d *demoLLM) Generate(_ context.Context, _ model.Request) (model.Response, error) {
	resp := d.responses[d.n]
	d.n++
	return resp, nil
}

// demoExecutor echoes back a canned result for every tool call it receives.
// Suitable for examples that need a tool round-trip without a real backend.
type demoExecutor struct{}

func (demoExecutor) Execute(_ context.Context, calls []model.ToolCall) ([]model.ToolResult, error) {
	results := make([]model.ToolResult, len(calls))
	for i, c := range calls {
		results[i] = model.ToolResult{
			ID:      c.ID,
			Name:    c.Name,
			Status:  "success",
			Content: []string{fmt.Sprintf("result_of_%s", c.Name)},
		}
	}
	return results, nil
}

func main() {
	// Hypothetical: ask an assistant to look up a stock price.
	llm := &demoLLM{} // swap for agent.NewLiteLLMClient(...)

	defs := []model.ToolDefinition{
		{
			Name:        "search_web",
			Description: "Search the web for up-to-date information",
			Parameters: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"query": map[string]any{"type": "string"},
				},
				"required": []string{"query"},
			},
		},
	}

	_ = agent.New(llm, defs, demoExecutor{}).
		WithInstructions("You are a precise research assistant.").
		WithMaxSteps(15)
}

func (*Agent) Act

func (a *Agent) Act(ctx context.Context, execCtx *ExecutionContext, calls []model.ToolCall) error

Act executes all requested tool calls via ToolExecutor and records the results. The agent's tool-call decision is appended as an "agent" event BEFORE execution, then tool results are appended as a "tools" event AFTER execution. Note: events are not emitted when calling Act directly; use Run for observability.

func (*Agent) Resume added in v1.0.1

func (a *Agent) Resume(ctx context.Context, suspended SuspendedRun, response InteractionResponse) (*Result, rxgo.Observable, error)

Resume continues a suspended run after an external interaction response arrives.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, userMessage string) (*Result, rxgo.Observable, error)

Run executes the full ReAct loop for a single user message. It returns a Result, a replayable Observable of AgentEvents, and any error.

The Observable is a cold, replayable stream: each call to Observe() replays all events from the completed run. It is safe for multiple subscribers.

Example

ExampleAgent_Run demonstrates a single-step run where the LLM answers directly without calling any tools.

package main

import (
	"context"
	"fmt"
	"log"

	agent "github.com/v8tix/react-agent"
	"github.com/v8tix/react-agent/model"
)

// demoLLM is a scripted LLM stub: it returns responses[0], responses[1], …
// in sequence. Use it to write deterministic, self-contained documentation
// examples that produce a known output without any network calls.
type demoLLM struct {
	responses []model.Response
	n         int
}

func (d *demoLLM) Generate(_ context.Context, _ model.Request) (model.Response, error) {
	resp := d.responses[d.n]
	d.n++
	return resp, nil
}

func main() {
	llm := &demoLLM{
		responses: []model.Response{
			{Content: []model.ContentItem{
				model.Message{Role: "assistant", Content: "The capital of France is Paris."},
			}},
		},
	}

	a := agent.New(llm, nil, nil).
		WithInstructions("You are a helpful assistant.")

	result, _, err := a.Run(context.Background(), "What is the capital of France?")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
	fmt.Println(result.ToolCalled)
}
Output:
The capital of France is Paris.
false
Example (EventStream)

ExampleAgent_Run_eventStream shows how to consume the observable event stream returned by Run to build logging, metrics, or tracing.

The observable is cold and replayable — calling Observe() again replays all events from the beginning, safe for multiple independent subscribers.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	agent "github.com/v8tix/react-agent"
	"github.com/v8tix/react-agent/model"
)

// demoLLM is a scripted LLM stub: it returns responses[0], responses[1], …
// in sequence. Use it to write deterministic, self-contained documentation
// examples that produce a known output without any network calls.
type demoLLM struct {
	responses []model.Response
	n         int
}

func (d *demoLLM) Generate(_ context.Context, _ model.Request) (model.Response, error) {
	resp := d.responses[d.n]
	d.n++
	return resp, nil
}

// demoExecutor echoes back a canned result for every tool call it receives.
// Suitable for examples that need a tool round-trip without a real backend.
type demoExecutor struct{}

func (demoExecutor) Execute(_ context.Context, calls []model.ToolCall) ([]model.ToolResult, error) {
	results := make([]model.ToolResult, len(calls))
	for i, c := range calls {
		results[i] = model.ToolResult{
			ID:      c.ID,
			Name:    c.Name,
			Status:  "success",
			Content: []string{fmt.Sprintf("result_of_%s", c.Name)},
		}
	}
	return results, nil
}

func main() {
	llm := &demoLLM{
		responses: []model.Response{
			{Content: []model.ContentItem{
				model.ToolCall{ID: "c1", Name: "search_web", Arguments: json.RawMessage(`{}`)},
			}},
			{Content: []model.ContentItem{
				model.Message{Role: "assistant", Content: "Done."},
			}},
		},
	}

	defs := []model.ToolDefinition{{
		Name:       "search_web",
		Parameters: map[string]any{"type": "object", "properties": map[string]any{"query": map[string]any{"type": "string"}}, "required": []string{"query"}},
	}}

	a := agent.New(llm, defs, demoExecutor{})

	_, events, err := a.Run(context.Background(), "What is the weather in Paris?")
	if err != nil {
		log.Fatal(err)
	}

	for item := range events.Observe() {
		switch e := item.V.(type) {
		case agent.RunStartEvent:
			fmt.Println("run started")
		case agent.LLMCallEvent:
			fmt.Printf("llm call step=%d\n", e.Step)
		case agent.ToolExecEvent:
			fmt.Printf("tool exec: %v\n", e.ToolNames)
		case agent.RunEndEvent:
			fmt.Println("run ended")
		}
	}
}
Output:
run started
llm call step=0
tool exec: [search_web]
llm call step=1
run ended
Example (ReasoningTrail)

ExampleAgent_Run_reasoningTrail shows how to inspect the full conversation history — every message, tool call, and tool result — after a run. Useful for debugging, audit logs, or displaying the chain of thought.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	agent "github.com/v8tix/react-agent"
	"github.com/v8tix/react-agent/model"
)

// demoLLM is a scripted LLM stub: it returns responses[0], responses[1], …
// in sequence. Use it to write deterministic, self-contained documentation
// examples that produce a known output without any network calls.
type demoLLM struct {
	responses []model.Response
	n         int
}

func (d *demoLLM) Generate(_ context.Context, _ model.Request) (model.Response, error) {
	resp := d.responses[d.n]
	d.n++
	return resp, nil
}

// demoExecutor echoes back a canned result for every tool call it receives.
// Suitable for examples that need a tool round-trip without a real backend.
type demoExecutor struct{}

func (demoExecutor) Execute(_ context.Context, calls []model.ToolCall) ([]model.ToolResult, error) {
	results := make([]model.ToolResult, len(calls))
	for i, c := range calls {
		results[i] = model.ToolResult{
			ID:      c.ID,
			Name:    c.Name,
			Status:  "success",
			Content: []string{fmt.Sprintf("result_of_%s", c.Name)},
		}
	}
	return results, nil
}

func main() {
	llm := &demoLLM{
		responses: []model.Response{
			{Content: []model.ContentItem{
				model.ToolCall{ID: "c1", Name: "lookup", Arguments: json.RawMessage(`{"id":"42"}`)},
			}},
			{Content: []model.ContentItem{
				model.Message{Role: "assistant", Content: "Found it."},
			}},
		},
	}

	defs := []model.ToolDefinition{{
		Name:       "lookup",
		Parameters: map[string]any{"type": "object", "properties": map[string]any{"id": map[string]any{"type": "string"}}, "required": []string{"id"}},
	}}

	result, _, err := agent.New(llm, defs, demoExecutor{}).
		Run(context.Background(), "Look up record 42.")
	if err != nil {
		log.Fatal(err)
	}

	for _, event := range result.Context.Events() {
		for _, item := range event.Content {
			switch v := item.(type) {
			case model.Message:
				fmt.Printf("[%s] %s\n", event.Author, v.Content)
			case model.ToolCall:
				fmt.Printf("[%s] call %s\n", event.Author, v.Name)
			case model.ToolResult:
				fmt.Printf("[%s] result %s=%s\n", event.Author, v.Name, v.Content[0])
			}
		}
	}
}
Output:
[user] Look up record 42.
[agent] call lookup
[tools] result lookup=result_of_lookup
[agent] Found it.
Example (WithTools)

ExampleAgent_Run_withTools demonstrates a two-step run: the LLM first calls a tool, then produces a final answer once it has the search result.

This mirrors the classic ReAct scenario — the agent reasons about what information it needs, fetches it, then synthesises an answer.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	agent "github.com/v8tix/react-agent"
	"github.com/v8tix/react-agent/model"
)

// demoLLM is a scripted LLM stub: it returns responses[0], responses[1], …
// in sequence. Use it to write deterministic, self-contained documentation
// examples that produce a known output without any network calls.
type demoLLM struct {
	responses []model.Response
	n         int
}

func (d *demoLLM) Generate(_ context.Context, _ model.Request) (model.Response, error) {
	resp := d.responses[d.n]
	d.n++
	return resp, nil
}

// demoExecutor echoes back a canned result for every tool call it receives.
// Suitable for examples that need a tool round-trip without a real backend.
type demoExecutor struct{}

func (demoExecutor) Execute(_ context.Context, calls []model.ToolCall) ([]model.ToolResult, error) {
	results := make([]model.ToolResult, len(calls))
	for i, c := range calls {
		results[i] = model.ToolResult{
			ID:      c.ID,
			Name:    c.Name,
			Status:  "success",
			Content: []string{fmt.Sprintf("result_of_%s", c.Name)},
		}
	}
	return results, nil
}

func main() {
	llm := &demoLLM{
		responses: []model.Response{
			// Step 1: LLM decides to call search_web
			{Content: []model.ContentItem{
				model.ToolCall{
					ID:        "call_1",
					Name:      "search_web",
					Arguments: json.RawMessage(`{"query":"AAPL stock price January 9 2007"}`),
				},
			}},
			// Step 2: LLM reads the search result and gives the final answer
			{Content: []model.ContentItem{
				model.Message{Role: "assistant", Content: "Apple stock was $11.74 on January 9, 2007."},
			}},
		},
	}

	defs := []model.ToolDefinition{{
		Name:        "search_web",
		Description: "Search the web for current information",
		Parameters: map[string]any{
			"type":     "object",
			"required": []string{"query"},
			"properties": map[string]any{
				"query": map[string]any{"type": "string"},
			},
		},
	}}

	a := agent.New(llm, defs, demoExecutor{}).
		WithInstructions("You are a research assistant. Verify facts before answering.")

	result, _, err := a.Run(context.Background(), "What was Apple's stock price the day the iPhone was announced?")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
	fmt.Println("tool called:", result.ToolCalled)
}
Output:
Apple stock was $11.74 on January 9, 2007.
tool called: true

func (*Agent) Step

func (a *Agent) Step(ctx context.Context, execCtx *ExecutionContext) error

Step executes one Think → (optionally) Act cycle, mutating execCtx in place. It is exported so callers can drive the loop manually for checkpointing or human-in-the-loop interrupts. Use execCtx.Done() to check for a final answer. Note: events are not emitted when calling Step directly; use Run for observability.

Example

ExampleAgent_Step shows how to drive the ReAct loop manually step-by-step. This gives you control between steps — useful for streaming output to a UI, checkpointing long runs, or pausing for human approval before the agent acts.

package main

import (
	"context"
	"fmt"
	"log"

	agent "github.com/v8tix/react-agent"
	"github.com/v8tix/react-agent/model"
)

// demoLLM is a scripted LLM stub: it returns responses[0], responses[1], …
// in sequence. Use it to write deterministic, self-contained documentation
// examples that produce a known output without any network calls.
type demoLLM struct {
	responses []model.Response
	n         int
}

func (d *demoLLM) Generate(_ context.Context, _ model.Request) (model.Response, error) {
	resp := d.responses[d.n]
	d.n++
	return resp, nil
}

func main() {
	llm := &demoLLM{
		responses: []model.Response{
			{Content: []model.ContentItem{
				model.Message{Role: "assistant", Content: "The answer is 42."},
			}},
		},
	}

	a := agent.New(llm, nil, nil).WithMaxSteps(10)

	execCtx := agent.NewExecutionContextForTest()
	execCtx.AddEvent("user", model.Message{Role: "user", Content: "What is the answer to life, the universe and everything?"})

	for execCtx.CurrentStep() < 10 {
		if err := a.Step(context.Background(), execCtx); err != nil {
			log.Fatal(err)
		}
		if execCtx.Done() {
			break
		}
		execCtx.IncrementStep()
	}

	answer, _ := execCtx.FinalResult()
	fmt.Println(answer)
	fmt.Println("done:", execCtx.Done())
}
Output:
The answer is 42.
done: true

func (*Agent) Think

func (a *Agent) Think(ctx context.Context, execCtx *ExecutionContext) (model.Response, error)

Think calls the LLM with the current execution context and returns its response. Note: events are not emitted when calling Think directly; use Run for observability.

func (*Agent) WithAfterToolCallbacks added in v1.0.1

func (a *Agent) WithAfterToolCallbacks(callbacks ...AfterToolCallback) *Agent

WithAfterToolCallbacks appends tool callbacks that run after a tool result is produced.

func (*Agent) WithBeforeToolCallbacks added in v1.0.1

func (a *Agent) WithBeforeToolCallbacks(callbacks ...BeforeToolCallback) *Agent

WithBeforeToolCallbacks appends tool callbacks that run before executor dispatch.

func (*Agent) WithInstructions

func (a *Agent) WithInstructions(s string) *Agent

WithInstructions sets the system prompt sent on every LLM request.

func (*Agent) WithMaxSteps

func (a *Agent) WithMaxSteps(n int) *Agent

WithMaxSteps overrides the default step limit (10). Panics if n < 1 — zero or negative steps is a programming error.

type AgentEvent

type AgentEvent interface {
	// contains filtered or unexported methods
}

AgentEvent is the sealed sum type for all agent lifecycle events. Use a type switch to handle specific event types:

for item := range events.Observe() {
    switch e := item.V.(type) {
    case agent.LLMCallEvent:
        slog.Info("llm call", "latency_ms", e.Latency.Milliseconds())
    case agent.RunEndEvent:
        fmt.Println(e.Result.Output)
    }
}

type ApprovalPolicy added in v1.0.1

type ApprovalPolicy interface {
	RuleForTool(name string) (ApprovalRule, bool)
}

ApprovalPolicy decides whether a given tool requires human approval.

type ApprovalRule added in v1.0.1

type ApprovalRule struct {
	MessageTemplate string
	DeniedMessage   string
}

ApprovalRule defines how a tool approval request should be presented and denied.

type BeforeToolCallback added in v1.0.1

type BeforeToolCallback interface {
	BeforeTool(ctx context.Context, execCtx *ExecutionContext, call model.ToolCall) (*model.ToolResult, error)
}

BeforeToolCallback can short-circuit a tool call before the executor runs. Returning a non-nil ToolResult skips executor execution for that call.

type CallbackEvent added in v1.0.1

type CallbackEvent struct {
	RunID      string
	Step       int
	Phase      CallbackPhase
	Stage      CallbackStage
	Callback   string
	ToolCallID string
	ToolName   string
	Overrode   bool
	Latency    time.Duration
	Err        error
}

CallbackEvent is emitted before and after each callback invocation.

type CallbackPhase added in v1.0.1

type CallbackPhase string

CallbackPhase identifies which callback stage emitted the event.

const (
	CallbackPhaseBeforeTool CallbackPhase = "before_tool"
	CallbackPhaseAfterTool  CallbackPhase = "after_tool"
)

type CallbackStage added in v1.0.1

type CallbackStage string

CallbackStage identifies whether the event was emitted before invoking the callback or after it returned.

const (
	CallbackStageStart  CallbackStage = "start"
	CallbackStageFinish CallbackStage = "finish"
)

type CompactionStrategy added in v1.0.1

type CompactionStrategy struct{}

CompactionStrategy replaces bulky tool payloads with short, sanitized summaries.

func NewCompactionStrategy added in v1.0.1

func NewCompactionStrategy() CompactionStrategy

NewCompactionStrategy creates a compaction optimizer for large tool payloads.

func (CompactionStrategy) Optimize added in v1.0.1

Optimize compacts selected tool calls and tool results in-place.

type ConfirmationCallback added in v1.0.1

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

ConfirmationCallback suspends execution until selected tools are explicitly approved.

func NewConfirmationCallback added in v1.0.1

func NewConfirmationCallback(policy ApprovalPolicy) ConfirmationCallback

NewConfirmationCallback creates a before-tool callback backed by an approval policy.

func (ConfirmationCallback) BeforeTool added in v1.0.1

BeforeTool requests approval for matching tools and redacts sensitive arguments in the payload.

func (ConfirmationCallback) WithLogger added in v1.0.1

func (c ConfirmationCallback) WithLogger(logger *slog.Logger) ConfirmationCallback

WithLogger attaches structured approval lifecycle logs.

type ContextOptimizer added in v1.0.1

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

ContextOptimizer applies a list of optimization strategies once a token threshold has been exceeded.

func NewContextOptimizer added in v1.0.1

func NewContextOptimizer(counter TokenCounter, threshold int, strategies ...OptimizationStrategy) *ContextOptimizer

NewContextOptimizer builds a request mutator that conditionally applies optimization strategies.

func (*ContextOptimizer) Mutate added in v1.0.1

func (o *ContextOptimizer) Mutate(ctx context.Context, req *model.Request) error

Mutate applies optimization strategies when the request exceeds the configured threshold.

func (*ContextOptimizer) WithLogger added in v1.0.1

func (o *ContextOptimizer) WithLogger(logger *slog.Logger) *ContextOptimizer

WithLogger attaches structured optimization logs.

type DuplicateChecker added in v1.0.1

type DuplicateChecker interface {
	IsDuplicate(memory TaskMemory, existing []TaskMemory) bool
}

DuplicateChecker decides whether a memory is already represented in the store.

type Embedder added in v1.0.1

type Embedder interface {
	Embed(ctx context.Context, texts []string) ([][]float64, error)
}

Embedder converts one or more texts into vectors for semantic search.

type ExecutionContext

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

ExecutionContext is the central mutable state for one agent run. It records all Events across steps and holds the final result once the agent produces a terminal response. All public methods are safe for concurrent use.

func NewExecutionContextForTest

func NewExecutionContextForTest() *ExecutionContext

NewExecutionContextForTest exposes newExecutionContext for white-box unit tests.

func (*ExecutionContext) AddEvent

func (ec *ExecutionContext) AddEvent(author string, content ...model.ContentItem)

AddEvent appends an event authored by author with the given content items. ID and Timestamp are generated automatically. Safe for concurrent use.

func (*ExecutionContext) CurrentStep

func (ec *ExecutionContext) CurrentStep() int

CurrentStep returns the current step index. Safe for concurrent use.

func (*ExecutionContext) Done

func (ec *ExecutionContext) Done() bool

Done reports whether the agent has produced a final answer. Safe for concurrent use.

func (*ExecutionContext) Events

func (ec *ExecutionContext) Events() []model.Event

Events returns a defensive copy of the event log. Each Event's Content slice is independently copied so callers cannot corrupt internal state by mutating returned slices. Safe for concurrent use.

func (*ExecutionContext) FinalResult

func (ec *ExecutionContext) FinalResult() (string, bool)

FinalResult returns the agent's final answer and true once Done() is true. Returns ("", false) if the agent has not finished yet. Safe for concurrent use.

func (*ExecutionContext) GetState

func (ec *ExecutionContext) GetState(key string) (any, bool)

GetState retrieves a value from the run-scoped key-value store. Safe for concurrent use.

func (*ExecutionContext) ID

func (ec *ExecutionContext) ID() string

ID returns the unique identifier for this execution. Safe for concurrent use.

func (*ExecutionContext) IncrementStep

func (ec *ExecutionContext) IncrementStep()

IncrementStep advances the step counter by one. Safe for concurrent use.

func (*ExecutionContext) InteractionResponse added in v1.0.1

func (ec *ExecutionContext) InteractionResponse(requestID string) (InteractionResponse, bool)

InteractionResponse returns a previously supplied external response, if present.

func (*ExecutionContext) PendingInteraction added in v1.0.1

func (ec *ExecutionContext) PendingInteraction() (*InteractionRequest, bool)

PendingInteraction returns the active external interaction request, if any.

func (*ExecutionContext) SetState

func (ec *ExecutionContext) SetState(key string, value any)

SetState stores a value in the run-scoped key-value store. Safe for concurrent use.

type InMemorySessionManager added in v1.0.1

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

InMemorySessionManager is a thread-safe in-process SessionManager.

func NewInMemorySessionManager added in v1.0.1

func NewInMemorySessionManager() *InMemorySessionManager

NewInMemorySessionManager creates an empty in-memory session store.

func (*InMemorySessionManager) Create added in v1.0.1

func (m *InMemorySessionManager) Create(sessionID, userID string) (*Session, error)

Create inserts a new session for the given session and user identifiers.

func (*InMemorySessionManager) Get added in v1.0.1

func (m *InMemorySessionManager) Get(sessionID string) (*Session, error)

Get loads a previously saved session, or nil when it does not exist.

func (*InMemorySessionManager) GetOrCreate added in v1.0.1

func (m *InMemorySessionManager) GetOrCreate(sessionID, userID string) (*Session, error)

GetOrCreate returns an existing session for the same user or creates one.

func (*InMemorySessionManager) Save added in v1.0.1

func (m *InMemorySessionManager) Save(session *Session) error

Save upserts the supplied session snapshot.

type InMemoryVectorStore added in v1.0.1

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

InMemoryVectorStore is a thread-safe in-process vector store.

func NewInMemoryVectorStore added in v1.0.1

func NewInMemoryVectorStore() *InMemoryVectorStore

NewInMemoryVectorStore creates an empty in-memory vector store.

func (*InMemoryVectorStore) Add added in v1.0.1

Add appends new vector documents to the store.

func (*InMemoryVectorStore) Search added in v1.0.1

func (s *InMemoryVectorStore) Search(_ context.Context, query []float64, topK int) ([]VectorDocument, error)

Search returns the top-K documents ranked by cosine similarity.

type InteractionRequest added in v1.0.1

type InteractionRequest struct {
	ID         string
	Kind       string
	Prompt     string
	ToolCallID string
	ToolName   string
	Payload    map[string]any
}

InteractionRequest describes a prompt that must be answered from outside the agent.

type InteractionRequestedError added in v1.0.1

type InteractionRequestedError struct {
	Suspended SuspendedRun
}

InteractionRequestedError exposes a suspended run while still matching ErrInteractionRequested.

func (*InteractionRequestedError) Error added in v1.0.1

func (e *InteractionRequestedError) Error() string

func (*InteractionRequestedError) Unwrap added in v1.0.1

func (e *InteractionRequestedError) Unwrap() error

type InteractionRequestedEvent added in v1.0.1

type InteractionRequestedEvent struct {
	RunID   string
	Step    int
	Request InteractionRequest
}

InteractionRequestedEvent is emitted when the agent suspends to await external input.

type InteractionResponse added in v1.0.1

type InteractionResponse struct {
	RequestID string
	Approved  *bool
	Value     string
	Metadata  map[string]any
}

InteractionResponse carries the external answer to a pending interaction request.

type InteractionResumedEvent added in v1.0.1

type InteractionResumedEvent struct {
	RunID    string
	Step     int
	Response InteractionResponse
}

InteractionResumedEvent is emitted when a suspended interaction receives a response.

type LLMCallEvent

type LLMCallEvent struct {
	RunID   string
	Step    int
	Latency time.Duration
	Err     error
}

LLMCallEvent is emitted after every Generate() call, including on error. Latency covers only the LLM network round-trip. The full request and response content are available via result.Context.Events().

type LLMClient

type LLMClient interface {
	Generate(ctx context.Context, req model.Request) (model.Response, error)
}

LLMClient abstracts communication with a language model. Implement this interface to support any LLM provider.

type LiteLLMClient

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

LiteLLMClient adapts the openai-go client to the LLMClient interface. Works with OpenAI directly or with a LiteLLM proxy (same API surface).

func NewLiteLLMClient

func NewLiteLLMClient(client *openai.Client, model openai.ChatModel) *LiteLLMClient

NewLiteLLMClient creates a LiteLLMClient wrapping the provided openai-go client.

func (*LiteLLMClient) Generate

func (c *LiteLLMClient) Generate(ctx context.Context, req model.Request) (model.Response, error)

Generate translates a Request into an OpenAI chat completion and maps the response back to ContentItem types.

type MemoryInjector added in v1.0.1

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

MemoryInjector adds retrieved long-term memories to the prompt instructions.

func NewMemoryInjector added in v1.0.1

func NewMemoryInjector(searcher MemorySearcher, topK int) MemoryInjector

NewMemoryInjector creates a request mutator backed by semantic memory search.

func (MemoryInjector) Mutate added in v1.0.1

func (i MemoryInjector) Mutate(ctx context.Context, req *model.Request) error

Mutate looks up similar past tasks and injects them into the instructions.

type MemorySearcher added in v1.0.1

type MemorySearcher interface {
	Search(ctx context.Context, query string, topK int) ([]TaskMemory, error)
}

MemorySearcher retrieves similar task memories for prompt injection.

type MutatingLLMClient added in v1.0.1

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

MutatingLLMClient applies request mutators and then forwards the request to another LLMClient.

func NewMutatingLLMClient added in v1.0.1

func NewMutatingLLMClient(delegate LLMClient, mutators ...RequestMutator) *MutatingLLMClient

NewMutatingLLMClient wraps an LLMClient with one or more request mutators.

func (*MutatingLLMClient) Generate added in v1.0.1

func (c *MutatingLLMClient) Generate(ctx context.Context, req model.Request) (model.Response, error)

Generate clones the request, applies every mutator in order, and delegates the call.

type OptimizationStrategy added in v1.0.1

type OptimizationStrategy interface {
	Optimize(ctx context.Context, req *model.Request) error
}

OptimizationStrategy rewrites a request to reduce context size or noise.

type RequestMutator added in v1.0.1

type RequestMutator interface {
	Mutate(ctx context.Context, req *model.Request) error
}

RequestMutator rewrites a request immediately before the delegated LLM call.

func WithMutatorLogger added in v1.0.1

func WithMutatorLogger(mutator RequestMutator, logger *slog.Logger) RequestMutator

WithMutatorLogger wraps a request mutator with structured start/finish logging.

type RequestTokenCounter added in v1.0.1

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

RequestTokenCounter uses tiktoken-compatible tokenization to count request size.

func NewRequestTokenCounter added in v1.0.1

func NewRequestTokenCounter(modelName string) (*RequestTokenCounter, error)

NewRequestTokenCounter constructs a token counter for the given model name.

func (*RequestTokenCounter) Count added in v1.0.1

func (c *RequestTokenCounter) Count(req model.Request) (int, error)

Count returns the approximate token count for instructions, events, and tools.

type Result

type Result struct {
	// Output is the final answer produced by the LLM.
	Output string
	// ToolCalled reports whether at least one tool was invoked during the run.
	ToolCalled bool
	// Context is the full execution history for this run.
	Context *ExecutionContext
}

Result is the output of a successful Agent.Run() call.

type RunEndEvent

type RunEndEvent struct {
	RunID  string
	Result *Result
	Err    error
}

RunEndEvent is emitted once when Run returns, whether it succeeded. Result is nil when Err is non-nil.

type RunResult added in v1.0.1

type RunResult struct {
	Output             string
	ToolCalled         bool
	Status             RunStatus
	SessionID          string
	PendingInteraction *InteractionRequest
}

RunResult summarizes one SessionRunner invocation.

type RunStartEvent

type RunStartEvent struct {
	RunID       string
	UserMessage string
}

RunStartEvent is emitted once before the ReAct loop begins.

type RunStatus added in v1.0.1

type RunStatus string

RunStatus reports whether a session run finished or is waiting for input.

const (
	// StatusComplete indicates the session run finished with a final answer.
	StatusComplete RunStatus = "complete"
	// StatusPending indicates the run suspended awaiting external input.
	StatusPending RunStatus = "pending"
)

type Session added in v1.0.1

type Session struct {
	SessionID string
	UserID    string
	Events    []model.Event
	State     map[string]any
	CreatedAt time.Time
	UpdatedAt time.Time
}

Session stores the persisted conversation history and runner state for one user-facing conversation.

type SessionManager added in v1.0.1

type SessionManager interface {
	Create(sessionID, userID string) (*Session, error)
	Get(sessionID string) (*Session, error)
	Save(session *Session) error
	GetOrCreate(sessionID, userID string) (*Session, error)
}

SessionManager persists and reloads sessions for SessionRunner.

type SessionRunner added in v1.0.1

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

SessionRunner replays prior events from a session, executes the agent loop, and persists the updated state after each run or resume.

func NewSessionRunner added in v1.0.1

func NewSessionRunner(agent *Agent, sessions SessionManager, maxSteps int) *SessionRunner

NewSessionRunner builds a session-aware wrapper around Agent.

func (*SessionRunner) Resume added in v1.0.1

func (r *SessionRunner) Resume(ctx context.Context, sessionID, userID string, response InteractionResponse) (*RunResult, error)

Resume continues a previously suspended session using an external response.

func (*SessionRunner) Run added in v1.0.1

func (r *SessionRunner) Run(ctx context.Context, sessionID, userID, userInput string) (*RunResult, error)

Run appends the new user input to the stored conversation, executes until the run completes or suspends, and then persists the updated session state.

func (*SessionRunner) WithLogger added in v1.0.1

func (r *SessionRunner) WithLogger(logger *slog.Logger) *SessionRunner

WithLogger attaches structured lifecycle logging to the runner.

type SimpleDuplicateChecker added in v1.0.1

type SimpleDuplicateChecker struct{}

SimpleDuplicateChecker treats an identical TaskMemory payload as a duplicate.

func (SimpleDuplicateChecker) IsDuplicate added in v1.0.1

func (SimpleDuplicateChecker) IsDuplicate(memory TaskMemory, existing []TaskMemory) bool

IsDuplicate reports whether memory matches any candidate exactly.

type SlidingWindowStrategy added in v1.0.1

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

SlidingWindowStrategy keeps the latest user message plus a bounded tail of recent events.

func NewSlidingWindowStrategy added in v1.0.1

func NewSlidingWindowStrategy(windowSize int) SlidingWindowStrategy

NewSlidingWindowStrategy creates a sliding-window optimizer.

func (SlidingWindowStrategy) Optimize added in v1.0.1

Optimize trims the request history while preserving the most recent user turn.

type StaticApprovalPolicy added in v1.0.1

type StaticApprovalPolicy map[string]ApprovalRule

StaticApprovalPolicy maps tool names directly to approval rules.

func (StaticApprovalPolicy) RuleForTool added in v1.0.1

func (p StaticApprovalPolicy) RuleForTool(name string) (ApprovalRule, bool)

RuleForTool returns the configured rule for name, if any.

type StepEndEvent

type StepEndEvent struct {
	RunID string
	Step  int
	Err   error
}

StepEndEvent is emitted after each Think→Act cycle. Err is non-nil when the step failed.

type StepStartEvent

type StepStartEvent struct {
	RunID string
	Step  int
}

StepStartEvent is emitted at the beginning of each Think→Act cycle.

type SummarizationStrategy added in v1.0.1

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

SummarizationStrategy replaces older middle-history events with a generated summary.

func NewSummarizationStrategy added in v1.0.1

func NewSummarizationStrategy(generator SummaryGenerator, keepRecent int) SummarizationStrategy

NewSummarizationStrategy creates a summary-based optimization strategy.

func (SummarizationStrategy) Optimize added in v1.0.1

func (s SummarizationStrategy) Optimize(ctx context.Context, req *model.Request) error

Optimize inserts a summary into the instructions and removes summarized events.

type SummaryGenerator added in v1.0.1

type SummaryGenerator interface {
	Summarize(ctx context.Context, events []model.Event) (string, error)
}

SummaryGenerator compresses older events into a summary string.

type SuspendedRun added in v1.0.1

type SuspendedRun struct {
	Context     *ExecutionContext
	Interaction InteractionRequest
}

SuspendedRun contains the paused execution state and the pending interaction.

type TaskMemory added in v1.0.1

type TaskMemory struct {
	TaskSummary   string
	Approach      string
	FinalAnswer   string
	IsCorrect     bool
	ErrorAnalysis string
}

TaskMemory stores a reusable record of how a prior task was solved.

func (TaskMemory) EmbeddingText added in v1.0.1

func (m TaskMemory) EmbeddingText() string

EmbeddingText returns the text used to embed this memory for similarity search.

type TaskMemoryManager added in v1.0.1

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

TaskMemoryManager saves and retrieves semantically indexed task memories.

func NewTaskMemoryManager added in v1.0.1

func NewTaskMemoryManager(embedder Embedder, store VectorStore, duplicateChecker DuplicateChecker) *TaskMemoryManager

NewTaskMemoryManager creates a semantic memory manager from its pluggable components.

func (*TaskMemoryManager) Save added in v1.0.1

func (m *TaskMemoryManager) Save(ctx context.Context, memory TaskMemory) (string, bool, error)

Save embeds, de-duplicates, and persists a task memory.

func (*TaskMemoryManager) Search added in v1.0.1

func (m *TaskMemoryManager) Search(ctx context.Context, query string, topK int) ([]TaskMemory, error)

Search retrieves similar task memories for a natural-language query.

func (*TaskMemoryManager) WithLogger added in v1.0.1

func (m *TaskMemoryManager) WithLogger(logger *slog.Logger) *TaskMemoryManager

WithLogger attaches structured save and search logs.

type TokenCounter added in v1.0.1

type TokenCounter interface {
	Count(req model.Request) (int, error)
}

TokenCounter estimates the prompt size of a request before it is sent to the LLM.

type ToolExecEvent

type ToolExecEvent struct {
	RunID     string
	Step      int
	ToolNames []string
	Latency   time.Duration
	Err       error
}

ToolExecEvent is emitted after every executor.Execute() batch. ToolNames lists the names of tools that were called. Latency is 0 when the executor is nil.

type VectorDocument added in v1.0.1

type VectorDocument struct {
	ID     string
	Vector []float64
	Memory TaskMemory
}

VectorDocument binds an embedding to its original task memory payload.

type VectorStore added in v1.0.1

type VectorStore interface {
	Add(ctx context.Context, docs []VectorDocument) error
	Search(ctx context.Context, query []float64, topK int) ([]VectorDocument, error)
}

VectorStore persists vectorized memories and supports nearest-neighbor lookup.

Directories

Path Synopsis
Package mcpadapter bridges github.com/v8tix/mcp-toolkit with react-agent.
Package mcpadapter bridges github.com/v8tix/mcp-toolkit with react-agent.
Package model contains the core data types shared across react-agent and its sub-packages.
Package model contains the core data types shared across react-agent and its sub-packages.

Jump to

Keyboard shortcuts

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