Skip to content

Repository files navigation

rig

Minimal agent loop for OpenAI-compatible self-hosted models. A hardened client, a bounded tool-call loop, and a skills runner — a loop and a toolbox, not a framework.

Built for and extracted from kb. Designed to survive self-hosted reasoning models (Qwen, DeepSeek-style) whose serving templates leak chain-of-thought into message content.

Status: v0.x. The API is not stable until kb's migration onto it is complete.

Install

go get github.com/RandomCodeSpace/rig

Go 1.25+. Single third-party dependency: openai-go v3.

What it does

  • Hardened client — explicit-value construction only (ambient OPENAI_* environment values never reach the wire), a 1 MiB cap on decompressed response bodies, opaque error mapping that never leaks the endpoint URL or key, and a tool-calling probe that distinguishes "no tool support" from "reply truncated".
  • Bounded loopClient.Run sends, executes tool calls sequentially, feeds results back, and repeats. Iteration cap (default 8, hard cap 32), per-reply tool-call cap, explicit token budget required on every call, truncated replies are errors. Tool call ids and echoed assistant turns are normalized before anything reaches the next request.
  • Reasoning toleranceStripReasoning and DecodeJSONObject handle inline <think> blocks, templates that consume the opening tag, replies that die inside the reasoning, and reasoning that sketches the expected schema before the real answer. The JSON scan is cost-bounded against adversarial nested-brace payloads.
  • Skills — markdown files with name/description frontmatter, loaded from any fs.FS with override-by-name merging, advertised in the system prompt, and served to the model on demand through the built-in load_skill tool.

Example

client, err := rig.NewClient("http://127.0.0.1:11434/v1", "")
if err != nil {
    log.Fatal(err)
}

if err := client.ProbeToolCalling(ctx, "qwen3.5"); err != nil {
    log.Fatal(err) // model or backend cannot do tool calling
}

res, err := client.Run(ctx, rig.RunRequest{
    Model:     "qwen3.5",
    System:    "You are a release assistant.",
    Prompt:    "Summarize the open work.",
    MaxTokens: 4096,
    Tools: []rig.Tool{{
        Name:        "list_tasks",
        Description: "List open tasks on the board.",
        InputSchema: map[string]any{"type": "object", "properties": map[string]any{}},
        Run: func(ctx context.Context, input json.RawMessage) (string, error) {
            return listTasksJSON(ctx)
        },
    }},
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(res.Text)

What it deliberately does not do

No streaming, no memory system, no multi-agent graphs, no provider abstraction beyond OpenAI-compatible endpoints, no built-in tools with side effects. SSRF protection for the transport belongs to the caller: pass a hardened *http.Client via rig.WithHTTPClient.

Design

See DESIGN.md for the full contract: API surface, loop semantics, error taxonomy, and testing requirements.

License

MIT

About

Minimal agent loop for OpenAI-compatible self-hosted models: hardened client, tool-call loop, skills runner

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages