loom

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 11 Imported by: 0

README

English | 中文


A loom has only three moving parts — warp, weft, shuttle — yet it can weave any pattern.

Loom works the same way. The entire kernel is ~700 lines of Go and 5 type definitions. Combined, they express everything from a single chatbot to a hundred-agent orchestration.

type State  map[string]any                                           // data
type Step   func(ctx context.Context, state State) (State, error)    // compute
type Router func(ctx context.Context, state State) (string, error)   // control flow
type Store  interface { Get; Put; Delete; List; Tx }                 // persistence
type Graph  struct { steps; routers; Run(); Resume() }               // orchestration

No Agent class. No Chain abstraction. No Memory base type.

Every advanced feature is composed from these five primitives — not inherited from a framework.

Why Loom

There is no shortage of agent frameworks. What's missing is one you can actually own.

Most frameworks are feature-complete — tens of thousands of lines, rich abstraction layers, batteries included. But when you need to change their behavior, understand their internals, or embed them into your own system, you find yourself wrestling a giant.

Loom's design principle is the inverse: the kernel is small enough to read in an afternoon. Not because it does less, but because a mature, complex system must have a lean core. Complexity should emerge from composition, not be pre-baked into the framework.

30-Second Quickstart

package main

import (
    "context"
    "fmt"
    "github.com/jinyitao123/loom"
)

func main() {
    greet := func(_ context.Context, s loom.State) (loom.State, error) {
        return loom.State{"output": "Hello, " + s["name"].(string) + "!"}, nil
    }

    g := loom.NewGraph("greeter", "greet")
    g.AddStep("greet", greet, loom.End())

    result, _ := g.Run(context.Background(), loom.State{"name": "World"}, nil)
    fmt.Println(result.State["output"]) // Hello, World!
}
go get github.com/jinyitao123/loom

What Five Primitives Can Do

Tool-calling Agent

Three Steps, wired together.

g := loom.NewGraph("agent", "guard")
g.AddStep("guard", guardStep, Always("chat"))
g.AddStep("chat",  toolLoop,  End())

Pause for Human Approval

A Step returns __yield: true and the Graph freezes state automatically. After approval, Resume() picks up right where it left off.

result, _ := g.Run(ctx, input, store)
// result.StopReason == "yielded"

// After human approval
result, _ = g.Resume(ctx, result.RunID,
    State{"approved": true}, store)

10 Agents Collaborating

Each agent is a Graph, nested inside a parent Graph. Shared checkpoints, shared step budget.

parent := loom.NewGraph("orchestrator", "dispatch",
    WithStepBudget(500))

parent.AddStep("dispatch", router, Branch(...))
parent.AddStep("analyst", SubGraphStep(analystGraph), ...)
parent.AddStep("coder",   SubGraphStep(coderGraph),   ...)

Process Crashed?

Nothing to do. Every step auto-checkpoints to PostgreSQL. After restart, Resume() continues from the last checkpoint. Not a single step lost.

// Before crash: A → B → C ✓ → [crash]
// After restart:
result, _ = g.Resume(ctx, runID, State{}, pgStore)
// Continues from D, C's state fully preserved

What the Kernel Deliberately Doesn't Know

This is Loom's most important design decision.

The kernel doesn't know So you can
What an LLM is Use OpenAI, Claude, DeepSeek, local models — swap freely inside a Step
What MCP is Plug in any tool protocol — MCP / A2A / custom RPC
How to store memory RAG, graph DB, full-text search — what goes in State is your call
How to serve HTTP Gin, Echo, net/http — Loom is a library, not a service

The kernel does one thing: execute Steps in the order defined by the Graph, checkpoint along the way, pause on yield.

Everything else is your domain. That's freedom, not omission.

Architecture

┌──────────────────────────────────────────────────┐
│  Layer 3 · Your App                              │  ← HTTP / Auth / Multi-tenancy / Your business
├──────────────────────────────────────────────────┤
│  Layer 2 · Stdlib                    ~1500 LOC   │  ← Building blocks: ToolLoop / Guard / Handoff
├──────────────────────────────────────────────────┤
│  Layer 1 · Contract                   ~150 LOC   │  ← Pure interfaces: LLM / ToolDispatcher / Embedder
├──────────────────────────────────────────────────┤
│  Layer 0 · Kernel                     ~700 LOC   │  ← Five primitives. That's it.
└──────────────────────────────────────────────────┘

Dependency rule: Layer N may only import Layer N-1 or below. No exceptions.

Stdlib

Every component in the standard library is a composition of Steps or Routers. No new primitives, no special channels.

// ToolLoop: LLM call → tool execution → result → loop until done
chat := stdlib.NewToolLoopStep(llm, tools, stdlib.ToolLoopOpts{
    MaxIterations: 20,
    Compaction:    &compactionPolicy,
    ToolHooks:     []contract.ToolHook{auditHook},
})

// Declarative tool permissions, three levels: deny → ask → allow
safeTool := stdlib.NewPermissionDispatcherWithAsk(tools,
    []string{"rm_rf", "drop_table"},   // deny: always blocked
    []string{"send_email"},            // ask: executed with a user-confirmation hint
    []string{"read_*", "search_*"},    // allow: whitelist
)

// Auto-stop at $5
g.SetHooks(loom.HookPoints{
    After: []loom.StepHook{stdlib.CostBudgetHook(5.00)},
})

Read-only tools run in parallel automatically; stateful tools run serially. ToolLoop reads ToolDef.ReadOnly to decide.

The loom CLI

Loom is a library first — but the repo also ships loom, a standalone agent engine built on that library. It is the weave daemon's spawn-harness backend: prompt JSON on stdin, one agent turn, NDJSON events on stdout — with MCP tool servers, session resume, semantic memory, and deterministic sub-agent orchestration compiled from an agent spec.

# from a GitHub Release (linux / macOS):
curl -fsSL https://raw.githubusercontent.com/jinyitao123/loom/main/install.sh | sh

# or with Go (any platform):
go install github.com/jinyitao123/loom/cmd/loom@latest

See cmd/loom/README.md for usage and the event wire format, and docs/host-integration.md for how any host process can drive it.

Project Structure

loom/
├── graph.go          Execution engine: State × Step × Router → Run / Resume
├── state.go          Typed map with registrable merge policies
├── step.go           type Step func(ctx, State) (State, error)
├── router.go         Control flow: Always / Branch / Condition
├── store.go          5-method persistence interface
├── options.go        GraphOption: merge / checkpoint / budget
├── memstore.go       In-memory Store (for testing)
│
├── contract/         Pure interfaces: LLM / ToolDispatcher / Embedder
├── stdlib/           Pre-built Steps & Hooks
│   ├── toolloop.go   LLM ↔ Tool loop
│   ├── steps.go      Guard / HumanWait / SubGraph / Handoff
│   ├── permission.go Declarative tool permissions (deny / ask / allow)
│   ├── budget.go     Token & USD budget hooks
│   ├── prompt.go     Tiered prompt assembly
│   ├── session.go    Session history persistence
│   ├── specloader.go Agent-spec loading (identity / skills / sub-agents)
│   └── compiler/     AgentSpec → Graph: deterministic sub-agent orchestration
│
├── pgstore/          PostgreSQL Store
├── provider/         LLM Providers (OpenAI-compatible / DeepSeek)
├── cmd/loom/         The `loom` CLI — stdin JSON → agent turn → NDJSON stream
└── docs/             Host-integration contract & orchestration design

Comparison

Loom LangGraph OpenAI Agents SDK
Language Go Python Python
Kernel ~700 LOC ~15K LOC ~3K LOC
Persistence Auto checkpoint Auto checkpoint None
LLM coupling Zero Medium Strong (OpenAI-bound)
Tool protocol Any LangChain Tools function calling
Sub-graph nesting Native Native Not supported
Human-in-the-loop yield / resume interrupt Limited
Embeddable Yes (Go package) No (Python service) No (Python service)

Who Is This For

  • Long-running agents that need crash recovery
  • Enterprise workflows that need human-in-the-loop approval
  • Multi-agent orchestration without a heavyweight framework
  • Budget control (token / USD) to prevent runaway agents
  • Embedding agent capabilities in the Go ecosystem

License

MIT


A mature, complex product must have a lean, precise kernel.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrMaxIterations      = errors.New("loom: max iterations exceeded")
	ErrBudgetExhausted    = errors.New("loom: global step budget exhausted")
	ErrStepNotFound       = errors.New("loom: step not found")
	ErrCheckpointNotFound = errors.New("loom: checkpoint not found")
	ErrCorruptCheckpoint  = errors.New("loom: corrupt checkpoint")
	ErrNestedTx           = errors.New("loom: nested transactions not supported")
)

Functions

This section is empty.

Types

type CheckpointPolicy

type CheckpointPolicy int

CheckpointPolicy controls how checkpoint failures are handled.

const (
	// CheckpointBestEffort logs checkpoint failures but does not abort.
	CheckpointBestEffort CheckpointPolicy = iota
	// CheckpointRequired aborts the graph if checkpoint fails.
	CheckpointRequired
)

type Edge

type Edge struct {
	To    string `json:"to"`              // target step name ("" = end)
	Label string `json:"label,omitempty"` // condition label
}

Edge describes a possible transition from one step to another (for topology export).

type Graph

type Graph struct {
	Name string
	// contains filtered or unexported fields
}

Graph is an executable composition of Steps and Routers.

func NewGraph

func NewGraph(name string, entry string, opts ...GraphOption) *Graph

NewGraph creates a new graph with the given entry step.

func (*Graph) AddStep

func (g *Graph) AddStep(name string, step Step, after Router)

AddStep registers a step with an optional router for the "after" transition.

func (*Graph) Resume

func (g *Graph) Resume(ctx context.Context, runID string, input State, store Store) (*RunResult, error)

Resume restarts a yielded graph from its checkpoint.

func (*Graph) Run

func (g *Graph) Run(ctx context.Context, input State, store Store) (*RunResult, error)

Run executes the graph from the entry step to completion or yield.

func (*Graph) SetHooks

func (g *Graph) SetHooks(h HookPoints)

SetHooks attaches before/after hooks.

func (*Graph) SetTopology

func (g *Graph) SetTopology(topo []StepInfo)

SetTopology declares the graph's topology for visualization. Called by graph builders (CompileAgent, newMirrorGraph, etc.) after constructing the graph.

func (*Graph) Topology

func (g *Graph) Topology() []StepInfo

Topology returns the declared topology, or nil if not set.

type GraphOption

type GraphOption func(*Graph)

GraphOption configures a Graph at construction time.

func WithCheckpointPolicy

func WithCheckpointPolicy(p CheckpointPolicy) GraphOption

func WithMaxIterations

func WithMaxIterations(n int) GraphOption

func WithMergeConfig

func WithMergeConfig(cfg *MergeConfig) GraphOption

func WithStepBudget

func WithStepBudget(n int64) GraphOption

WithStepBudget sets a global step limit shared across parent and all sub-graphs.

type HookPoints

type HookPoints struct {
	Before []StepHook
	After  []StepHook
}

HookPoints holds before/after hooks for graph execution.

type MemStore

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

MemStore is an in-memory Store implementation for testing.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore creates a new empty in-memory store.

func (*MemStore) Delete

func (s *MemStore) Delete(_ context.Context, ns, key string) error

func (*MemStore) Get

func (s *MemStore) Get(_ context.Context, ns, key string) ([]byte, error)

func (*MemStore) List

func (s *MemStore) List(_ context.Context, ns, prefix string) ([]string, error)

func (*MemStore) Put

func (s *MemStore) Put(_ context.Context, ns, key string, value []byte) error

func (*MemStore) Tx

func (s *MemStore) Tx(_ context.Context, fn func(Store) error) error

Tx runs a function inside a simulated transaction using copy-on-write.

type MergeConfig

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

MergeConfig holds per-key merge policies. It is mutable during construction and frozen once passed to a Graph.

func DefaultMergeConfig

func DefaultMergeConfig() *MergeConfig

DefaultMergeConfig returns stdlib's recommended defaults.

func NewMergeConfig

func NewMergeConfig() *MergeConfig

func (*MergeConfig) Register

func (mc *MergeConfig) Register(key string, policy MergePolicy)

type MergePolicy

type MergePolicy func(existing, incoming any) any

MergePolicy defines how a specific key is merged.

var (
	Overwrite MergePolicy = func(_, incoming any) any { return incoming }

	AppendSlice MergePolicy = func(existing, incoming any) any {
		e, _ := existing.([]any)
		i, _ := incoming.([]any)
		return append(e, i...)
	}

	SumInt MergePolicy = func(existing, incoming any) any {
		e, _ := existing.(int)
		i, _ := incoming.(int)
		return e + i
	}

	SumFloat MergePolicy = func(existing, incoming any) any {
		e, _ := existing.(float64)
		i, _ := incoming.(float64)
		return e + i
	}
)

Built-in policies.

type Router

type Router func(ctx context.Context, state State) (string, error)

Router determines the next step based on current state. Returns the name of the next step, or "" to halt.

func Always

func Always(next string) Router

Always returns the same next step.

func Branch

func Branch(key string, routes map[string]string, fallback string) Router

Branch routes based on a state key's string value.

func BranchFunc

func BranchFunc(extract func(State) string, routes map[string]string, fallback string) Router

BranchFunc routes based on a user-supplied key extractor.

func Condition

func Condition(pred func(State) bool, ifTrue, ifFalse string) Router

Condition routes based on a predicate.

func End

func End() Router

End always halts the graph.

type RunResult

type RunResult struct {
	State      State
	LastStep   string
	Yielded    bool
	Steps      int
	RunID      string
	StopReason StopReason
}

RunResult contains the final state and metadata of a graph execution.

type State

type State map[string]any

State is the execution context shared across all steps. Keys are strings. Values are any JSON-serializable type.

func UnmarshalState

func UnmarshalState(b []byte) (State, error)

func (State) Marshal

func (s State) Marshal() ([]byte, error)

func (State) Merge

func (s State) Merge(update State, cfg *MergeConfig) State

Merge applies the update to the current state using registered policies.

type Step

type Step func(ctx context.Context, state State) (State, error)

Step is the atomic unit of execution. It receives the current state and returns the delta to merge. Returning a non-nil error aborts the graph.

type StepHook

type StepHook func(ctx context.Context, stepName string, state State) error

StepHook runs before or after each step execution.

type StepInfo

type StepInfo struct {
	Name   string `json:"name"`
	Detail string `json:"detail,omitempty"` // human-readable annotation
	Edges  []Edge `json:"edges"`
}

StepInfo describes a step and its outgoing edges for topology export.

type StopReason

type StopReason string

StopReason classifies why a graph execution ended.

const (
	StopCompleted StopReason = "completed"  // normal termination (router returned "")
	StopYielded   StopReason = "yielded"    // HITL pause (__yield)
	StopMaxIter   StopReason = "max_iter"   // per-graph circuit breaker
	StopBudget    StopReason = "budget"     // global step budget exhausted
	StopError     StopReason = "error"      // Step returned non-nil error
	StopHookAbort StopReason = "hook_abort" // Before/After hook returned error
)

type Store

type Store interface {
	Get(ctx context.Context, ns string, key string) ([]byte, error)
	Put(ctx context.Context, ns string, key string, value []byte) error
	Delete(ctx context.Context, ns string, key string) error
	List(ctx context.Context, ns string, prefix string) ([]string, error)

	// Tx runs a function inside a database transaction.
	// The Store passed to fn is bound to the transaction.
	// If fn returns nil, the transaction commits. If fn returns an error,
	// the transaction rolls back.
	// Nested Tx calls are NOT supported and must return an error.
	Tx(ctx context.Context, fn func(Store) error) error
}

Store provides durable key-value storage with namespace isolation.

Directories

Path Synopsis
cmd
loom command
Command loom is a thin CLI wrapper around the Loom engine that speaks the weave daemon's spawn-harness protocol: it reads a prompt JSON on stdin, runs an agent turn, and emits a stdout-json (NDJSON) event stream the daemon's LoomBackend parses (see weave src/cli/daemon/agent/loom.ts and plans/loom-cli-design.md).
Command loom is a thin CLI wrapper around the Loom engine that speaks the weave daemon's spawn-harness protocol: it reads a prompt JSON on stdin, runs an agent turn, and emits a stdout-json (NDJSON) event stream the daemon's LoomBackend parses (see weave src/cli/daemon/agent/loom.ts and plans/loom-cli-design.md).
Package pgstore implements loom.Store backed by PostgreSQL.
Package pgstore implements loom.Store backed by PostgreSQL.
provider
deepseek
Package deepseek implements the contract.LLM interface for the DeepSeek API.
Package deepseek implements the contract.LLM interface for the DeepSeek API.
openai
Package openai implements contract.LLM for any OpenAI-compatible API.
Package openai implements contract.LLM for any OpenAI-compatible API.
compiler
Package compiler turns a stdlib.AgentSpec into a runnable loom.Graph for deterministic orchestration: a parent agent routes to named sub-agents by writing a RouteKey into state, instead of relying on the model to "decide the next step" in a flat tool loop.
Package compiler turns a stdlib.AgentSpec into a runnable loom.Graph for deterministic orchestration: a parent agent routes to named sub-agents by writing a RouteKey into state, instead of relying on the model to "decide the next step" in a flat tool loop.

Jump to

Keyboard shortcuts

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