glue

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 16 Imported by: 0

README

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-2.5-flash",
	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,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-2.5-flash",
	})
	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 four 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.
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.
// 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.

NVIDIA and OpenRouter share the providers/openaicompat core. Both can have multi-second first-byte latency on cold routing. To add your own provider, see docs/provider-guide.md and examples/echo-provider.

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.

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 (ADR-0002, ADR-0007).

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

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:

# 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 gemini):
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

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, multi-line input, streaming text, tool-call cards with inline permission prompts, and a small edit_file diff preview before you approve a change. Slash commands: /help, /exit, /clear, /usage, /tools, /model <id>, /session [id]. Ctrl+C cancels the current turn; press again to quit. The TUI dependencies (charmbracelet/{bubbletea,bubbles,lipgloss}) 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), --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.

  • agents/peggy — a long-running personal-assistant agent: CLI + Telegram + a shared HTTP+SSE daemon, durable sqlite+FTS5 memory with curated recall, opt-in coding tools, MCP servers, scheduled/proactive runs, and per-channel permission tiers. The best reference for a feature-rich agent. Tracker: #110.

    go install github.com/erain/glue/agents/peggy/cmd/peggy@latest
    codex login
    peggy "Hello — what should I be working on today?"
    

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

The harness is feature-complete for the 0.x series, tagged at v0.1.0, and in active use behind the agents/glue-review and agents/peggy reference agents. The library remains pre-1.0: the public Agent / Session surface is stable in practice, but minor versions may still break API. The full stability stance is ADR-0013; 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.

Documentation

Overview

Package glue provides the public API for defining and running agents.

It is intentionally thin over the lower-level loop package. Users construct an Agent with NewAgent, open a named Session with Agent.Session, and drive turns with Session.Prompt. Tools are defined with NewTool, skills and roles are discovered from a WorkDir, and structured output is produced with Session.PromptJSON.

Provider implementations live in subpackages (providers/gemini, providers/codex, providers/nvidia, providers/openrouter, with the shared OpenAI-compatible core in providers/openaicompat and a driver-style registry in providers). Session persistence is provided by stores subpackages (stores/file for the simple default, stores/sqlite for cross-session FTS5 search). Reusable tools live under tools (tools/fs, tools/git, tools/shell, tools/coding, tools/mcp).

Normalized message and event types are re-exported here so that callers only need to import "github.com/erain/glue". For a guided, end-to-end walkthrough of building an agent, see docs/building-agents.md.

Index

Constants

View Source
const (
	DefaultSearchLimit = 20
	MaxSearchLimit     = 100
)

Search-related limits. Exported so callers can size paging.

View Source
const (
	RememberNever         = loop.RememberNever
	RememberSession       = loop.RememberSession
	RememberSessionTarget = loop.RememberSessionTarget
	RememberForever       = loop.RememberForever
)

RememberScope constants re-exported from package loop.

View Source
const (
	MessageRoleUser      = loop.MessageRoleUser
	MessageRoleAssistant = loop.MessageRoleAssistant
	MessageRoleTool      = loop.MessageRoleTool
)

MessageRole constants re-exported from package loop.

View Source
const (
	ContentTypeText     = loop.ContentTypeText
	ContentTypeThinking = loop.ContentTypeThinking
	ContentTypeImage    = loop.ContentTypeImage
	ContentTypeToolCall = loop.ContentTypeToolCall
)

ContentType constants re-exported from package loop.

View Source
const (
	StopReasonStop     = loop.StopReasonStop
	StopReasonLength   = loop.StopReasonLength
	StopReasonToolUse  = loop.StopReasonToolUse
	StopReasonError    = loop.StopReasonError
	StopReasonCanceled = loop.StopReasonCanceled
	StopReasonMaxTurns = loop.StopReasonMaxTurns
)

StopReason constants re-exported from package loop.

View Source
const (
	EventLoopStart    = loop.EventLoopStart
	EventLoopEnd      = loop.EventLoopEnd
	EventTurnStart    = loop.EventTurnStart
	EventTurnEnd      = loop.EventTurnEnd
	EventMessageStart = loop.EventMessageStart
	EventMessageEnd   = loop.EventMessageEnd
	EventTextDelta    = loop.EventTextDelta
	EventToolStart    = loop.EventToolStart
	EventToolEnd      = loop.EventToolEnd
	EventError        = loop.EventError
)

EventType constants re-exported from package loop.

View Source
const (
	ProviderEventStart         = loop.ProviderEventStart
	ProviderEventTextDelta     = loop.ProviderEventTextDelta
	ProviderEventThinkingDelta = loop.ProviderEventThinkingDelta
	ProviderEventToolCall      = loop.ProviderEventToolCall
	ProviderEventDone          = loop.ProviderEventDone
	ProviderEventError         = loop.ProviderEventError
)

ProviderEventType constants re-exported from package loop.

View Source
const DefaultExecMaxOutputBytes = 64 * 1024

DefaultExecMaxOutputBytes is the per-stream output cap used by LocalExecutor when ExecCommand.MaxOutputBytes is zero.

View Source
const DefaultSummarizingSystemPrompt = `` /* 580-byte string literal not displayed */

DefaultSummarizingSystemPrompt is the instruction the SummarizingCompactor sends to the provider when no override is configured. The prompt biases for fact retention over polish.

View Source
const SessionStateVersion = 1

SessionStateVersion is the on-disk version tag for SessionState.

Variables

View Source
var ErrSearchNotSupported = errors.New("glue: store does not support search")

ErrSearchNotSupported is returned when the active Store does not implement Searcher. Callers can fall back gracefully (e.g. show "search not configured" in a UI).

View Source
var ErrSkipTool = loop.ErrSkipTool

ErrSkipTool re-exported from package loop.

Functions

This section is empty.

Types

type Agent

type Agent struct {
	// contains filtered or unexported fields
}

Agent owns shared configuration and an in-memory session map.

func NewAgent

func NewAgent(options AgentOptions) *Agent

NewAgent creates an agent. When AgentOptions.Store is set, sessions are loaded from and saved to that store; otherwise sessions are in-memory. The Provider must be supplied for Session.Prompt to succeed.

func (*Agent) SearchSessions

func (a *Agent) SearchSessions(ctx context.Context, query string, opts ...SearchOption) ([]SearchHit, error)

SearchSessions returns FTS-ranked hits across all sessions stored by the agent's Store. Returns ErrSearchNotSupported if the active store does not implement Searcher.

query is passed straight through to the underlying Searcher. For the SQLite/FTS5 backend the syntax is FTS5's MATCH expression — a bare word matches that word; quoted phrases match exactly; AND / OR / NOT are supported.

func (*Agent) Session

func (a *Agent) Session(ctx context.Context, id string, options ...SessionOption) (*Session, error)

Session returns an existing session by id, or creates a new one. When the agent has a configured Store, an existing on-disk state is loaded into the new in-memory session. Empty ids resolve to the default session ("default").

func (*Agent) ToolCatalog

func (a *Agent) ToolCatalog() []ToolSpec

ToolCatalog returns a cloned provider-visible catalog of the agent's configured tools, including permission metadata for hosts that need to display or expose the tool surface without starting a session.

type AgentOptions

type AgentOptions struct {
	Provider     Provider
	Model        string
	SystemPrompt string
	Tools        []Tool
	Options      map[string]any
	MaxTurns     int
	Store        Store
	WorkDir      string
	Skills       map[string]Skill
	Roles        []Role
	Role         string
	Permission   Permission
	Hooks        []Hook

	// Compactor, if non-nil and CompactionThreshold > 0, is invoked
	// before every prompt whenever the in-memory transcript has more
	// than CompactionThreshold messages. The compactor's output replaces
	// the in-memory transcript before [loop.Run] is called and is
	// persisted by the next save.
	Compactor           Compactor
	CompactionThreshold int
}

AgentOptions configures a Glue agent.

All fields are wired. When WorkDir is set, the agent loads `<WorkDir>/AGENTS.md` (non-fatal if missing), discovers Markdown skills under `<WorkDir>/.agents/skills/<name>/SKILL.md`, and discovers Markdown roles under `<WorkDir>/roles/*.md`. Programmatic entries supplied via Skills and Roles are merged with the on-disk catalog; programmatic entries win on name collision.

Role is the agent-default role applied to every prompt unless overridden at session or call level. Effective role precedence is call (WithRole) > session (WithSessionRole) > agent (Role). Effective model precedence is call (WithModel) > effective role's Model > Model.

type AllowAll

type AllowAll = loop.AllowAll

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type Compactor

type Compactor interface {
	Compact(ctx context.Context, messages []Message) ([]Message, error)
}

Compactor rewrites a session transcript before it is sent to the provider. Implementations should preserve the user's most recent intent and the assistant context that depends on it; older turns can be dropped, summarized, or replaced. Compactors must not mutate the input slice; they return a new slice.

func KeepRecentMessages

func KeepRecentMessages(n int) Compactor

KeepRecentMessages returns a Compactor that keeps the last n messages of a transcript and replaces everything older with a single system-style summary message that records how many turns were dropped.

This is the simplest useful compaction policy: it has no token model and never calls a provider. It is appropriate when a session is allowed to accumulate but the caller can tolerate losing older context past a fixed window.

n must be positive. If the input transcript has n or fewer messages, the compactor returns it unchanged.

type CompactorFunc

type CompactorFunc func(ctx context.Context, messages []Message) ([]Message, error)

CompactorFunc adapts a function to the Compactor interface.

func (CompactorFunc) Compact

func (f CompactorFunc) Compact(ctx context.Context, messages []Message) ([]Message, error)

Compact implements Compactor.

type ContentPart

type ContentPart = loop.ContentPart

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ContentType

type ContentType = loop.ContentType

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type DenyAll

type DenyAll = loop.DenyAll

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type Event

type Event = loop.Event

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type EventType

type EventType = loop.EventType

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ExecCommand

type ExecCommand struct {
	// Argv is the executable plus arguments. It must be non-empty and
	// is never interpreted through a shell.
	Argv []string

	// Dir, when non-empty, is the child process working directory.
	Dir string

	// Env is the exact child environment. Nil means inherit no
	// environment from the agent process.
	Env []string

	// Stdin, when non-nil, is connected to the child process stdin.
	Stdin io.Reader

	// Timeout limits this command independently from the caller's
	// context. Zero means no executor-level timeout.
	Timeout time.Duration

	// MaxOutputBytes caps stdout and stderr independently. Zero uses
	// DefaultExecMaxOutputBytes.
	MaxOutputBytes int
}

ExecCommand describes one argv-style command execution request.

type ExecResult

type ExecResult struct {
	Stdout    []byte
	Stderr    []byte
	ExitCode  int
	TimedOut  bool
	Truncated bool
}

ExecResult is the captured result of one command execution.

type Executor

type Executor interface {
	Run(ctx context.Context, cmd ExecCommand) (ExecResult, error)
}

Executor abstracts command execution for shell-capable tools. The default implementation, LocalExecutor, runs commands on the local machine; sandboxed or remote executors can implement the same interface without changing tool code.

type FailoverAttempt

type FailoverAttempt struct {
	Index int
	Err   error
}

FailoverAttempt is one provider's result inside a FailoverError.

type FailoverError

type FailoverError struct {
	Attempts []FailoverAttempt
}

FailoverError aggregates per-provider failures from a WithFailover stream attempt. It implements error and exposes Attempts so callers can inspect each provider's outcome.

func (*FailoverError) Error

func (e *FailoverError) Error() string

type Hook

type Hook = loop.Hook

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ImageContent

type ImageContent = loop.ImageContent

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ListSessionsOptions

type ListSessionsOptions struct {
	// Prefix restricts results to session ids beginning with Prefix.
	Prefix string

	// Limit caps returned rows. Non-positive values use the store default.
	Limit int

	// Offset skips rows after filtering and ordering. Negative values are
	// treated as zero.
	Offset int
}

ListSessionsOptions filters and pages a session history listing.

type LocalExecutor

type LocalExecutor struct{}

LocalExecutor runs commands on the local machine with os/exec. It is intentionally not a sandbox.

func (LocalExecutor) Run

Run implements Executor.

type Message

type Message = loop.Message

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type MessageRole

type MessageRole = loop.MessageRole

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type Permission

type Permission = loop.Permission

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type PermissionDecision

type PermissionDecision = loop.PermissionDecision

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type PermissionRequest

type PermissionRequest = loop.PermissionRequest

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ProjectContext

type ProjectContext struct {
	AgentsMD string
	Skills   map[string]Skill
	Roles    map[string]Role
}

ProjectContext is the WorkDir-loaded state injected by LoadContext. AgentsMD is appended to the system prompt; Skills are exposed via Session.Skill; Roles are looked up by name during prompt configuration.

func LoadContext

func LoadContext(workDir string) (ProjectContext, error)

LoadContext loads AGENTS.md (non-fatal if missing) and skills under `<workDir>/.agents/skills/<name>/SKILL.md`. An empty workDir returns an empty context.

type PromptOption

type PromptOption func(*promptConfig)

PromptOption configures one Session.Prompt invocation.

func WithEvents

func WithEvents(handler func(Event)) PromptOption

WithEvents registers a per-prompt event handler. It receives every loop event for that prompt in addition to any session-scoped subscribers installed via Session.Subscribe.

func WithJSONSchema

func WithJSONSchema(schema any) PromptOption

WithJSONSchema attaches a JSON Schema for Session.PromptJSON. The schema may be passed as a Go value (map / struct), a json.RawMessage, a []byte, or a string; bytes/string forms are JSON-decoded once. When the active provider supports structured output, the schema is forwarded as the provider's structured-response config (Gemini: `response_json_schema`).

func WithMaxTurns

func WithMaxTurns(maxTurns int) PromptOption

WithMaxTurns overrides the loop max-turn guard for one prompt.

func WithModel

func WithModel(model string) PromptOption

WithModel overrides the model for one prompt. When set, this beats any role's Model and the agent's Model.

func WithPermission

func WithPermission(permission Permission) PromptOption

WithPermission overrides the agent's Permission implementation for one prompt. Passing nil explicitly disables permission handling for that prompt, so side-effecting tools are denied by the loop.

func WithProviderOptions

func WithProviderOptions(options map[string]any) PromptOption

WithProviderOptions replaces provider options for one prompt.

func WithRole

func WithRole(role string) PromptOption

WithRole overrides the effective role for one prompt. The named role must exist in AgentOptions.Roles or the loaded WorkDir context, or the prompt fails with a typed error.

func WithStreamWriter

func WithStreamWriter(w io.Writer) PromptOption

WithStreamWriter mirrors EventTextDelta.Delta to w on every prompt event. Composes additively with WithEvents and other event-related options — installing one does not displace any other.

Errors from the writer are silently dropped: this is a convenience option, not a delivery-guaranteed pipe. Callers that need backpressure or error visibility should register a custom WithEvents handler instead.

A nil writer is a no-op so callers can pass conditional writers without branching.

func WithSystemPrompt

func WithSystemPrompt(systemPrompt string) PromptOption

WithSystemPrompt overrides the agent system prompt for one prompt.

func WithToolLogger

func WithToolLogger(w io.Writer) PromptOption

WithToolLogger mirrors EventToolStart events to w as "[tool] <name>\n". Composes additively with other event-related options. Errors from the writer are silently dropped; nil w is a no-op.

func WithTools

func WithTools(tools []Tool) PromptOption

WithTools replaces the agent tools for one prompt.

type PromptResult

type PromptResult struct {
	// Text concatenates all text parts of the final assistant message.
	Text string
	// Message is the final assistant message of this run, cloned. Nil if no
	// assistant message was produced.
	Message *Message
	// NewMessages contains every message produced during this run, in append
	// order (assistant + tool messages, possibly across multiple turns).
	NewMessages []Message
	// Messages is a snapshot of the full session transcript after this run.
	Messages []Message
}

PromptResult is returned by Session.Prompt.

type Provider

type Provider = loop.Provider

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

func WithFailover

func WithFailover(provs ...Provider) Provider

WithFailover returns a Provider that tries each underlying provider in order until one accepts a Stream. Failure modes that trigger fallover:

  • the underlying Stream call returns a non-nil error before any event is emitted;
  • the underlying stream's first event is a ProviderEventError;
  • the underlying stream closes immediately with no events.

Once any non-error event is observed the wrapper commits to that provider for the rest of the turn — it does not recover mid-stream. This preserves the loop's "no half-streamed transcripts on retry" invariant.

Callers that want env-key probing (skip provider X when its API key is unset) should pre-filter via [providers.KeyAvailable] from github.com/erain/glue/providers and pass only the candidates whose keys are present.

Returns a typed error implementing FailoverError when all providers fail; use errors.As to inspect per-provider attempts.

type ProviderEvent

type ProviderEvent = loop.ProviderEvent

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ProviderEventType

type ProviderEventType = loop.ProviderEventType

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ProviderRequest

type ProviderRequest = loop.ProviderRequest

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type RememberScope

type RememberScope = loop.RememberScope

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type Role

type Role struct {
	Name         string
	Description  string
	Instructions string
	Model        string
}

Role is a named instruction profile with an optional model override. Roles are loaded from `<WorkDir>/roles/*.md` (with `name:`, `description:`, `model:` frontmatter) or supplied via AgentOptions.Roles.

type SearchHit

type SearchHit struct {
	// SessionID identifies which session this message lives in.
	SessionID string

	// Index is the ordinal position of the message within its session
	// (zero-based).
	Index int

	// Role is the message author role.
	Role MessageRole

	// Snippet is a Searcher-supplied excerpt with highlighting around
	// the matched terms (FTS5's snippet() output for the SQLite
	// backend; markers are << and >>).
	Snippet string

	// Score is the implementation-specific relevance score. For the
	// SQLite/FTS5 backend this is BM25 — lower is better.
	Score float64

	// Timestamp is the message's CreatedAt (or the session updated_at
	// if the message had no per-message timestamp).
	Timestamp time.Time
}

SearchHit is one row returned by a Searcher.

type SearchOption

type SearchOption func(*SearchOptions)

SearchOption configures a SearchOptions via functional-options.

func WithLimit

func WithLimit(n int) SearchOption

WithLimit overrides SearchOptions.Limit. Non-positive values fall back to DefaultSearchLimit; values > MaxSearchLimit are clamped.

func WithOffset

func WithOffset(n int) SearchOption

WithOffset overrides SearchOptions.Offset. Negative values are treated as zero.

func WithSessionID

func WithSessionID(id string) SearchOption

WithSessionID restricts the search to a single session.

func WithSince

func WithSince(t time.Time) SearchOption

WithSince sets the lower time bound (inclusive).

func WithUntil

func WithUntil(t time.Time) SearchOption

WithUntil sets the upper time bound (inclusive).

type SearchOptions

type SearchOptions struct {
	// SessionID restricts results to one session. Empty means
	// "across all sessions".
	SessionID string

	// Limit caps the number of hits returned. Zero falls back to
	// DefaultSearchLimit; values larger than MaxSearchLimit are
	// clamped silently.
	Limit int

	// Offset skips this many hits. Useful for paging.
	Offset int

	// Since restricts results to messages with timestamps ≥ Since.
	// Zero means no lower bound.
	Since time.Time

	// Until restricts results to messages with timestamps ≤ Until.
	// Zero means no upper bound.
	Until time.Time
}

SearchOptions controls a Searcher.Search call. Zero values mean "no filter" for SessionID / Since / Until, "default" for Limit (20), and zero Offset.

type Searcher

type Searcher interface {
	Search(ctx context.Context, query string, opts SearchOptions) ([]SearchHit, error)
}

Searcher is the optional capability a Store may implement to support cross-session content search. Stores that do not implement Searcher cause Agent.SearchSessions and Session.Search to return ErrSearchNotSupported.

Designed in docs/adr/0007-memory-layer.md §3.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is an in-memory conversation with an Agent. Sessions are goroutine-safe but a single session executes one Session.Prompt at a time.

func (*Session) ID

func (s *Session) ID() string

ID returns the session id.

func (*Session) Messages

func (s *Session) Messages() []Message

Messages returns a snapshot of the session transcript.

func (*Session) Prompt

func (s *Session) Prompt(ctx context.Context, text string, options ...PromptOption) (PromptResult, error)

Prompt sends a user message through the agent loop and stores the resulting transcript in memory.

func (*Session) PromptJSON

func (s *Session) PromptJSON(ctx context.Context, text string, outPtr any, options ...PromptOption) (PromptResult, error)

PromptJSON sends a prompt and decodes the assistant's final text into outPtr, which must be a non-nil pointer. It augments the user prompt with JSON-only instructions so non-Gemini providers can still produce parseable output, and sets `response_mime_type: application/json` on the provider request. When WithJSONSchema is provided, the schema is also forwarded as `response_json_schema`.

V1 validation is intentionally limited to JSON decoding into the caller's Go type; full JSON Schema validation is out of scope.

func (*Session) Search

func (s *Session) Search(ctx context.Context, query string, opts ...SearchOption) ([]SearchHit, error)

Search restricts Agent.SearchSessions to this session. Any WithSessionID in opts is overridden with this session's id.

func (*Session) Skill

func (s *Session) Skill(ctx context.Context, name string, args any, options ...PromptOption) (PromptResult, error)

Skill renders the named skill (looked up from AgentOptions.Skills or the agent's WorkDir context), appends args as JSON, and runs the result through Session.Prompt. Unknown skill names return a typed error.

func (*Session) State

func (s *Session) State() SessionState

State returns a snapshot of the durable session state.

func (*Session) Subscribe

func (s *Session) Subscribe(handler func(Event)) func()

Subscribe registers a session-scoped event handler that receives every loop event for every prompt run on this session. The returned function removes the handler.

type SessionLister

type SessionLister interface {
	ListSessions(ctx context.Context, opts ListSessionsOptions) ([]SessionSummary, error)
}

SessionLister is an optional store capability for provider-free session history browsers.

type SessionOption

type SessionOption func(*sessionConfig)

SessionOption configures a session at creation time.

func WithSessionRole

func WithSessionRole(role string) SessionOption

WithSessionRole sets the session-default role used when no per-call WithRole is provided.

type SessionState

type SessionState struct {
	Version   int            `json:"version"`
	ID        string         `json:"id"`
	Messages  []Message      `json:"messages,omitempty"`
	Metadata  map[string]any `json:"metadata,omitempty"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
}

SessionState is the durable representation of a session.

type SessionSummary

type SessionSummary struct {
	ID                string    `json:"id"`
	CreatedAt         time.Time `json:"created_at"`
	UpdatedAt         time.Time `json:"updated_at"`
	Messages          int       `json:"messages"`
	UserMessages      int       `json:"user_messages"`
	AssistantMessages int       `json:"assistant_messages"`
}

SessionSummary is provider-free metadata about one stored session.

type Skill

type Skill struct {
	Name         string
	Description  string
	Instructions string
}

Skill is a Markdown-defined reusable prompt loaded from `<WorkDir>/.agents/skills/<name>/SKILL.md` or supplied directly via AgentOptions.Skills.

type StopReason

type StopReason = loop.StopReason

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type Store

type Store interface {
	Load(ctx context.Context, id string) (SessionState, bool, error)
	Save(ctx context.Context, id string, state SessionState) error
	Delete(ctx context.Context, id string) error
}

Store persists Glue session state. Implementations are expected to be goroutine-safe across distinct session ids; concurrent calls for the same id within a single session are serialized by the session itself.

Load returns found=false when the id is not present, with a zero-valued SessionState and a nil error. Save must be atomic against partial writes. Delete must be idempotent — removing a missing id is a no-op success.

type SubagentOptions

type SubagentOptions struct {
	// Name is the model-visible tool name.
	Name string

	// Description is the model-visible tool description.
	Description string

	// Agent is the child agent invoked when the tool runs. Required.
	Agent *Agent

	// SessionID is an optional prefix for generated child session ids.
	// Each tool instance and tool call appends a generated suffix so calls
	// use isolated transcripts. Empty uses "subagent:<Name>".
	SessionID string

	// MaxTurns optionally overrides the child prompt's loop turn budget.
	// Zero uses the child agent/default loop budget.
	MaxTurns int

	// SystemPrompt optionally overrides the child agent system prompt for
	// each delegated prompt.
	SystemPrompt string
}

SubagentOptions configures SubagentTool.

type SummarizingCompactor

type SummarizingCompactor struct {
	// Provider streams the summary. Required.
	Provider Provider

	// Model is the model id used for the summary call. When empty the
	// provider's default applies.
	Model string

	// TargetTokens is the soft cap: when the estimated transcript size
	// is below this value the compactor returns its input unchanged.
	// Zero or negative falls back to the default (8000).
	TargetTokens int

	// KeepRecent is the number of most-recent messages retained
	// verbatim. Zero or negative falls back to the default (8). When
	// the input transcript has KeepRecent or fewer messages the
	// compactor returns it unchanged regardless of TargetTokens.
	KeepRecent int

	// SystemPrompt is the instruction sent to the summarizer. When
	// empty [DefaultSummarizingSystemPrompt] is used.
	SystemPrompt string
}

SummarizingCompactor is a token-aware Compactor that summarizes older transcript messages by calling the configured Provider. It replaces older messages with a single assistant-role marker whose text content is the summary and whose metadata records the compaction.

SummarizingCompactor is the token-aware drop-in anticipated by ADR-0002 and designed in ADR-0007 §1. It composes with — and does not replace — KeepRecentMessages; callers pick the policy that matches their agent's lifetime.

Provider must be set. The compactor calls Provider.Stream once per invocation with a single user message containing the formatted transcript-to-summarize.

Token estimation is intentionally a heuristic in v0.1: a word-count-based proxy that does not need to match any specific tokenizer. A later PR can swap the implementation without changing this type's public surface.

Errors from the underlying provider propagate. The compactor does not silently fall back to dropping context; callers that want a degraded-mode behavior should wire a CompactorFunc that tries SummarizingCompactor first and falls back to KeepRecentMessages explicitly.

func (*SummarizingCompactor) Compact

func (s *SummarizingCompactor) Compact(ctx context.Context, in []Message) ([]Message, error)

Compact implements Compactor. See the type docs for behavior.

type Tool

type Tool = loop.Tool

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

func NewTool

func NewTool[T any](spec ToolSpec, fn func(ctx context.Context, args T) (ToolResult, error)) Tool

NewTool builds a Tool whose executor decodes ToolCall.Arguments into a typed Go value before invoking fn. Empty arguments decode as the zero value of T. JSON decode failures surface to the model as an error ToolResult — matching the existing manual pattern — so a malformed call does not crash the loop.

Callers still supply spec.Parameters (a JSON Schema). Schema generation from T is intentionally out of scope.

func SubagentTool

func SubagentTool(opts SubagentOptions) (Tool, error)

SubagentTool exposes a child Agent as a Tool. Each tool call creates a fresh child session, forwards only the explicit prompt argument, and returns the child agent's final text as the tool result. Child prompt failures become model-visible error results except for context cancellation/deadline errors, which are returned as Go errors so the parent loop stops promptly.

type ToolCall

type ToolCall = loop.ToolCall

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ToolExecutor

type ToolExecutor = loop.ToolExecutor

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type ToolResult

type ToolResult = loop.ToolResult

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

func ErrorResult

func ErrorResult(err error) ToolResult

ErrorResult wraps an error in a ToolResult tagged with IsError=true so the model sees it as a tool failure rather than the loop crashing. The error's Error() string becomes the visible text.

func TextResult

func TextResult(s string) ToolResult

TextResult wraps a string in a ToolResult with a single text content part. Use it from tool executors when the tool succeeds with textual output.

type ToolSpec

type ToolSpec = loop.ToolSpec

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

type Usage

type Usage = loop.Usage

Re-export the loop package's normalized types as part of the public API so callers only need to import `glue`.

Directories

Path Synopsis
agents
glue-review command
Command glue-review is a free, local pre-push branch reviewer built on the Glue agent harness.
Command glue-review is a free, local pre-push branch reviewer built on the Glue agent harness.
peggy
Package peggy is a long-running personal-assistant agent built on the glue framework.
Package peggy is a long-running personal-assistant agent built on the glue framework.
peggy/channels/telegram
Package telegram is the Telegram-bot channel adapter for Peggy.
Package telegram is the Telegram-bot channel adapter for Peggy.
peggy/cmd/peggy command
Command peggy is the CLI entry point for the personal-assistant agent.
Command peggy is the CLI entry point for the personal-assistant agent.
peggy/cmd/peggy-telegram command
Command peggy-telegram is the binary form of the Telegram channel adapter.
Command peggy-telegram is the binary form of the Telegram channel adapter.
Package cli provides small helpers downstream agents share with the canonical cmd/glue runner.
Package cli provides small helpers downstream agents share with the canonical cmd/glue runner.
cmd
glue command
Command glue is the local CLI runner for Glue agents.
Command glue is the local CLI runner for Glue agents.
glue/tui
Package tui implements the interactive multi-turn terminal interface for `glue run`.
Package tui implements the interactive multi-turn terminal interface for `glue run`.
Package daemon serves local Glue sessions over the ADR-0010 HTTP+SSE protocol.
Package daemon serves local Glue sessions over the ADR-0010 HTTP+SSE protocol.
examples
echo-provider
Package echo is a minimal example of a custom Glue provider.
Package echo is a minimal example of a custom Glue provider.
local-agent command
Command local-agent is a small Gemini-backed CLI built directly on the glue library.
Command local-agent is a small Gemini-backed CLI built directly on the glue library.
Package loop contains Glue's provider-agnostic agent loop.
Package loop contains Glue's provider-agnostic agent loop.
Package prompts provides a small versioned-prompt loader that wraps an embed.FS rooted at a directory of `<name>.md` files.
Package prompts provides a small versioned-prompt loader that wraps an embed.FS rooted at a directory of `<name>.md` files.
Package providers exposes a small driver-style registry of provider constructors.
Package providers exposes a small driver-style registry of provider constructors.
codex
Package codex is a glue.Provider that routes through the Codex Responses endpoint at chatgpt.com/backend-api/codex/responses, authenticated with a ChatGPT subscription via OAuth tokens read from the upstream Codex CLI's auth.json (run "codex login" once outside glue).
Package codex is a glue.Provider that routes through the Codex Responses endpoint at chatgpt.com/backend-api/codex/responses, authenticated with a ChatGPT subscription via OAuth tokens read from the upstream Codex CLI's auth.json (run "codex login" once outside glue).
codex/auth
Package auth handles ChatGPT-subscription OAuth tokens for the providers/codex transport.
Package auth handles ChatGPT-subscription OAuth tokens for the providers/codex transport.
gemini
Package gemini adapts Google's Gemini API to Glue's provider interface using the google.golang.org/genai SDK.
Package gemini adapts Google's Gemini API to Glue's provider interface using the google.golang.org/genai SDK.
nvidia
Package nvidia implements a Glue provider for the NVIDIA build inference API (https://build.nvidia.com), which exposes an OpenAI-compatible chat-completions endpoint at https://integrate.api.nvidia.com/v1.
Package nvidia implements a Glue provider for the NVIDIA build inference API (https://build.nvidia.com), which exposes an OpenAI-compatible chat-completions endpoint at https://integrate.api.nvidia.com/v1.
openaicompat
Package openaicompat implements the shared streaming, tool-call, and convert logic for OpenAI-style chat-completions endpoints used by the concrete providers under providers/.
Package openaicompat implements the shared streaming, tool-call, and convert logic for OpenAI-style chat-completions endpoints used by the concrete providers under providers/.
openrouter
Package openrouter implements a Glue provider for OpenRouter (https://openrouter.ai), an OpenAI-compatible aggregator that routes requests across many underlying model providers.
Package openrouter implements a Glue provider for OpenRouter (https://openrouter.ai), an OpenAI-compatible aggregator that routes requests across many underlying model providers.
stores
file
Package file provides a local JSON-backed session store for Glue.
Package file provides a local JSON-backed session store for Glue.
sqlite
Package sqlite provides a SQLite-backed glue.Store with FTS5 over message text.
Package sqlite provides a SQLite-backed glue.Store with FTS5 over message text.
tools
coding
Package coding assembles the reusable local coding-agent tool bundle.
Package coding assembles the reusable local coding-agent tool bundle.
fs
Package fs provides filesystem helpers and tool factories that agents can register without reinventing path safety, output truncation, or the sensitive-file blocklist.
Package fs provides filesystem helpers and tool factories that agents can register without reinventing path safety, output truncation, or the sensitive-file blocklist.
git
Package git provides git tool factories and shell-out helpers that agents can register without re-implementing PATH lookup, timeout management, or pathspec construction.
Package git provides git tool factories and shell-out helpers that agents can register without re-implementing PATH lookup, timeout management, or pathspec construction.
mcp
Package mcp implements the client foundation for consuming Model Context Protocol servers from glue hosts.
Package mcp implements the client foundation for consuming Model Context Protocol servers from glue hosts.
shell
Package shell provides permission-gated command execution tools for agents that explicitly opt into local coding workflows.
Package shell provides permission-gated command execution tools for agents that explicitly opt into local coding workflows.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL