An agent SDK
you can read

Ordinary Go packages and explicit types, with no framework to learn. Start at the top with one call, and move down a layer when you need to.

$go get github.com/sonnes/pi-go/pkg/pi

Read the documentation

first_call.go
prompt := ai.Prompt{
Messages: []ai.Message{
ai.UserMessage("Say hello in one sentence."),
},
}
msg, err := pi.GenerateText(ctx, spec, prompt)
fmt.Println(msg.JoinText(""))
$ go run ./first_call.go
Hello! How can I help you today?

pkg/pi

Start with one durable agent

pi.DurableAgent is the top of the stack. One call gives you an agent with tools and a transcript on disk.

pi-go reads the credentials from your environment. The spec string names the provider and the model. Next week, the same session ID resumes the same conversation.

How pi-go wires a provider
durable_agent.go
chat, err := pi.DurableAgent(ctx,
"anthropic-messages/claude-opus-4-7",
durable.WithStore(store),
durable.WithSessionID("ticket-8472"),
pi.WithTools(lookupOrder),
)
defer chat.Close()
msgs, err := chat.Run(ctx,
durable.Text("Where is order 1234?"),
).Wait()

pkg/durable

A session is an append-only tree

Each entry is written once and never changed, so the state of a session is a replay of its log. A branch moves the leaf to an earlier entry and the next run grows a sibling path beside the first. Both paths stay, and one JSONL file holds them.

Sessions, branches, compaction
ticket-8472.jsonl
  1. entryrolecontentnote
  2. e1userMy name is Ravi.
  3. e2assistantNoted.
  4. e3userWhat is my name?
  5. e4assistantRavi.leaf
  6. e3′userCall me R.Branch(e2)
  7. e4′assistantGot it, R.leaf

pkg/agent · pkg/ai

Tools are functions, runs are iterators

ai.DefineTool takes a Go function and derives the JSON schema from its input type, so the schema and the handler cannot differ.

A run returns a Go iterator of typed events. The cancel function of the context stops it — there is no second stop API and no goroutine to signal.

Retries, rate limits, sub-agents, and planning are functions you compose.

Inside the run loop
agent.go
weather := ai.DefineTool(
"get_weather",
"Get the current weather for a city",
func(ctx context.Context, in WeatherIn) (WeatherOut, error) {
return WeatherOut{Temp: "22°C"}, nil
},
)
a, err := pi.Agent(spec, pi.WithTools(weather))
for e, err := range a.Run(ctx, msg).Events() {
switch e.Type {
case agent.EventMessageUpdate:
io.WriteString(w, e.AssistantEvent.Delta)
}
}

pkg/ai/provider

The provider contract is one method

ai.TextProvider has one method, and every provider in the repository is that interface and nothing more. Four talk to an HTTP API; four drive an installed CLI as a subprocess. Each is a separate Go module, so you can import Anthropic without the Google SDK.

What each provider supports
providers
providertransportauthit drivesgo module
anthropic-messageshttpkey · oauththe Messages APIprovider/anthropic
openai-completionshttpkey · oauththe Completions APIprovider/openai
openai-responseshttpkey · oauththe Responses APIprovider/openairesponses
google-generativehttpkeythe Gemini APIprovider/google
claude-clisubprocessclithe Claude Code CLIprovider/claudecli
codex-clisubprocessclithe Codex CLIprovider/codexcli
cursor-clisubprocessclithe Cursor CLIprovider/cursorcli
antigravity-clisubprocessclithe Antigravity CLIprovider/antigravitycli

cmd/pi

Run it four ways

The same core runs as a library in your service, as a chat on your terminal, around an installed coding CLI, and inside a browser.

Driving an installed CLI
  1. library

    Import the packages into your own service. This is the main way.

    pkg/pi

  2. cli

    The pi command logs in, picks a model, and chats over the same core.

    pi login anthropic

  3. subprocess

    A spec prefix routes a run to an installed coding CLI.

    pi -m codex/gpt-5

  4. wasm

    The core packages build for the browser and run the same loop.

    GOOS=js GOARCH=wasm

the documentation

Every layer has a page of its own

Start at the top, or go straight to the one you need.