loopkit

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

LoopKit

Make the agent loop a durable, typed, replayable, budgeted execution primitive — engineered, tested, and operated like any other production system.

"Temporal for agent loops" — not another LangChain port.

CI Go Reference Go Report Card Apache 2.0


60-Second Example

package main

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

	loopkit "github.com/wolmi/loopkit"
	"github.com/wolmi/loopkit/adapters/store/sqlite"
	"github.com/wolmi/loopkit/core/action"
	"github.com/wolmi/loopkit/core/event"
	"github.com/wolmi/loopkit/core/loop"
	"github.com/wolmi/loopkit/core/loop/outcome"
	"github.com/wolmi/loopkit/runtime"
)

type State struct{ Done bool }
type Result struct{ Answer string }

func main() {
	store, _ := sqlite.Open("/tmp/demo.db")

	def := loop.Definition[State, Result]{
		Name:      "demo",
		InitState: func() State { return State{} },
		Transition: func(_ loop.Context, s State, ev event.Envelope) (State, []action.Action, error) {
			switch ev.Kind {
			case event.KindIterationStarted:
				if !s.Done {
					args, _ := json.Marshal(map[string]string{"query": "hello"})
					return s, []action.Action{action.ToolCall{Tool: "echo", Args: args}}, nil
				}
			case event.KindToolCalled:
				res, _ := json.Marshal(Result{Answer: "42"})
				return State{Done: true}, []action.Action{action.Finish{Result: res}}, nil
			}
			return s, nil, nil
		},
	}

	runner := loopkit.New(def,
		loopkit.WithStore(store),
		loopkit.WithTools(echoTools{}),
		loopkit.WithMaxIterations(5),
	)

	o, _ := runner.Start(context.Background(), nil)
	outcome.Match(o,
		func(c outcome.Completed[Result]) string {
			fmt.Println("Answer:", c.Result.Answer)
			return ""
		},
		func(outcome.BudgetExceeded[Result]) string { fmt.Println("over budget"); return "" },
		func(outcome.Stuck[Result]) string { fmt.Println("stuck"); return "" },
		func(outcome.Interrupted[Result]) string { fmt.Println("interrupted"); return "" },
		func(outcome.PolicyDenied[Result]) string { fmt.Println("policy denied"); return "" },
		func(e outcome.Failed[Result]) string { fmt.Println("failed:", e.Err); return "" },
	)
}

type echoTools struct{}

func (echoTools) Describe() []runtime.ToolDescriptor {
	return []runtime.ToolDescriptor{{Name: "echo", Description: "Echo the query back"}}
}
func (echoTools) Execute(_ context.Context, req runtime.ToolRequest) (runtime.ToolResult, error) {
	return runtime.ToolResult{Result: req.Args}, nil
}

Install:

go get github.com/wolmi/loopkit@latest

Why LoopKit

Agent frameworks tell you how to write the loop. LoopKit tells you how to run it — crash-safely, on budget, with full replay and regression testing.

Problem LoopKit answer
Process dies, hours of agent work lost SQLite event log survives; runner.Resume continues from last committed event
Over-budget model calls, runaway loops WithBudget enforces token/USD/wall-clock/iteration ceilings; violation = typed BudgetExceeded outcome
"Why did it do that?" loopctl replay --strict replays the exact production run; loopctl diff shows where two trajectories diverge
PR changed a prompt, agent regressed looptest.Replay(t, "fixture.loopfix") + eval suite in go test; CI blocks on regression
Blind trust of tool execution Capability-based policy engine; WASM sandbox; PolicyDenied outcome when capability absent

Architecture

┌────────────────────────────────────────────────────────────────┐
│  L5  TOOLING                                                   │
│      loopctl CLI · Trajectory Inspector UI · CI eval-gate      │
├────────────────────────────────────────────────────────────────┤
│  L4  QUALITY ENGINE                                            │
│      .loopfix fixtures · looptest.Replay · Eval harness        │
├────────────────────────────────────────────────────────────────┤
│  L3  GOVERNANCE                                                │
│      Budget (tokens/$/time/iter) · Policy · Convergence guard  │
├────────────────────────────────────────────────────────────────┤
│  L2  PROVIDERS & TOOLS                                         │
│      openaicompat · anthropic · gemini · MCP · A2A · WASM      │
├────────────────────────────────────────────────────────────────┤
│  L1  KERNEL                                                    │
│      event-sourced loop · sealed outcomes · durability proto   │
└────────────────────────────────────────────────────────────────┘

Hexagonal rules: core/* imports stdlib only. Adapters import core/runtime; never the reverse. OTel confined to adapters/telemetry/otel. Enforced by arch_test.go.


Feature Matrix

Legend: ✅ native · 🟡 partial · ❌ absent

Capability ADK Go Eino Genkit Go LangChainGo LoopKit
Core loop
Prebuilt loop patterns (ReAct, Supervisor) 🟡
Loop as typed state machine (exhaustive outcomes) 🟡
Compile-time typed topology 🟡
Durability & replay
Crash-durable (survives process death)
Deterministic replay of a production run
Fork trajectory at iteration N and diff
Checkpoint/resume for human-in-the-loop
Governance
Token/cost budgets enforced per loop & subtree
Convergence / stuck-loop detection
Capability-based tool permission policy 🟡
Sandboxed tool execution (WASM) 🟡
Quality engineering
Trajectory recording as test fixtures 🟡
go test loop assertions in CI 🟡
LLM-judge scoring built in 🟡 🟡
Observability
OpenTelemetry tracing 🟡 🟡
Local trajectory inspector UI 🟡

Quickstart

1. Install
go get github.com/wolmi/loopkit@latest
2. Run the research-agent demo
# Interrupt + resume demo (flagship)
go run ./examples/research-agent/

# Full kill/resume walkthrough
bash examples/research-agent/demo.sh
3. Record and replay a run
# Build loopctl
go build -o loopctl ./cmd/loopctl/

# Record a loop run from a SQLite store
./loopctl record <loop-id> --store sqlite://path.db -o run.loopfix

# Strict replay (verifies chain hash)
./loopctl replay run.loopfix --strict

# Diff two trajectories
./loopctl diff run_a.loopfix run_b.loopfix --json
4. Test your loop
// In your _test.go:
func TestMyLoop(t *testing.T) {
    looptest.Replay(t, "testdata/my_run.loopfix",
        looptest.ExpectCompleted[MyResult](),
    )
}

Providers

Provider Package
OpenAI / Ollama / OpenRouter / vLLM adapters/provider/openaicompat
Anthropic adapters/provider/anthropic
Gemini adapters/provider/gemini
Amazon Bedrock adapters/provider/bedrock
Azure OpenAI adapters/provider/azure

Tools

Integration Package
MCP (official SDK) adapters/tool/mcp
A2A agent consumption adapters/interop/a2a
WASM sandbox adapters/sandbox/wasm
In-process sandbox adapters/sandbox/inproc

Stores

Store Package Use case
SQLite (no CGO) adapters/store/sqlite Default — single-binary, crash-durable
In-memory adapters/store/memory Tests and transient use

Documentation


License

Apache 2.0 — see LICENSE.

Documentation

Overview

Package loopkit is the public facade for the LoopKit loop kernel. It provides a generic, type-safe API for creating and running durable, replayable agent loops.

Quick start:

def := loop.Definition[MyState, MyResult]{
    Name:      "my-loop",
    InitState: func() MyState { return MyState{} },
    Transition: func(tc loop.Context, s MyState, ev event.Envelope) (MyState, []action.Action, error) {
        // ... pure state transition logic
    },
}

runner := loopkit.New(def,
    loopkit.WithStore(myStore),
    loopkit.WithProvider(myProvider),
    loopkit.WithTools(myTools),
)

outcome, err := runner.Start(ctx, input)
outcome.Match(o,
    func(c outcome.Completed[MyResult]) string { return "done" },
    // ... other cases
)

Index

Constants

View Source
const Version = "0.1.0"

Version is the loopkit module version.

Variables

This section is empty.

Functions

func MustRegister

func MustRegister[S, R any](reg *Registry, def loop.Definition[S, R])

MustRegister is like Register but panics on duplicate names.

func Register

func Register[S, R any](reg *Registry, def loop.Definition[S, R]) error

Register adds a loop definition to the registry by name. Returns an error if a definition with that name is already registered.

func Respond

func Respond(ctx context.Context, store runtime.EventStore, id event.LoopID, response json.RawMessage) error

Respond appends a HumanResponded event to a loop awaiting human input. The loop can then be resumed with Resume.

Types

type ChildResult

type ChildResult struct {
	LoopID      event.LoopID
	OutcomeKind string
	RespJSON    []byte
	ErrDetail   string
	Spend       budget.Spend
}

ChildResult is the outcome of a spawned child loop.

type Option

type Option func(*runtime.RunnerOptions)

Option configures a Runner.

func WithArtifactStore

func WithArtifactStore(as runtime.ArtifactStore) Option

WithArtifactStore sets the ArtifactStore for the ArtifactOffload compaction strategy.

func WithBudget

func WithBudget(b budget.Budget) Option

WithBudget sets the budget caps for the runner. Zero values mean unlimited for that dimension.

func WithClock

func WithClock(clock runtime.Clock) Option

WithClock sets the Clock for the runner.

func WithContextManager

func WithContextManager(cm runtime.ContextManager) Option

WithContextManager sets the context compaction strategy. The strategy is applied before each ModelCall to keep context within token limits.

func WithConvergence

func WithConvergence(cfg *converge.Config) Option

WithConvergence enables convergence monitoring with the given configuration. Pass nil to disable (default). Use converge.DefaultConfig() for sensible defaults.

func WithMaxIterations

func WithMaxIterations(n int) Option

WithMaxIterations sets the maximum number of iterations before Stuck outcome. 0 means unlimited.

func WithPolicy

func WithPolicy(grant *policy.Grant) Option

WithPolicy sets the capability grant for policy enforcement. Pass nil for allow-all (backward-compatible default). With a non-nil grant: deny-by-default for any capability not listed.

func WithPolicyMode

func WithPolicyMode(mode runtime.PolicyMode) Option

WithPolicyMode sets how the runner handles policy denials. PolicyModeTerminate (default): denied action terminates loop with PolicyDenied outcome. PolicyModeDenyAction: denied action records error result; loop continues.

func WithPricing

func WithPricing(table model.Table) Option

WithPricing sets the model pricing table for cost calculation. If not set, model.DefaultTable is used.

func WithProvider

func WithProvider(provider runtime.ModelProvider) Option

WithProvider sets the ModelProvider for the runner.

func WithSeed

func WithSeed(seed int64) Option

WithSeed sets the random seed for deterministic Rand in transitions.

func WithSnapshotEvery

func WithSnapshotEvery(n int) Option

WithSnapshotEvery configures automatic snapshots every n events (0 = disabled).

func WithSpawner

func WithSpawner(s runtime.ChildSpawner) Option

WithSpawner sets the ChildSpawner (Registry) used to resolve Spawn actions. Without a spawner, Spawn actions return ErrUnsupported.

func WithStore

func WithStore(store runtime.EventStore) Option

WithStore sets the EventStore for the runner.

func WithStreamObserver

func WithStreamObserver(observer runtime.StreamObserver) Option

WithStreamObserver sets an optional callback for model streaming chunks. The callback is invoked for each chunk received during Stream operations.

func WithTelemetry

func WithTelemetry(tel runtime.Telemetry) Option

WithTelemetry sets the telemetry implementation. The default is NoopTelemetry, which performs zero allocations on the hot path. Use adapters/telemetry/otel.New() for OpenTelemetry integration.

func WithTokenEstimator

func WithTokenEstimator(est runtime.TokenEstimator) Option

WithTokenEstimator sets a custom token estimator for pre-authorizing model calls. The estimator should return the expected total token count for a request. Default heuristic: sum(content lengths)/4 + MaxTokens.

func WithTools

func WithTools(tools runtime.ToolExecutor) Option

WithTools sets the ToolExecutor for the runner.

type Registry

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

Registry is a name→definition mapping used by the runner to resolve Spawn actions. It is safe for concurrent use. Register/MustRegister add definitions.

Usage:

reg := loopkit.NewRegistry()
loopkit.Register(reg, myDef)
runner := loopkit.New(parentDef, loopkit.WithRegistry(reg), ...)

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty Registry.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (*RegistryEntry, bool)

Lookup returns the registered entry by name, or (nil, false) if not found.

func (*Registry) ResumeChild

func (r *Registry) ResumeChild(
	ctx context.Context,
	opts runtime.RunnerOptions,
	childID event.LoopID,
	definitionName string,
) runtime.ChildResult

ResumeChild implements runtime.ChildSpawner by resuming an existing child loop.

func (*Registry) SpawnChild

func (r *Registry) SpawnChild(
	ctx context.Context,
	opts runtime.RunnerOptions,
	parentID event.LoopID,
	depth int,
	childBudget budget.Budget,
	childGrant policy.Grant,
	definitionName string,
	request []byte,
) runtime.ChildResult

SpawnChild implements runtime.ChildSpawner by looking up the definition name in the registry and delegating to the registered factory.

type RegistryEntry

type RegistryEntry struct {
	Name string
	// SpawnChild creates and executes a child loop, returning the child's outcome.
	SpawnChild func(
		ctx context.Context,
		opts runtime.RunnerOptions,
		parentID event.LoopID,
		depth int,
		childBudget budget.Budget,
		childGrant policy.Grant,
		request []byte,
	) ChildResult
	// ResumeChild resumes an existing unfinished child loop.
	ResumeChild func(
		ctx context.Context,
		opts runtime.RunnerOptions,
		childID event.LoopID,
	) ChildResult
}

RegistryEntry holds a definition factory that is closed over the generic types at Register[S,R] time. The runtime uses it to spawn and resume children.

type ReplayDivergenceError

type ReplayDivergenceError = runtime.ReplayDivergenceError

ReplayDivergenceError is returned by Replay when the chain hash doesn't verify.

type Runner

type Runner[S, R any] struct {
	// contains filtered or unexported fields
}

Runner is the generic loop runner. S is the state type, R is the result type. Create one with New(), then call Start, Resume, or Replay.

func New

func New[S, R any](def loop.Definition[S, R], opts ...Option) *Runner[S, R]

New creates a new Runner for the given loop definition. Options configure the store, provider, tools, clock, and other settings.

func (*Runner[S, R]) Replay

func (r *Runner[S, R]) Replay(ctx context.Context, id event.LoopID) (Trajectory[S], error)

Replay strictly replays the event log for the given loop. Returns an error if the chain hash doesn't verify (data corruption or non-deterministic transition).

func (*Runner[S, R]) Resume

func (r *Runner[S, R]) Resume(ctx context.Context, id event.LoopID) (outcome.Outcome[R], error)

Resume continues a previously interrupted or crashed loop. id is the LoopID returned in the Interrupted outcome's Token field, or the LoopID of any loop that can be resumed.

func (*Runner[S, R]) Start

func (r *Runner[S, R]) Start(ctx context.Context, input any) (outcome.Outcome[R], error)

Start begins a new loop execution with the given input. Returns the terminal outcome when the loop finishes.

type Trajectory

type Trajectory[S any] = runtime.Trajectory[S]

Trajectory is the result of a Replay operation.

Directories

Path Synopsis
adapters
interop/a2a
Package a2a provides an A2A (Agent-to-Agent) protocol adapter for LoopKit.
Package a2a provides an A2A (Agent-to-Agent) protocol adapter for LoopKit.
provider/anthropic
Package anthropic provides a ModelProvider adapter for the Anthropic Messages API.
Package anthropic provides a ModelProvider adapter for the Anthropic Messages API.
provider/azure
Package azure provides a ModelProvider adapter for Azure OpenAI.
Package azure provides a ModelProvider adapter for Azure OpenAI.
provider/bedrock
Package bedrock provides a ModelProvider adapter for AWS Bedrock Converse API.
Package bedrock provides a ModelProvider adapter for AWS Bedrock Converse API.
provider/gemini
Package gemini provides a ModelProvider adapter for Google's Gemini API.
Package gemini provides a ModelProvider adapter for Google's Gemini API.
provider/internal/httpx
Package httpx provides HTTP utilities for model providers.
Package httpx provides HTTP utilities for model providers.
provider/openaicompat
Package openaicompat provides a ModelProvider adapter for OpenAI-compatible APIs.
Package openaicompat provides a ModelProvider adapter for OpenAI-compatible APIs.
sandbox/inproc
Package inproc implements the TierInProc sandbox (trusted, direct execution).
Package inproc implements the TierInProc sandbox (trusted, direct execution).
sandbox/wasm
Package wasm implements the TierWASM sandbox using wazero (WASI preview1).
Package wasm implements the TierWASM sandbox using wazero (WASI preview1).
store/memory
Package memory provides an in-memory EventStore implementation.
Package memory provides an in-memory EventStore implementation.
store/sqlite
Package sqlite provides a durable SQLite-backed EventStore implementation.
Package sqlite provides a durable SQLite-backed EventStore implementation.
telemetry/otel
Package otel provides an OpenTelemetry adapter for the LoopKit Telemetry port.
Package otel provides an OpenTelemetry adapter for the LoopKit Telemetry port.
tool/local
Package local provides a ToolExecutor that executes Go functions as tools.
Package local provides a ToolExecutor that executes Go functions as tools.
tool/mcp
Package mcp provides a ToolExecutor that connects to an MCP server and exposes its tools.
Package mcp provides a ToolExecutor that connects to an MCP server and exposes its tools.
cmd
loopctl command
Command loopctl is the CLI for LoopKit.
Command loopctl is the CLI for LoopKit.
core
action
Package action defines the sealed action types that a transition may return.
Package action defines the sealed action types that a transition may return.
budget
Package budget defines resource tracking types for loop execution.
Package budget defines resource tracking types for loop execution.
converge
Package converge implements convergence monitoring for agent loops.
Package converge implements convergence monitoring for agent loops.
ctxmgr
Package ctxmgr provides context management strategies for long-running loops.
Package ctxmgr provides context management strategies for long-running loops.
event
Package event defines the core event types for the LoopKit event-sourced loop kernel.
Package event defines the core event types for the LoopKit event-sourced loop kernel.
loop
Package loop defines the core loop aggregate types.
Package loop defines the core loop aggregate types.
loop/outcome
Package outcome defines the sealed set of terminal outcomes for a loop.
Package outcome defines the sealed set of terminal outcomes for a loop.
message
Package message provides typed agent-to-agent message contracts for LoopKit.
Package message provides typed agent-to-agent message contracts for LoopKit.
model
Package model defines LoopKit's provider-agnostic model domain types.
Package model defines LoopKit's provider-agnostic model domain types.
policy
glob.go implements capability pattern matching.
glob.go implements capability pattern matching.
examples
budgeted-agent command
Command budgeted-agent demonstrates LoopKit's budget enforcement.
Command budgeted-agent demonstrates LoopKit's budget enforcement.
migration-agent command
Command migration-agent demonstrates snapshots, context compaction, and convergence guard in a database migration workflow.
Command migration-agent demonstrates snapshots, context compaction, and convergence guard in a database migration workflow.
minimal command
Command minimal demonstrates the LoopKit kernel with a simple counting loop.
Command minimal demonstrates the LoopKit kernel with a simple counting loop.
multi-provider command
Command multi-provider demonstrates the LoopKit kernel running the same loop against two different provider backends (OpenAI-compatible and Anthropic) via httptest fake servers.
Command multi-provider demonstrates the LoopKit kernel running the same loop against two different provider backends (OpenAI-compatible and Anthropic) via httptest fake servers.
readme_example command
Command readme_example is the 60-second example from the LoopKit README.
Command readme_example is the 60-second example from the LoopKit README.
regression-gate
Package regressiongate demonstrates LoopKit's quality engine: fixture replay assertions and eval suite with regression gate.
Package regressiongate demonstrates LoopKit's quality engine: fixture replay assertions and eval suite with regression gate.
research-agent command
Command research-agent is the LoopKit flagship durability demo.
Command research-agent is the LoopKit flagship durability demo.
supervisor command
Command supervisor demonstrates LoopKit's multi-agent spawn capability.
Command supervisor demonstrates LoopKit's multi-agent spawn capability.
support-triage command
Command support-triage demonstrates budget enforcement and AskHuman escalation.
Command support-triage demonstrates budget enforcement and AskHuman escalation.
internal
inspector
Package inspector provides the read-only HTTP inspector UI for LoopKit.
Package inspector provides the read-only HTTP inspector UI for LoopKit.
testsupport
Package testsupport provides shared helpers for subprocess-based tests.
Package testsupport provides shared helpers for subprocess-based tests.
ulid
Package ulid provides a hand-rolled ULID generator.
Package ulid provides a hand-rolled ULID generator.
Package looptest provides go test helpers for replaying .loopfix fixtures against loop definitions and asserting outcomes, spend, iterations, and state.
Package looptest provides go test helpers for replaying .loopfix fixtures against loop definitions and asserting outcomes, spend, iterations, and state.
Package providertest provides a test contract kit for model provider implementations.
Package providertest provides a test contract kit for model provider implementations.
quality
diff
Package diff provides trajectory comparison for LoopKit.
Package diff provides trajectory comparison for LoopKit.
eval
Package eval provides an evaluation harness for LoopKit loops.
Package eval provides an evaluation harness for LoopKit loops.
fixture
Package fixture provides read/write support for .loopfix fixture files.
Package fixture provides read/write support for .loopfix fixture files.
Package runtime defines the application-layer ports for the loop kernel.
Package runtime defines the application-layer ports for the loop kernel.
crashchild command
Command crashchild is the test child process for the crash-proof integration test.
Command crashchild is the test child process for the crash-proof integration test.
Package sandboxtest provides a contract test suite for Sandbox implementations.
Package sandboxtest provides a contract test suite for Sandbox implementations.
Package storetest provides a published contract test suite for EventStore implementations.
Package storetest provides a published contract test suite for EventStore implementations.

Jump to

Keyboard shortcuts

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