Quickstart
From zero to a running agent with a durable event log and a verified replay.
Build a single-tool agent, persist it to SQLite, verify replay.
Install
go get github.com/jerkeyray/starlingGo 1.26+. SQLite is pure-Go via modernc.org/sqlite; no CGo.
Hello agent
package main
import (
"context"
"fmt"
"os"
"time"
starling "github.com/jerkeyray/starling"
"github.com/jerkeyray/starling/eventlog"
"github.com/jerkeyray/starling/provider/openai"
"github.com/jerkeyray/starling/step"
"github.com/jerkeyray/starling/tool"
)
type clockOut struct {
UTC string `json:"utc"`
}
func main() {
prov, err := openai.New(openai.WithAPIKey(os.Getenv("OPENAI_API_KEY")))
if err != nil {
panic(err)
}
// step.Now records the timestamp on the live run and returns the
// recorded value on replay, so the tool stays deterministic.
clock := tool.Typed(
"current_time",
"Return the current UTC time in RFC3339.",
func(ctx context.Context, _ struct{}) (clockOut, error) {
return clockOut{UTC: step.Now(ctx).UTC().Format(time.RFC3339)}, nil
},
)
a := &starling.Agent{
Provider: prov,
Tools: []tool.Tool{clock},
Log: eventlog.NewInMemory(),
Config: starling.Config{Model: "gpt-4o-mini", MaxTurns: 4},
}
res, err := a.Run(context.Background(), "What is the current UTC time?")
if err != nil {
panic(err)
}
fmt.Println(res.FinalText)
}tool.Typed derives the JSON schema from the input type. The agent
records ToolCallScheduled + ToolCallCompleted around every dispatch.
Persist the run
Swap the in-memory log for SQLite to keep events on disk:
log, err := eventlog.NewSQLite("starling.db")
if err != nil {
panic(err)
}
defer log.Close()
a := &starling.Agent{
Provider: prov,
Tools: []tool.Tool{clock},
Log: log,
Config: starling.Config{Model: "gpt-4o-mini", MaxTurns: 4},
}NewSQLite opens in WAL mode and auto-migrates. One writer, many readers.
Replay the run
Once a run is persisted you can re-execute it against the same agent and verify every emitted event matches the recording byte-for-byte:
import "errors"
if err := starling.Replay(ctx, log, runID, a); err != nil {
if errors.Is(err, starling.ErrNonDeterminism) {
// A tool output, prompt, or model changed since the original run.
// err carries a *replay.Divergence with seq + class fields.
}
panic(err)
}Replay reads recorded LLM and tool outputs from the log and never re-invokes
the provider. If your code path hits step.Now, step.Random, or
step.SideEffect, the recorded values are returned instead. Anything else
is non-deterministic and should be moved behind a step helper.
Inspect a run
go run github.com/jerkeyray/starling/cmd/starling-inspect starling.dbEmbedded read-only web UI: runs list, timeline, payload detail, replay controls, divergence rendering. Dark by default, click-to-pick diff page, syntax-highlighted JSON. Full tour in Inspector.