loom

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 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.

This is more than an engineering aesthetic. A kernel you can read is a kernel you can govern. Because an agent in Loom is a graph of explicit steps over an inspectable State — not a loose prompt loop — its execution is deterministic and replayable. And you can only safely hold someone accountable for what you can foresee and reconstruct. That property is the precondition for putting an agent to real work under human oversight: a steady, legible hand is the thing a governance layer can actually hold onto. Weave is that layer — it builds the write-approval gate, the audit trail, and the addressable runtime on top of this kernel. Loom makes the hand steady; Weave decides which of its acts may reach the world.

The control flow is data, not a guess

Most agent frameworks run a prompt loop: the model re-decides, every turn, what to do next. Flexible — but you can't foresee the path and you can't replay it; the same input can take two different routes.

In Loom the control flow is a Graph — an explicit structure of steps and routers, fixed before the run, not improvised during it. The graph owns the how (what runs, in what order); the prompt only supplies the what (the content, and the criteria for "good"). That single separation is what makes a Loom agent deterministic and replayable: same graph, same state, same path — with every step checkpointed, so you can reconstruct exactly what happened.

And because the flow is a structure, not a script, it is also data. A graph can be written in Go, or compiled from an agent spec that carries no code (cmd/loom does exactly this). Tools can then treat an agent as a versioned, diffable, portable document — while its execution stays exactly as predictable. That is the line between an agent you can put to supervised, consequential work and a chatbot that improvises: when a wrong turn can't be undone, you want the path decided, inspectable, and repeatable — you want a graph.

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

stdlib.NewSubGraphStep follows the Step contract by returning only the child's per-key JSON changes relative to the state it received. By default, changed slice values are returned as suffixes, so a parent using AppendSlice does not append the inherited prefix twice; a child that reorders or replaces that prefix fails closed. For SumInt, SumFloat, or an explicit overwrite policy, pass the parent's merge configuration with stdlib.WithParentMergeConfig(parentMergeConfig) so the step can return the numeric difference or full replacement required by that policy. A host using a sum policy must always pass this configuration explicitly.

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

Checkpoint schema compatibility

Every newly written checkpoint carries schema_version: 1. A newer Loom binary accepts legacy checkpoints that have no version field (treated as version 0), while an older binary rejects checkpoints whose version is newer than it supports. This applies consistently to Resume, ResumeAt, and history reads; unsupported formats fail closed with both versions in the error.

The checkpoint envelope also reserves optional meta data for future provenance, such as the source of sensitive state keys or the policy version that produced them. Loom does not consume this field yet.

When rolling back Loom, keep the existing deployment discipline: isolate the checkpoint store from checkpoints written by the newer binary before starting the older binary. Schema rejection prevents unsafe resume; it does not make a newer checkpoint readable by older code.

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.

Tool results may optionally carry control-plane StatePatch v1 and StateOps v2. Both channels are fail-closed and opt-in: configure ToolLoopOpts.StatePatchPolicy by actual dispatched tool name, key, and validator, otherwise any patch or operation is a protocol error. Unknown or empty tool names fail closed. The __ namespace requires explicit tool-by-key authorization and a non-nil validator. output, usage, __deleted_keys, __toolloop_*, __yield*, and __resumed_tool_results are always reserved. A nil validator for a non-__ key is an explicit choice to accept any JSON value. ValidateWithState optionally receives the old and proposed new values without changing the v1 validator signature.

StatePatch v1 replaces each authorized key as a whole: it does not deep-merge, increment, or delete. A nil value means JSON null, not deletion. StateOps v2 carries an ordered array of typed operations: replace performs the same whole-value replacement, debit subtracts a JSON number from the old JSON number (or zero when absent), delete removes the key, and cas replaces only when the current value deeply equals expect. A failed CAS is a protocol error for the entire step. When one result carries both versions, its patch is applied first and its ops then run in array order, so multiple operations on one key observe preceding changes.

Valid patches and ops are staged until the ToolLoop finishes naturally with no tool calls and are then returned in the step delta for the engine to merge; forced completion after tool-budget exhaustion or cycle detection discards them. Park checkpoints preserve the staged results, and Resume revalidates them through the same policy before eventual commit. State changes are never copied into tool messages. Future operation types extend the type enumeration; they do not reinterpret or change the existing fields or v1 map semantics.

The commit is atomic only for the state delta; it does not roll back external side effects already performed by tools. An invalid or malicious state change fails the entire ToolLoop step. An ordinary IsError result without state changes does not prevent a valid sibling result from being staged.

A tool result may also set stop_loop: true to commit-and-stop. This is valid only when that same result carries a StatePatch or StateOps, the state change passes the tool-by-key StatePatchPolicy, and the result is neither IsError nor Park; a bare or otherwise invalid stop is a protocol error. The entire tool-call batch still runs and every patch is validated and staged before stop takes effect. If any sibling result parks, park takes priority: the stop-bearing result and accumulated usage are checkpointed with the staged patches, and a completed Resume commits and stops immediately when the saved stop is still valid.

Commit-and-stop is a normal completion, so it commits staged patches even when it occurs on the final iteration or at the cycle limit. Unlike natural completion, it makes no final LLM call; unlike forced completion after tool-budget exhaustion or cycle detection, it does not discard staged patches. The step output is the last assistant message's content (which may be empty), and usage is the cumulative usage of LLM calls already made. Because there is no final LLM text, the host is responsible for the final presentation.

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

View Source
const CurrentCheckpointSchema = 1

CurrentCheckpointSchema is the newest checkpoint format this binary can read.

Variables

View Source
var (
	ErrMaxIterations      = errors.New("loom: max iterations exceeded")           // 单图迭代熔断:本图步数超过 maxIter 上限(防路由环路死循环)
	ErrBudgetExhausted    = errors.New("loom: global step budget exhausted")      // 全局预算耗尽:父图与所有子图共享的步数预算被扣完
	ErrStepNotFound       = errors.New("loom: step not found")                    // 拓扑错误:路由到了图中未注册的步骤名
	ErrCheckpointNotFound = errors.New("loom: checkpoint not found")              // 恢复失败:按 runID 找不到检查点(从未运行或已被清理)
	ErrCorruptCheckpoint  = errors.New("loom: corrupt checkpoint")                // 恢复失败:检查点存在但 JSON 反序列化失败(数据损坏/版本不兼容)
	ErrNestedTx           = errors.New("loom: nested transactions not supported") // 协议约束:Store.Tx 内再开事务必须返回此错误,禁止静默的伪嵌套
)

包级哨兵错误:内核所有可预期的失败模式收敛为这几种固定值, 供调用方(含 Store 实现方)做程序化分支处理。

Functions

This section is empty.

Types

type CheckpointInfo added in v0.4.0

type CheckpointInfo struct {
	Seq        int64     `json:"seq"`                   // 步序号:History 按此对应的零填充键升序返回
	LastStep   string    `json:"last_step"`             // 该快照完成(或暂停于)的步骤名
	YieldPhase string    `json:"yield_phase,omitempty"` // 暂停阶段;普通步骤完成快照为空
	SavedAt    time.Time `json:"saved_at"`              // 检查点成功写入 latest 前生成的时间戳
}

CheckpointInfo is the public metadata view of one historical checkpoint. CheckpointInfo 是单条历史检查点的公开元数据视图:不暴露完整 State,供列表与审计使用。

type CheckpointPolicy

type CheckpointPolicy int

CheckpointPolicy controls how checkpoint failures are handled. CheckpointPolicy(检查点策略)决定检查点落盘失败时引擎的取舍: 是“尽力而为、失败只告警继续跑”,还是“必须落盘、失败即中止”。

const (
	// CheckpointBestEffort logs checkpoint failures but does not abort.
	// 尽力而为策略(默认):检查点写失败仅记 Warn 日志、执行继续。
	// 取舍:优先保证运行可用性,代价是失败后若进程崩溃将无法从最新进度恢复。
	CheckpointBestEffort CheckpointPolicy = iota
	// CheckpointRequired aborts the graph if checkpoint fails.
	// 强制落盘策略:检查点写失败立即中止整个图。
	// 取舍:优先保证可恢复性(每一步都有可靠快照),适合 HITL 等必须能恢复的场景。
	CheckpointRequired
)

type Edge

type Edge struct {
	// To:目标步骤名;空串 "" 表示终点,与路由器“返回空串即停机”的协议一致。
	To string `json:"to"` // target step name ("" = end)
	// Label:该边的条件标签(例如分支取值),仅供可视化展示。
	Label string `json:"label,omitempty"` // condition label
}

Edge describes a possible transition from one step to another (for topology export). Edge 描述一条可能的转移边,仅用于拓扑导出/可视化,不参与实际路由决策。

type Graph

type Graph struct {
	Name string // 图名:用作检查点命名空间("checkpoint:"+Name)及日志/错误标识
	// contains filtered or unexported fields
}

Graph is an executable composition of Steps and Routers. Graph(图)是步骤与路由器的可执行组合体,是内核对外的执行入口。 一切上层能力(agent、子图、HITL)都由这一个结构承载;字段在构造期定型,运行期只读, 因此同一个 Graph 可被多个 goroutine 并发 Run(各自的状态互相独立)。

func NewGraph

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

NewGraph creates a new graph with the given entry step. NewGraph 构造一个图:设定图名与入口步骤,应用函数式选项,最后冻结合并配置。

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. AddStep 注册一个步骤及其“执行后”路由器;after 传 nil 表示该步骤是终端步骤,执行完即整图停机。

func (*Graph) History added in v0.4.0

func (g *Graph) History(ctx context.Context, store Store, runID string) ([]CheckpointInfo, error)

History lists valid historical checkpoint metadata for a run in ascending step order. History 列出某次运行的有效历史元数据:依赖 Store.List 的字典序契约与零填充键,天然按步升序返回。 单条键读取失败或 JSON 损坏只记 Warn 并跳过,不能让局部坏档阻断其余历史的审计与恢复。

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. Resume 从检查点恢复一次已暂停(HITL)的运行: 读检查点 → 把人工提供的 input 增量合并进快照状态 → 按 yield_phase 决定重入方式 (mid_step=重跑暂停的那一步;after_step=跳过该步直接走它的路由)。 兼容差异:Resume 对空 phase 按 mid_step 处理,因为 v1.0 latest 表示“暂停中”的恢复点; ResumeAt 则对空 phase 按 after_step 处理,因为历史条目是步骤完成后的快照,重跑会造成双重作用。

func (*Graph) ResumeAt added in v0.4.0

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

ResumeAt forks a new run from an immutable historical checkpoint. ResumeAt 从任意历史检查点分叉出全新运行:读取源快照但只向新 UUID 写检查点,源存档树绝不改写。 与 Resume 的 v1.0 兼容语义不同,空 yield phase 在这里按 after_step 处理;历史条目记录的是步骤完成 后的状态,若按 mid_step 重跑原步骤,外部副作用与状态增量都可能重复发生。fork 默认继承源快照 在分叉点的 __budget_remaining;宿主可在 input 中显式提供同名键,借由下方 Merge 覆盖该余额。

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. Run 从入口步骤开始执行图,直到正常完成、HITL 暂停或出错。 主循环各阶段固定顺序:单图熔断 → 全局预算 → Before 钩子 → 执行步骤 → 合并增量 → After 钩子 → 检查点 → yield(暂停)检查 → 路由决定下一跳。

func (*Graph) SetHooks

func (g *Graph) SetHooks(h HookPoints)

SetHooks attaches before/after hooks. SetHooks 挂载前置/后置钩子集合(整体替换而非追加)。

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. SetTopology 声明图的拓扑(仅供可视化);由图构建器建图完成后调用,对执行路径无任何影响。

func (*Graph) Topology

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

Topology returns the declared topology, or nil if not set. Topology 返回已声明的拓扑;未声明时返回 nil,调用方需自行判空。

type GraphOption

type GraphOption func(*Graph)

GraphOption configures a Graph at construction time. GraphOption 是构造期配置函数(函数式选项模式):只在 NewGraph 时生效,图建成后配置即定型。

func WithCheckpointHistory added in v0.4.0

func WithCheckpointHistory(keep int) GraphOption

WithCheckpointHistory configures optional per-step checkpoint history retention. WithCheckpointHistory(keep) 配置可选的按步检查点历史:0=关闭(默认,保持 latest 覆盖写); -1=全部保留;n>0=只保留最近 n 条。历史写入始终是尽力而为,不改变 checkpointPolicy。

func WithCheckpointPolicy

func WithCheckpointPolicy(p CheckpointPolicy) GraphOption

WithCheckpointPolicy 设置检查点失败策略(默认 CheckpointBestEffort)。

func WithMaxIterations

func WithMaxIterations(n int) GraphOption

WithMaxIterations 设置单图迭代上限(per-graph 熔断,默认 100),防止路由环路导致死循环。

func WithMergeConfig

func WithMergeConfig(cfg *MergeConfig) GraphOption

WithMergeConfig 指定图的合并配置;注意 NewGraph 会随即将其冻结(frozen),运行期不可再改策略。

func WithStepBudget

func WithStepBudget(n int64) GraphOption

WithStepBudget sets a global step limit shared across parent and all sub-graphs. WithStepBudget 设置全局步数预算:与 maxIter 不同,它在根图 Run 时装入 atomic 计数器、 经 context 传递给所有子图共享,是第二层熔断——即使每个子图各自不超 maxIter, 全体累计步数也不能超出此预算。只应在根图上设置。

type HookPoints

type HookPoints struct {
	Before []StepHook // 前置钩子:在步骤执行前运行,任一报错则该步骤不会被执行
	After  []StepHook // 后置钩子:在步骤增量合并进状态之后运行,可观察到合并后的完整状态
}

HookPoints holds before/after hooks for graph execution. HookPoints 汇集图执行的前置/后置钩子,各自按注册顺序依次执行。

type MemStore

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

MemStore is an in-memory Store implementation for testing. MemStore 是内存版 Store 实现,仅供测试使用:不落盘、进程退出即失。 并发安全手段:一把 sync.RWMutex 保护两级 map——读操作持读锁可并行,写操作持写锁互斥。

func NewMemStore

func NewMemStore() *MemStore

NewMemStore creates a new empty in-memory store. NewMemStore 创建一个空的内存存储。

func (*MemStore) Delete

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

Delete 并发安全删除:取写锁后委托给无锁内核。

func (*MemStore) Get

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

Get 并发安全读取:取读锁后委托给无锁内核。

func (*MemStore) List

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

List 并发安全列举:取读锁后委托给无锁内核。

func (*MemStore) Put

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

Put 并发安全写入:取写锁后委托给无锁内核。

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. Tx 用“整库深拷贝 + 整体替换”模拟事务:持写锁期间在快照副本上执行 fn, fn 成功则把副本整体换入(提交),失败则丢弃副本(回滚),原数据全程不被触碰。 代价是 O(全库) 的拷贝且事务期间阻塞所有其他操作——仅测试场景可接受。

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. MergeConfig(合并配置)保存“键 → 合并策略”的映射及兜底策略。 生命周期协议:构造期可随意 Register;一旦挂到 Graph 上即被冻结—— 原因是配置可能被多个图共享,且运行中改策略会让同一图内不同时刻的合并语义不一致, 破坏检查点的可重放性。

func DefaultMergeConfig

func DefaultMergeConfig() *MergeConfig

DefaultMergeConfig returns stdlib's recommended defaults. DefaultMergeConfig 返回 stdlib 推荐默认值:messages 键用 AppendSlice 累积会话历史,其余键覆盖。

func NewMergeConfig

func NewMergeConfig() *MergeConfig

NewMergeConfig 创建一份空白配置:无按键策略、兜底为 Overwrite。

func (*MergeConfig) Register

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

Register 为指定键注册合并策略;只允许在配置尚未挂到 Graph(未冻结)时调用。

type MergePolicy

type MergePolicy func(existing, incoming any) any

MergePolicy defines how a specific key is merged. MergePolicy(合并策略)定义单个键的合并规则:输入旧值与新值,返回合并结果。 策略只关心“怎么合”,“何时合”由引擎在每个步骤之后统一驱动。

var (
	// Overwrite:新值直接覆盖旧值,是最朴素也是默认的合并语义。
	Overwrite MergePolicy = func(_, incoming any) any { return incoming }

	// AppendSlice:把新切片追加到旧切片之后,典型用于 messages 这类只增不减的会话历史。
	AppendSlice MergePolicy = func(existing, incoming any) any {
		e, _ := existing.([]any)
		i, _ := incoming.([]any)
		return append(e, i...)
	}

	// SumInt:整型累加,适合进程内计数器类键(如重试次数)。
	SumInt MergePolicy = func(existing, incoming any) any {
		e, _ := existing.(int)
		i, _ := incoming.(int)
		return e + i
	}

	// SumFloat:浮点累加,适合成本、token 用量等度量;JSON 反序列化的数字默认就是 float64,可安全跨检查点累加。
	SumFloat MergePolicy = func(existing, incoming any) any {
		e, _ := existing.(float64)
		i, _ := incoming.(float64)
		return e + i
	}
)

Built-in policies. 内置合并策略:覆盖最常见的四种合并语义。 各策略均使用宽容的类型断言(断言失败取零值),保证 JSON 往返后的类型漂移不会引发 panic。

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. Router(路由器)在每个步骤之后决定下一跳:输入当前状态,返回下一个步骤名。 核心协议:返回空字符串 "" 表示停机(图正常结束)。 设计立场:路由是确定性代码而非模型决策——LLM 只产出状态内容,走哪条边由代码裁决, 因此控制流可测试、可回放、与模型输出解耦。

func Always

func Always(next string) Router

Always returns the same next step. Always 构造无条件路由器:无论状态如何都固定跳到 next,用于线性流水线的顺序衔接。

func Branch

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

Branch routes based on a state key's string value. Branch 按状态中 key 对应值的字符串形式查表路由:命中 routes 走对应步骤,未命中走 fallback。

func BranchFunc

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

BranchFunc routes based on a user-supplied key extractor. BranchFunc 是 Branch 的泛化版本:路由键由调用方提供的提取函数计算, 适合键值需要组合多个状态字段或额外加工的场景。

func Condition

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

Condition routes based on a predicate. Condition 构造二元条件路由器:谓词为真走 ifTrue、为假走 ifFalse,即最简单的 if/else 分支。

func End

func End() Router

End always halts the graph. End 构造终止路由器:恒定返回空字符串,按协议表示停机(图正常完成)。

type RunResult

type RunResult struct {
	State      State      // 停机时的完整状态快照(可能含 __error 等引擎协议键)
	LastStep   string     // 最后执行(或试图执行)的步骤名,Resume 与排障都依赖它定位
	Yielded    bool       // 是否因 HITL 暂停而停:true 时调用方应保留 RunID 以便后续 Resume
	Steps      int        // 本次调用实际执行的步骤数(不含 Resume 之前的历史步数)
	RunID      string     // 运行标识(__run_id):同一次业务运行跨 Run/Resume 保持不变,是检查点的主键
	StopReason StopReason // 停机原因分类
}

RunResult contains the final state and metadata of a graph execution. RunResult 是一次图执行的结果:停机时的完整状态 + 停机元数据。

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. State(状态)是贯穿所有步骤的共享执行上下文:键为字符串、值为任意可 JSON 序列化类型的 map。 约束:值必须能过 JSON 往返(检查点依赖 Marshal),塞入不可序列化的值会在落盘时报错; 双下划线前缀键(__run_id、__yield、__yield_phase、__error、__failed_step)为引擎协议键,业务侧不应占用。

func UnmarshalState

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

UnmarshalState 从 JSON 字节串还原状态;注意 JSON 往返后数值统一变为 float64、数组变为 []any。

func (State) Marshal

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

Marshal 把状态序列化为 JSON 字节串,是检查点落盘所用的编码格式。

func (State) Merge

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

Merge applies the update to the current state using registered policies. Merge 把步骤返回的增量 update 按策略并入当前状态,并且每次都返回一个全新 map、绝不原地修改。 这是内核的核心不变量:步骤只产出增量、引擎负责合并,旧状态保持不可变—— 由此每个检查点都是自洽快照,恢复重放与并发读取都不会被后续修改污染。

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. Step(步骤)是引擎的最小执行单元:输入当前状态(State),返回“状态增量”而非完整状态。 核心协议:步骤只产出增量,合并由引擎统一调用 State.Merge 完成——这保证每个检查点都是 合并后的自洽快照,也让恢复(resume)时重跑步骤是安全的。 返回非 nil error 会中止整个图:引擎会把错误现场写入 __error / __failed_step 协议键, 并尽力落一次检查点保存现场。

type StepHook

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

StepHook runs before or after each step execution. StepHook(步骤钩子)在每个步骤执行前/后被调用,承载日志、鉴权、指标等横切逻辑; 返回非 nil error 会以 StopHookAbort 中止整个图——钩子对执行拥有否决权。

type StepInfo

type StepInfo struct {
	Name string `json:"name"` // 步骤名
	// Detail:人类可读的补充说明(如该步骤使用的模型或工具)。
	Detail string `json:"detail,omitempty"` // human-readable annotation
	Edges  []Edge `json:"edges"`            // 出边列表:该步骤可能转移到的目标
}

StepInfo describes a step and its outgoing edges for topology export. StepInfo 描述一个步骤及其全部出边,仅用于拓扑导出/可视化。

type StopReason

type StopReason string

StopReason classifies why a graph execution ended. StopReason(停机原因)对图执行的终止方式做枚举分类,供调用方在结果上做程序化分支。

const (
	// StopCompleted:正常结束——路由器返回空字符串表示停机。
	StopCompleted StopReason = "completed" // normal termination (router returned "")
	// StopYielded:HITL 暂停——步骤置 __yield 主动让出控制权,等待人工介入后 Resume。
	StopYielded StopReason = "yielded" // HITL pause (__yield)
	// StopMaxIter:单图迭代熔断——本图步数超过 maxIter 上限。
	StopMaxIter StopReason = "max_iter" // per-graph circuit breaker
	// StopBudget:全局预算耗尽——父图与所有子图共享的步数预算被扣完。
	StopBudget StopReason = "budget" // global step budget exhausted
	// StopError:步骤返回非 nil error(错误现场已写入 __error / __failed_step)。
	StopError StopReason = "error" // Step returned non-nil error
	// StopHookAbort:前置/后置钩子返回 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)       // 读取 ns 下 key 的值;协议约定键不存在必须返回错误(而非 nil, nil)
	Put(ctx context.Context, ns string, key string, value []byte) error   // 写入或覆盖 ns 下 key 的值;实现方应保证写入的持久性
	Delete(ctx context.Context, ns string, key string) error              // 删除 ns 下的 key;删除不存在的键视为成功(幂等语义)
	List(ctx context.Context, ns string, prefix string) ([]string, error) // 列出 ns 下以 prefix 开头的全部键,约定按字典序返回(结果确定可复现)

	// 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 在数据库事务内执行 fn:传给 fn 的 Store 已绑定到该事务,
	// fn 返回 nil 则整体提交、返回 error 则整体回滚——调用方以此获得多次读写的原子性。
	// 协议约束:不支持嵌套事务,在事务内再调 Tx 必须返回 ErrNestedTx,实现方不得静默忽略。
	Tx(ctx context.Context, fn func(Store) error) error
}

Store provides durable key-value storage with namespace isolation. Store(存储)是引擎的持久化抽象:带命名空间(ns)隔离的 KV 接口。 引擎用它落检查点(命名空间固定为 "checkpoint:<图名>"),stdlib 用它存记忆等长期数据; 命名空间隔离保证不同用途的数据互不污染。实现方(SQLite/Postgres/内存)只需满足此接口。

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).
contract 包是 LOOM 内核与 LLM 世界之间唯一的纯接口桥: 内核五原语(State/Step/Router/Store/Graph)本身完全不知道 LLM 的存在, 所有与模型提供商相关的能力都被抽象成本包中提供商无关的接口与类型。
contract 包是 LOOM 内核与 LLM 世界之间唯一的纯接口桥: 内核五原语(State/Step/Router/Store/Graph)本身完全不知道 LLM 的存在, 所有与模型提供商相关的能力都被抽象成本包中提供商无关的接口与类型。
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.
Skill activation mechanism S1 moves selection from the matcher to the model.
Skill activation mechanism S1 moves selection from the matcher to the model.
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