rig3.5.3

developing

rig is a module before it is a binary.

The binary at cmd/rig is one wiring of github.com/mrsirg97-rgb/rig/v3. Import the module and the loop, the seams, the stores and the event engine are yours — with the same ordering and failure semantics the binary has, because it is the same code.

This page is the embedder's contract: the five seams, the nine kernel options, one working example, and the freeze that keeps the surface honest.

get it

One import, no runtime

go get github.com/mrsirg97-rgb/rig/v3@latest

// the module
import (
    "github.com/mrsirg97-rgb/rig/v3"        // the kernel: rig.New
    "github.com/mrsirg97-rgb/rig/v3/core"     // the seams and the wire types
    "github.com/mrsirg97-rgb/rig/v3/loop"     // loop.Run(ctx, k)
    "github.com/mrsirg97-rgb/rig/v3/store"    // SQLite stores, migrations
)

The core is standard library only. The one store dependency is pure-Go SQLite, so an embedding binary cross-builds with CGO_ENABLED=0. The event engine is its own module — github.com/mrsirg97-rgb/evt — and it is the engine, not a directory in this tree.

the seams

Five interfaces hold the shape

A seam is one method (or one small family) with a name that says what crosses it. The loop names none of the concrete types; every dependency is resolved at the composition root.

seamshaperoleswapped with
ProviderStream(ctx, Request) (<-chan Event, error)model access, streamingWithProvider
ToolName / Description / Schema / Exec(ctx, args)a capabilityWithTools
FrontendInput(ctx) (string, error); Notify(Event)human I/O and the event sinkWithFrontend
ContextPolicyAssemble(ctx, session) ([]Message, error)context construction, per turnWithPolicy
ToolMiddlewareWrap(ToolExec) ToolExeccross-cutting tool policyWithMiddleware

Two capabilities ride the middleware by assertion, not obligation: TurnStart lets a link observe the turn boundary (the guard clears its counts there), and Guidelines lets it contribute system-prompt prose. One mechanism, not a second.

the kernel

Nine options, wired once

rig.New(opts ...Option) is the composition root as a struct. Duplicate tool or command names panic at New, not at the first call. The binary wires seven of the nine; WithCommands rides the frontend dispatchers and WithParallel stays at its default.

optionwhat it isthe binary's wiring
WithProviderone turn, streamedopenai.New(...) / anthropic.New(...) on the row's wire
WithToolsthe tool tablethe tool/* adapters plus the plugin door
WithFrontendinput pull, event notifytui, cli or oneshot
WithPolicymessage assemblycompact.New, the per-model trigger
WithCommandsthe slash-command setunwired at the root: commands dispatch at the frontend seam
WithMiddlewarethe tool-exec chaintoolset, approve, cutoff, perm, guard, paths; the graph tap inside, the decision links outside
WithPrioritya priority per tool callthe registry's predicate: a write 60, a read 50
WithParallelthe in-flight boundunset; DefaultParallel is 8
WithEnginethe event enginethe root's engine, the fleet room's queue

The kernel also names the queue's priorities and the room's member ids, so an embedder can speak the same order the binary does: PriorityInput 90, PriorityDelegate 70, PriorityWrite 60, PriorityRead 50, PriorityFleet 30, PriorityReview 10.

a worked example

A kernel, a fake provider, one turn

The whole contract in one file: a frontend that returns one line, a policy that passes the session through, a provider that answers done, and loop.Run. Swap the three fakes for the real openai, compact and tui constructors and you have the binary's turn.

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mrsirg97-rgb/evt"
    "github.com/mrsirg97-rgb/rig/v3"
    "github.com/mrsirg97-rgb/rig/v3/core"
    "github.com/mrsirg97-rgb/rig/v3/loop"
)

// the human seam: one prompt, and the events it sees
type frontend struct {
    prompt string
    events []evt.Signal
}

func (f *frontend) Input(ctx context.Context) (string, error) { return f.prompt, nil }
func (f *frontend) Notify(ev evt.Signal)                     { f.events = append(f.events, ev) }

// the context seam: hand the transcript through unchanged
type passthrough struct{}

func (passthrough) Assemble(ctx context.Context, s *core.Session) ([]core.Message, error) {
    return s.Messages, nil
}

// the model seam: every stream ends in Done or Fault
type stubProvider struct{}

func (stubProvider) Stream(ctx context.Context, req core.Request) (<-chan evt.Signal, error) {
    ch := make(chan evt.Signal, 2)
    ch <- core.TextDelta{Text: "done"}
    ch <- core.Done{StopReason: "stop"}
    close(ch)
    return ch, nil
}

func main() {
    f := &frontend{prompt: "say something"}
    k := rig.New(
        rig.WithProvider(stubProvider{}),
        rig.WithFrontend(f),
        rig.WithPolicy(passthrough{}),
    )
    if err := loop.Run(context.Background(), k); err != nil {
        log.Fatal(err)
    }
    fmt.Println("events:", len(f.events))
}

What the loop does with that: assemble the context, stream the provider, execute any tool calls through the middleware chain, feed results back, repeat — and return on Done, Fault, or context cancel. A panic in a tool is recovered as that call's error; a fault surfaces through Notify and aborts the turn, and the session survives to its last complete message.

The stores are outside the kernel on purpose. The kernel holds no store handles: todo.Create, todo.Complete and their kin are the board's verbs, and the worker's loop.Run owns the turn. An embedding service wires both, and the worker cannot outlive the process.

the freeze

core/ and loop/ are closed to modification

The frozen surface is the part you can depend on: core/ and loop/ are open to pure addition only. The gate is specs/FREEZE.txt, read by cmd/freeze in the freeze job: every path a PR touches must match a line, and a touched path matching none fails, naming the path and the file. A real change to the frozen surface needs a reopen <path> <version> line, reviewed in the PR.

pure addition

A new event type, a new seam method alongside the old, a new file in core/: lands without reopening.

a reopening

One line in specs/FREEZE.txt naming the path and the version that reopened it. The PR says why; the spec says what the surface now is.

a refactor branch

A branch carrying -refactor skips the gate; the PR names the reopening.

The structural test behind the freeze is simple: the loop never names a concrete tool, provider, policy, frontend or middleware. If an addition needs the loop to learn a name, it is a change to the surface, not an addition beside it.

extending it

One file, one registration line

a tool

Implement core.Tool in tool/<name>/<name>.go; register it in the root's WithTools and the default allow-list.tool/registry.json carries the model's words

a provider

Implement core.Provider: every stream ends in Done or Fault.provider/openai · provider/anthropic

a policy

Implement core.ContextPolicy; the compaction policy is the one the binary wires.policy/compact

a frontend

Implement core.Frontend; the TUI adds to the CLI's bytes, never changes them.frontend/tui · frontend/cli · frontend/web · frontend/oneshot

a middleware

Wrap(ToolExec) ToolExec, composed first-listed innermost; optional TurnStart and Guidelines.middleware/

a store leaf

The SQLite substrate is shared: statements, a schema version, a migration on open.store/ · store/sqlx

A Python plugin needs no Go at all: one file under ~/.rig/plugins/ is one tool, and the model's authoring lands in pending/. That is the plugins page.

the process

Spec first, tests before code

Every behavior is one sentence in specs/ with a test that holds it there; the governing spec is written and agreed before the code. Every Go package carries a PACKAGE.md that is its spec file: what it is, what it includes, how it is consumed, the gotchas. Read it before touching the package; keep it current when behavior changes.

make test      # go vet ./... then go test -race ./...
make fmt-check # gofmt -l . must be empty; CI runs both

# the PR gates CI runs beyond those:
go run ./cmd/freeze -branch "$BRANCH"   # every touched path matches FREEZE.txt
scripts/wire-check                    # the request bodies, merge-base vs head
python3 scripts/readme-measured check # the measured block did not drift

The house patterns are worth reading before a first PR: interface first (the struct is unexported, the constructor returns the interface), one-method seams, a thing is whole when it is constructed, sorted output, one shape per concept, and no comments in Go — the code documents itself and the PACKAGE.md carries the English. CONTRIBUTING.md is the process; AGENTS.md is the map.