saige
Super Artificial Intelligence Graph Environment
A Go SDK for building AI agents, giving them context and memory, and evaluating them.
Install
·
Report Bug
·
Go Docs
Contents
Features
Build agents, supply their context and memory, and evaluate the results through shared Go interfaces.
- Streaming agents with typed message parts, parallel tool calls, sub-agents and handoffs. Use typed Go functions, MCP tools or the built-in harness tools.
- Model adapters and routing for Anthropic, OpenAI, Google and local Ollama, with catalog presets, retry, fallback and model-neutral dials.
- Versioned agent definitions loaded from Markdown, directories, HTTPS or Postgres. Share one definition across the CLI, HTTP server, MCP and editor harnesses. See agent definitions.
- Approvals and guardrails for tool execution and model input/output, with approval policies and guardrails.
- Context and memory through branching conversations, hybrid vector/BM25/graph retrieval, citations and approval-gated long-term memory. See RAG and knowledge graphs.
- Durable execution with resumable local or Postgres-backed runs, response caching and batch jobs. See durable runs.
- Evals with composable scorers, regression gates, comparisons, experiments and LLM judges. Run suites with
saige evalor use the Go API.
Installation
Prerequisites
| Requirement | When |
|---|---|
| Go 1.26.9 or newer | Library use and go install |
One of ANTHROPIC_API_KEY, OPENAI_API_KEY, Vertex AI credentials (GOOGLE_GENAI_USE_VERTEXAI=true and GOOGLE_CLOUD_PROJECT) or GOOGLE_API_KEY, or a local Ollama | Model-backed agent calls and live evals |
| PostgreSQL 18 with pgvector and pg_search | Postgres-backed retrieval, memory, conversation trees and durable runs |
Basic agent calls need no database. RAG also supports an in-memory store. For persistent deployments, one Postgres can hold vectors, BM25 indexes, conversation trees and durable runs. See deployment for the single-Postgres architecture.
CLI and MCP server
Install a pre-built binary (Linux and macOS, amd64 and arm64, checksum-verified):
curl -fsSL https://raw.githubusercontent.com/urmzd/saige/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/urmzd/saige/main/install.sh | BIN=saige-mcp bash
Or build from source:
go install github.com/urmzd/saige/cmd/saige@latest
go install github.com/urmzd/saige/cmd/saige-mcp@latest
Each release also attaches Windows amd64 builds and a SHA256SUMS file. Update an installed CLI with saige update (saige update --check only reports).
Library
go get github.com/urmzd/saige
Quick Start
CLI
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, or Vertex AI credentials
saige ask "What is retrieval-augmented generation?"
Without flags, saige ask uses the default catalog preset and available credentials. See catalog presets for model selection and failover. Run fully local with a model served by Ollama:
ollama pull qwen3.5:4b
saige ask --provider ollama "What is retrieval-augmented generation?"
--provider ollama serves qwen3.5:4b, or any other chat model you have pulled.
saige chat opens an interactive session. See the CLI reference for serve, RAG, knowledge graph, eval and catalog commands.
The smallest agent
Run the checked-in example from a clone, using your configured provider credentials or a running Ollama server with a chat model pulled:
git clone https://github.com/urmzd/saige.git
cd saige
go run ./examples/quickstart/agent
The example builds the default catalog preset, invokes one agent turn and prints the answer:
// The smallest agent: a catalog preset instead of a hand-built adapter.
// "default" serves the cheapest model of each vendor whose credentials are
// set (Anthropic, OpenAI, Google), then a model pulled into a local Ollama,
// failing over in that order.
package main
import (
"context"
"fmt"
"log"
"github.com/urmzd/saige/agent"
"github.com/urmzd/saige/agent/provider/catalog"
"github.com/urmzd/saige/agent/provider/preset"
"github.com/urmzd/saige/agent/types"
)
func main() {
ctx := context.Background()
bundle, err := preset.Build(ctx, catalog.Default(), "default", nil, preset.Options{})
if err != nil {
log.Fatal(err)
}
defer bundle.Close(ctx)
a, err := agent.New(agent.Config{SystemPrompt: "Answer in one sentence."}, agent.WithPreset(bundle))
if err != nil {
log.Fatal(err)
}
text, err := agent.CollectText(a.Invoke(ctx, []types.Message{types.UserMsg(types.Text("What is RAG?"))}))
if err != nil {
log.Fatal(err)
}
fmt.Println(text)
}
A typed tool
agent.Func derives the tool’s JSON schema from a struct and decodes the model’s arguments into it.
// A typed tool: agent.Func derives the JSON schema from WeatherIn and
// decodes the model's arguments into it.
package main
import (
"context"
"fmt"
"log"
"github.com/urmzd/saige/agent"
"github.com/urmzd/saige/agent/provider/catalog"
"github.com/urmzd/saige/agent/provider/preset"
"github.com/urmzd/saige/agent/types"
)
type WeatherIn struct {
City string `json:"city" description:"City name"`
}
func main() {
ctx := context.Background()
bundle, err := preset.Build(ctx, catalog.Default(), "default", nil, preset.Options{})
if err != nil {
log.Fatal(err)
}
defer bundle.Close(ctx)
weather := agent.Func("weather", "Current weather for a city",
func(rc agent.RunContext[agent.NoDeps], in WeatherIn) (string, error) {
return "18C and sunny in " + in.City, nil
})
a, err := agent.New(agent.Config{Tools: types.NewToolRegistry(weather)}, agent.WithPreset(bundle))
if err != nil {
log.Fatal(err)
}
text, err := agent.CollectText(a.Invoke(ctx, []types.Message{types.UserMsg(types.Text("What's the weather in Lisbon?"))}))
if err != nil {
log.Fatal(err)
}
fmt.Println(text)
}
Structured output
agent.Structured[T] returns the answer as a Go value, checked against the schema derived from T, and asks the model to repair an invalid answer.
// Structured output: agent.Structured decodes the answer into a Go struct,
// checks it against the schema derived from the struct, and asks the model
// to repair an invalid answer.
package main
import (
"context"
"fmt"
"log"
"github.com/urmzd/saige/agent"
"github.com/urmzd/saige/agent/provider/catalog"
"github.com/urmzd/saige/agent/provider/preset"
"github.com/urmzd/saige/agent/types"
)
type Triage struct {
Priority string `json:"priority" enum:"low,medium,high"`
Summary string `json:"summary"`
}
func main() {
ctx := context.Background()
bundle, err := preset.Build(ctx, catalog.Default(), "default", nil, preset.Options{})
if err != nil {
log.Fatal(err)
}
defer bundle.Close(ctx)
a, err := agent.New(agent.Config{SystemPrompt: "Triage support tickets."}, agent.WithPreset(bundle))
if err != nil {
log.Fatal(err)
}
ticket := []types.Message{types.UserMsg(types.Text("Checkout returns HTTP 500 for every customer."))}
out, _, err := agent.Structured(ctx, a, ticket, agent.OutputSpec[Triage]{Repair: 1})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", out.Priority, out.Summary)
}
Images and documents
A message is an ordered list of typed parts. Media is a part whose Source holds bytes, an https URL, a workspace reference or a vendor upload:
msg := types.UserMsg(types.Text("What is in this image?"), types.Image(types.Bytes(types.MediaPNG, data)))
A part the serving model cannot take is rejected unless you permit a conversion, such as extracting a PDF’s text. Full programs: examples/parts. See message parts and modality conversion.
Dials
Dials name an intent instead of a vendor parameter, and are compiled for whichever model serves each call, so one setting works across vendors and survives a failover:
agent.WithDials(types.Dials{Creativity: new(types.CreativityFocused), Reasoning: &types.ReasoningDial{Depth: types.DepthHigh}})
A dial a model cannot honor is mapped to the nearest value or dropped, and reasoning outranks creativity when a model rules out sampling while it thinks. Full program: examples/quickstart/dials. See dials.
RAG on Postgres
Start Postgres 18 with pgvector and pg_search, and pull a local embedding model:
docker run -d --name saige-postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres paradedb/paradedb:0.26.1-pg18
ollama pull nomic-embed-text
Then ingest a document and run a hybrid search (vector similarity fused with BM25). embedderregistry.Text turns any adapter’s text embedder into the pipeline’s embedder:
// RAG on Postgres: ingest a document, then run a hybrid search that fuses
// pgvector similarity with pg_search BM25. Embeddings come from a local
// Ollama runtime serving nomic-embed-text (768 dimensions, the default
// column size).
package main
import (
"context"
"fmt"
"log"
"github.com/urmzd/saige/agent/provider/ollama"
"github.com/urmzd/saige/postgres"
"github.com/urmzd/saige/rag"
"github.com/urmzd/saige/rag/embedderregistry"
"github.com/urmzd/saige/rag/extractor"
"github.com/urmzd/saige/rag/pgstore"
ragtypes "github.com/urmzd/saige/rag/types"
)
const dsn = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
func main() {
ctx := context.Background()
pool, err := postgres.NewPool(ctx, postgres.Config{URL: dsn})
if err != nil {
log.Fatal(err)
}
defer pool.Close()
if err := postgres.RunMigrations(ctx, pool, postgres.MigrationOptions{}); err != nil {
log.Fatal(err)
}
client, err := ollama.NewClient(ollama.Config{Host: "http://localhost:11434", EmbeddingModel: "nomic-embed-text"})
if err != nil {
log.Fatal(err)
}
store, err := pgstore.New(pgstore.Config{Pool: pool})
if err != nil {
log.Fatal(err)
}
emb := ollama.NewEmbedder(client)
pipe, err := rag.New(rag.Config{Store: store},
rag.WithContentExtractor(extractor.NewAuto()),
rag.WithEmbedders(embedderregistry.NewTextOnly(embedderregistry.Text(emb))),
rag.WithRecursiveChunker(512, 64),
rag.WithBM25(nil), // keyword search runs in Postgres through pg_search
)
if err != nil {
log.Fatal(err)
}
defer pipe.Close(ctx)
doc := "saige stores vectors, BM25 indexes and conversation trees in one Postgres 18 database."
if _, err := pipe.Ingest(ctx, &ragtypes.RawDocument{SourceURI: "notes://saige", MIMEType: "text/plain", Data: []byte(doc)}); err != nil {
log.Fatal(err)
}
res, err := pipe.Search(ctx, "Which database does saige use?", ragtypes.WithLimit(3))
if err != nil {
log.Fatal(err)
}
for _, hit := range res.Hits {
fmt.Printf("%.3f %s\n", hit.Score, hit.Variant.Text)
}
}
Run the other programs from the same clone using the commands below.
Concepts in 2 minutes
| Piece | What it is | Guide |
|---|---|---|
| Agent | The loop: model turn, tool calls, results, repeat | agent |
| Adapter | A types.Provider for one serving API: a vendor (Anthropic, OpenAI, Google) or a runtime (Ollama) | concepts |
| Catalog and presets | What each model accepts, and named chains of complete configurations | catalog |
| Parts | Messages and model output as ordered typed parts: text, media, tool calls, citations | parts |
| Dials | Model-neutral settings compiled per attempt | dials |
| Tools | Typed Go functions, MCP imports and tool packs; parallelism and tool choice | tool calling, typed tools |
| Delegation | Handoffs and sub-agents | delegation |
| Agent definitions | One agent as a versioned Markdown file the CLI, server and MCP server all load | agent definitions |
| RAG | Ingest, chunk, embed, hybrid search on Postgres, citations | rag |
| Evals | Scorers, gates and experiments for agents, retrieval and graphs | eval |
| Batches | Many single-turn calls as one vendor batch job, resumable by job ID | batch processing |
| Durable runs | Journaled steps that resume after a crash, on a local engine or Postgres | durable execution |
See concepts for the full picture.
Examples
| Example | Run from the repository root |
|---|---|
| Minimal agent | go run ./examples/quickstart/agent |
| Typed tool | go run ./examples/quickstart/tool |
| Structured output | go run ./examples/quickstart/structured |
| Model-neutral dials | go run ./examples/quickstart/dials |
| Postgres RAG | go run ./examples/quickstart/rag |
The agent examples require a configured model provider. The RAG example requires the Postgres and Ollama setup above. See the full example index for sub-agents, handoffs, durability, caching, approvals and knowledge graphs.
Documentation
Every guide is listed in the docs index. Each package also has its own README:
| Package | Documentation | Covers |
|---|---|---|
agent | agent/README.md | Adapters, deltas, tools, sub-agents, markers, conversation tree, RLHF feedback, TUI, testing |
rag | rag/README.md | Data model, chunking, retrieval, reranking, HyDE, metrics, tool bindings |
rag/knowledge | rag/knowledge/README.md | Knowledge graph backend: graph interface, hybrid search, deduplication, PostgreSQL backend, formatting |
eval | eval/README.md | Scorers, gates, comparisons, experiments, LLM-as-judge, stream timing, live eval harness (saige eval) |
cmd/saige | cmd/saige/README.md | CLI reference: chat, ask, serve, rag, kg, eval |
cmd/saige-mcp | cmd/saige-mcp/README.md | MCP server setup for Claude Code, Codex, Gemini CLI |
tools/research | tools/research/README.md | Web search, file, and knowledge graph tools |
tools | docs/harness-tools.md | Harness toolset: groups, sandboxes, approvals, spill, CLI --tools |
tools/fs | tools/fs/README.md | Workspace read, list, glob, grep, write, edit with root confinement |
tools/exec | tools/exec/README.md | Sandboxed bash and execute_code with command, environment, and network policy; subprocess and Docker sandboxes |
tools/fetch | tools/fetch/README.md | URL fetch through a client that blocks private and metadata addresses |
examples | examples/README.md | Runnable example index |
API reference for every package: pkg.go.dev/github.com/urmzd/saige
Agent Skill
This repo’s conventions are available as portable agent skills in skills/.
License
Apache 2.0. See LICENSE.