Skip to main content

saige

Active

Go SDK, CLI and MCP server for streaming agents, tools, memory, retrieval and evaluations, with pluggable model providers and durable execution.

Go 6Updated Oct 20, 2026
ai-agentsai-sdkanthropiccliconversation-treegoknowledge-graphllmmcpmcp-serverollamaopenaipostgresqlragrlhfshowcasestreamingtool-usetuivector-search

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

CI Go Reference License

Basic agent demo

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 eval or use the Go API.

Installation

Prerequisites

RequirementWhen
Go 1.26.9 or newerLibrary 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 OllamaModel-backed agent calls and live evals
PostgreSQL 18 with pgvector and pg_searchPostgres-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

PieceWhat it isGuide
AgentThe loop: model turn, tool calls, results, repeatagent
AdapterA types.Provider for one serving API: a vendor (Anthropic, OpenAI, Google) or a runtime (Ollama)concepts
Catalog and presetsWhat each model accepts, and named chains of complete configurationscatalog
PartsMessages and model output as ordered typed parts: text, media, tool calls, citationsparts
DialsModel-neutral settings compiled per attemptdials
ToolsTyped Go functions, MCP imports and tool packs; parallelism and tool choicetool calling, typed tools
DelegationHandoffs and sub-agentsdelegation
Agent definitionsOne agent as a versioned Markdown file the CLI, server and MCP server all loadagent definitions
RAGIngest, chunk, embed, hybrid search on Postgres, citationsrag
EvalsScorers, gates and experiments for agents, retrieval and graphseval
BatchesMany single-turn calls as one vendor batch job, resumable by job IDbatch processing
Durable runsJournaled steps that resume after a crash, on a local engine or Postgresdurable execution

See concepts for the full picture.

Examples

ExampleRun from the repository root
Minimal agentgo run ./examples/quickstart/agent
Typed toolgo run ./examples/quickstart/tool
Structured outputgo run ./examples/quickstart/structured
Model-neutral dialsgo run ./examples/quickstart/dials
Postgres RAGgo 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:

PackageDocumentationCovers
agentagent/README.mdAdapters, deltas, tools, sub-agents, markers, conversation tree, RLHF feedback, TUI, testing
ragrag/README.mdData model, chunking, retrieval, reranking, HyDE, metrics, tool bindings
rag/knowledgerag/knowledge/README.mdKnowledge graph backend: graph interface, hybrid search, deduplication, PostgreSQL backend, formatting
evaleval/README.mdScorers, gates, comparisons, experiments, LLM-as-judge, stream timing, live eval harness (saige eval)
cmd/saigecmd/saige/README.mdCLI reference: chat, ask, serve, rag, kg, eval
cmd/saige-mcpcmd/saige-mcp/README.mdMCP server setup for Claude Code, Codex, Gemini CLI
tools/researchtools/research/README.mdWeb search, file, and knowledge graph tools
toolsdocs/harness-tools.mdHarness toolset: groups, sandboxes, approvals, spill, CLI --tools
tools/fstools/fs/README.mdWorkspace read, list, glob, grep, write, edit with root confinement
tools/exectools/exec/README.mdSandboxed bash and execute_code with command, environment, and network policy; subprocess and Docker sandboxes
tools/fetchtools/fetch/README.mdURL fetch through a client that blocks private and metadata addresses
examplesexamples/README.mdRunnable 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.