cogito

package module
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 18 Imported by: 4

README

Gemini_Generated_Image_jbv0xajbv0xajbv0

Cogito is a powerful Go library for building intelligent, co-operative agentic software and LLM-powered workflows, focusing on improving results for small, open source language models that scales to any LLM.

🧪 Tested on Small Models ! Our test suite runs on 0.6b Qwen (not fine-tuned), proving effectiveness even with minimal resources.

📝 Working on Official Paper
I am currently working on the official academic/white paper for Cogito. The paper will provide detailed theoretical foundations, experimental results, and comprehensive analysis of the framework's capabilities.

🏗️ Architecture

Cogito is the result of building LocalAI, LocalAGI and LocalOperator (yet to be released).

Cogito uses an internal pipeline to first make the LLM reason about a specific task, forcing the model to reason and later extracts with BNF grammars exact data structures from the LLM. This is applied to every primitive exposed by the framework.

It provides a comprehensive framework for creating conversational AI systems with advanced reasoning, tool execution, goal-oriented planning, iterative content refinement capabilities, and seamless integration with external tools also via the Model Context Protocol (MCP).

🔧 Composable Primitives
Cogito primitives can be combined to form more complex pipelines, enabling sophisticated AI workflows.

🚀 Quick Start

Installation

go get github.com/mudler/cogito

Basic Usage

package main

import (
    "context"
    "fmt"
    "github.com/mudler/cogito"
    "github.com/mudler/cogito/clients"

)

func main() {
    // Create an LLM client
    llm := clients.NewOpenAILLM("your-model", "api-key", "https://api.openai.com")
    
    // Create a conversation fragment
    fragment := cogito.NewEmptyFragment().
        AddMessage("user", "Tell me about artificial intelligence")
    
    // Get a response
    newFragment, err := llm.Ask(context.Background(), fragment)
    if err != nil {
        panic(err)
    }
    
    fmt.Println(newFragment.LastMessage().Content)
}

Using Tools

Creating Custom Tools

To create a custom tool, implement the Tool[T] interface:

type MyToolArgs struct {
    Param string `json:"param" description:"A parameter"`
}

type MyTool struct{}

// Implement the Tool interface
func (t *MyTool) Run(args MyToolArgs) (string, error) {
    // Your tool logic here
    return fmt.Sprintf("Processed: %s", args.Param), nil
}

// Create a ToolDefinition using NewToolDefinition helper
myTool := cogito.NewToolDefinition(
    &MyTool{},
    MyToolArgs{},
    "my_tool",
    "A custom tool",
)

Tools in Cogito are added by calling NewToolDefinition on your tool, which automatically generates openai.Tool via the Tool() method. Tools are then passed by to cogito.WithTools:

// Define tool argument types
type WeatherArgs struct {
    City string `json:"city" description:"The city to get weather for"`
}

type SearchArgs struct {
    Query string `json:"query" description:"The search query"`
}

// Create tool definitions - these automatically generate openai.Tool
weatherTool := cogito.NewToolDefinition(
    &WeatherTool{},
    WeatherArgs{},
    "get_weather",
    "Get the current weather for a city",
)

searchTool := cogito.NewToolDefinition(
    &SearchTool{},
    SearchArgs{},
    "search",
    "Search for information",
)

// Create a fragment with user input
fragment := cogito.NewFragment(openai.ChatCompletionMessage{
    Role:    "user",
    Content: "What's the weather in San Francisco?",
})

// Execute with tools - you can pass multiple tools with different types
result, err := cogito.ExecuteTools(llm, fragment, 
    cogito.WithTools(weatherTool, searchTool))
if err != nil {
    panic(err)
}

// result.Status.ToolsCalled will contain all the tools being called
// The LLM can select one or multiple tools per iteration
// When parallel execution is enabled, multiple tools can run concurrently
Tool Call Callbacks and Adjustments

Cogito allows you to intercept and adjust tool calls before they are executed. This enables interactive workflows where users can review, approve, modify, or directly edit tool calls.

ToolCallDecision Struct:

The callback returns a ToolCallDecision struct that provides fine-grained control:

type ToolCallDecision struct {
    Approved   bool         // true to proceed, false to interrupt
    Adjustment string       // Feedback for LLM to interpret (empty = no adjustment)
    Modified   *ToolChoice  // Direct modification (takes precedence over Adjustment)
    Skip       bool         // Skip this tool call but continue execution
}

Basic Tool Call Callback:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithToolCallBack(func(tool *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
        fmt.Printf("Agent wants to run: %s with args: %v\n", tool.Name, tool.Arguments)
        return cogito.ToolCallDecision{Approved: true} // Proceed without adjustment
    }))

Interactive Tool Call Approval with Adjustments:

import (
    "bufio"
    "encoding/json"
    "os"
    "strings"
)

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithMaxAdjustmentAttempts(3), // Limit adjustment attempts
    cogito.WithToolCallBack(func(tool *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
        args, _ := json.Marshal(tool.Arguments)
        fmt.Printf("The agent wants to run the tool %s with arguments: %s\n", tool.Name, string(args))
        fmt.Println("Do you want to run the tool? (y/n/adjust)")
        
        reader := bufio.NewReader(os.Stdin)
        text, _ := reader.ReadString('\n')
        text = strings.TrimSpace(text)
        
        switch text {
        case "y":
            return cogito.ToolCallDecision{Approved: true}
        case "n":
            return cogito.ToolCallDecision{Approved: false}
        default:
            // Provide adjustment feedback - the LLM will re-evaluate the tool call
            return cogito.ToolCallDecision{
                Approved:   true,
                Adjustment: text,
            }
        }
    }))

Direct Tool Modification:

You can directly modify tool arguments without relying on LLM interpretation:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithToolCallBack(func(tool *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
        // Validate and fix arguments directly
        if tool.Name == "search" {
            query, ok := tool.Arguments["query"].(string)
            if !ok || len(query) < 3 {
                // Directly modify instead of asking LLM
                fixed := *tool
                fixed.Arguments = map[string]any{
                    "query": "default search query",
                }
                return cogito.ToolCallDecision{
                    Approved: true,
                    Modified: &fixed,
                }
            }
        }
        return cogito.ToolCallDecision{Approved: true}
    }))

Skipping Tool Calls:

You can skip a tool call while continuing execution (useful for conditional tool execution):

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithToolCallBack(func(tool *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
        // Skip certain tools based on conditions
        if tool.Name == "search" && someCondition {
            return cogito.ToolCallDecision{
                Approved: true,
                Skip:     true, // Skip this tool but continue execution
            }
        }
        return cogito.ToolCallDecision{Approved: true}
    }))

When a tool is skipped:

  • The tool call is added to the conversation fragment
  • A message indicating the tool was skipped is added
  • Execution continues to the next iteration
  • This is different from Approved: false which interrupts execution entirely

Session State and Resuming Execution:

The SessionState contains the current tool choice and fragment, allowing you to save and resume execution:

var savedState *cogito.SessionState

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithToolCallBack(func(tool *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
        // Save the state for later resumption
        savedState = state
        
        // Interrupt execution
        return cogito.ToolCallDecision{Approved: false}
    }))

// Later, resume execution from the saved state
if savedState != nil {
    resumedFragment, err := savedState.Resume(llm, cogito.WithTools(searchTool))
    if err != nil {
        panic(err)
    }
    // Continue with resumedFragment
}

Starting with a Specific Tool Choice:

You can pre-select one or more tools to start execution with:

// Single tool
initialTool := &cogito.ToolChoice{
    Name: "search",
    Arguments: map[string]any{
        "query": "artificial intelligence",
    },
}

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithStartWithAction(initialTool))

// Multiple tools
initialTools := []*cogito.ToolChoice{
    {
        Name: "search",
        Arguments: map[string]any{"query": "AI"},
    },
    {
        Name: "get_weather",
        Arguments: map[string]any{"city": "San Francisco"},
    },
}

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, weatherTool),
    cogito.WithStartWithAction(initialTools...))

Forcing Reasoning:

Enable forced reasoning to ensure the LLM provides detailed reasoning for tool selections:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithForceReasoning())
Injecting Messages During Tool Execution

Cogito allows you to inject new conversation messages during the main tool execution loop. This enables dynamic user interaction where messages can be added while the agent is executing tools, and you can track whether injected messages were successfully added to the conversation.

Basic Message Injection:

Create a channel to send messages and pass it to ExecuteTools:

import (
    "github.com/sashabaranov/go-openai"
    "github.com/mudler/cogito"
)

// Create a channel for message injection
messageInjectionChan := make(chan openai.ChatCompletionMessage, 10)

// Start tool execution in a goroutine
go func() {
    result, err := cogito.ExecuteTools(llm, fragment,
        cogito.WithTools(searchTool, weatherTool),
        cogito.WithMessageInjectionChan(messageInjectionChan))
    if err != nil {
        panic(err)
    }
    fmt.Println(result.LastMessage().Content)
}()

// Inject messages while execution is running
messageInjectionChan <- openai.ChatCompletionMessage{
    Role:    "user",
    Content: "Also check for recent updates on this topic",
}

// Continue injecting messages as needed
messageInjectionChan <- openai.ChatCompletionMessage{
    Role:    "user",
    Content: "Focus on the most recent data",
}

// Close the channel to stop accepting messages
close(messageInjectionChan)

Receiving Feedback About Injected Messages:

Use a result channel to get feedback about whether messages were successfully injected:

messageInjectionChan := make(chan openai.ChatCompletionMessage, 10)
resultChan := make(chan cogito.MessageInjectionResult, 10)

go func() {
    result, err := cogito.ExecuteTools(llm, fragment,
        cogito.WithTools(searchTool),
        cogito.WithMessageInjectionChan(messageInjectionChan),
        cogito.WithMessageInjectionResultChan(resultChan))  // Receive feedback
    // ...
}()

// Inject a message and get feedback
messageInjectionChan <- openai.ChatCompletionMessage{
    Role:    "user",
    Content: "Prioritize recent sources",
}

// Wait for feedback
result := <-resultChan
if result.Err != nil {
    fmt.Printf("Injection failed: %v\n", result.Err)
} else {
    fmt.Printf("Successfully injected 1 message at position %d\n", result.Position)
}

MessageInjectionResult Structure:

The feedback channel returns MessageInjectionResult with:

  • Count: Number of messages successfully injected
  • Position: Position in the fragment where messages were added
  • Err: Error if injection failed (validation errors, etc.)

Complete Interactive Example:

package main

import (
    "bufio"
    "os"
    "strings"
    "github.com/sashabaranov/go-openai"
    "github.com/mudler/cogito"
)

func main() {
    messageInjectionChan := make(chan openai.ChatCompletionMessage, 10)
    resultChan := make(chan cogito.MessageInjectionResult, 10)

    // Start execution in background
    go func() {
        _, err := cogito.ExecuteTools(llm, fragment,
            cogito.WithTools(searchTool),
            cogito.WithMessageInjectionChan(messageInjectionChan),
            cogito.WithMessageInjectionResultChan(resultChan))
        if err != nil {
            panic(err)
        }
        close(messageInjectionChan) // Signal no more messages will be sent
    }()

    // Interactive message injection from stdin
    scanner := bufio.NewScanner(os.Stdin)
    for {
        fmt.Print("Enter message to inject (or 'quit'): ")
        if !scanner.Scan() {
            break
        }
        
        text := strings.TrimSpace(scanner.Text())
        if text == "quit" {
            close(messageInjectionChan)
            break
        }
        
        // Send message
        messageInjectionChan <- openai.ChatCompletionMessage{
            Role:    "user",
            Content: text,
        }
        
        // Receive feedback (non-blocking)
        select {
        case result := <-resultChan:
            if result.Err != nil {
                fmt.Printf("Error: %v\n", result.Err)
            } else {
                fmt.Printf("Message injected at position %d\n", result.Position)
            }
        default:
        }
    }
}

How It Works:

  1. Message Injection Timing: Messages are injected at the end of each iteration after tool results are added, but before the next tool selection. This ensures injected messages are considered in the next planning step.

  2. Context Awareness: Injected messages are tracked in Fragment.Status.InjectedMessages with the iteration number, allowing you to see when each message was added.

  3. Channel Semantics:

    • Nil channels are safe and simply ignored (idiomatic Go)
    • Closed channels signal that no more messages will be sent
    • Non-blocking sends on result channels prevent deadlocks if no one is listening
  4. Concurrent Safety: The tool loop uses a select statement to monitor both the context cancellation and message injection channel, ensuring responsive message handling.

Use Cases:

  • Interactive Workflows: Allow users to provide real-time guidance while the agent is executing tools
  • Adaptive Execution: Inject corrective instructions based on intermediate results
  • Multi-Stage Tasks: Guide the agent through complex multi-step processes
  • User Feedback Integration: Incorporate user input during long-running tool executions

Notes:

  • The message injection channel is optional and completely independent of tool call callbacks
  • Both WithMessageInjectionChan and WithMessageInjectionResultChan accept nil (default) - simply don't use them if you don't need this feature
  • Channel capacity should account for the maximum concurrent messages you expect to inject
  • The result feedback channel uses non-blocking sends, so it won't block the tool execution loop
  • Successfully injected messages are part of the returned fragment and appear in Fragment.Status.InjectedMessages
  • When the message injection channel is closed, the loop continues to accept context cancellation and normal tool execution
Multiple Tool Selection and Parallel Execution

Cogito supports selecting and executing multiple tools in a single iteration. When enabled, the LLM can select multiple tools that can be executed concurrently, improving efficiency for independent operations.

Multiple Tool Selection (Sequential):

By default, Cogito can select multiple tools, but they execute sequentially:

// The LLM can select multiple tools, executed one after another
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, weatherTool, factCheckTool))
// Tools are selected and executed sequentially

Parallel Tool Execution:

Enable parallel execution to run multiple tools concurrently:

// Enable parallel tool execution
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, weatherTool, factCheckTool),
    cogito.EnableParallelToolExecution)

// When parallel execution is enabled:
// - The LLM uses a different intention tool that allows selecting multiple tools
// - Selected tools are executed concurrently using goroutines
// - Results are collected and added to the conversation in order

When to Use Parallel Execution:

  • Use parallel execution when:

    • Tools are independent and don't depend on each other's results
    • You want to improve performance for multiple independent operations
    • Tools can safely run concurrently (e.g., fetching weather for multiple cities, searching multiple queries)
  • Use sequential execution (default) when:

    • Tools depend on each other's results
    • You need to maintain strict execution order
    • You want simpler debugging and error handling

Sink State with Multiple Tools:

When multiple tools are selected and one of them is a sink state tool, Cogito will:

  1. Execute all non-sink tools first (in parallel or sequential based on the option)
  2. Stop execution after non-sink tools complete
  3. The sink state indicates that no further tools are needed
// Example: LLM selects [searchTool, weatherTool, replyTool]
// - searchTool and weatherTool execute first
// - replyTool (sink state) is detected
// - Execution stops after searchTool and weatherTool complete
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, weatherTool),
    cogito.EnableParallelToolExecution)

Error Handling:

When a tool call callback interrupts execution, Cogito returns cogito.ErrToolCallCallbackInterrupted:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithToolCallBack(func(tool *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
        return cogito.ToolCallDecision{Approved: false} // Interrupt
    }))

if err != nil {
    if errors.Is(err, cogito.ErrToolCallCallbackInterrupted) {
        fmt.Println("Execution was interrupted by tool call callback")
    }
}

Notes:

  • The callback receives both the ToolChoice and SessionState for full context
  • Approved: false interrupts execution entirely
  • Approved: true, Skip: true skips the tool call but continues execution (useful for conditional execution)
  • Adjustment (non-empty) triggers an adjustment loop where the LLM re-evaluates the tool call
  • Modified (non-nil) directly uses the modified tool choice without re-querying the LLM
  • When a tool is skipped, it's added to the conversation with a "skipped" message, preserving history
  • The adjustment loop has a maximum attempt limit (default: 5, configurable via WithMaxAdjustmentAttempts)
  • SessionState can be serialized to JSON for persistence
  • The adjustment prompt has been improved to provide better guidance to the LLM
Configuring Sink State

When the LLM determines that no tool is needed to respond to the user, Cogito uses a "sink state" tool to handle the response. By default, Cogito uses a built-in reply tool, but you can customize or disable this behavior.

Disable Sink State:

// Disable sink state entirely - the LLM will return an error if no tool is selected
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(weatherTool, searchTool),
    cogito.DisableSinkState)

Custom Sink State Tool:

// Define a custom sink state tool
type CustomReplyArgs struct {
    Reasoning string `json:"reasoning" description:"The reasoning for the reply"`
}

type CustomReplyTool struct{}

func (t *CustomReplyTool) Run(args CustomReplyArgs) (string, error) {
    // Custom logic to process the reasoning and generate a response
    return fmt.Sprintf("Based on: %s", args.Reasoning), nil
}

// Create a custom sink state tool
customSinkTool := cogito.NewToolDefinition(
    &CustomReplyTool{},
    CustomReplyArgs{},
    "custom_reply",
    "Custom tool for handling responses when no other tool is needed",
)

// Use the custom sink state tool
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(weatherTool, searchTool),
    cogito.WithSinkState(customSinkTool))

Notes:

  • The sink state tool is enabled by default with a built-in reply tool
  • When enabled, the sink state tool appears as an option in the tool selection enum
  • The sink state tool receives a reasoning parameter containing the LLM's reasoning about why no tool is needed
  • Custom sink state tools must accept a reasoning parameter in their arguments
Sub-Agent Spawning

Cogito supports spawning sub-agents via tools, allowing the LLM to delegate tasks to independent child agents. Sub-agents can run in the foreground (blocking — waits for result) or background (non-blocking — returns an ID immediately so the parent can continue working).

Enabling Sub-Agents:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, weatherTool),
    cogito.EnableAgentSpawning, // Adds spawn_agent, check_agent, get_agent_result tools
    cogito.WithIterations(10),
)

When enabled, three built-in tools are injected:

  • spawn_agent — Spawns a sub-agent with a task. Set background: true for non-blocking execution.
  • check_agent — Checks the status of a background agent by ID.
  • get_agent_result — Retrieves the result of a background agent. Set wait: true to block until done.

Foreground Agents (Blocking):

The LLM calls spawn_agent with background: false. The sub-agent runs synchronously inside the tool call, and its result is returned as the tool output:

// The LLM might call: spawn_agent(task="Research quantum computing", background=false)
// → Sub-agent runs ExecuteTools with the same tools (minus agent tools)
// → Sub-agent completes and returns result
// → Parent receives result as tool output and continues
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.EnableAgentSpawning,
    cogito.WithIterations(5),
)

Background Agents (Non-Blocking):

The LLM calls spawn_agent with background: true. The sub-agent launches in a goroutine, and the parent immediately gets back an agent ID. ExecuteTools automatically stays alive while background agents are running — when the LLM has no more tools to call, the loop blocks until a background agent finishes. The completion notification is injected into the conversation, and the LLM can react to it:

// The LLM might call: spawn_agent(task="Analyze data", background=true)
// → Returns "Agent spawned in background with ID: abc-123"
// → Parent continues working on other tasks
// → When sub-agent finishes, parent sees:
//   "Background agent abc-123 has completed. Task: Analyze data. Result: ..."
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, analysisTool),
    cogito.EnableAgentSpawning,
    cogito.WithIterations(10),
)

Sharing Agent State Across Calls:

Use WithAgentManager to share the agent registry across multiple ExecuteTools calls, allowing you to track background agents across conversation turns:

manager := cogito.NewAgentManager()

// First turn: spawns a background agent
result1, _ := cogito.ExecuteTools(llm, fragment1,
    cogito.WithTools(searchTool),
    cogito.EnableAgentSpawning,
    cogito.WithAgentManager(manager),
    cogito.WithIterations(5),
)

// Second turn: can check on or retrieve results from previously spawned agents
result2, _ := cogito.ExecuteTools(llm, fragment2,
    cogito.WithTools(searchTool),
    cogito.EnableAgentSpawning,
    cogito.WithAgentManager(manager),
    cogito.WithIterations(5),
)

// Programmatic access to all agents
for _, agent := range manager.List() {
    fmt.Printf("Agent %s: %s\n", agent.ID, agent.Status)
}

Using a Separate LLM for Sub-Agents:

mainLLM := clients.NewOpenAILLM("gpt-4", apiKey, baseURL)
subAgentLLM := clients.NewOpenAILLM("gpt-3.5-turbo", apiKey, baseURL)

result, err := cogito.ExecuteTools(mainLLM, fragment,
    cogito.WithTools(searchTool),
    cogito.EnableAgentSpawning,
    cogito.WithAgentLLM(subAgentLLM), // Sub-agents use a different model
    cogito.WithIterations(5),
)

Completion Callbacks:

Use WithAgentCompletionCallback to get programmatic notification when any background sub-agent finishes — useful for UI updates or external monitoring:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.EnableAgentSpawning,
    cogito.WithAgentCompletionCallback(func(agent *cogito.AgentState) {
        fmt.Printf("Agent %s finished with status: %s\n", agent.ID, agent.Status)
        if agent.Status == cogito.AgentStatusCompleted {
            fmt.Println("Result:", agent.Result)
        }
    }),
    cogito.WithIterations(10),
)

Tool Filtering:

By default, sub-agents receive all parent tools except the agent management tools themselves (preventing unbounded recursion). The LLM can also specify a subset of tools:

// The LLM can call: spawn_agent(task="Search only", tools=["search"])
// → Sub-agent only has access to the "search" tool

To allow sub-agents to also spawn their own sub-agents, the LLM can explicitly include agent tools:

// spawn_agent(task="Complex task", tools=["search", "spawn_agent", "check_agent", "get_agent_result"])

Streaming Sub-Agent Events:

When streaming is enabled, sub-agent events are tagged with a StreamEventSubAgent type and include the agent's ID:

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.EnableAgentSpawning,
    cogito.WithTools(searchTool),
    cogito.WithStreamCallback(func(ev cogito.StreamEvent) {
        if ev.Type == cogito.StreamEventSubAgent {
            fmt.Printf("[Agent %s] %s\n", ev.AgentID, ev.Content)
        } else {
            fmt.Print(ev.Content)
        }
    }),
    cogito.WithIterations(5),
)

When to Use Sub-Agents:

  • Use foreground agents when the parent needs the result before continuing (e.g., research a topic, then summarize)
  • Use background agents when tasks are independent and the parent can continue working (e.g., start multiple research tasks in parallel)
  • Use WithAgentManager when you need to track agents across multiple conversation turns
  • Use WithAgentLLM when sub-agents should use a cheaper/faster model
Field Annotations for Tool Arguments

Cogito supports several struct field annotations to control how tool arguments are defined in the generated JSON schema:

Available Annotations:

  • json:"field_name" - Required. Defines the JSON field name for the parameter.
  • description:"text" - Provides a description for the field that helps the LLM understand what the parameter is for.
  • enum:"value1,value2,value3" - Restricts the field to a specific set of allowed values (comma-separated).
  • required:"false" - Makes the field optional. By default, all fields are required unless marked with required:"false".

Examples:

// Basic required field with description
type BasicArgs struct {
    Query string `json:"query" description:"The search query"`
}

// Optional field
type OptionalArgs struct {
    Query string `json:"query" required:"false" description:"Optional search query"`
    Limit int    `json:"limit" required:"false" description:"Maximum number of results"`
}

// Field with enum values
type EnumArgs struct {
    Action string `json:"action" enum:"create,read,update,delete" description:"The action to perform"`
}

// Field with enum and description
type WeatherArgs struct {
    City        string `json:"city" description:"The city name"`
    Unit        string `json:"unit" enum:"celsius,fahrenheit" description:"Temperature unit"`
    Format      string `json:"format" enum:"short,detailed" required:"false" description:"Output format"`
}

// Complete example with multiple field types
type AdvancedSearchArgs struct {
    // Required field with description
    Query string `json:"query" description:"The search query"`
    
    // Optional field with enum
    SortBy string `json:"sort_by" enum:"relevance,date,popularity" required:"false" description:"Sort order"`
    
    // Optional numeric field
    Limit int `json:"limit" required:"false" description:"Maximum number of results"`
    
    // Optional boolean field
    IncludeImages bool `json:"include_images" required:"false" description:"Include images in results"`
}

// Create tool with advanced arguments
searchTool := cogito.NewToolDefinition(
    &AdvancedSearchTool{},
    AdvancedSearchArgs{},
    "advanced_search",
    "Advanced search with sorting and filtering options",
)

Notes:

  • Fields without required:"false" are automatically marked as required in the JSON schema
  • Enum values are case-sensitive and should match exactly what you expect in Run()
  • The json tag is required for all fields that should be included in the tool schema
  • Descriptions help the LLM understand the purpose of each parameter, leading to better tool calls

Alternatively, you can implement ToolDefinitionInterface directly if you prefer more control:

type CustomTool struct{}

func (t *CustomTool) Tool() openai.Tool {
    return openai.Tool{
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "custom_tool",
            Description: "A custom tool",
            Parameters: jsonschema.Definition{
                // Define your schema
            },
        },
    }
}

func (t *CustomTool) Execute(args map[string]any) (string, error) {
    // Your execution logic
    return "result", nil
}

Guidelines for Intelligent Tool Selection

Guidelines provide a powerful way to define conditional rules for tool usage. The LLM intelligently selects which guidelines are relevant based on the conversation context, enabling dynamic and context-aware tool selection.

// Create tool definitions
searchTool := cogito.NewToolDefinition(
    &SearchTool{},
    SearchArgs{},
    "search",
    "Search for information",
)

weatherTool := cogito.NewToolDefinition(
    &WeatherTool{},
    WeatherArgs{},
    "get_weather",
    "Get weather information",
)

// Define guidelines with conditions and associated tools
guidelines := cogito.Guidelines{
    cogito.Guideline{
        Condition: "User asks about information or facts",
        Action:    "Use the search tool to find information",
        Tools: cogito.Tools{
            searchTool,
        },
    },
    cogito.Guideline{
        Condition: "User asks for the weather in a city",
        Action:    "Use the weather tool to find the weather",
        Tools: cogito.Tools{
            weatherTool,
        },
    },
}

// Get relevant guidelines for the current conversation
fragment := cogito.NewEmptyFragment().
    AddMessage("user", "When was Isaac Asimov born?")

// Execute tools with guidelines
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithGuidelines(guidelines),
    cogito.EnableStrictGuidelines) // Only use tools from relevant guidelines
if err != nil {
    panic(err)
}
Automatic Tool Guidance with EnableGuidedTools

The EnableGuidedTools option enables intelligent filtering of tools through guidance, even when explicit guidelines aren't provided for all tools. This feature automatically creates "virtual guidelines" from tool descriptions, allowing the LLM to intelligently filter and select tools based on their descriptions.

When No Guidelines Exist:

When EnableGuidedTools is enabled and no guidelines are provided, Cogito automatically creates virtual guidelines for all tools using their descriptions as the condition. This allows the LLM to filter tools based on relevance to the conversation context.

// Create tools without any guidelines
searchTool := cogito.NewToolDefinition(
    &SearchTool{},
    SearchArgs{},
    "search",
    "A search engine to find information about a topic",
)

weatherTool := cogito.NewToolDefinition(
    &WeatherTool{},
    WeatherArgs{},
    "get_weather",
    "Get weather information for a specific city",
)

// EnableGuidedTools creates virtual guidelines from tool descriptions
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool, weatherTool),
    cogito.EnableGuidedTools) // Creates virtual guidelines from descriptions

When Guidelines Exist:

When guidelines are provided but some tools aren't included in any guideline, EnableGuidedTools creates virtual guidelines only for those "unguided" tools. This allows you to have explicit guidelines for some tools while automatically handling others.

// Define guidelines for search tool only
guidelines := cogito.Guidelines{
    cogito.Guideline{
        Condition: "User asks about information or facts",
        Action:    "Use the search tool to find information",
        Tools: cogito.Tools{
            searchTool,
        },
    },
}

// weatherTool is not in any guideline - EnableGuidedTools will create
// a virtual guideline for it using its description
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithGuidelines(guidelines...),
    cogito.WithTools(weatherTool),
    cogito.EnableGuidedTools) // Creates virtual guidelines for unguided tools

How It Works:

  1. No Guidelines Scenario: When no guidelines exist and EnableGuidedTools is enabled:

    • Virtual guidelines are created for ALL tools
    • Each tool's description becomes the condition/guidance in the template
    • The LLM filters tools based on how well their descriptions match the conversation context
  2. With Guidelines Scenario: When guidelines exist and EnableGuidedTools is enabled:

    • Only tools NOT in any guideline get virtual guidelines
    • Virtual guidelines use the format: "The task requires: [tool description]"
    • Real guidelines and virtual guidelines are merged and filtered together

Benefits:

  • Automatic Tool Filtering: No need to create guidelines for every tool
  • Better Tool Selection: LLM can intelligently filter tools based on descriptions
  • Flexible Configuration: Mix explicit guidelines with automatic guidance
  • Reduced Configuration: Especially useful when you have many tools

Notes:

  • Tool descriptions should be meaningful and descriptive for best results
  • Virtual guidelines follow the same filtering process as real guidelines
  • When no guidelines exist, tool descriptions serve as both condition and guidance in the template
  • The feature adds LLM call overhead for filtering virtual guidelines

Goal-Oriented Planning

// Extract a goal from conversation
goal, err := cogito.ExtractGoal(llm, fragment)
if err != nil {
    panic(err)
}

// Create tool definition
searchTool := cogito.NewToolDefinition(
    &SearchTool{},
    SearchArgs{},
    "search",
    "Search for information",
)

// Create a plan to achieve the goal
plan, err := cogito.ExtractPlan(llm, fragment, goal, 
    cogito.WithTools(searchTool))
if err != nil {
    panic(err)
}

// Execute the plan
result, err := cogito.ExecutePlan(llm, fragment, plan, goal,
    cogito.WithTools(searchTool))
if err != nil {
    panic(err)
}

Planning with TODOs

Planning with TODOs addresses context accumulation by starting each iteration with fresh context while persisting TODOs and feedback between iterations. This pattern uses separate worker and judge models: the worker executes tasks, and one or more judge LLMs review the work to determine if goal execution is completed or needs rework.

How it works:

  1. Work Phase: Worker model executes tasks with fresh context that includes:
    • Overall goal
    • TODO list with progress (markdown checkboxes)
    • Previous feedback from review phase
  2. Review Phase: One or more judge LLMs review the work and decide if goal execution is completed or incomplete (needs rework). When multiple reviewers are provided, a majority vote determines the final decision.
  3. Persistence: TODOs and feedback persist between iterations; conversation history is cleared
  4. Iteration: Loop continues until goal execution is completed or max iterations

Automatic TODO Generation (Recommended):

TODOs are automatically generated from plan subtasks when WithReviewerLLM() (judge LLM) is provided:

workerLLM := cogito.NewOpenAILLM("worker-model", "key", "url")
judgeLLM := cogito.NewOpenAILLM("judge-model", "key", "url")

goal, _ := cogito.ExtractGoal(workerLLM, fragment)
plan, _ := cogito.ExtractPlan(workerLLM, fragment, goal)

// Execute plan with Planning with TODOs enabled
// TODOs are automatically generated from plan subtasks
result, err := cogito.ExecutePlan(
    workerLLM,  // worker LLM
    fragment,
    plan,
    goal,
    cogito.WithTools(searchTool, writeTool),
    cogito.WithIterations(5),  // TODO iterations
    cogito.WithReviewerLLM(judgeLLM),  // Provide judge LLM for review (enables Planning with TODOs)
    cogito.WithTODOPersistence("./todos.json"),  // Optional: file persistence
)

Manual TODO List:

You can also provide a manual TODO list:

import "github.com/mudler/cogito/structures"

// Initialize TODO list manually
todoList := &structures.TODOList{
    TODOs: []structures.TODO{
        {ID: "1", Description: "Research topic", Completed: false},
        {ID: "2", Description: "Write draft", Completed: false},
    },
}

// Execute plan with manually provided TODOs
result, err := cogito.ExecutePlan(
    workerLLM,
    fragment,
    plan,
    goal,
    cogito.WithTools(searchTool, writeTool),
    cogito.WithIterations(5),
    cogito.WithTODOs(todoList),   // Manually provide TODO list
    cogito.WithReviewerLLM(judgeLLM),  // Provide judge LLM for review
    cogito.WithTODOPersistence("./todos.json"),
)

When to use Planning with TODOs:

  • When dealing with complex, multi-step tasks that may require multiple iterations
  • When you want to prevent context accumulation from failed attempts
  • When you need separate models for work and review phases (worker and judge)
  • When you want to track progress explicitly with checkboxes

Multiple Reviewer LLMs (Majority Voting):

You can provide multiple reviewer LLMs for more robust decision-making. When multiple reviewers are provided, Cogito uses majority voting to determine if the goal has been achieved:

workerLLM := cogito.NewOpenAILLM("worker-model", "key", "url")
judgeLLM1 := cogito.NewOpenAILLM("judge-model-1", "key", "url")
judgeLLM2 := cogito.NewOpenAILLM("judge-model-2", "key", "url")
judgeLLM3 := cogito.NewOpenAILLM("judge-model-3", "key", "url")

goal, _ := cogito.ExtractGoal(workerLLM, fragment)
plan, _ := cogito.ExtractPlan(workerLLM, fragment, goal)

// Execute plan with multiple reviewers
// The goal is considered achieved if more than half of reviewers agree
result, err := cogito.ExecutePlan(
    workerLLM,
    fragment,
    plan,
    goal,
    cogito.WithTools(searchTool, writeTool),
    cogito.WithIterations(5),
    cogito.WithReviewerLLM(judgeLLM1, judgeLLM2, judgeLLM3),  // Multiple reviewers
    cogito.WithTODOPersistence("./todos.json"),
)

How Majority Voting Works:

  • Each reviewer LLM independently evaluates whether the goal has been achieved
  • If more than half of the reviewers determine the goal is achieved, the overall decision is positive
  • The review result from the majority is used as the final feedback
  • This approach provides more reliable and consistent goal achievement detection, especially when using smaller or less reliable models

File-based Persistence:

TODOs can be persisted to a file and loaded between sessions:

result, err := cogito.ExecutePlan(
    workerLLM,
    fragment,
    plan,
    goal,
    cogito.WithReviewerLLM(judgeLLM),  // Single reviewer (or multiple)
    cogito.WithTODOPersistence("./todos.json"),  // Save/load TODOs from file
)

The TODO file is automatically saved after each iteration and loaded at the start of execution.

Content Refinement

// Create tool definition
searchTool := cogito.NewToolDefinition(
    &SearchTool{},
    SearchArgs{},
    "search",
    "Search for information",
)

// Refine content through iterative improvement
refined, err := cogito.ContentReview(llm, fragment,
    cogito.WithIterations(3),
    cogito.WithTools(searchTool))
if err != nil {
    panic(err)
}

Iterative Content Improvement

An example on how to iteratively improve content by using two separate models:

llm := cogito.NewOpenAILLM("your-model", "api-key", "https://api.openai.com")
reviewerLLM := cogito.NewOpenAILLM("your-reviewer-model", "api-key", "https://api.openai.com")

// Create content to review
initial := cogito.NewEmptyFragment().
    AddMessage("user", "Write about climate change")

response, _ := llm.Ask(ctx, initial)

// Create tool definitions
searchTool := cogito.NewToolDefinition(
    &SearchTool{},
    SearchArgs{},
    "search",
    "Search for information",
)

factCheckTool := cogito.NewToolDefinition(
    &FactCheckTool{},
    FactCheckArgs{},
    "fact_check",
    "Verify facts",
)

// Iteratively improve with tool support
improvedResponse, _ := cogito.ContentReview(reviewerLLM, response,
    cogito.WithIterations(3),
    cogito.WithTools(searchTool, factCheckTool),
    cogito.EnableToolReasoner)

Model Context Protocol (MCP) Integration

Cogito supports the Model Context Protocol (MCP) for seamless integration with external tools and services. MCP allows you to connect to remote tool providers and use their capabilities directly within your Cogito workflows.

import (
    "github.com/modelcontextprotocol/go-sdk/mcp"
)

// Create MCP client sessions
command := exec.Command("docker", "run", "-i", "--rm", "ghcr.io/mudler/mcps/weather:master")
transport := &mcp.CommandTransport{ Command: command }

client := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "v1.0.0"}, nil)
mcpSession, _ := client.Connect(context.Background(), transport, nil)

// Use MCP tools in your workflows
result, _ := cogito.ExecuteTools(llm, fragment,
    cogito.WithMCPs(mcpSession))

MCP with Guidelines
// Define guidelines that include MCP tools
guidelines := cogito.Guidelines{
    cogito.Guideline{
        Condition: "User asks about information or facts",
        Action:    "Use the MCP search tool to find information",
    },
}

// Execute with MCP tools and guidelines
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithMCPs(searchSession),
    cogito.WithGuidelines(guidelines),
    cogito.EnableStrictGuidelines)

Automatic Conversation Compaction

Cogito can automatically compact conversations to prevent context overflow when token usage exceeds a threshold. This is useful for long-running conversations with LLMs that have context limits.

How it works:

  1. After each LLM call, Cogito checks if the token count exceeds the threshold
  2. If exceeded, it generates a summary of the conversation history using an LLM
  3. The original messages are replaced with a condensed summary, preserving context

Basic Usage:

// Enable automatic compaction with a token threshold of 4000
// This will trigger compaction when the conversation exceeds 4000 tokens
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithCompactionThreshold(4000))

Customizing Compaction:

// Set custom compaction options
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithTools(searchTool),
    cogito.WithCompactionThreshold(4000),      // Trigger at 4000 tokens
    cogito.WithCompactionKeepMessages(5),      // Keep last 5 messages (default: 10)
)

Notes:

  • Compaction requires token usage data from the LLM (supported by OpenAI, LocalAI with token usage enabled)
  • If LastUsage is not available, Cogito falls back to estimating tokens from message count
  • The summary prompt uses the conversation compaction prompt type
  • Compaction preserves Status fields like LastUsage, ToolsCalled, etc.

Auto-Improving Agent (Self-Editing System Prompt)

Cogito supports an "autoimproving" feature where the agent can self-edit an additional system prompt across executions. After each ExecuteTools run, a review step analyzes the conversation and optionally updates the system prompt to improve future performance.

How it works:

  1. Before the main loop: If the state contains a non-empty SystemPrompt, it is injected as a system message at the start of the conversation.
  2. Main loop: Runs normally (unchanged).
  3. After the main loop: A review step runs that analyzes the conversation and may update the system prompt via an internal edit_system_prompt tool.

Basic Usage:

// Create a state object — the caller owns and persists this across calls
state := &cogito.AutoImproveState{}

// First execution — state starts empty, review may create an initial system prompt
result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithAutoImproveState(state),
    cogito.WithTools(searchTool),
)
// state.SystemPrompt was updated in-place by the review step
saveState(state) // Persist however you like (JSON, database, etc.)

// Next execution — load the state and pass it again
loadState(state)
result, err = cogito.ExecuteTools(llm, fragment,
    cogito.WithAutoImproveState(state),
    cogito.WithTools(searchTool),
)
// state.SystemPrompt may have been further refined

Using a Separate Reviewer LLM:

You can use a different (potentially larger or more capable) LLM for the review step:

workerLLM := clients.NewOpenAILLM("worker-model", "key", "url")
reviewerLLM := clients.NewOpenAILLM("reviewer-model", "key", "url")

state := &cogito.AutoImproveState{}

result, err := cogito.ExecuteTools(workerLLM, fragment,
    cogito.WithAutoImproveState(state),
    cogito.WithAutoImproveReviewerLLM(reviewerLLM),
    cogito.WithTools(searchTool),
)

AutoImproveState Structure:

type AutoImproveState struct {
    SystemPrompt string `json:"system_prompt"` // The self-editing system prompt
    ReviewCount  int    `json:"review_count"`  // Number of reviews performed
}

Notes:

  • The state is mutated in-place through the pointer — no need to read it back from the fragment
  • The review step is non-fatal: if it fails, the main execution result is returned unchanged
  • The review step uses an allowlist of safe options (no callbacks, guidelines, or autoimprove) to prevent recursion
  • ReviewCount is incremented on each successful review, regardless of whether the prompt was changed
  • The reviewer sees the complete conversation including the final response
  • Serialize AutoImproveState to JSON for easy persistence between sessions

Custom Prompts

customPrompt := cogito.NewPrompt(`Your custom prompt template with {{.Context}}`)

result, err := cogito.ExecuteTools(llm, fragment,
    cogito.WithPrompt(cogito.ToolReasonerType, customPrompt))

🎮 Examples

Interactive Chat Bot

# Run the example chat application
make example-chat

This starts an interactive chat session with tool support including web search capabilities.

Custom Tool Implementation

See examples/internal/search/search.go for a complete example of implementing a DuckDuckGo search tool.

🧪 Testing

The library includes comprehensive test coverage using Ginkgo and Gomega. Tests use containerized LocalAI for integration testing.

Running Tests

# Run all tests
make test

# Run with specific log level
LOG_LEVEL=debug make test

# Run with custom arguments
GINKGO_ARGS="--focus=Fragment" make test

📄 License

Ettore Di Giacinto 2025-now. Cogito is released under the Apache 2.0 License.

📚 Citation

If you use Cogito in your research or academic work, please cite our paper:

@article{cogito2025,
  title={Cogito: A Framework for Building Intelligent Agentic Software with LLM-Powered Workflows},
  author={Ettore Di Giacinto <mudler@localai.io>},
  journal={https://github.com/mudler/cogito},
  year={2025},
  note={}
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNoToolSelected              error = errors.New("no tool selected by the LLM")
	ErrLoopDetected                error = errors.New("loop detected: same tool called repeatedly with same parameters")
	ErrToolCallCallbackInterrupted error = errors.New("interrupted via ToolCallCallback")
)
View Source
var ErrDispatchFallback = errors.New("cogito: dispatch fallback to in-process")

ErrDispatchFallback signals that an AgentDispatcher declined to handle a run and cogito should execute it in-process instead.

View Source
var (
	ErrGoalNotAchieved error = errors.New("goal not achieved")
)

Functions

func CoerceNullableTypes added in v0.10.0

func CoerceNullableTypes(props map[string]any)

CoerceNullableTypes is an exported alias for the same workaround so downstream tests can verify their own MCP servers stay compatible with the import path. Most callers won't need it.

func ExtractBoolean

func ExtractBoolean(llm LLM, f Fragment, opts ...Option) (*structures.Boolean, error)

ExtractBoolean extracts a boolean from a conversation

func ExtractGoal

func ExtractGoal(llm LLM, f Fragment, opts ...Option) (*structures.Goal, error)

ExtractGoal extracts a goal from a conversation

func ExtractKnowledgeGaps

func ExtractKnowledgeGaps(llm LLM, f Fragment, opts ...Option) ([]string, error)

func ExtractPlan

func ExtractPlan(llm LLM, f Fragment, goal *structures.Goal, opts ...Option) (*structures.Plan, error)

ExtractPlan extracts a plan from a conversation To override the prompt, define a PromptPlanType, PromptReEvaluatePlanType and PromptSubtaskExtractionType

func ExtractTODOs added in v0.8.0

func ExtractTODOs(llm LLM, plan *structures.Plan, goal *structures.Goal, opts ...Option) (*structures.TODOList, error)

ExtractTODOs generates a TODO list from plan subtasks using the LLM

func IsGoalAchieved

func IsGoalAchieved(llm LLM, f Fragment, goal *structures.Goal, opts ...Option) (*structures.Boolean, error)

IsGoalAchieved checks if a goal has been achieved

func ReEvaluatePlan

func ReEvaluatePlan(llm LLM, f, subtaskFragment Fragment, goal *structures.Goal, toolStatuses []ToolStatus, subtask string, opts ...Option) (*structures.Plan, error)

ExtractPlan extracts a plan from a conversation to override the prompt, define a PromptReEvaluatePlanType and PromptSubtaskExtractionType

func SetAgentDone added in v0.10.0

func SetAgentDone(a *AgentState, ch chan struct{})

SetAgentDone sets the done channel on an AgentState. Used for testing.

func WithCompactionKeepMessages added in v0.9.2

func WithCompactionKeepMessages(count int) func(o *Options)

WithCompactionKeepMessages sets the number of recent messages to keep after compaction. Default is 10. This only applies when WithCompactionThreshold is set.

func WithCompactionThreshold added in v0.9.2

func WithCompactionThreshold(threshold int) func(o *Options)

WithCompactionThreshold sets the token count threshold that triggers automatic conversation compaction. When total tokens in the response >= threshold, the conversation will be compacted to stay within the limit. Set to 0 (default) to disable automatic compaction.

func WithContext

func WithContext(ctx context.Context) func(o *Options)

WithContext sets the execution context for the agent

func WithFeedbackCallback

func WithFeedbackCallback(fn func() *Fragment) func(o *Options)

WithFeedbackCallback sets a callback to get continous feedback during execution of plans

func WithForceReasoning added in v0.4.0

func WithForceReasoning() func(o *Options)

WithForceReasoning enables forcing the LLM to reason before selecting tools

func WithForceReasoningTool added in v0.9.1

func WithForceReasoningTool() func(o *Options)

WithForceReasoningTool enables forcing the LLM to use the reasoning tool before selecting tools. This ensures structured output from the LLM instead of free text that might accidentally contain tool call JSON.

func WithGaps

func WithGaps(gaps ...string) func(o *Options)

WithGaps adds knowledge gaps that the agent should address

func WithGuidelines

func WithGuidelines(guidelines ...Guideline) func(o *Options)

WithGuidelines adds behavioral guidelines for the agent to follow. The guildelines allows a more curated selection of the tool to use and only relevant are shown to the LLM during tool selection.

func WithIterations

func WithIterations(i int) func(o *Options)

WithIterations allows to set the number of refinement iterations

func WithLoopDetection added in v0.4.0

func WithLoopDetection(steps int) func(o *Options)

WithLoopDetection enables loop detection to prevent repeated tool calls If the same tool with the same parameters is called more than 'steps' times, it will be detected

func WithMCPArgs added in v0.3.0

func WithMCPArgs(args map[string]string) func(o *Options)

WithMCPArgs sets the arguments for the MCP prompts

func WithMCPToolFilter added in v0.10.0

func WithMCPToolFilter(fn MCPToolFilter) func(o *Options)

WithMCPToolFilter installs a per-tool gate applied during the initial tool-discovery pass over each MCP session. fn is invoked once per (session, tool) pair after ListTools returns; tools for which fn returns false are dropped from the agent's discovered set and the LLM never sees them.

The filter is not invoked on subsequent CallTool requests — the LLM can only request tools it learned about during discovery, so dropping at discovery time is sufficient. A nil fn (or no option set) means every tool from every session is exposed.

Example: per-user enable/disable of remote MCP server tools.

enabled := map[*mcp.ClientSession]map[string]bool{ ... }
cogito.WithMCPToolFilter(func(s *mcp.ClientSession, tool string) bool {
    if e, ok := enabled[s]; ok {
        return e[tool]
    }
    return true // sessions not in the map are unfiltered
})

func WithMCPs added in v0.2.0

func WithMCPs(sessions ...*mcp.ClientSession) func(o *Options)

WithMCPs adds Model Context Protocol client sessions for external tool integration. When specified, the tools available in the MCPs will be available to the cogito pipelines

func WithMaxAdjustmentAttempts added in v0.7.0

func WithMaxAdjustmentAttempts(attempts int) func(o *Options)

WithMaxAdjustmentAttempts sets the maximum number of adjustment attempts when using tool call callbacks This prevents infinite loops when the user provides adjustment feedback Default is 5 attempts

func WithMaxAttempts

func WithMaxAttempts(i int) func(o *Options)

WithMaxAttempts sets the maximum number of execution attempts

func WithMaxRetries added in v0.4.0

func WithMaxRetries(retries int) func(o *Options)

WithMaxRetries sets the maximum number of retries for LLM calls

func WithMessageInjectionChan added in v0.9.2

func WithMessageInjectionChan(ch chan openai.ChatCompletionMessage) func(o *Options)

WithMessageInjectionChan sets a channel for injecting new messages during tool loop execution. Users can send messages through this channel, and they will be appended to the fragment after each iteration. Passing nil (default) disables this feature. When the channel is closed, no more messages will be accepted.

func WithMessageInjectionResultChan added in v0.9.2

func WithMessageInjectionResultChan(ch chan MessageInjectionResult) func(o *Options)

WithMessageInjectionResultChan sets a channel to receive feedback about injected messages. For each message injection attempt, a MessageInjectionResult is sent back indicating: - Count: number of messages successfully added - Position: where in the fragment they were added - Err: error if validation or injection failed Passing nil (default) disables feedback.

func WithMessagesManipulator added in v0.9.0

func WithMessagesManipulator(fn func([]openai.ChatCompletionMessage) []openai.ChatCompletionMessage) func(o *Options)

WithMessagesManipulator allows to manipulate the messages before they are sent to the LLM This is useful to add additional system messages or other context to the messages that needs to change during execution

func WithPrompt

func WithPrompt(t prompt.PromptType, p prompt.StaticPrompt) func(o *Options)

WithPrompt allows to set a custom prompt for a given PromptType

func WithReasoningCallback added in v0.4.1

func WithReasoningCallback(fn func(string)) func(o *Options)

WithReasoningCallback sets a callback function to receive reasoning updates during execution

func WithReviewerLLM added in v0.8.0

func WithReviewerLLM(reviewerLLMs ...LLM) func(o *Options)

WithReviewerLLM specifies a judge LLM for Planning with TODOs. When provided along with a plan, enables Planning with TODOs where the judge LLM reviews work after each iteration and decides whether goal execution is completed or needs rework.

func WithSinkState added in v0.6.0

func WithSinkState(tool ToolDefinitionInterface) func(o *Options)

func WithStartWithAction added in v0.7.0

func WithStartWithAction(tool ...*ToolChoice) func(o *Options)

WithStartWithAction sets the initial tool choice to start with

func WithStatusCallback

func WithStatusCallback(fn func(string)) func(o *Options)

WithStatusCallback sets a callback function to receive status updates during execution

func WithStreamCallback added in v0.10.0

func WithStreamCallback(fn StreamCallback) func(o *Options)

WithStreamCallback sets a callback to receive streaming events during execution. When set alongside a StreamingLLM, final answer generation will stream token-by-token.

func WithTODOPersistence added in v0.8.0

func WithTODOPersistence(path string) func(o *Options)

WithTODOPersistence enables file-based TODO persistence. TODOs will be saved to and loaded from the specified file path.

func WithTODOs added in v0.8.0

func WithTODOs(todoList *structures.TODOList) func(o *Options)

WithTODOs provides an in-memory TODO list for TODO-based iterative execution. If not provided, TODOs will be automatically generated from plan subtasks.

func WithToolCallBack

func WithToolCallBack(fn func(*ToolChoice, *SessionState) ToolCallDecision) func(o *Options)

WithToolCallBack allows to set a callback to intercept and modify tool calls before execution The callback receives the proposed tool choice and session state, and returns a ToolCallDecision that can approve, reject, provide adjustment feedback, or directly modify the tool choice

func WithToolCallResultCallback

func WithToolCallResultCallback(fn func(ToolStatus)) func(o *Options)

WithToolCallResultCallback runs the callback on every tool result

func WithTools

func WithTools(tools ...ToolDefinitionInterface) func(o *Options)

WithTools allows to set the tools available to the Agent. Pass *ToolDefinition[T] instances - they will automatically generate openai.Tool via their Tool() method. Example: WithTools(&ToolDefinition[SearchArgs]{...}, &ToolDefinition[WeatherArgs]{...})

Types

type AgentDefinition added in v0.11.0

type AgentDefinition struct {
	Name         string   // unique identifier referenced by spawn_agent.agent_type
	Description  string   // shown to the LLM in the spawn tool description
	SystemPrompt string   // seeded as the sub-agent's first system message
	Tools        []string // tool-name allow-list for this type (empty = all parent tools)
	Model        string   // optional model override resolved via the agent LLM factory
	Temperature  float32  // optional sampling temperature for this type
	// Metadata is an optional per-request metadata object for this type,
	// passed to the agent LLM factory and attached to its requests.
	Metadata    map[string]string
	Iterations  int // optional per-type iteration cap (0 = inherit parent)
	MaxAttempts int // optional per-type attempt cap (0 = inherit parent)
	MaxRetries  int // optional per-type retry cap (0 = inherit parent)
}

AgentDefinition is a named sub-agent "type" (persona). The embedder registers definitions via WithAgentDefinitions; spawn_agent selects one by Name.

type AgentDispatcher added in v0.11.0

type AgentDispatcher func(ctx context.Context, spec AgentRunSpec) (Fragment, error)

AgentDispatcher is the execution seam: when set via WithAgentDispatcher, cogito calls it instead of running the sub-agent in-process. It must return the sub-agent's final Fragment (whose last message content becomes the agent's Result). Returning ErrDispatchFallback makes cogito transparently fall back to the in-process ExecuteTools path. Any other error marks the agent failed. The context governs the sub-agent's lifetime.

type AgentEvent added in v0.11.0

type AgentEvent struct {
	AgentID string // the sub-agent's registry ID
	Kind    string // one of: running | delta | done | error
	Delta   string // incremental text (for kind=delta)
	Result  string // terminal result text (for kind=done)
	Err     string // error message (for kind=error)
}

AgentEvent is a transport-friendly progress event emitted by an out-of-process executor through AgentRunSpec.Emit. cogito translates it into a tagged StreamEvent for the parent stream callback.

type AgentManager added in v0.10.0

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

AgentManager is a thread-safe registry of background sub-agents.

func NewAgentManager added in v0.10.0

func NewAgentManager() *AgentManager

NewAgentManager creates a new AgentManager.

func (*AgentManager) Detach added in v0.11.0

func (m *AgentManager) Detach(id string) error

Detach promotes a running foreground agent to background. The blocked spawn_agent call returns immediately with the agent ID; the agent's goroutine keeps running and the agent becomes an ordinary background agent. Returns an error if the agent is unknown or not detachable (already-background agents carry a nil detach channel).

func (*AgentManager) Get added in v0.10.0

func (m *AgentManager) Get(id string) (*AgentState, bool)

Get retrieves an agent by ID.

func (*AgentManager) HasRunning added in v0.10.0

func (m *AgentManager) HasRunning() bool

HasRunning returns true if any registered agent is still running.

func (*AgentManager) Inject added in v0.11.0

func (m *AgentManager) Inject(id, message string) error

Inject pushes a user-role follow-up message into a running agent's loop. Returns an error if the agent is unknown or has no injection channel.

func (*AgentManager) List added in v0.10.0

func (m *AgentManager) List() []*AgentState

List returns all registered agents.

func (*AgentManager) Register added in v0.10.0

func (m *AgentManager) Register(agent *AgentState)

Register adds an agent to the manager.

func (*AgentManager) Wait added in v0.10.0

func (m *AgentManager) Wait(id string) (*AgentState, error)

Wait blocks until the agent with the given ID completes, then returns it.

type AgentRunSpec added in v0.11.0

type AgentRunSpec struct {
	ID           string            // registry ID assigned by cogito
	Type         string            // requested agent type name (empty for generic)
	Task         string            // the user task driving the sub-agent
	SystemPrompt string            // resolved system prompt (from definition or empty)
	Model        string            // resolved model override (may be empty)
	Temperature  float32           // resolved sampling temperature (may be 0)
	Metadata     map[string]string // per-request metadata (may be nil)
	Tools        []string          // tool-name allow-list for this run
	Background   bool              // whether spawned in the background
	// Emit, when non-nil, lets the executor stream progress back to the
	// embedder. cogito wires this to forward tagged sub-agent StreamEvents to
	// the parent stream callback. It is safe to ignore.
	Emit func(AgentEvent)
}

AgentRunSpec is a portable, self-contained description of a single sub-agent run. It carries everything an out-of-process executor needs to reproduce the run that cogito would otherwise perform in-process via ExecuteTools. cogito still owns all lifecycle bookkeeping (registration, status, done channel, callbacks, completion injection, detach) regardless of where execution happens — the spec is only the execution payload.

type AgentState added in v0.10.0

type AgentState struct {
	ID       string
	Task     string
	Type     string // requested agent type name (empty for generic)
	Status   AgentStatusType
	Result   string
	Fragment *Fragment
	Error    error
	Cancel   context.CancelFunc
	// Background reports whether the agent was spawned to run in the background
	// (spawn_agent background=true) rather than in the foreground. Embedders use
	// it to tell unattended background work apart from a foreground sub-agent
	// whose result is consumed inline by the spawn call.
	Background bool
	// contains filtered or unexported fields
}

AgentState tracks the lifecycle of a single sub-agent.

type AgentStatusType added in v0.10.0

type AgentStatusType string

AgentStatusType represents the lifecycle state of a sub-agent.

const (
	AgentStatusRunning   AgentStatusType = "running"
	AgentStatusCompleted AgentStatusType = "completed"
	AgentStatusFailed    AgentStatusType = "failed"
)

type AutoImproveState added in v0.10.0

type AutoImproveState struct {
	SystemPrompt string `json:"system_prompt"`
	ReviewCount  int    `json:"review_count"`
}

AutoImproveState holds the state for the autoimproving feature. The caller owns this struct and passes a pointer to ExecuteTools via WithAutoImproveState. The state is mutated in-place through the pointer across executions.

type CheckAgentArgs added in v0.10.0

type CheckAgentArgs struct {
	AgentID string `json:"agent_id" description:"The ID of the background agent to check"`
}

CheckAgentArgs are the arguments for checking a background agent's status.

type CheckAgentRunnerForTest added in v0.10.0

type CheckAgentRunnerForTest struct {
	Manager *AgentManager
}

CheckAgentRunnerForTest exposes the checkAgentRunner for testing.

func (*CheckAgentRunnerForTest) Run added in v0.10.0

type Fragment

type Fragment struct {
	Messages       []openai.ChatCompletionMessage
	ParentFragment *Fragment
	Status         *Status
	Multimedia     []Multimedia
}

func ContentReview

func ContentReview(llm LLM, originalFragment Fragment, opts ...Option) (Fragment, error)

ContentReview refines an LLM response until for a fixed number of iterations or if the LLM doesn't find anymore gaps

func ExecutePlan

func ExecutePlan(llm LLM, conv Fragment, plan *structures.Plan, goal *structures.Goal, opts ...Option) (Fragment, error)

ExecutePlan Executes an already-defined plan with a set of options. To override its prompt, configure PromptPlanExecutionType, PromptPlanType, PromptReEvaluatePlanType and PromptSubtaskExtractionType

func ExecuteTools

func ExecuteTools(llm LLM, f Fragment, opts ...Option) (result Fragment, retErr error)

ExecuteTools runs a fragment through an LLM, and executes Tools. It returns a new fragment with the tool result at the end The result is guaranteed that can be called afterwards with llm.Ask() to explain the result to the user.

func NewEmptyFragment

func NewEmptyFragment() Fragment

func NewFragment

func NewFragment(messages ...openai.ChatCompletionMessage) Fragment

func (Fragment) AddLastMessage

func (f Fragment) AddLastMessage(f2 Fragment) Fragment

func (Fragment) AddMessage

func (r Fragment) AddMessage(role MessageRole, content string, mm ...Multimedia) Fragment

func (Fragment) AddStartMessage

func (r Fragment) AddStartMessage(role MessageRole, content string, mm ...Multimedia) Fragment

func (Fragment) AddToolMessage added in v0.5.1

func (r Fragment) AddToolMessage(content, toolCallID string) Fragment

AddToolMessage adds a tool result message with the specified tool_call_id

func (Fragment) AllFragmentsStrings

func (f Fragment) AllFragmentsStrings() string

AllFragmentsStrings walks through all the fragment parents to retrieve all the conversations and represent that as a string This is particularly useful if chaining different fragments and want to still feed the conversation as a context to the LLM.

func (Fragment) Extract added in v0.9.2

func (r Fragment) Extract(ctx context.Context, llm LLM, obj any) error

func (Fragment) ExtractStructure

func (r Fragment) ExtractStructure(ctx context.Context, llm LLM, s structures.Structure) error

ExtractStructure extracts a structure from the result using the provided JSON schema definition and unmarshals it into the provided destination

func (Fragment) GetMessages added in v0.4.0

func (f Fragment) GetMessages() []openai.ChatCompletionMessage

Messages returns the chat completion messages from this fragment, automatically prepending a force-text-reply system message if tool calls are detected. This ensures LLMs provide natural language responses instead of JSON tool syntax when Ask() is called after ExecuteTools().

func (Fragment) LastAssistantAndToolMessages added in v0.3.0

func (f Fragment) LastAssistantAndToolMessages() []openai.ChatCompletionMessage

func (Fragment) LastMessage

func (f Fragment) LastMessage() *openai.ChatCompletionMessage

func (Fragment) SelectTool

func (f Fragment) SelectTool(ctx context.Context, llm LLM, availableTools Tools, forceTool string) (Fragment, *ToolChoice, error)

SelectTool allows the LLM to select a tool from the fragment of conversation

func (Fragment) String

func (f Fragment) String() string

type GetAgentResultArgs added in v0.10.0

type GetAgentResultArgs struct {
	AgentID string `json:"agent_id" description:"The ID of the background agent"`
	Wait    bool   `json:"wait" description:"If true, blocks until the agent finishes. If false, returns immediately with current status."`
}

GetAgentResultArgs are the arguments for retrieving a background agent's result.

type GetAgentResultRunnerForTest added in v0.10.0

type GetAgentResultRunnerForTest struct {
	Manager *AgentManager
	Ctx     context.Context
}

GetAgentResultRunnerForTest exposes the getAgentResultRunner for testing.

func (*GetAgentResultRunnerForTest) Run added in v0.10.0

type Guideline

type Guideline struct {
	Condition string
	Action    string
	Tools     Tools
}

type GuidelineMetadata

type GuidelineMetadata struct {
	Condition string
	Action    string
	Tools     []string
}

type GuidelineMetadataList

type GuidelineMetadataList []GuidelineMetadata

type Guidelines

type Guidelines []Guideline

func GetRelevantGuidelines

func GetRelevantGuidelines(llm LLM, guidelines Guidelines, fragment Fragment, opts ...Option) (Guidelines, error)

func (Guidelines) ToMetadata

func (g Guidelines) ToMetadata() GuidelineMetadataList

type InjectedMessage added in v0.9.2

type InjectedMessage struct {
	Message   openai.ChatCompletionMessage
	Iteration int // Iteration number when message was injected
}

type IntentionResponseMultiple added in v0.9.0

type IntentionResponseMultiple struct {
	Tools     []string `json:"tools"`
	Reasoning string   `json:"reasoning"`
}

IntentionResponseMultiple is used to extract multiple tool choices from the intention tool

type IntentionResponseSingle added in v0.9.0

type IntentionResponseSingle struct {
	Tool      string `json:"tool"`
	Reasoning string `json:"reasoning"`
}

IntentionResponseSingle is used to extract a single tool choice from the intention tool

type LLM

type LLM interface {
	Ask(ctx context.Context, f Fragment) (Fragment, error)
	CreateChatCompletion(ctx context.Context, request openai.ChatCompletionRequest) (LLMReply, LLMUsage, error)
}

type LLMReply added in v0.9.2

type LLMReply struct {
	ChatCompletionResponse openai.ChatCompletionResponse
	ReasoningContent       string
}

type LLMUsage added in v0.9.2

type LLMUsage struct {
	PromptTokens     int
	CompletionTokens int
	TotalTokens      int
}

LLMUsage represents token usage information from an LLM response

type MCPToolFilter added in v0.10.0

type MCPToolFilter = func(session *mcp.ClientSession, toolName string) bool

MCPToolFilter is invoked once per (session, tool) pair during the initial tool-discovery pass. Return false to drop the tool from the agent's discovered set (the LLM never sees it). A nil filter is equivalent to "always allow".

type MessageInjectionResult added in v0.9.2

type MessageInjectionResult struct {
	Count    int // Number of messages successfully injected
	Position int // Position in fragment where messages were added
}

MessageInjectionResult provides feedback about injected messages

type MessageRole added in v0.9.0

type MessageRole string
const (
	AssistantMessageRole MessageRole = "assistant"
	UserMessageRole      MessageRole = "user"
	ToolMessageRole      MessageRole = "tool"
	SystemMessageRole    MessageRole = "system"
)

func (MessageRole) String added in v0.9.0

func (m MessageRole) String() string

type Multimedia

type Multimedia interface {
	URL() string
}

TODO: Video, Audio, Image input

type Option

type Option func(*Options)
var (
	// EnableDeepContext enables full context to the LLM when chaining conversations
	// It might yield to better results to the cost of bigger context use.
	EnableDeepContext Option = func(o *Options) {
		o.deepContext = true
	}

	// EnableToolReasoner enables the reasoning about the need to call other tools
	// before each tool call, preventing calling more tools than necessary.
	EnableToolReasoner Option = func(o *Options) {
		o.toolReasoner = true
	}

	// DisableSinkState disables the use of a sink state
	// when the LLM decides that no tool is needed
	DisableSinkState Option = func(o *Options) {
		o.sinkState = false
	}

	// EnableInfiniteExecution enables infinite, long-term execution on Plans
	EnableInfiniteExecution Option = func(o *Options) {
		o.infiniteExecution = true
	}

	// EnableStrictGuidelines enforces cogito to pick tools only from the guidelines
	EnableStrictGuidelines Option = func(o *Options) {
		o.strictGuidelines = true
	}

	// EnableAutoPlan enables cogito to automatically use planning if needed
	EnableAutoPlan Option = func(o *Options) {
		o.autoPlan = true
	}

	// EnableAutoPlanReEvaluator enables cogito to automatically re-evaluate the need to use planning
	EnableAutoPlanReEvaluator Option = func(o *Options) {
		o.planReEvaluator = true
	}

	// EnableMCPPrompts enables the use of MCP prompts
	EnableMCPPrompts Option = func(o *Options) {
		o.mcpPrompts = true
	}

	// EnableGuidedTools enables filtering tools through guidance using their descriptions.
	// When no guidelines exist, creates virtual guidelines for all tools using their descriptions.
	// When guidelines exist, creates virtual guidelines for tools not in any guideline.
	EnableGuidedTools Option = func(o *Options) {
		o.guidedTools = true
	}

	// EnableParallelToolExecution enables parallel execution of multiple tool calls.
	// When enabled, the LLM can select multiple tools and they will be executed concurrently.
	EnableParallelToolExecution Option = func(o *Options) {
		o.parallelToolExecution = true
	}
)
var EnableAgentSpawning Option = func(o *Options) {
	o.enableAgentSpawning = true
}

EnableAgentSpawning enables sub-agent spawning tools (spawn_agent, check_agent, get_agent_result). When enabled, the LLM can delegate tasks to sub-agents that run in foreground (blocking) or background (non-blocking).

func WithAgentCompletionCallback added in v0.10.0

func WithAgentCompletionCallback(fn func(*AgentState)) Option

WithAgentCompletionCallback sets a callback that fires when any background sub-agent finishes. Useful for external monitoring or UI updates outside the LLM loop.

func WithAgentCompletionFormatter added in v0.10.0

func WithAgentCompletionFormatter(fn func(*AgentState) string) Option

WithAgentCompletionFormatter overrides the message a finished background sub-agent injects into the parent loop. By default cogito injects a fixed prose notification ("Background agent <id> has completed…"); set this to control exactly what the parent LLM sees on wake — e.g. a clean structured summary, or a marker a host application can intercept and render itself rather than leaving the model to re-parse prose. The returned string is injected verbatim as a user-role message; returning "" injects an empty message (the default prose is only used when no formatter is set).

func WithAgentDefinitions added in v0.11.0

func WithAgentDefinitions(defs ...AgentDefinition) Option

WithAgentDefinitions registers named sub-agent types (personas). spawn_agent can select one via its agent_type argument; the chosen definition supplies the system prompt, tool allow-list, model, temperature, and per-type execution limits.

func WithAgentDispatcher added in v0.11.0

func WithAgentDispatcher(d AgentDispatcher) Option

WithAgentDispatcher installs an execution seam for spawned sub-agents. When set, cogito calls the dispatcher instead of running the sub-agent in-process (e.g. to dispatch the run to a remote worker), while still owning every part of the sub-agent lifecycle: registration, status transitions, the done channel, completion/spawn callbacks, completion-message injection, and foreground detach. A nil dispatcher (the default) preserves the in-process behavior exactly. Returning ErrDispatchFallback from the dispatcher makes cogito run the sub-agent in-process for that call.

func WithAgentLLM added in v0.10.0

func WithAgentLLM(llm LLM) Option

WithAgentLLM sets a separate LLM for sub-agents to use. If not set, sub-agents share the parent's LLM.

func WithAgentLLMFactory added in v0.11.0

func WithAgentLLMFactory(fn func(model string, temperature float32, metadata map[string]string) LLM) Option

WithAgentLLMFactory sets a factory that builds an LLM for a sub-agent from a model name, temperature, and per-request metadata. Used to resolve per-agent-type or per-spawn model/metadata overrides while reusing the parent's endpoint/credentials.

func WithAgentManager added in v0.10.0

func WithAgentManager(m *AgentManager) Option

WithAgentManager provides an existing AgentManager for sharing across multiple ExecuteTools calls. If not provided and EnableAgentSpawning is set, a new AgentManager is created automatically.

func WithAgentSpawnCallback added in v0.11.0

func WithAgentSpawnCallback(fn func(*AgentState)) Option

WithAgentSpawnCallback sets a callback that fires when a sub-agent starts (is registered and about to run), for both foreground and background spawns. Useful for UIs that show running agents. The AgentState has Status=running.

func WithAutoImproveReviewerLLM added in v0.10.0

func WithAutoImproveReviewerLLM(llm LLM) Option

WithAutoImproveReviewerLLM sets a separate LLM for the autoimprove review step. If not set, the same LLM passed to ExecuteTools is used.

func WithAutoImproveState added in v0.10.0

func WithAutoImproveState(state *AutoImproveState) Option

WithAutoImproveState enables the autoimproving feature. The provided state is mutated in-place: after ExecuteTools returns, state.SystemPrompt may have been updated by the review step. Persist and reuse the same pointer across calls.

func WithOnPark added in v0.10.0

func WithOnPark(fn func(reply string)) Option

WithOnPark registers a callback fired immediately BEFORE the loop blocks on the message-injection channel at a park gate (i.e. when background work — cogito's own running agents or an embedder's WithPendingWork predicate — is still pending). The callback receives the assistant reply text that preceded the park — the no-tool text reply recorded in the fragment just before the loop blocked — or "" when the model produced none (e.g. a sink-state park). An embedder can use this to surface the parked reply and finalize the current assistant turn the instant the loop parks.

Across a single run the loop may park and resume multiple times (e.g. several injected messages), so onPark may fire multiple times — that is expected.

func WithOnResume added in v0.10.0

func WithOnResume(fn func()) Option

WithOnResume registers a callback fired immediately AFTER an injected message wakes the loop at a park gate (the resume path). It does NOT fire when the loop unblocks because the injection channel was closed or the context was cancelled. An embedder can use this to start a fresh assistant turn the instant the loop resumes.

Across a single run the loop may park and resume multiple times (e.g. several injected messages), so onResume may fire multiple times — that is expected.

func WithPendingWork added in v0.10.0

func WithPendingWork(fn func() bool) Option

WithPendingWork makes the loop park (waiting on the message-injection channel) while fn returns true, even when cogito's own AgentManager has no running agents. For embedder-owned background work whose completion is delivered by injecting a message (see WithMessageInjectionChan). Pair it with WithMessageInjectionChan so there is a channel to wake on.

type Options

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

Options contains all configuration options for the Cogito agent It allows customization of behavior, tools, prompts, and execution parameters

func (*Options) Apply

func (o *Options) Apply(opts ...Option)

type PlanStatus added in v0.3.2

type PlanStatus struct {
	Plan  structures.Plan
	Tools []ToolStatus
}

type ReasoningResponse added in v0.9.1

type ReasoningResponse struct {
	Reasoning string `json:"reasoning"`
}

ReasoningResponse is used to extract reasoning from the reasoning tool

type SendAgentMessageArgs added in v0.11.0

type SendAgentMessageArgs struct {
	AgentID string `json:"agent_id" description:"The ID of the agent to message"`
	Message string `` /* 150-byte string literal not displayed */
}

SendAgentMessageArgs is the argument for the unified resume/inject tool.

type SessionState added in v0.7.0

type SessionState struct {
	ToolChoice *ToolChoice `json:"tool_choice"`
	Fragment   Fragment    `json:"fragment"`
	// AgentID identifies the sub-agent whose tool call is being evaluated.
	// Empty for the root agent. Set when the tool-call callback is invoked
	// from within a spawned sub-agent (see WithToolCallBack propagation).
	AgentID string `json:"agent_id,omitempty"`
}

func (*SessionState) Resume added in v0.7.0

func (s *SessionState) Resume(llm LLM, opts ...Option) (Fragment, error)

type SpawnAgentArgs added in v0.10.0

type SpawnAgentArgs struct {
	AgentType  string   `` /* 140-byte string literal not displayed */
	Task       string   `json:"task" description:"The task or prompt for the sub-agent to execute"`
	Background bool     `` /* 148-byte string literal not displayed */
	Tools      []string `` /* 149-byte string literal not displayed */
	Model      string   `json:"model" description:"Optional model override for this sub-agent."`
}

SpawnAgentArgs are the arguments the LLM provides when spawning a sub-agent.

type Status

type Status struct {
	LastUsage        LLMUsage // Track token usage from the last LLM call
	CumulativeUsage  LLMUsage // Sum of token usage across every LLM call in the run
	Iterations       int
	ToolsCalled      Tools
	ToolResults      []ToolStatus
	Plans            []PlanStatus
	PastActions      []ToolStatus         // Track past actions for loop detections
	ReasoningLog     []string             // Track reasoning for each iteration
	TODOs            *structures.TODOList // TODO tracking for iterative execution
	TODOIteration    int                  // Current TODO iteration
	TODOPhase        string               // Current phase: "work" or "review"
	InjectedMessages []InjectedMessage    // Track successfully injected messages with timing
}

type StreamCallback added in v0.10.0

type StreamCallback func(StreamEvent)

StreamCallback is a function that receives streaming events.

type StreamEvent added in v0.10.0

type StreamEvent struct {
	Type          StreamEventType
	Content       string   // text delta (reasoning/content)
	ToolName      string   // for tool_call/tool_result — name (first chunk only)
	ToolArgs      string   // for tool_call: argument delta string
	ToolCallID    string   // OpenAI tool_call ID (first chunk only)
	ToolCallIndex int      // which tool call (for parallel tool calls)
	ToolResult    string   // tool result text
	FinishReason  string   // "stop", "tool_calls", etc. (populated on done)
	Error         error    // populated on error
	Usage         LLMUsage // populated on done
	AgentID       string   // populated for sub-agent events
}

StreamEvent represents a single streaming event from the LLM or tool pipeline.

type StreamEventType added in v0.10.0

type StreamEventType string

StreamEventType identifies the kind of streaming event.

const (
	StreamEventReasoning  StreamEventType = "reasoning"   // LLM thinking delta
	StreamEventContent    StreamEventType = "content"     // answer text delta
	StreamEventToolCall   StreamEventType = "tool_call"   // tool selected + args
	StreamEventToolResult StreamEventType = "tool_result" // tool execution result
	StreamEventStatus     StreamEventType = "status"      // status message
	StreamEventDone       StreamEventType = "done"        // stream complete
	StreamEventError      StreamEventType = "error"       // error
	StreamEventSubAgent   StreamEventType = "sub_agent"   // sub-agent event
)

type StreamingLLM added in v0.10.0

type StreamingLLM interface {
	LLM
	CreateChatCompletionStream(ctx context.Context, request openai.ChatCompletionRequest) (<-chan StreamEvent, error)
}

StreamingLLM extends LLM with streaming support. Consumers should type-assert: if sllm, ok := llm.(StreamingLLM); ok { ... }

type Tool

type Tool[T any] interface {
	Run(args T) (string, any, error)
}

type ToolCallDecision added in v0.7.0

type ToolCallDecision struct {
	// Approved: true to proceed with the tool call, false to interrupt execution
	Approved bool

	// Adjustment: feedback string for the LLM to interpret and adjust the tool call
	// Empty string means no adjustment needed. If provided, the LLM will re-evaluate
	// the tool call based on this feedback.
	Adjustment string

	// Modified: directly modified tool choice that takes precedence over Adjustment
	// If set, this tool choice is used directly without re-querying the LLM
	// This allows programmatic modification of tool arguments
	Modified *ToolChoice

	// Skip: skip this tool call but continue execution (alternative to Approved: false)
	// When true, the tool call is skipped and execution continues
	Skip bool
}

ToolCallDecision represents the decision made by a tool call callback It allows the callback to approve, reject, provide adjustment feedback, or directly modify the tool choice

type ToolChoice

type ToolChoice struct {
	Name      string         `json:"name"`
	Arguments map[string]any `json:"arguments"`
	ID        string         `json:"id"`
	Reasoning string         `json:"reasoning"`
}

type ToolDefinition added in v0.5.0

type ToolDefinition[T any] struct {
	ToolRunner        Tool[T]
	InputArguments    any
	Name, Description string
}

func (*ToolDefinition[T]) Execute added in v0.5.0

func (t *ToolDefinition[T]) Execute(args map[string]any) (string, any, error)

Execute implements ToolDef.Execute by marshaling the arguments map to type T and calling ToolRunner.Run

func (ToolDefinition[T]) Tool added in v0.5.0

func (t ToolDefinition[T]) Tool() openai.Tool

type ToolDefinitionInterface added in v0.5.0

type ToolDefinitionInterface interface {
	Tool() openai.Tool
	// Execute runs the tool with the given arguments (as JSON map) and returns the result
	Execute(args map[string]any) (string, any, error)
}

func NewToolDefinition added in v0.5.0

func NewToolDefinition[T any](toolRunner Tool[T], inputArguments any, name, description string) ToolDefinitionInterface

type ToolStatus

type ToolStatus struct {
	Executed      bool
	ToolArguments ToolChoice
	Result        string
	Name          string
	ResultData    any
}

type Tools

func FilterToolsForSubAgent added in v0.10.0

func FilterToolsForSubAgent(parentTools Tools, requestedTools []string) Tools

FilterToolsForSubAgent returns a subset of parent tools suitable for a sub-agent. If requestedTools is non-empty, only those named tools are included. Agent management tools are excluded by default.

func (Tools) Definitions

func (t Tools) Definitions() []*openai.FunctionDefinition

func (Tools) Find

func (t Tools) Find(name string) ToolDefinitionInterface

func (Tools) Names added in v0.9.0

func (t Tools) Names() []string

func (Tools) ToOpenAI

func (t Tools) ToOpenAI() []openai.Tool

Directories

Path Synopsis
examples
chat command
sub-agents command
tests

Jump to

Keyboard shortcuts

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