Skip to content
urmzdPublic

About

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

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Latest commit

 

History

290 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 evals or 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.

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

Ask 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)
}

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 in RAG on Postgres. See the full example index for sub-agents, handoffs, durability, caching, approvals and knowledge graphs.

Usage

Concepts

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.

CLI

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.

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

Development

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.

Documentation

Agent Skill

This repo's conventions are available as portable agent skills in skills/.

License

Apache 2.0. See LICENSE.

About

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

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages