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.
go get github.com/RandomCodeSpace/rig
Go 1.25+. Single third-party dependency: openai-go v3.
- 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 loop —
Client.Runsends, 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 tolerance —
StripReasoningandDecodeJSONObjecthandle 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/descriptionfrontmatter, loaded from anyfs.FSwith override-by-name merging, advertised in the system prompt, and served to the model on demand through the built-inload_skilltool.
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)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.
See DESIGN.md for the full contract: API surface, loop semantics, error taxonomy, and testing requirements.
MIT