Skip to content
erainPublic

About

A small Go agent framework — provider-agnostic loop, typed tools, four providers, optional persistence + FTS5 search. Two reference agents on top.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

257 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

glue

CI

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.

Inspired by Flue and pi-mono.

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)

Install

go get github.com/erain/glue

Module 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.

Quickstart

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.

Concepts

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

Build your own agent

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:

➡️ docs/building-agents.md

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.

Providers

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.

Choosing models and thinking levels

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 --json

The 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' --coding

See 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.

Failover across providers

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.

Tools

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.

Persistent sessions with search

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, roles, skills, structured output

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

Harness reliability

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 / Gemini RetryInfo hints; auth and invalid-request errors fail fast. Context overflow surfaces as a typed *loop.OverflowError that sessions answer by compacting once and retrying once. Opt out with RunRequest.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.AutoContinue nudges a model that narrates "I will now…" and stops without acting — bounded to twice per run; the glue binary 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.

Coding tools

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_file repairs 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_exec keeps 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_file pages 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 via ToolSpec.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.

The glue CLI

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.MaxTurns

Reference agents

Real 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 against main, deep-reads files when needed, and posts one sticky GitHub comment with a fenced ```markdown fix block downstream coding agents can paste. Runs as a CLI or a GitHub Action. Defaults to openrouter/free with automatic provider failover.

Testing without API keys

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.

Run the tests

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 Live

Project status & contributing

Glue 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.

About

A small Go agent framework — provider-agnostic loop, typed tools, four providers, optional persistence + FTS5 search. Two reference agents on top.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages