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
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 evalsor use the Go API.
| 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.
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 bashOr build from source:
go install github.com/urmzd/saige/cmd/saige@latest
go install github.com/urmzd/saige/cmd/saige-mcp@latestEach release also attaches Windows amd64 builds and a SHA256SUMS file. Update an installed CLI with saige update (saige update --check only reports).
go get github.com/urmzd/saigeAsk a question from the CLI:
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, or Vertex AI credentials
saige agent run - "What is retrieval-augmented generation?"Or run fully local with Ollama:
ollama pull qwen3.5:4b
saige agent run - --provider ollama "What is retrieval-augmented generation?"Build the smallest agent in Go. From a clone, run go run ./examples/quickstart/agent:
// 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)
}| 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 in RAG on Postgres. See the full example index for sub-agents, handoffs, durability, caching, approvals and knowledge graphs.
| 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.
Without flags, saige agent run - uses the default catalog preset and available credentials. --provider ollama serves qwen3.5:4b or any other chat model you have pulled. saige agent run NAME --session ID continues a saved conversation; --trace reports what loaded and ran on stderr. See the CLI reference for serve, RAG, knowledge graph, eval and catalog commands, and catalog presets for model selection and failover.
To use saige from an editor harness, run saige-mcp. See the MCP server setup.
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)
}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)
}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 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.
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-textThen 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)
}
}git clone https://github.com/urmzd/saige.git
cd saige
just check # lint, vet and test
just test-postgres # tests against Postgres
fsrc run README.md # re-embed examples into this README (CI fails on drift)See CONTRIBUTING.md.
- Docs index
- Go API reference
- agent: adapters, deltas, tools, sub-agents, conversation tree, testing
- rag: chunking, retrieval, reranking, HyDE, tool bindings
- rag/knowledge: knowledge graph backend
- eval: scorers, gates, experiments, LLM judges,
saige evals - CLI reference
- MCP server
- Harness tools, fs, exec, fetch, research
- Examples index
This repo's conventions are available as portable agent skills in skills/.
Apache 2.0. See LICENSE.
