Repository navigation
Getting Started
Go 1.26 or later. Every module's go directive is 1.26, so the current Go release and the one before it both build them, and CI runs every module on 1.26 and 1.27. The core's only dependencies are a pure-Go SQLite driver and a JSON Schema validator; no network access is needed after modules download, and nothing in the library calls a model unless you give it one.
git clone https://github.com/joeylking/agent-runtime.git
cd agent-runtime
go test -race ./...
go run ./examples/scriptedThe example drives a scripted agent through three in-memory tools, including one malformed request the runtime rejects and one destructive request the policy denies, then prints the persisted event trace. There is no model and no network. Reading that trace is the fastest way to see what the runtime records.
It writes a fresh database under your per-user cache directory (os.UserCacheDir()/agentrt, created readable by you only) and prints its path as database: <path>; -db path names another. The database itself is created readable and writable by its owner only, like every database the runtime creates.
examples/live is the shortest complete consumer: the scripted example's tools plus a terminal one to finish on, an agent that is render.Messages and render.Decide around a model, a stderr trace, and a policy that sends the first change to the counter to an operator. It needs a local Ollama server holding qwen3:30b-a3b and nothing else: no key, no account, no cost.
The examples are their own modules, pinned to the released nested modules, so running them from a clone needs a workspace once:
go work init . ./providers/ollama ./providers/anthropic ./providers/openai ./mcp ./bench ./export/otel ./proxy ./examples/live ./examples/mcpThen:
go run ./examples/live # pauses, and prints the two below
go run ./cmd/agentrt -db <path> approve <run> # prints the request, then asks
go run ./examples/live -db <path> -resume <run>Its database defaults to a fixed name, live.db, under the same per-user cache directory, so the first and third commands find the same file without -db, but another account on a shared machine cannot pre-create or read it the way it could a fixed name in the shared temporary directory. The example prints both follow-up commands with the run id and the path filled in.
approve shows you the waiting request (the tool, its arguments one field per line, the presentation the policy wrote) and then asks approve approval <id> of run <run>? [y/N]. Type y. From a script, where there is no terminal to ask at, pass -approval <id> or -yes instead; with neither, it refuses rather than guesses. See The Operator Command.
Those three commands are the whole point of the project in one sequence: the run stops itself before a change, a human grants a specific request, and the consumer continues it. Nothing between the pause and the resume is the model's decision.
It runs ollama:qwen3:30b-a3b by default; -model openai:<id> with -base-url http://127.0.0.1:11434/v1 runs the same agent through the OpenAI-compatible adapter. Ctrl-C cancels between steps rather than killing the process outright, and once a run has been recorded the example prints the exact -resume command before it exits.
examples/mcp is the demo the project leads with: a local model, the MCP filesystem server, reads allowed, and a write that waits for an operator. It also needs Node. The server is pinned by version and integrity hash in examples/mcp/package-lock.json, so install it once, without running any package's install scripts:
(cd examples/mcp && npm ci --ignore-scripts)
go run ./examples/mcp pin
go run ./examples/mcp runMCP Tools walks through it.
cmd/agentrt is the operator surface over any runs database, including the scripted example's. Point it at the path the example printed:
go run ./cmd/agentrt -db <path> runs # every run, newest first, a page at a time
go run ./cmd/agentrt -db <path> show <run> # steps, and every approval waiting
go run ./cmd/agentrt -db <path> events <run> # the audit log; -json for JSON LinesIt reads -db or AGENTRT_DB, opens only a database that already exists (a mistyped path is an error, never a new empty file), opens it read-only for these three commands, and exits 0, 1, or 2. See The Operator Command.
import agentrt "github.com/joeylking/agent-runtime"
store, err := agentrt.OpenStore("runs.db")
driver, err := agentrt.NewDriver(agentrt.Config{
Store: store,
Agent: myAgent, // Decide(ctx, StepInput) (Decision, error)
Policy: agentrt.DefaultPolicy(), // read and local allowed, remote needs approval, destructive denied
Tools: []agentrt.Tool{...}, // each with a JSON schema, side-effect class, and timeout
})
run, err := driver.Start(ctx, "describe the goal", agentrt.DefaultLimits())OpenStore creates the file owner-only if it is new, and refuses one that group or others can write. Start blocks until the run ends or pauses. run.Status is then COMPLETED, FAILED with run.Reason, or WAITING_FOR_APPROVAL. A waiting run is continued with Approve or Reject, then Resume. While a run executes, the driver holds a lease on it, so another process cannot resume it at the same time.
driver, err := agentrt.NewDriver(agentrt.Config{
...
Model: &agentrt.ModelConfig{Model: myModel, Prices: prices, MaxRetries: 2},
})The agent then receives a ModelCaller in its StepInput. It is the agent's only handle to the model and does the accounting, retries, and limit checks. Retries are off unless MaxRetries is set. The model itself comes from a nested module:
go get github.com/joeylking/agent-runtime/providers/ollama@v0.2.0model, err := ollama.New(ollama.Config{Model: "qwen3:30b-a3b"})
prices := ollama.Free(model)See Models and Replay, Provider Adapters, and Rendering Model Context for the agent that goes with it. To put an MCP server's tools behind the same controls, see MCP Tools.
Four additions in v0.3.1 sit around the loop rather than in it. Two are nested modules, so each is its own go get:
go get github.com/joeylking/agent-runtime/bench
go get github.com/joeylking/agent-runtime/export/otelbench has no dependencies and requires no core version; export/otel requires core v0.5.0 at its current tag (v0.1.2); v0.1.1 required core v0.4.0, and v0.1.0 core v0.3.1. The other two are packages in the core you already have. In a clone of the repository both modules resolve through the workspace above.
// measuring: read result files, print the comparison, refusing mixed commits
files, _ := bench.ReadDir("results"); out, err := bench.Compare(bench.Latest(files), bench.CompareOptions{})
// testing: crash after the tool's effect, resume, and assert the contract held
res := testkit.Run(t, testkit.Scenario{Tools: tools, Policy: policy, Agent: agent}, testkit.Crash{Step: 0, At: testkit.AfterToolEffect})
// watching: follow the database from another process and send spans to the collector
// OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 go run ./export/otel/example -db runs.db
// approving: serve the signed channel over the database; the secret is at least 32 bytes
h, _ := webhook.New(webhook.Config{Approver: approver.New(store, nil), Secret: secret}); http.ListenAndServe(addr, h)Measuring Agents, Testing a Consumer, Exporting Events, and Approval Channels are the pages for each. The approval exchange runs with no model and no network beyond loopback:
go run ./examples/approverIf your tools have side effects that survive a crash, set Reconcile in the configuration. It runs before an interrupted run continues and tells the runtime whether to continue, complete, wait, or stop. Without it, a run whose crash interrupted a tool that is not read-only pauses for an operator rather than letting the agent ask for the same thing again. See Interruption and Reconciliation.
Read Upgrading to v0.3 before v0.3.0 or later opens an existing database: every older process and operator binary must be stopped first. v0.3.1 through v0.4.0 need no step beyond that; see Upgrading to v0.4 if you are coming from v0.3. v0.5.0 adds a migration of its own, so every older process must be stopped before it first opens the database; see Upgrading to v0.5.
Concepts names every type you will meet. Writing Tools and Writing a Policy are where the safety of your agent is actually decided.
Using it
- Getting Started
- Upgrading to v0.5
- Upgrading to v0.4
- Upgrading to v0.3
- Concepts
- Writing an Agent
- The Gate
- Writing Tools
- MCP Tools
- MCP Proxy
- Writing a Policy
- Models and Replay
- Provider Adapters
- Rendering Model Context
- Approvals and Resume
- Interruption and Reconciliation
- Run Lease
- Limits
- Testing a Consumer
- Re-checking a Run
- Measuring Agents
Inside it
- Internals
- Persistence and Inspection
- Performance
- The Operator Command
- Approval Channels
- Exporting Events
- Testing
Deciding on it
- Safety Properties
- Security Model
- Comparison with Frameworks
- Status and Roadmap
- Roadmap
- Design Decisions
- Essays
- Contributing
- Releasing
Reference