loop

package module
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Mar 23, 2026 License: MIT Imports: 7 Imported by: 3

README

axon-loop

Primitives · Part of the lamina workspace

Provider-agnostic conversation loop for LLM-powered agents. Handles message exchange, tool call dispatch, and streaming with no HTTP, persistence, or UI concerns. Bring your own LLMClient implementation (e.g. Ollama, OpenAI, Anthropic) and axon-loop drives the send-stream-tool-repeat cycle.

Getting started

go get github.com/benaskins/axon-loop@latest

Requires Go 1.24+.

result, err := loop.Run(ctx, client, &loop.Request{
    Model:    "llama3",
    Messages: []loop.Message{{Role: "user", Content: "Hello"}},
    Stream:   true,
}, tools, nil, loop.Callbacks{
    OnToken: func(t string) { fmt.Print(t) },
})

See example/main.go for a runnable sketch.

Key types

  • LLMClient — interface for communicating with any LLM backend
  • Request / Response — provider-agnostic request and streamed response chunk
  • Message — a single message in a conversation
  • ToolCall — an LLM's decision to invoke a tool
  • Run() — executes the conversation loop with tool dispatch and streaming callbacks
  • Callbacks — optional hooks for tokens, thinking, tool use, and completion

License

MIT — see LICENSE.

Documentation

Overview

Package loop provides a provider-agnostic conversation loop for LLM-powered agents. It handles message exchange, tool call dispatch, and streaming — with no HTTP, persistence, or UI concerns.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Stream added in v0.4.0

func Stream(ctx context.Context, cfg RunConfig) <-chan Event

Stream executes a conversation loop and returns a channel of Events. The channel is closed when the loop completes or fails. The caller reads events without building a callback-to-channel bridge.

Any Callbacks in cfg are ignored — Stream bridges all events to the returned channel.

Types

type Callbacks

type Callbacks struct {
	OnToken    func(token string)
	OnThinking func(token string)
	OnToolUse  func(name string, args map[string]any)
	OnTrim     func(dropped []Message) // Called when context trimming drops messages.
	OnDone     func(durationMs int64)
}

Callbacks receives streaming events from the conversation loop. All fields are optional — nil callbacks are skipped.

type ContextStrategy added in v0.6.1

type ContextStrategy interface {
	// Trim returns the messages to send to the LLM.
	// The input slice must not be modified.
	Trim(messages []Message) []Message
}

ContextStrategy controls how conversation history is trimmed before each LLM request. Implementations receive the full message slice and return the subset to send. The system prompt (if present as the first message) should generally be preserved.

func SlidingWindow added in v0.6.1

func SlidingWindow(n int) ContextStrategy

SlidingWindow keeps the system prompt plus the last n conversation messages. Useful when you want a fixed-size history regardless of token count.

func TokenBudget added in v0.6.1

func TokenBudget(budget int) ContextStrategy

TokenBudget trims conversation history to fit within an estimated token budget. The system prompt is always preserved; older messages are dropped first. This is the strategy used when Request.MaxTokens is set without an explicit ContextStrategy.

func TokenBudgetWithMinWindow added in v0.6.1

func TokenBudgetWithMinWindow(budget, minMessages int) ContextStrategy

TokenBudgetWithMinWindow trims to a token budget but guarantees at least minMessages conversation messages are kept (plus the system prompt). Whichever approach retains more messages wins. This prevents a large system prompt from starving the conversation history.

type ContextStrategyFunc added in v0.6.1

type ContextStrategyFunc func(messages []Message) []Message

ContextStrategyFunc adapts a plain function into a ContextStrategy.

func (ContextStrategyFunc) Trim added in v0.6.1

func (f ContextStrategyFunc) Trim(messages []Message) []Message

type DoneEvent added in v0.4.0

type DoneEvent struct {
	Content    string
	Thinking   string
	DurationMs int64
}

DoneEvent is emitted when the loop completes successfully.

type Event added in v0.4.0

type Event struct {
	// Exactly one of these is set per event.
	Token    string        // incremental content token
	Thinking string        // incremental thinking token
	ToolUse  *ToolUseEvent // a tool was invoked
	Trim     *TrimEvent    // context was trimmed
	Done     *DoneEvent    // the loop completed
	Err      error         // the loop failed
}

Event is a streaming event emitted by Stream. Consumers receive these on a channel instead of registering callbacks.

type LLMClient added in v0.2.0

type LLMClient interface {
	Chat(ctx context.Context, req *Request, fn func(Response) error) error
}

LLMClient abstracts communication with an LLM backend. Implementations translate to/from provider-specific APIs (e.g. Ollama, OpenAI, Anthropic).

type Message

type Message struct {
	Role       Role
	Content    string
	Thinking   string
	ToolCalls  []ToolCall
	ToolCallID string // Correlates a tool result with its originating ToolCall.
}

Message represents a single message in a conversation.

type Request added in v0.3.0

type Request struct {
	Model         string
	Messages      []Message
	Tools         []tool.ToolDef
	Stream        bool
	Think         *bool
	Options       map[string]any
	MaxIterations int // Maximum tool-call loop iterations. Defaults to 20 if 0.
	MaxTokens     int // Maximum estimated token budget for messages. 0 means no limit.
}

Request is a provider-agnostic request to an LLM.

type Response added in v0.3.0

type Response struct {
	Content   string
	Thinking  string
	Done      bool
	ToolCalls []ToolCall
}

Response is a provider-agnostic streamed response chunk from an LLM.

type Result

type Result struct {
	Content  string
	Thinking string
}

Result is the final output of a conversation loop run.

func Run

func Run(ctx context.Context, cfg RunConfig) (*Result, error)

Run executes a conversation loop: sends messages to the LLM, streams the response, executes tool calls, and repeats until no more tool calls are made.

tools and toolCtx may be nil for simple chat without tool support.

type Role added in v0.6.0

type Role string

Role represents the sender of a message.

const (
	RoleSystem    Role = "system"
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleTool      Role = "tool"
)

type RunConfig added in v0.6.0

type RunConfig struct {
	Client  LLMClient
	Request *Request
	Tools   map[string]tool.ToolDef
	ToolCtx *tool.ToolContext
	Context ContextStrategy // Optional; trims messages before each LLM call.
	Callbacks
}

RunConfig bundles parameters for Run, keeping the function signature small.

type ToolCall

type ToolCall struct {
	ID        string // Provider-assigned correlation ID (e.g. OpenAI tool_call_id).
	Name      string
	Arguments map[string]any
}

ToolCall represents an LLM's decision to invoke a tool.

type ToolUseEvent added in v0.4.0

type ToolUseEvent struct {
	Name string
	Args map[string]any
}

ToolUseEvent is emitted when the LLM invokes a tool.

type TrimEvent added in v0.6.1

type TrimEvent struct {
	Dropped []Message
}

TrimEvent is emitted when the context strategy drops messages.

Jump to

Keyboard shortcuts

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