agent

package module
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: MIT Imports: 22 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

🧾 Terminology Guide

These terms show up across the library because they describe the shape of agent work, not one specific app.

Term Plain-English meaning Friendly example Related API
Chunk a small piece of source text you can retrieve later one paragraph from a refund policy your app's indexing layer
Chunk context enrichment adding source details so a chunk still makes sense by itself "Refund Policy — refunds accepted within 30 days" instead of just "refunds accepted within 30 days" ChunkContextEnricher
Lexical retrieval finding results by exact words or phrases query "refund policy" matches a chunk containing those exact words app-defined retriever
Semantic retrieval finding results by meaning, even if the wording changes query "money back rules" still finds the refund chunk embedder + vector store
Hybrid retrieval combining more than one retrieval signal into one shortlist exact words + meaning together HybridRetriever
Reranking taking a rough shortlist and reordering it with a stronger second pass top 20 search hits become the best 3 Reranker
Approval loop pausing before a risky action so a human can approve it ask before delete_file or charge_card ConfirmationCallback, Suspend, Resume
Callback a hook that can inspect, block, or rewrite work around tool execution reject a tool call or shorten a huge tool result BeforeToolCallback, AfterToolCallback
Dynamic tools showing the model only the tools that make sense right now planning turn sees create_tasks, recovery turn sees research_conversion WithDynamicToolsCallback
Workflow-owned control keeping business rules in your app while the library keeps running the loop the workflow decides "plan -> convert -> fallback -> verify", not the generic runtime callbacks + session/state in your app
Deterministic phase a step where the workflow should decide what happens next, not the model after an unsupported conversion, force fallback instead of asking the model to choose dynamic tools + callbacks
Grounding making the final answer explicitly rely on authoritative facts already gathered reject an answer that ignores the recovered meter value FinalAnswerCallback
Circuit breaker stopping a repeated bad action instead of looping forever block the same illegal tool twice, then fail loudly BeforeToolCallback
Compression / context optimization shrinking noisy history before the next LLM call turn a 2-page HTML result into 3 useful lines ContextOptimizer, CompactionStrategy, SummarizationStrategy
Task memory storing solved-task patterns so the agent can reuse them later "we solved a similar outage last week" TaskMemoryManager, MemoryInjector
Selective write storing only high-value memories instead of every completed task skip saving "what is 2+2" but keep "how we debugged the checkout outage" MemoryWritePolicy, ThresholdMemoryWritePolicy

🙂 Friendly rule: retrieve wide, then narrow; pause before risky actions; shrink context before it becomes noise.

Sequence: chunk context enrichment

Use this when a raw chunk is too small to stand on its own.

sequenceDiagram
    participant App as 👤 App
    participant E as 🧩 ChunkContextEnricher
    participant I as 🗂️ Index

    App->>E: EnrichChunk("refunds accepted within 30 days", metadata)
    E-->>App: "Refund Policy — refunds accepted within 30 days"
    App->>I: store enriched chunk

Without enrichment, the chunk may be technically correct but hard to understand once it is separated from the full document.

Sequence: hybrid retrieval

Use this when exact wording matters and meaning matters.

sequenceDiagram
    participant App as 👤 App
    participant H as 🔀 HybridRetriever
    participant L as 🔎 Lexical search
    participant S as 🧠 Semantic search

    App->>H: Retrieve("money back rules", 10)
    H->>L: exact-word lookup
    L-->>H: candidates with phrase overlap
    H->>S: meaning-based lookup
    S-->>H: candidates with semantic similarity
    H-->>App: merged RetrievalCandidate list

Think of hybrid retrieval as two flashlights pointed at the same shelf: one catches exact labels, the other catches similar ideas.

Sequence: reranking

Use this when the first retrieval pass is broad but not precise enough.

sequenceDiagram
    participant App as 👤 App
    participant R as 🎯 Reranker

    App->>R: Rerank(query, roughTopK, 3)
    R-->>App: better-ordered top 3

The common rhythm is: retrieve 20 quickly, rerank to 3 carefully.

Sequence: approval loop

Use this when a tool can do something expensive, destructive, or externally visible.

sequenceDiagram
    participant App as 👤 App
    participant A as 🤖 Agent
    participant C as ✅ ConfirmationCallback
    participant UI as 🙋 Human approver
    participant X as 🔧 ToolExecutor

    App->>A: "Delete danger.txt"
    A->>C: BeforeTool(delete_file)
    C-->>A: Suspend(InteractionRequest)
    A-->>UI: approval needed
    UI-->>A: Resume(approved=true)
    A->>X: Execute(delete_file)
    X-->>A: ToolResult(success)
    A-->>App: final answer

If the answer is "no", the callback can return a synthetic error result instead of letting the tool run.

Sequence: compression and context optimization

Use this when tool output or long conversations start crowding out the useful parts.

sequenceDiagram
    participant App as 👤 App
    participant M as 🪄 MutatingLLMClient
    participant O as 🪶 ContextOptimizer
    participant C as 📦 Compaction / summary strategy
    participant L as 🧠 LLM

    App->>M: Generate(request)
    M->>O: Mutate(request)
    O->>C: shrink bulky history
    C-->>O: trimmed request
    O-->>M: optimized request
    M->>L: Generate(optimized request)

This is less about deleting information and more about keeping the signal while dropping the clutter.

💡 react-agent intentionally keeps retrieval, reranking, approvals, and context control as small contracts. You can plug in your own search stack, vector DB, safety policy, or compression strategy without changing the core ReAct loop.

Sequence: workflow-owned control

Use this when the loop is still useful, but some steps must follow a strict app-defined path.

flowchart TD
    U["👤 User asks for a bounded workflow"] --> P["🗂️ Planning phase\nshow only create_tasks"]
    P --> DC["📏 Direct conversion phase\ncontroller emits convert_units"]
    DC --> DECIDE{{"unsupported\nconversion?"}}
    DECIDE -->|"yes"| FC["🛟 Fallback phase\ncontroller emits research_conversion"]
    DECIDE -->|"no"| E["🔎 Evidence phase\nshow / emit gather_fact"]
    FC --> E
    E --> G["🧾 Final answer gate\ncheck grounding"]
    G -->|"grounded"| A["✅ Final answer"]
    G -->|"not grounded"| R["↩️ corrective user message"]
    R --> A

Friendly mental model: the library still runs the same loop, but your workflow can narrow the lane. The model keeps its reasoning ability, while your app decides which parts are too important to leave open-ended.


🔧 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"]
    START -->|"Durable production agent"| DURABLE["🔄 Flow 9 + 🧵 Flow 5"]
    START -->|"Bounded workflow with rules"| CONTROL["🧭 Flow 10 + 🔧 Flow 2"]
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 9 — 🔄 Durable Workflows persistent sessions + selective memory ⭐⭐⭐⭐ SessionRunner, persister, memory policies
Flow 10 — 🧭 Workflow-Owned Control bounded flows with deterministic steps ⭐⭐⭐⭐ dynamic tools, callbacks, final-answer gates

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.


Sequence: selective memory write

Use this when some task outcomes are worth keeping long-term, but many are just noise.

sequenceDiagram
    participant App as 👤 App
    participant MM as 📚 TaskMemoryManager
    participant P as 🎯 MemoryWritePolicy
    participant VS as 🗂️ VectorStore

    App->>MM: Save(TaskMemory)
    MM->>P: Decide(memory)
    alt high value
        P-->>MM: ShouldStore=true
        MM->>VS: Add(memory)
        VS-->>MM: stored
        MM-->>App: saved=true
    else low value
        P-->>MM: ShouldStore=false
        MM-->>App: saved=false
    end

Think of this as memory budgeting: keep the hard-won lessons, skip the trivia.

memories := agent.NewTaskMemoryManager(
    embedder,
    agent.NewInMemoryVectorStore(),
    agent.SimpleDuplicateChecker{},
).WithWritePolicy(agent.NewThresholdMemoryWritePolicy(0.6))

_, saved, err := memories.Save(ctx, agent.TaskMemory{
    TaskSummary: "debug checkout timeout with retries",
    Approach:    "trace logs, isolate retry loop, then patch",
    FinalAnswer: "fixed with bounded retry backoff",
    IsCorrect:   true,
})
if err != nil {
    log.Fatal(err)
}
fmt.Println("saved:", saved)

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 9 — 🔄 Durable Workflows: persistent sessions, selective memory, and cache-friendly prompts

Study this flow when: your agent must survive restarts, remember only useful lessons, and keep repeated prompt setup cheap.

sequenceDiagram
    participant App as 👤 App / API
    participant SR as 🧵 SessionRunner
    participant PS as 💾 SessionPersister
    participant MC as 🪄 MutatingLLMClient
    participant SPD as 📍 StablePrefixDetector
    participant AG as 🤖 Agent
    participant MM as 📚 TaskMemoryManager
    participant WP as 🎯 MemoryWritePolicy

    App->>SR: Run(sessionID, userID, question)
    SR->>PS: LoadSession(sessionID)
    PS-->>SR: prior events + state
    SR->>AG: replay and continue workflow
    AG->>MC: Generate(request)
    MC->>SPD: Detect stable prefix
    SPD-->>MC: reusable prefix
    MC-->>AG: optimized request
    AG-->>SR: result
    SR->>MM: Save(TaskMemory)
    MM->>WP: Decide(memory value)
    WP-->>MM: store or skip
    SR->>PS: SaveSession(updated)
    SR-->>App: result

This flow combines four ideas that often show up together in production:

  1. Persistent sessions keep the conversation alive across restarts or separate workers.
  2. Selective memory writes stop long-term memory from filling with low-value tasks.
  3. Stable prefix detection helps your LLM client identify the reusable part of a request for prompt-caching workflows.
  4. Workflow composition lets later turns depend on facts or state captured earlier in the same session.
Persistent sessions

InMemorySessionManager is great for local development. In production, swap in NewPersistedSessionManager so a restart does not erase the conversation.

type myPersister struct{}

func (p *myPersister) SaveSession(ctx context.Context, session agent.Session) error {
    return saveToStore(ctx, session) // your DB, Redis, S3, etc.
}

func (p *myPersister) LoadSession(ctx context.Context, sessionID string) (agent.Session, error) {
    return loadFromStore(ctx, sessionID)
}

sessions := agent.NewPersistedSessionManager(&myPersister{})
runner := agent.NewSessionRunner(a, sessions, 8)
Stable prefixes and caching

A stable prefix is the part of the request that barely changes across turns: instructions, fixed tool setup, and maybe the early session scaffold. If your provider supports prompt caching, this is the part you usually want to mark as reusable.

Friendly example:

  • stable: "You are a support assistant..." + fixed tool definitions
  • unstable: the newest user message, fresh tool output, current turn state

react-agent does not force one provider-specific caching implementation. It gives you the seam so your own LLM client can use the detected prefix.

Workflow composition

Treat Session.State like shared scratch space across turns.

first, _ := runner.Run(ctx, "order-42", "user-9", "Find the root cause")
session, _ := sessions.Get("order-42")
session.State["root_cause"] = first.Output
_ = sessions.Save(session)

second, _ := runner.Run(ctx, "order-42", "user-9", "Propose the safest fix")
_ = second

That pattern is what turns a chat session into a multi-step workflow.


Flow 10 — 🧭 Workflow-Owned Control: deterministic phases inside an open-ended loop

Study this flow when: your agent still benefits from ReAct, but some steps are too important, expensive, or risky to leave fully open-ended.

Good examples:

  1. plan first, then force a conversion step
  2. switch to a fallback path after a permanent tool failure
  3. require supporting facts before the answer
  4. reject a final answer that ignores authoritative values already collected
sequenceDiagram
    participant U as 👤 User
    participant A as 🤖 Agent
    participant D as 🧰 Dynamic tool callback
    participant B as 🪝 Before/After callbacks
    participant X as 🔧 ToolExecutor
    participant F as ✅ Final answer gate

    U->>A: "Run the bounded workflow"
    A->>D: Which tools are visible now?
    D-->>A: create_tasks only
    A->>X: Execute(create_tasks)
    B-->>A: move to direct conversion phase
    A->>D: Which tools are visible now?
    D-->>A: convert_units only
    A->>X: Execute(convert_units)
    B-->>A: unsupported conversion -> fallback phase
    A->>D: Which tools are visible now?
    D-->>A: research_conversion only
    A->>X: Execute(research_conversion)
    B-->>A: collect recovered facts
    A->>F: Proposed final answer
    F-->>A: accept or reject with corrective message
    A-->>U: grounded final answer

This is the pattern we used for a bounded adaptive workflow:

  • the library still owns the loop, history, callbacks, suspension, and resume
  • the workflow owns phases, allowed tools, fallback rules, circuit breakers, and grounding checks
phaseTracker := newMyWorkflowStateMachine()

a := agent.New(client, defs, executor).
    WithDynamicToolsCallback(func(execCtx *agent.ExecutionContext) []model.ToolDefinition {
        return phaseTracker.AllowedTools(defs)
    }).
    WithBeforeToolCallbacks(phaseTracker).
    WithAfterToolCallbacks(phaseTracker).
    WithFinalAnswerCallbacks(myWorkflowGate{tracker: phaseTracker})

What to study here:

  1. Use dynamic tools when the model should only see the tools that make sense in the current phase.
  2. Use before/after callbacks when the workflow needs to track failures, state transitions, or repeated bad behavior.
  3. Use a final answer callback when the answer must mention or rely on specific verified facts.
  4. Keep the workflow rules in your app code — react-agent gives you the seams, not one hardcoded business process.

🙂 Friendly rule: let the model stay flexible where reasoning helps, but take the wheel for the steps that must be correct in the same way every time.


🔗 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 + Flow 9 observe runs, persist turns, keep prompts small, and save only useful lessons
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 + Flow 9 remember the conversation, manage token budget, learn from resolved cases, and survive restarts
Bounded adaptive workflow Flow 2 + Flow 3 + Flow 10 let the model reason, but keep critical phases deterministic and grounded

🧩 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
WithDynamicToolsCallback 🧰 Per-turn tool visibility — show only what this phase should allow
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)

When a workflow should expose only a subset of tools at a given step, add Agent.WithDynamicToolsCallback:

a := agent.New(client, toolDefs, executor).
         WithDynamicToolsCallback(func(execCtx *agent.ExecutionContext) []model.ToolDefinition {
             _ = execCtx // inspect current events or state here
             return toolDefs[:1]
         })

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

If you want live logging while the run is still executing, attach a sink with Agent.WithLiveEventSink:

a := agent.New(client, toolDefs, executor).
         WithLiveEventSink(func(event agent.AgentEvent) {
             if e, ok := event.(agent.LLMCallEvent); ok {
                 slog.Info("live llm call", "step", e.Step, "latency_ms", e.Latency.Milliseconds())
             }
         })

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. Use NewPersistedSessionManager with a SessionPersister when that state must survive process boundaries:

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. Read and write [Session.State] when later turns depend on facts captured earlier — it works well as shared scratch space for multi-step workflows.

Planning and reflection policies

For tasks that benefit from explicit planning, pair NewPlanningExecutor with PlanningToolDefinition. Add NewPlanningReflectionTracker plus NewPlanningReflectionPolicy when you want the agent to revise its task list before finalizing. Use WithPlanningReflectionStagnationThreshold to turn on a stricter loop where repeated planning-only churn triggers an explicit reflection step before more planning is allowed. When final answers must be grounded in gathered facts, add NewVerificationGate after an EvidenceCollector has started recording support for the answer.

Workflow-owned control

Some applications need more than generic tool use. They need a bounded workflow where the model can still reason, but the application controls the critical phases. A friendly way to think about this is:

plan -> deterministic step -> fallback if needed -> gather evidence -> grounded answer

`react-agent` keeps those workflow rules out of the core runtime. Instead it exposes small seams so the application can own the policy:

A typical workflow-controlled setup looks like:

phaseTracker := newMyWorkflowTracker()
a := agent.New(client, defs, executor).
         WithDynamicToolsCallback(func(execCtx *agent.ExecutionContext) []model.ToolDefinition {
             return phaseTracker.AllowedTools(defs)
         }).
         WithBeforeToolCallbacks(phaseTracker).
         WithAfterToolCallbacks(phaseTracker).
         WithFinalAnswerCallbacks(myWorkflowGate{tracker: phaseTracker})

In that pattern, the library still owns the ReAct loop, history, events, and suspension/resume flow, while the application owns the business-specific workflow phases.

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)

Attach [WithWritePolicy] to TaskMemoryManager when only higher-value task completions should be stored as long-term memory. The built-in ThresholdMemoryWritePolicy is a good default when you want to skip trivial or low-detail task outcomes instead of saving every successful run.

Retrieval-heavy applications can keep retrieval logic outside the agent while still sharing common contracts. HybridRetriever expresses a query-to-candidate retrieval step, Reranker refines those candidates, and ChunkContextEnricher can add document-aware context before indexing.

Retrieval terminology

A few retrieval words show up often when building agent systems:

  • A "chunk" is a small piece of source text that can be stored and retrieved later. For example, one paragraph from a refund policy.
  • "Chunk context enrichment" means attaching source-level context so the chunk still makes sense by itself. For example, turning "refunds accepted within 30 days" into "Refund Policy — refunds accepted within 30 days".
  • "Lexical retrieval" means matching exact words or phrases.
  • "Semantic retrieval" means matching by meaning, even when wording changes.
  • "Hybrid retrieval" means combining more than one retrieval signal into a single shortlist.
  • "Reranking" means taking that rough shortlist and reordering it with a slower, more precise second pass.
  • "Dynamic tools" means showing the model only the tools that make sense in the current phase of the workflow.
  • "Grounding" means requiring the final answer to rely on authoritative facts already captured in the run.
  • A "circuit breaker" means stopping a repeated bad action instead of letting the loop retry the same blocked path forever.

`react-agent` does not force one retrieval stack. Instead it exposes small contracts so applications can plug in their own lexical search, vector search, reranking model, or indexing pipeline without changing the core agent loop.

Approval and compression terminology

Two other concepts appear frequently in production agents:

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 FormatPlanTasks added in v1.0.2

func FormatPlanTasks(tasks []PlanTask) string

FormatPlanTasks renders a full plan in the format expected by the planning flow.

func MarshalPlanTasks added in v1.0.2

func MarshalPlanTasks(tasks []PlanTask) (json.RawMessage, error)

MarshalPlanTasks marshals plan tasks into the JSON payload expected by the create_tasks tool.

func PlanningToolDefinition added in v1.0.2

func PlanningToolDefinition() model.ToolDefinition

PlanningToolDefinition returns a reusable ToolDefinition for Chapter 7 style planning.

func QueueDeferredUserMessage added in v1.0.2

func QueueDeferredUserMessage(execCtx *ExecutionContext, content string)

QueueDeferredUserMessage schedules a user message to be appended after the current tool result event finishes. Use this from callbacks that need to steer the next LLM turn without breaking the event ordering invariants of the run.

func Suspend added in v1.0.1

func Suspend(req InteractionRequest) error

Suspend requests external interaction from inside a callback.

Returning Suspend(req) from a callback is what turns a normal run into an approval loop or any other human-in-the-loop step.

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) WithDynamicToolsCallback added in v1.0.2

func (a *Agent) WithDynamicToolsCallback(cb DynamicToolsCallback) *Agent

WithDynamicToolsCallback overrides the tool definitions sent to the LLM for each turn.

Returning nil or an empty slice hides all tools for that turn.

func (*Agent) WithFinalAnswerCallbacks added in v1.0.2

func (a *Agent) WithFinalAnswerCallbacks(callbacks ...FinalAnswerCallback) *Agent

WithFinalAnswerCallbacks appends callbacks that validate a proposed final answer before the run is allowed to finish.

func (*Agent) WithInstructions

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

WithInstructions sets the system prompt sent on every LLM request.

func (*Agent) WithLiveEventSink added in v1.0.2

func (a *Agent) WithLiveEventSink(sinks ...LiveEventSink) *Agent

WithLiveEventSink appends callbacks that receive agent events while the run is active.

The replayable observable returned by Run/Resume remains unchanged; this hook is for live logging, metrics, or tracing during execution.

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. Events are collected during a run and replayed afterward through a cold observable, so multiple Observe() calls are safe and deterministic. 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 shown to a human and what message should come back if the action is 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 ChunkContextEnricher added in v1.0.2

type ChunkContextEnricher interface {
	EnrichChunk(context.Context, string, string, map[string]string) (string, error)
}

ChunkContextEnricher adds source-level context to a chunk before downstream indexing or retrieval so the chunk still makes sense on its own.

Typical enrichments include document title, URL, section heading, or other metadata that would be lost if the raw chunk were indexed by itself.

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 pauses execution before selected tools so an external UI, API, or human can approve or deny the action.

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 before they are exposed in the interaction 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, letting callers compress or summarize history before the next model call.

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 when a request grows past the configured budget.

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 DynamicToolsCallback added in v1.0.2

type DynamicToolsCallback func(*ExecutionContext) []model.ToolDefinition

DynamicToolsCallback can tailor the tool list visible to the LLM for the next turn.

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 EvidenceCollector added in v1.0.2

type EvidenceCollector interface {
	Evidence() []EvidenceItem
}

EvidenceCollector exposes the current set of gathered evidence items.

type EvidenceItem added in v1.0.2

type EvidenceItem struct {
	Source  string
	Content string
	Score   float64
}

EvidenceItem represents one supporting fact that can justify a final answer.

type EvidenceTracker added in v1.0.2

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

EvidenceTracker records supporting evidence from successful tool results.

func NewEvidenceTracker added in v1.0.2

func NewEvidenceTracker(mapper func(model.ToolResult) (EvidenceItem, bool)) *EvidenceTracker

NewEvidenceTracker creates a reusable evidence tracker with a caller-provided mapper.

func (*EvidenceTracker) AfterTool added in v1.0.2

AfterTool implements AfterToolCallback.

func (*EvidenceTracker) Evidence added in v1.0.2

func (t *EvidenceTracker) Evidence() []EvidenceItem

Evidence returns the recorded evidence items.

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 FinalAnswerCallback added in v1.0.2

type FinalAnswerCallback interface {
	BeforeFinalAnswer(ctx context.Context, execCtx *ExecutionContext, answer string) error
}

FinalAnswerCallback can reject a proposed final answer before the agent ends the run. Returning an error keeps the loop alive and lets the caller inject a corrective message back into the conversation.

type HybridRetriever added in v1.0.2

type HybridRetriever interface {
	Retrieve(context.Context, string, int) ([]RetrievalCandidate, error)
}

HybridRetriever returns a ranked candidate list for a query.

Implementations commonly combine lexical search, semantic search, metadata filters, or any other retrieval signal behind one method.

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 question that must be answered from outside the agent before the run can continue.

Common examples are "approve this tool call?" or "which account should be used for this action?"

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 LiveEventSink added in v1.0.2

type LiveEventSink func(AgentEvent)

LiveEventSink consumes agent events as they are emitted during a run.

Unlike the replayable observable returned by Agent.Run, a live sink receives events while the run is still in progress. Sinks should stay lightweight and non-blocking because they execute on the event collector goroutine.

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 so the model can reuse prior approaches instead of starting from scratch.

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 MemoryWriteDecision added in v1.0.2

type MemoryWriteDecision struct {
	ShouldStore bool
	Reason      string
	Score       float64
}

MemoryWriteDecision captures whether a long-term memory record should be stored.

type MemoryWritePolicy added in v1.0.2

type MemoryWritePolicy interface {
	Decide(context.Context, TaskMemory) (MemoryWriteDecision, error)
}

MemoryWritePolicy decides whether a task memory is worth persisting.

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 Observation added in v1.0.2

type Observation struct {
	ToolCallID string
	ToolName   string
	Content    string
}

Observation is a tool result captured during synthesis.

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 PlanRevision added in v1.0.2

type PlanRevision struct {
	Index     int
	Plan      string
	TaskCount int
}

PlanRevision represents a captured planning snapshot.

type PlanRevisionEvent added in v1.0.2

type PlanRevisionEvent struct {
	RunID    string
	Step     int
	Revision PlanRevision
}

PlanRevisionEvent is emitted when a planning tool call records a new revision.

type PlanTask added in v1.0.2

type PlanTask struct {
	Content string
	Status  PlanTaskStatus
}

PlanTask represents a single item in a planning tool result.

func ParsePlanTasks added in v1.0.2

func ParsePlanTasks(data []byte) ([]PlanTask, error)

ParsePlanTasks unmarshals the JSON payload used by the create_tasks tool into the typed plan representation.

func (PlanTask) String added in v1.0.2

func (t PlanTask) String() string

String formats the task using the chapter's checklist-style representation.

type PlanTaskStatus added in v1.0.2

type PlanTaskStatus string

PlanTaskStatus describes the execution state of a task in a generated plan.

const (
	// PlanTaskPending is used for tasks that have not started yet.
	PlanTaskPending PlanTaskStatus = "pending"
	// PlanTaskInProgress is used for the next task the agent should work on.
	PlanTaskInProgress PlanTaskStatus = "in_progress"
	// PlanTaskCompleted is used for tasks that are already finished.
	PlanTaskCompleted PlanTaskStatus = "completed"
)

type PlanningExecutor added in v1.0.2

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

PlanningExecutor executes create_tasks calls, returning the formatted plan and capturing each plan snapshot for later inspection. Unknown tools can be delegated to another executor when composition is needed.

func NewPlanningExecutor added in v1.0.2

func NewPlanningExecutor(delegate model.ToolExecutor) *PlanningExecutor

NewPlanningExecutor creates a PlanningExecutor that optionally delegates non-planning tools to another executor.

func (*PlanningExecutor) Execute added in v1.0.2

func (e *PlanningExecutor) Execute(ctx context.Context, calls []model.ToolCall) ([]model.ToolResult, error)

Execute handles planning tool calls directly and delegates all other calls to the underlying executor when present.

func (*PlanningExecutor) LatestPlan added in v1.0.2

func (e *PlanningExecutor) LatestPlan() (string, bool)

LatestPlan returns the most recently captured plan snapshot.

func (*PlanningExecutor) Plans added in v1.0.2

func (e *PlanningExecutor) Plans() []string

Plans returns the recorded formatted plan snapshots.

func (*PlanningExecutor) Revisions added in v1.0.2

func (e *PlanningExecutor) Revisions() []PlanRevision

Revisions returns the recorded planning snapshots in a typed form that is easier to reuse in policies, tests, and observers.

func (*PlanningExecutor) TaskCounts added in v1.0.2

func (e *PlanningExecutor) TaskCounts() []int

TaskCounts returns the recorded task count for each captured plan snapshot.

func (*PlanningExecutor) WithObservers added in v1.0.2

func (e *PlanningExecutor) WithObservers(observers ...PlanningObserver) *PlanningExecutor

WithObservers registers planning observers that are notified for each new captured revision.

type PlanningObserver added in v1.0.2

type PlanningObserver interface {
	OnPlanRevision(revision PlanRevision)
}

PlanningObserver receives each captured planning revision as it is recorded.

type PlanningPolicy added in v1.0.2

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

PlanningPolicy enforces a minimum number of planning revisions before the agent can return a final answer.

func NewPlanningPolicy added in v1.0.2

func NewPlanningPolicy(source planRevisionSource, minRevisions int) *PlanningPolicy

NewPlanningPolicy creates a reusable planning-specific final-answer policy. A minimum revision count of zero or less defaults to two revisions.

func (*PlanningPolicy) BeforeFinalAnswer added in v1.0.2

func (p *PlanningPolicy) BeforeFinalAnswer(
	_ context.Context,
	_ *ExecutionContext,
	_ string,
) error

BeforeFinalAnswer implements FinalAnswerCallback.

type PlanningReflectionEvent added in v1.0.2

type PlanningReflectionEvent struct {
	RunID   string
	Step    int
	Kind    PlanningReflectionEventKind
	Content string
}

PlanningReflectionEvent is emitted when the unified planning/reflection tracker detects insufficient progress or records a reflection about plan revision needs.

type PlanningReflectionEventKind added in v1.0.2

type PlanningReflectionEventKind string

PlanningReflectionEventKind describes the type of planning/reflection event.

const (
	PlanningReflectionEventInsufficientProgress PlanningReflectionEventKind = "insufficient_progress"
	PlanningReflectionEventStagnationObserved   PlanningReflectionEventKind = "stagnation_observed"
	PlanningReflectionEventReflectionRecorded   PlanningReflectionEventKind = "reflection_recorded"
	PlanningReflectionEventRevisionNeeded       PlanningReflectionEventKind = "revision_needed"
	PlanningReflectionEventRevisionResolved     PlanningReflectionEventKind = "revision_resolved"
)

type PlanningReflectionOption added in v1.0.2

type PlanningReflectionOption func(*PlanningReflectionTracker)

PlanningReflectionOption configures a PlanningReflectionTracker.

func WithPlanningReflectionStagnationThreshold added in v1.0.2

func WithPlanningReflectionStagnationThreshold(n int) PlanningReflectionOption

WithPlanningReflectionStagnationThreshold enables stagnation-aware reflection after repeated planning-only revisions without meaningful progress.

type PlanningReflectionPolicy added in v1.0.2

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

PlanningReflectionPolicy enforces that the agent revises its plan after an early draft answer or a stagnation-triggered reflection before it can finalize.

func NewPlanningReflectionPolicy added in v1.0.2

func NewPlanningReflectionPolicy(
	source planRevisionSource,
	tracker *PlanningReflectionTracker,
	minRevisions int,
) *PlanningReflectionPolicy

NewPlanningReflectionPolicy creates a unified planning/reflection policy.

func (*PlanningReflectionPolicy) BeforeFinalAnswer added in v1.0.2

func (p *PlanningReflectionPolicy) BeforeFinalAnswer(
	ctx context.Context,
	execCtx *ExecutionContext,
	answer string,
) error

BeforeFinalAnswer implements FinalAnswerCallback.

type PlanningReflectionTracker added in v1.0.2

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

PlanningReflectionTracker coordinates plan revision after either an early answer or repeated planning-only stagnation.

func NewPlanningReflectionTracker added in v1.0.2

func NewPlanningReflectionTracker(options ...PlanningReflectionOption) *PlanningReflectionTracker

NewPlanningReflectionTracker creates a new unified planning/reflection tracker.

func (*PlanningReflectionTracker) AfterTool added in v1.0.2

func (t *PlanningReflectionTracker) AfterTool(
	ctx context.Context,
	execCtx *ExecutionContext,
	result model.ToolResult,
) (*model.ToolResult, error)

AfterTool records when planning stagnates and when a later plan revision resolves a previously requested revision.

func (*PlanningReflectionTracker) BeforeTool added in v1.0.2

BeforeTool blocks continued tool churn while a reflection is required.

func (*PlanningReflectionTracker) LatestReflection added in v1.0.2

func (t *PlanningReflectionTracker) LatestReflection() string

LatestReflection returns the most recently recorded planning reflection text.

func (*PlanningReflectionTracker) NeedsReflection added in v1.0.2

func (t *PlanningReflectionTracker) NeedsReflection() bool

NeedsReflection reports whether the agent must reflect before continuing.

func (*PlanningReflectionTracker) NeedsRevision added in v1.0.2

func (t *PlanningReflectionTracker) NeedsRevision() bool

NeedsRevision reports whether the current plan still must be revised before a final answer can be accepted.

func (*PlanningReflectionTracker) RecordReflection added in v1.0.2

func (t *PlanningReflectionTracker) RecordReflection(
	ctx context.Context,
	execCtx *ExecutionContext,
	reflection string,
	revisions int,
)

RecordReflection stores explicit reflection after a stagnation block and then requires a revised plan before the final answer.

type PolicyDecision added in v1.0.2

type PolicyDecision string

PolicyDecision is the outcome of a final-answer policy check.

const (
	PolicyDecisionAccept PolicyDecision = "accept"
	PolicyDecisionReject PolicyDecision = "reject"
)

type PolicyEvent added in v1.0.2

type PolicyEvent struct {
	RunID      string
	Step       int
	PolicyName string
	Decision   PolicyDecision
	Answer     string
	Reason     string
	Latency    time.Duration
}

PolicyEvent is emitted after a final-answer callback evaluates a proposed answer. It makes policy decisions observable in the same stream as other agent lifecycle events.

type RecoveryAttempt added in v1.0.2

type RecoveryAttempt struct {
	ToolCallID string
	ToolName   string
	Output     string
}

RecoveryAttempt captures a successful retry after a previous failure.

type RecoveryEvent added in v1.0.2

type RecoveryEvent struct {
	RunID      string
	Step       int
	Kind       RecoveryEventKind
	ToolCallID string
	ToolName   string
	Reason     string
}

RecoveryEvent is emitted when a recovery tracker observes a failed tool result or a successful retry.

type RecoveryEventKind added in v1.0.2

type RecoveryEventKind string

RecoveryEventKind describes where the agent is in an error-recovery flow.

const (
	RecoveryEventFailureObserved    RecoveryEventKind = "failure_observed"
	RecoveryEventRecovered          RecoveryEventKind = "recovered"
	RecoveryEventReflectionRecorded RecoveryEventKind = "reflection_recorded"
)

type RecoveryFailure added in v1.0.2

type RecoveryFailure struct {
	ToolCallID string
	ToolName   string
	Reason     string
}

RecoveryFailure captures a failed tool result that may require reflection and a retry before the agent can finish.

type RecoveryPolicy added in v1.0.2

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

RecoveryPolicy blocks final answers while unresolved tool failures remain.

func NewRecoveryPolicy added in v1.0.2

func NewRecoveryPolicy(source recoveryStateSource) *RecoveryPolicy

NewRecoveryPolicy creates a reusable recovery policy.

func (*RecoveryPolicy) BeforeFinalAnswer added in v1.0.2

func (p *RecoveryPolicy) BeforeFinalAnswer(
	ctx context.Context,
	execCtx *ExecutionContext,
	answer string,
) error

BeforeFinalAnswer implements FinalAnswerCallback.

type RecoveryTracker added in v1.0.2

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

RecoveryTracker records failed tool results and successful retries. It can be plugged directly into the agent as an AfterToolCallback.

func NewRecoveryTracker added in v1.0.2

func NewRecoveryTracker() *RecoveryTracker

NewRecoveryTracker creates a reusable recovery tracker.

func (*RecoveryTracker) AfterTool added in v1.0.2

func (r *RecoveryTracker) AfterTool(
	ctx context.Context,
	execCtx *ExecutionContext,
	result model.ToolResult,
) (*model.ToolResult, error)

AfterTool records failed tool results and successful recovery attempts.

func (*RecoveryTracker) Attempts added in v1.0.2

func (r *RecoveryTracker) Attempts() []RecoveryAttempt

Attempts returns the recorded successful recovery attempts.

func (*RecoveryTracker) BeforeTool added in v1.0.2

func (r *RecoveryTracker) BeforeTool(
	_ context.Context,
	execCtx *ExecutionContext,
	call model.ToolCall,
) (*model.ToolResult, error)

BeforeTool blocks retries until a reflection message has been recorded after a failure.

func (*RecoveryTracker) Failures added in v1.0.2

func (r *RecoveryTracker) Failures() []RecoveryFailure

Failures returns the recorded failures.

func (*RecoveryTracker) HasUnresolvedFailures added in v1.0.2

func (r *RecoveryTracker) HasUnresolvedFailures() bool

HasUnresolvedFailures reports whether any tool failures still lack a successful follow-up attempt.

func (*RecoveryTracker) LatestReflection added in v1.0.2

func (r *RecoveryTracker) LatestReflection() string

LatestReflection returns the most recently recorded reflection text.

func (*RecoveryTracker) RecordReflection added in v1.0.2

func (r *RecoveryTracker) RecordReflection(
	ctx context.Context,
	execCtx *ExecutionContext,
	reflection string,
) error

RecordReflection stores the recovery reflection text and clears the reflection requirement.

func (*RecoveryTracker) RequiresReflection added in v1.0.2

func (r *RecoveryTracker) RequiresReflection() bool

RequiresReflection reports whether a reflection message is still required before retrying.

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 Reranker added in v1.0.2

type Reranker interface {
	Rerank(context.Context, string, []RetrievalCandidate, int) ([]RetrievalCandidate, error)
}

Reranker makes a second, usually more precise pass over an existing candidate set for a query.

A common pattern is "retrieve 20 quickly, rerank to 5 carefully".

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 RetrievalCandidate added in v1.0.2

type RetrievalCandidate struct {
	ID       string
	Content  string
	Metadata map[string]string
	Score    float64
}

RetrievalCandidate is a normalized shortlist item that lets lexical, semantic, or hybrid retrieval stages speak the same shape before reranking.

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.

func NewPersistedSessionManager added in v1.0.2

func NewPersistedSessionManager(persister SessionPersister) SessionManager

NewPersistedSessionManager adapts a SessionPersister to the SessionManager interface.

type SessionPersister added in v1.0.2

type SessionPersister interface {
	SaveSession(context.Context, Session) error
	LoadSession(context.Context, string) (Session, error)
}

SessionPersister stores raw session snapshots for durable runners.

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 so conversations can continue across separate calls.

func NewSessionRunner added in v1.0.1

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

NewSessionRunner builds a session-aware wrapper around Agent for chat-style or workflow-style conversations that span multiple turns.

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 StablePrefixDetector added in v1.0.2

type StablePrefixDetector interface {
	Detect(model.Request) string
}

StablePrefixDetector identifies cache-friendly prefixes in evolving requests.

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 plus the interaction that must be answered before the run can resume.

type SynthesisEvent added in v1.0.2

type SynthesisEvent struct {
	RunID      string
	Step       int
	Kind       SynthesisEventKind
	ToolCallID string
	ToolName   string
	Content    string
}

SynthesisEvent is emitted when a synthesis tracker records an observation or completes a synthesis.

type SynthesisEventKind added in v1.0.2

type SynthesisEventKind string

SynthesisEventKind describes the type of synthesis event.

const (
	SynthesisEventObservationRecorded SynthesisEventKind = "observation_recorded"
	SynthesisEventSynthesisComplete   SynthesisEventKind = "synthesis_complete"
)

type SynthesisPolicy added in v1.0.2

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

SynthesisPolicy blocks final answers while analysis remains incomplete.

func NewSynthesisPolicy added in v1.0.2

func NewSynthesisPolicy(source synthesisStateSource) *SynthesisPolicy

NewSynthesisPolicy creates a reusable synthesis policy.

func (*SynthesisPolicy) BeforeFinalAnswer added in v1.0.2

func (p *SynthesisPolicy) BeforeFinalAnswer(
	ctx context.Context,
	execCtx *ExecutionContext,
	_ string,
) error

BeforeFinalAnswer implements FinalAnswerCallback.

type SynthesisRecord added in v1.0.2

type SynthesisRecord struct {
	Observations []Observation
}

SynthesisRecord captures a completed synthesis with its observations.

type SynthesisTracker added in v1.0.2

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

SynthesisTracker records tool observations and tracks synthesis completion. It can be plugged directly into the agent as an AfterToolCallback.

func NewSynthesisTracker added in v1.0.2

func NewSynthesisTracker() *SynthesisTracker

NewSynthesisTracker creates a reusable synthesis tracker.

func (*SynthesisTracker) AfterTool added in v1.0.2

func (s *SynthesisTracker) AfterTool(
	ctx context.Context,
	execCtx *ExecutionContext,
	result model.ToolResult,
) (*model.ToolResult, error)

AfterTool records tool results as observations.

func (*SynthesisTracker) HasIncompleteAnalysis added in v1.0.2

func (s *SynthesisTracker) HasIncompleteAnalysis() bool

HasIncompleteAnalysis reports whether analysis remains incomplete.

func (*SynthesisTracker) MarkSynthesisComplete added in v1.0.2

func (s *SynthesisTracker) MarkSynthesisComplete(ctx context.Context, execCtx *ExecutionContext) error

MarkSynthesisComplete marks the current synthesis as complete and starts tracking next one.

func (*SynthesisTracker) Observations added in v1.0.2

func (s *SynthesisTracker) Observations() []Observation

Observations returns the current observations.

func (*SynthesisTracker) SynthesisHistory added in v1.0.2

func (s *SynthesisTracker) SynthesisHistory() []SynthesisRecord

SynthesisHistory returns all completed synthesis records.

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.

Think of it as "we solved a similar problem before; bring that pattern back when a new request looks close enough."

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.

func (*TaskMemoryManager) WithWritePolicy added in v1.0.2

func (m *TaskMemoryManager) WithWritePolicy(policy MemoryWritePolicy) *TaskMemoryManager

WithWritePolicy attaches an optional policy that can skip low-value memories.

type ThresholdMemoryWritePolicy added in v1.0.2

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

ThresholdMemoryWritePolicy stores only memories whose heuristic score meets the configured threshold.

func NewThresholdMemoryWritePolicy added in v1.0.2

func NewThresholdMemoryWritePolicy(threshold float64) ThresholdMemoryWritePolicy

NewThresholdMemoryWritePolicy creates a simple heuristic write policy.

func (ThresholdMemoryWritePolicy) Decide added in v1.0.2

Decide scores the supplied memory and reports whether it should be stored.

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.

type VerificationGate added in v1.0.2

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

VerificationGate blocks final answers until enough evidence has been gathered.

func NewVerificationGate added in v1.0.2

func NewVerificationGate(collector EvidenceCollector, minItems int, options ...VerificationOption) *VerificationGate

NewVerificationGate creates a reusable evidence gate.

func (*VerificationGate) BeforeFinalAnswer added in v1.0.2

func (g *VerificationGate) BeforeFinalAnswer(
	_ context.Context,
	_ *ExecutionContext,
	answer string,
) error

BeforeFinalAnswer implements FinalAnswerCallback.

func (*VerificationGate) BeforeTool added in v1.0.2

func (g *VerificationGate) BeforeTool(
	_ context.Context,
	execCtx *ExecutionContext,
	call model.ToolCall,
) (*model.ToolResult, error)

BeforeTool blocks further evidence gathering until an actionable reflection is recorded.

func (*VerificationGate) LatestReflection added in v1.0.2

func (g *VerificationGate) LatestReflection() string

LatestReflection returns the most recently recorded reflection, if any.

func (*VerificationGate) NeedsReflection added in v1.0.2

func (g *VerificationGate) NeedsReflection() bool

NeedsReflection reports whether the gate is waiting for an actionable reflection.

type VerificationOption added in v1.0.2

type VerificationOption func(*VerificationGate)

VerificationOption configures a VerificationGate.

func WithActionableVerificationReflection added in v1.0.2

func WithActionableVerificationReflection() VerificationOption

WithActionableVerificationReflection requires a short reflection before more evidence can be gathered after an insufficiently verified answer attempt.

Directories

Path Synopsis
internal
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