Glue is a Go framework for building agents. It gives you a reusable,
provider-agnostic agent loop, a small code-first Agent / Session
API, typed tools, pluggable model providers, and optional persistence —
so you can build anything from a one-shot CLI to a long-running,
multi-channel assistant without rewriting the loop each time.
agent := glue.NewAgent(glue.AgentOptions{
Provider: gemini.New(gemini.Options{}),
Model: "gemini-3.1-pro-preview",
Tools: []glue.Tool{weatherTool},
})
session, _ := agent.Session(ctx, "demo")
result, _ := session.Prompt(ctx, "What's the weather in Toronto?")
fmt.Println(result.Text)- New here and want to build an agent? → docs/building-agents.md
- Just want to send a prompt? → Quickstart
- Want the architecture? → Concepts · docs/design.md
- Want the direction? → Local coding + planned AX execution
go get github.com/erain/glueModule path: github.com/erain/glue. Key subpackages:
| Import | Purpose |
|---|---|
github.com/erain/glue |
Public API: Agent, Session, Tool, options. |
.../loop |
The provider-agnostic agent loop. |
.../providers/{gemini,codex,deepseek,nvidia,openrouter} |
Model providers (+ shared openaicompat core, driver registry in providers). |
.../stores/{file,sqlite} |
Session persistence (sqlite adds FTS5 search). |
.../tools/{fs,git,shell,coding,mcp} |
Reusable tool bundles. |
.../prompts |
Versioned-prompt catalog. |
.../cli |
Shared standard flags for agent binaries. |
Pick a provider and send a prompt. With Gemini:
export GEMINI_API_KEY=...package main
import (
"context"
"fmt"
"log"
"github.com/erain/glue"
"github.com/erain/glue/providers/gemini"
)
func main() {
ctx := context.Background()
agent := glue.NewAgent(glue.AgentOptions{
Provider: gemini.New(gemini.Options{}),
Model: "gemini-3.1-pro-preview",
})
session, err := agent.Session(ctx, "demo")
if err != nil {
log.Fatal(err)
}
result, err := session.Prompt(ctx, "Reply with the single word: glue.")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text)
}The session keeps an in-memory transcript, so a second Prompt
continues the conversation. Other providers are one import away — see
Providers. To go from here to a real tool-calling,
persistent agent, follow docs/building-agents.md.
Glue has a small vocabulary. Once these click, the rest is API surface.
| Type | What it is |
|---|---|
Provider |
A model backend that streams assistant events. |
Agent |
The configured unit: provider, model, tools, store, work dir, roles. Built with glue.NewAgent. |
Session |
A named conversation with its own transcript, opened from an Agent; driven with session.Prompt. |
Tool |
A function the model can call. Define with glue.NewTool[Args]. |
Store |
Where transcripts persist (stores/file or stores/sqlite). Optional. |
Skill / Role |
Markdown-driven reusable instructions and named instruction profiles. |
loop |
The engine: stream → run tools → append results → repeat until the model stops. |
Every session.Prompt runs the same loop:
prompt ─▶ provider streams events ─▶ text? emit deltas
└─▶ tool calls? run tools, append results, loop
└─▶ stop ─▶ return final text
The loop is provider-agnostic, and product concerns (sandboxing, channels, scheduling, policy) enter only as interfaces you fill in — they are not baked into core glue (ADR-0005).
The full walkthrough — typed tools, persistence, streaming, project context, subagents, structured output, multi-provider failover, packaging as a CLI, and testing — lives in one place:
The shortest complete example is
examples/local-agent (~100 lines: provider +
store + a typed local_time tool + streaming). Real agents live under
agents/.
The sections below are a feature reference for when you need the specifics.
Glue ships five providers and a driver-style registry. Construct one
directly, or select by name via providers.New.
| Provider | Import | Auth | Notes |
|---|---|---|---|
| Gemini | providers/gemini |
GEMINI_API_KEY |
Google genai SDK. |
| Codex | providers/codex |
ChatGPT subscription (codex login) |
No per-token bill; reuses the upstream Codex CLI's auth.json. |
| DeepSeek | providers/deepseek |
DEEPSEEK_API_KEY |
deepseek-flash (V4.1 Flash), with reasoning replay for tool calls. |
| NVIDIA build | providers/nvidia |
NVIDIA_API_KEY |
OpenAI-compatible; Kimi K2, Llama, Qwen, etc. by org/name. |
| OpenRouter | providers/openrouter |
OPENROUTER_API_KEY |
OpenAI-compatible aggregator; openrouter/free auto-picks a free model. |
CLI defaults depend on activity: glue run and glue serve use
openrouter / openrouter/free; --coding and glue goal use
gemini / gemini-3.8-flash. Explicit --provider and --model
override these choices. glue models labels the chat and coding defaults.
// Codex — bill against your ChatGPT subscription instead of an API key:
agent := glue.NewAgent(glue.AgentOptions{
Provider: codex.New(codex.Options{}),
Model: codex.DefaultModel, // "gpt-5-codex"
})Codex quarantines all subscription-auth fragility (OAuth, token refresh,
Cloudflare cookies) to its package — run codex login once; Glue reads
~/.codex/auth.json (override with $GLUE_CODEX_AUTH / $CODEX_HOME).
Subscription-auth via third-party tools is not formally documented by
OpenAI; the provider is intended for personal use. See
ADR-0006.
DeepSeek, NVIDIA, and OpenRouter share the providers/openaicompat core. NVIDIA and OpenRouter can
have multi-second first-byte latency on cold routing. To add your own
provider, see docs/provider-guide.md and
examples/echo-provider.
The Gemini adapter accepts ProviderRequest.Options["thinking_level"] for
Gemini 3 and 4 models. Gemini 4 Argon supports low, medium, and high;
omitting the option preserves the API default.
Discover provider and model choices without API credentials:
glue models
glue models --provider gemini --jsonThe catalog is curated, includes each provider's default, and shows verified
thinking levels. It does not check account access or fetch a live model list.
Use --model <id> for models outside the catalog; JSON output includes each
provider's credential variable and live catalog URL when available.
DeepSeek is available directly with --provider deepseek and
DEEPSEEK_API_KEY. Its default model, deepseek-flash, serves V4.1 Flash.
The adapter preserves streamed reasoning when replaying tool conversations.
Images are not yet supported by Glue's shared Chat Completions transport.
export DEEPSEEK_API_KEY=...
glue run --provider deepseek --prompt 'Explain this repository' --codingSee the DeepSeek thinking and tool replay requirements.
Select model-specific thinking effort in run, serve, and goal:
glue run --provider gemini --model gemini-4-argon --thinking high --prompt 'Review this design'
glue run --provider deepseek --thinking low --coding --prompt 'Find the entry point'
glue serve --provider deepseek --thinking max
glue goal --provider gemini --model gemini-4-argon --thinking medium 'Explain the parser'Omit --thinking or use --thinking default to preserve API defaults.
Gemini Argon supports low, medium, and high; DeepSeek Flash supports
off, low, high, and max. glue models lists each model's supported
levels. Unsupported combinations fail before provider construction. Models
without verified controls, including custom IDs, accept only the default.
These settings apply to all prompts in the local run or goal and to daemon
runs started through connect. They do not change provider defaults globally.
For interactive selection, run:
glue run --select --coding
glue run --select --prompt 'Explain this code'Use the arrow keys and Enter to choose a provider, a model, and its supported
thinking level. Esc or Ctrl+C cancels before provider construction. The menus
include a custom model ID option. --provider, --model, and --thinking
set initial choices; the confirmed menu choices take precedence. Changing
providers resets the model choice to that provider's default. Unsupported
thinking preselection resets to the API default. Choices apply to this run.
Menus require terminal stdin and stderr and render on stderr, leaving stdout
available for one-shot text or JSON output. In scripts, use glue models
and the explicit flags instead. Account access is checked when the provider
runs. Provider/model switching during an active TUI session is not included.
glue.WithFailover(provs...) tries providers in order until one accepts
the stream — handy when a CLI supports several backends and should skip
those whose keys aren't set:
import (
"github.com/erain/glue"
"github.com/erain/glue/providers"
_ "github.com/erain/glue/providers/codex"
_ "github.com/erain/glue/providers/gemini"
_ "github.com/erain/glue/providers/nvidia"
)
var provs []glue.Provider
for _, name := range []string{"codex", "nvidia", "gemini"} {
if p, _, _, err := providers.New(name); err == nil {
provs = append(provs, p)
}
}
agent := glue.NewAgent(glue.AgentOptions{Provider: glue.WithFailover(provs...)})Failover only falls through before the first event commits to the
consumer; once a non-error event arrives it stays on that provider for
the turn. All-providers-failed surfaces as a typed *glue.FailoverError.
Define typed tools with glue.NewTool[Args]. It decodes
ToolCall.Arguments into your Go type before the executor runs and
turns malformed arguments into a model-visible error result instead of a
panic. Pair it with glue.TextResult / glue.ErrorResult:
type weatherArgs struct {
City string `json:"city"`
}
weather := glue.NewTool[weatherArgs](
glue.ToolSpec{
Name: "weather",
Description: "Look up current weather for a city.",
Parameters: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
},
func(ctx context.Context, a weatherArgs) (glue.ToolResult, error) {
report, err := lookup(ctx, a.City)
if err != nil {
return glue.ErrorResult(err), nil
}
return glue.TextResult(report), nil
},
)Return ErrorResult for recoverable failures (the model can retry); a
Go error only for failures that should stop the run. Schema generation
is out of scope — write Parameters by hand.
Ready-made bundles under tools/: tools/fs (read/write/edit, plus
read-only list_dir/find_files/grep), tools/git, tools/shell,
and the assembled tools/coding bundle. See Coding tools.
Subagents. glue.SubagentTool wraps a child *glue.Agent as a
tool, so a parent can delegate a focused task to a fresh, isolated
transcript:
researchTool, _ := glue.SubagentTool(glue.SubagentOptions{
Name: "research",
Description: "Delegate a focused research question.",
Agent: researcher, // a *glue.Agent
})MCP servers. tools/mcp consumes Model Context
Protocol servers (stdio / Streamable
HTTP), mapping their tools to permission-gated glue.Tool values. See
ADR-0011.
stores/file writes one JSON file per session — the dependency-free
default. stores/sqlite implements the same glue.Store against a
pure-Go SQLite DB with FTS5 over message text, for cross-session recall:
store, err := sqlite.Open(sqlite.Options{Path: "agent.db"})
defer store.Close()
agent := glue.NewAgent(glue.AgentOptions{Provider: prov, Store: store})
hits, _ := agent.SearchSessions(ctx, "Australian Shepherd", glue.WithLimit(5))
for _, h := range hits {
fmt.Printf("[%s#%d] %s\n", h.SessionID, h.Index, h.Snippet)
}Search options: WithLimit, WithOffset, WithSessionID, WithSince,
WithUntil. The query is FTS5 MATCH syntax; hits come back by BM25
score. stores/file does not implement search, so both
Agent.SearchSessions and Session.Search return
glue.ErrSearchNotSupported there — picking stores/sqlite is the
signal that you want it. Uses
modernc.org/sqlite (no CGo). Schema
details in ADR-0007.
Streaming. Mirror text deltas with the convenience options, or subscribe for full control:
session.Prompt(ctx, "Stream a haiku.",
glue.WithStreamWriter(os.Stdout), // EventTextDelta → writer
glue.WithToolLogger(os.Stderr), // "[tool] <name>" on tool start
)
unsubscribe := session.Subscribe(func(e glue.Event) {
if e.Type == glue.EventTextDelta { fmt.Print(e.Delta) }
})
defer unsubscribe()Per-prompt overrides: glue.WithModel, glue.WithSystemPrompt,
glue.WithMaxTurns.
Roles are named instruction profiles with optional model overrides,
from AgentOptions.Roles or <WorkDir>/roles/*.md. Precedence:
WithRole (call) > WithSessionRole (session) > AgentOptions.Role.
Project context & skills. Set AgentOptions.WorkDir:
<WorkDir>/AGENTS.md is appended to the system prompt;
<WorkDir>/.agents/skills/<name>/SKILL.md becomes a runnable skill via
session.Skill(ctx, name, args).
Structured output. session.PromptJSON(ctx, prompt, &out) requests
JSON-only and decodes into your Go type; glue.WithJSONSchema(schema)
forwards an explicit schema.
Versioned prompts. prompts.NewCatalog(embedFS, dir, default) wraps
an embed.FS of <version>.md files so you can A/B-test and roll back
system prompts; unknown versions error with the available list.
Long context. AgentOptions.Compactor + CompactionThreshold.
glue.KeepRecentMessages(n) is the zero-dependency default;
SummarizingCompactor is token-aware and produces a structured
state snapshot (goal / constraints / progress / next steps, exact
paths and errors preserved) with a prompt-injection firewall, a
cumulative read/modified file ledger that survives repeated
compactions, splits that never sever a tool-call/result pair, and an
inflation guard; KeepRecentTokens keeps the recent tail by token
budget instead of message count
(ADR-0002,
ADR-0007).
The loop absorbs the failure shapes that waste agent turns — on by default, each with an opt-out (docs/coding-harness-roadmap.md records the analysis behind them):
- History hardening. Every run repairs the transcript first:
dangling tool calls from an interrupted turn get synthesized error
results, orphaned results and empty turns are dropped, and turns
from a different model lose their thinking signatures — the things
providers reject with opaque 400s (
loop.HardenHistory). - Classified retries. Transient provider failures (429/5xx,
dropped streams) retry with backoff, honoring
Retry-After/ GeminiRetryInfohints; auth and invalid-request errors fail fast. Context overflow surfaces as a typed*loop.OverflowErrorthat sessions answer by compacting once and retrying once. Opt out withRunRequest.Retry.Disabled(ADR-0017). - Guardrails. Repeating the same tool call with identical
arguments, or burning consecutive all-error tool rounds, first
draws a corrective message and then halts the run with a typed
error (
RunRequest.Guardrails). - Stall recovery.
AgentOptions.AutoContinuenudges a model that narrates "I will now…" and stops without acting — bounded to twice per run; thegluebinary enables it for providers that declare the stall in the capability registry.
Per-model capabilities. Providers declare harness-relevant facts
at registration — context window, parallel-tool safety, prompt
variant, auto-continue proneness — queried via
providers.CapabilitiesFor(name) instead of if-provider-name
switches.
tools/coding.Tools(...) assembles a permission-gated local coding
bundle — read_file, write_file, edit_file, list_dir,
find_files, grep, shell_exec, git_diff_branch, git_log_branch
— over tools/fs, tools/git, tools/shell, and glue.Executor. The
glue binary exposes it directly:
go run ./cmd/glue run --provider codex --coding --work . \
--prompt "Run the tests and fix the first failure."The bundle is built to tolerate model sloppiness instead of bouncing it back:
edit_filerepairs near-miss matches — a deterministic ladder (whitespace → indentation, with the replacement re-indented to the file's real indentation → smart-quote/dash folding → block-anchor) plus over-escape repair; non-exact matches are named in the result, and success echoes the updated lines so the model doesn't re-read the file. CRLF and BOMs are preserved.shell_execkeeps head and tail of long output with an omitted-bytes marker and the complete stream spooled to a named temp file; timeouts keep the partial output.read_filepages by line offset and says exactly how to continue.- The system prompt is assembled from the active toolset
(
coding.SystemPrompt): one line per registered tool plus their usage guidelines, in a terse variant for frontier models and an explicit variant for open-weight ones — it cannot drift from the tools actually available. Tools contribute their own text viaToolSpec.PromptSnippet/PromptGuidelines.
Side-effecting tools (write_file, edit_file, shell_exec) are
permission-gated; reads and navigation are not. Execution defaults to
the local process via glue.Executor — not a sandbox. Implement your
own Executor to run in a container/VM. See
ADR-0012.
A thin CLI over the same library API, for trying things without writing
a main.go — and a full terminal coding agent in its own right
(homepage: https://glue-coding-agent-site.vercel.app):
# Interactive TUI (default when stdin/stdout are a terminal and no --prompt):
go run ./cmd/glue run --provider codex --coding --work .
# One-shot run (any registered provider; default openrouter/free):
go run ./cmd/glue run --prompt "Say hi" --id demo --store .glue/sessions
go run ./cmd/glue run --provider codex --coding --work . --prompt "Fix the failing test."
# Scripted: pipe the prompt in.
echo "summarize main.go" | go run ./cmd/glue run --provider codex --coding
# Local HTTP+SSE daemon + a client that streams and brokers permissions:
go run ./cmd/glue serve --store .glue/sessions
go run ./cmd/glue connect --inspect
go run ./cmd/glue connect --prompt "Say hi" --id demo
# Headless goal loop (schedulable from cron/CI; exit code reflects the
# outcome: 0 achieved · 2 blocked · 3 max-iterations · 4 budget · 1 error):
go run ./cmd/glue goal --coding --yolo --worktree "Make the linter pass on ./..."
go run ./cmd/glue goal --list
go run ./cmd/glue goal --resume
# Fire and forget a coding job (detach with Ctrl-C, reattach from any terminal, or open http://127.0.0.1:8787 in a browser):
go run ./cmd/glue serve --coding --state-dir .glue/state --listen 127.0.0.1:8787 --token "$GLUE_TOKEN" &
id=$(go run ./cmd/glue job submit --server http://127.0.0.1:8787 --repo . --objective "Add a Makefile test target")
go run ./cmd/glue job attach "$id" # replays from last seen seq; Ctrl-C detaches without cancelling
go run ./cmd/glue job result "$id"Interactive mode (designed in ADR-0014).
With no --prompt and a terminal on both stdin and stdout, glue run
opens a bubbletea TUI: scrollable transcript with sticky-scroll,
multi-line input, streaming text (re-rendered as markdown after each
turn settles via charmbracelet/glamour), tool-call cards with a
moving spinner while running and an inline [a] [s] [t] [n] permission
prompt right inside the card when a side-effecting tool needs approval,
and a small edit_file diff preview. Slash commands: /help,
/exit, /clear / /new, /usage, /tools, /model <id>,
/session [id], /compact (token-aware summarization of older
messages to free context window), /resume (modal picker over
past sessions; ↑/↓ navigate, Enter replays the chosen one into the
transcript), /fork [N] (branch from message N — defaults to
"just before my last user turn" — into a new session id, keeping the
original intact), /clone (full duplicate of the current
session), /tree (visualize the session lineage with
├─ / └─ glyphs, current node marked ◉; pick one to switch — see
ADR-0015), and /goal <objective>
(pursue a goal autonomously in the background via Agent.PursueGoal —
plan → maker → independent checker per iteration, with a live [x]/[ ]
checklist card in the transcript, a ◎ goal · iter 2/10 · 1/4 ✓ status-bar
segment, and /goal status / pause / resume (continues from the verified
checklist without re-planning — even in a new process, since progress is
checkpointed to the session store) / list (recent goals with status and
age) / clear subcommands; /goal -w <objective> isolates the run in its
own git worktree at .glue/worktrees/<goal-id> on branch goal/<id>, so
the loop never touches your checkout and the result is a reviewable
branch — see ADR-0016), and /copy [N] (copy an
assistant reply's raw markdown — not the glamour-styled, ANSI-laden form
you see on screen — to the system clipboard via an OSC 52 sequence that works
over SSH with no external binary; defaults to the last reply, /copy 2
targets reply #2, and Ctrl+Y copies the last reply with no typing).
Anywhere in a prompt,
@<path> inlines that file's
contents (@"path with space" for spaces, @@literal to escape — and
the workspace blocklist refuses .env / id_rsa / etc.). Typing
@ in the TUI input also opens an inline file-picker popup that
fuzzy-matches workspace files; ↑/↓ to navigate, Tab/Enter to
insert. Enter
sends; Ctrl+J inserts a newline (works on every terminal —
Shift+Enter does not). Esc cancels the
current turn; Ctrl+C once cancels (and a second press quits);
Ctrl+Y copies the last assistant reply to the clipboard;
mouse wheel scrolls the transcript; PgUp/PgDn does too. The TUI
dependencies
(charmbracelet/{bubbletea,bubbles,lipgloss,glamour}) live under
cmd/glue/tui/ only — go get github.com/erain/glue consumers pull
zero TUI code.
run flags include --provider, --model, --id, --store,
--work, --coding (+ --allow-binary, --coding-allow-overwrite),
--tools name1,name2 (allowlist) / --no-tools (text-only),
--mode text|json (one-shot output format; json emits stable
JSONL events for scripting), --yolo (auto-approve every
side-effecting tool call — daily-driver mode for trusted feature
branches), --usage, and repeatable --env. serve brokers coding-tool
permission requests to the connected connect client; it writes
connection metadata to the user config dir (never the bearer token).
The daemon protocol is ADR-0010.
Standard flags for your own binary. cli.RegisterStandardFlags
wires the same six flags (--provider, --model, --id, --store,
--work, --max-turns) onto a flag.FlagSet:
fs := flag.NewFlagSet("my-agent", flag.ContinueOnError)
get := cli.RegisterStandardFlags(fs, nil)
fs.Parse(os.Args[1:])
cfg := get() // cfg.Provider, cfg.Model, cfg.ID, cfg.Store, cfg.Work, cfg.MaxTurnsReal agents built on the framework live under agents/ (peer of the
harness), not examples/ (tutorial demos only).
agents/glue-review— a free, local pre-push branch reviewer. Reads the diff againstmain, deep-reads files when needed, and posts one sticky GitHub comment with a fenced```markdownfix block downstream coding agents can paste. Runs as a CLI or a GitHub Action. Defaults toopenrouter/freewith automatic provider failover.
The Provider interface is tiny, so tests drive sessions with a fake —
no credentials, no network. Test tools by calling
tool.Execute(ctx, glue.ToolCall{...}) and asserting on the
ToolResult (including IsError). See the
testing step
of the build guide for a copy-paste fake.
go build ./...
go vet ./...
go test ./...CI runs the same commands on every PR. Live provider tests are gated behind their API keys and skipped in CI, e.g.:
GEMINI_API_KEY=... go test ./providers/gemini -run LiveGlue focuses on the agent framework and the coding agent (interactive
TUI, coding tools, and autonomous goals). Peggy is retired. Local coding is
available today; asynchronous remote execution through
AX is in planning. Execution mode is independent
of model/provider selection. The project plan defines
the work needed to produce the detailed roadmap under
tracker #392. Releases are tagged on
a 1.x line
(currently well past v1.10), but the stability stance is still the
pre-stability one recorded in
ADR-0013 (see its
addendum for why the tags say 1.x): the public Agent / Session
surface is stable in practice, but minor versions may still break
API until a deliberate surface-review pass. Breaking changes always
land with a **Breaking:** entry in CHANGELOG.md,
never on a patch release. Security reports go through
SECURITY.md.
Glue is built one GitHub issue at a time. The contributor workflow,
branch/PR conventions, and the active tracker are documented in
CONTRIBUTING.md; the roadmap shape lives in
docs/project-plan.md, and durable design
decisions are recorded as ADRs under docs/adr/. The
canonical architecture reference is docs/design.md.