bonnie

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 23 Imported by: 0

README

BONNIE

BONNIE

Durable agent runs for Go.
Survive a crash. Wait days for a human. Answer over HTTP.

Go Reference MIT

[!WARNING] Early and experimental. Use at your own risk. BONNIE is pre-1.0 software under active development. The API can change without notice, the durability and sandboxing claims are tested but not yet proven in production, and no release is suitable for workloads whose loss would hurt. Read Limits before you deploy anything with it.


An agent turn normally lives and dies with your process. Kill it mid-tool-call and the work is gone. Ask the user a question and you have to hold the process open until they answer.

BONNIE fixes that. It wraps the Kit agent SDK so a run becomes durable:

run, _ := runner.Start(ctx, "deploy-42", runtime.Input{Text: "Deploy the app."})

if run.State == runtime.RunWaiting {
    fmt.Println(run.Suspend.Prompt) // "Which region?"
    os.Exit(0)                      // ← the process can end here
}

Come back tomorrow, in a different process, and finish it:

run, _ := runner.Resume(ctx, "deploy-42",
    []runtime.InputResponse{{Text: "eu-west-1"}})

fmt.Println(run.Response) // "Deployed to eu-west-1."

The agent remembers the whole conversation, including which tools it already called — so it does not repeat a side effect it has already performed.

Contents

Install

As a library:

go get github.com/mark3labs/bonnie

As a CLI:

go install github.com/mark3labs/bonnie/cmd/bonnie@latest

With Nix. This gives you the CLI with the microsandbox CLI (msb) already on its PATH:

nix profile install github:mark3labs/bonnie   # or: nix run github:mark3labs/bonnie

Set a provider key. BONNIE uses whatever Kit is configured for:

export ANTHROPIC_API_KEY=sk-ant-...   # or OPENAI_API_KEY, or GEMINI_API_KEY

Requires Go 1.27+. Sandboxing is optional and needs Docker or msb.

Development shell

The flake also gives you a shell with Go 1.27, golangci-lint, goreleaser, and the microsandbox CLI:

nix develop
go test -race ./...

The repository ships an .envrc, so direnv allow enters the same shell on cd.

Other flake outputs:

Output What it is
packages.default, packages.bonnie the BONNIE CLI
packages.microsandbox the msb CLI plus its libkrunfw
apps.msb nix run github:mark3labs/bonnie#msb
overlays.default both packages, for your own nixpkgs

Quickstart: scaffold an agent

Scaffold an agent, edit one file, run it.

bonnie init my-agent --model anthropic/claude-sonnet-4-5
cd my-agent
go mod tidy
# edit instructions.md — that file is the agent's system prompt
bonnie dev

The tree is four things: main.go (one call — this is where the model, the sandbox, and the channels are configured, in code), instructions.md (the system prompt, read fresh at every start), and skills/ and workspace/ (seed directories — files under workspace/ are mirrored into every run's sandbox, and a file the model already wrote is never overwritten).

// main.go — the whole default agent
package main

import "github.com/mark3labs/bonnie"

func main() {
	bonnie.New(
		bonnie.WithModel("anthropic/claude-sonnet-4-5"),
	).Serve()
}
# talk to it over HTTP
curl -s localhost:8080/runs -d '{"text":"What are you?"}'

# or talk to it in the terminal — one durable conversation, live streamed
bonnie chat --addr :8080

There is no config file. A setting is either a file at a known path (instructions.md, workspace/, tools/) or an option in main.go, so a setting that does not exist is a compile error rather than a key nothing reads. -addr and -model are operator flags on the built binary and win over the options, so one binary can move port or model without a rebuild.

When you are ready to ship it, bonnie build compiles the tree — tools, instructions, and seed files embedded — into one static binary that serves on a host with no Go and no BONNIE install.

Quickstart

A durable run in 20 lines. The journal on disk is what makes it durable.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/mark3labs/bonnie/runtime"
	kit "github.com/mark3labs/kit/pkg/kit"
)

func main() {
	// Every message is journalled here before it is kept.
	journal, err := runtime.OpenSQLiteJournal(".bonnie")
	if err != nil {
		log.Fatal(err)
	}
	defer journal.Close()

	runner := runtime.NewRunner(journal, runtime.KitAgent(
		kit.WithModel("anthropic/claude-sonnet-4-5"),
	))

	run, err := runner.Start(context.Background(), "run-1",
		runtime.Input{Text: "In one sentence, what is a durable agent run?"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(run.Response)
}

Run it again with a different message and the same run ID. BONNIE replays the conversation first, so the agent remembers:

run, _ := runner.Start(ctx, "run-1", runtime.Input{Text: "What did I just ask?"})
// `You asked me "In one sentence, what is a durable agent run?"`

Inspect what happened, without a server:

bonnie runs list --journal .bonnie
bonnie runs show --journal .bonnie run-1
RUN    STATE      STEPS  LAST
run-1  completed  2      You asked me "In one sentence, what is a durable agent run?"

Park and resume

This is the headline feature. An agent asks a question, your process exits, and a completely new process finishes the job.

BONNIE ships two tools for this. Register them and the model can call them:

Tool Parks the run to...
ask_human ask the operator a question
request_approval get approval before a risky action

runtime.KitAgent registers both automatically.

runner := runtime.NewRunner(journal, runtime.KitAgent(
	kit.WithModel("anthropic/claude-sonnet-4-5"),
	kit.WithSystemPrompt("Before you deploy anything, use ask_human to ask "+
		"which region to deploy to."),
))

run, err := runner.Start(ctx, "deploy-42", runtime.Input{Text: "Deploy the app."})
if err != nil {
	log.Fatal(err)
}

if run.State == runtime.RunWaiting {
	fmt.Println("agent asks:", run.Suspend.Prompt)
	return // nothing is holding compute — the process may exit
}

Later, anywhere, as long as it can read the same journal:

run, err := runner.Resume(ctx, "deploy-42",
	[]runtime.InputResponse{{Text: "eu-west-1"}})

A parked run holds no compute. It costs nothing to wait a week.

See examples/hitl-restart for a runnable version that genuinely calls os.Exit between the two phases:

go run ./examples/hitl-restart -phase ask
# ...process exits, run is parked on disk...
go run ./examples/hitl-restart -phase answer -answer "eu-west-1"

Your own tools

A BONNIE tool is a Kit tool. Pass it through and it joins the sandboxed and human-in-the-loop sets:

type chargeInput struct {
	Amount int    `json:"amount" description:"Amount in cents."`
	UserID string `json:"user_id" description:"Who to charge."`
}

chargeCard := kit.NewTool("charge_card", "Charge a customer's card.",
	func(ctx context.Context, in chargeInput) (kit.ToolOutput, error) {
		// Runs in YOUR process, with your secrets. The model sees only
		// the result you return.
		if err := stripe.Charge(in.UserID, in.Amount); err != nil {
			return kit.ErrorResult(err.Error()), nil
		}
		return kit.TextResult("Charged."), nil
	})

runner := runtime.NewRunner(journal, runtime.KitAgent(
	kit.WithModel("anthropic/claude-sonnet-4-5"),
	kit.WithExtraTools(chargeCard),
))

You can also write a tool that parks the run — that is all ask_human is:

return kit.ToolOutput{
	Content: "Waiting for the finance team.",
	Halt:    true,
	FinalValue: runtime.SuspendRequest{
		Kind:   "approval",
		Prompt: "Approve a $4,000 refund?",
	},
}, nil

The run stops, Start returns with State == RunWaiting, and run.Suspend.Prompt carries your question.

Prefer scaffolding over hand-wiring? bonnie init --tools creates a tree with one sample tool and a main.go you own; tools there live in tools/<name>/tool.go as func Tool() kit.Tool, and the directory name is the tool's name. bonnie dev and bonnie build regenerate the wiring, so main.go never has to name a tool.

Sandboxing

By default, tool calls run as your process — your files, your network, your credentials. For anything untrusted, put them in a sandbox.

In an agent tree, that is one option — bonnie init scaffolds both lines commented out, so the choice is visible rather than silent:

bonnie.New(
	bonnie.WithSandbox(sandbox.Docker(sandbox.WithDockerImage("python:3.12-slim"))),
	bonnie.WithNetwork(sandbox.NetworkPolicy{Mode: sandbox.NetworkDenyAll}),
).Serve()

Wiring the runner yourself, it is the same provider one layer down:

provider := sandbox.Docker(sandbox.WithDockerImage("python:3.12-slim"))

runner := runtime.NewRunner(journal, sandbox.Agent(provider,
	kit.WithModel("anthropic/claude-sonnet-4-5"),
))

The model now gets bash, read_file, write_file, and list_files that run inside a container rooted at /workspace.

Backend Isolation You install Extra Go deps
sandbox.Local() none — dev only 0
sandbox.Docker() container namespaces Docker 0
sandbox.Microsandbox() microVM, guest kernel msb 0

All three drive a CLI, so BONNIE stays a single static binary.

The microsandbox adapter is verified on Linux with KVM (msb 0.6.18, all 18 conformance cases, network policies enforced with real egress). It has not been run on macOS with Apple Silicon, and its network policy is fixed at create time: reattaching under a different policy fails with ErrPolicyMismatch rather than silently using the old rules.

Lock down the network. A policy the backend cannot enforce is refused, and so is a policy with no sandbox to enforce it — never a silent allow-all:

provider := sandbox.Docker()
provider.SetNetworkPolicy(sandbox.NetworkPolicy{Mode: sandbox.NetworkDenyAll})

Pick the best backend available, without silently falling back to no isolation:

provider, err := sandbox.Select(ctx, sandbox.Microsandbox(), sandbox.Docker())

The sandbox opens on the first tool call that needs it, so a parked run holds no container. Read docs/SANDBOX.md before deploying.

Serve over HTTP

bonnie serve --journal .bonnie --model anthropic/claude-sonnet-4-5 --sandbox docker

Or run an agent tree — its configuration is Go in its own main.go, so the tree is served by running it. See Quickstart: scaffold an agent:

bonnie dev my-agent          # hot reload while you work on it
bonnie build my-agent        # one static binary, then run it anywhere

Or mount it in your own server:

runner := runtime.NewRunner(journal, runtime.KitAgent(opts...))
http.ListenAndServe(":8080", bonniehttp.New(runner).Handler())
Route Does
POST /runs start a run, or route to the one serving an address
GET /runs/{id} report a run's durable state
POST /runs/{id} send a message to an existing run
POST /runs/{id}/respond answer a parked run
POST /runs/{id}/cancel stop the turn in flight
GET /runs/{id}/stream NDJSON event stream, resumable via ?cursor=
# Start a run. It parks on a question.
curl -s localhost:8080/runs -d '{"text":"Deploy the app. Ask me the region first."}'
# {"run_id":"run-e8b3fa...","state":"waiting",
#  "suspend":{"kind":"question","prompt":"Which region?"}}

# Answer it.
curl -s localhost:8080/runs/run-e8b3fa.../respond \
  -d '{"responses":[{"text":"eu-west-1"}]}'
# {"run_id":"run-e8b3fa...","state":"completed","response":"Deployed to eu-west-1."}

Addresses

Chat platforms have threads, not run IDs. Pass an address and BONNIE keeps the mapping in the journal, so a restart does not orphan a conversation:

curl -s localhost:8080/runs -d '{"address":"slack:C123/T456","text":"hi"}'

The same address always resolves to the same run. POST /runs/{id} is the opposite: it targets one exact run and returns 404 rather than creating one.

Chat channels

Slack, Discord, and Telegram put the same durable runs into a conversation. Mount one in main.go, put its credentials in the environment, and run:

bonnie.New(
	bonnie.WithSlack(slack.Config{}),
	bonnie.WithDiscord(discord.Config{}),
	bonnie.WithTelegram(telegram.Config{Username: "mybot"}),
).Serve()
export SLACK_BOT_TOKEN=xoxb-... SLACK_SIGNING_SECRET=...
export DISCORD_BOT_TOKEN=... DISCORD_PUBLIC_KEY=...
export TELEGRAM_BOT_TOKEN=... TELEGRAM_WEBHOOK_SECRET=...
bonnie dev

Credentials are read from the environment and never from code; a missing one is a startup error that names the variable.

Each channel mounts one webhook (/slack/events, /discord/interactions, /telegram), verifies its platform's signature — Slack's v0 HMAC, Discord's Ed25519, Telegram's shared secret — and answers within the platform's ACK deadline while the turn runs on. The reply posts back to the thread; a parked run posts its question, and the next message on the thread is the answer.

The details are in docs/CHANNELS.md: the per-platform setup, the dispatch and steering rules, and what is deliberately not implemented (streaming edits, button HITL, attachments, gateway transports).

CLI

bonnie init     Scaffold an agent tree (main.go, instructions, seeds)
bonnie dev      Run an agent tree with hot reload and the built-in TUI
bonnie build    Compile an agent tree into one static binary
bonnie serve    Mount the HTTP channel and serve durable runs, with no tree
bonnie chat     Talk to a running agent in a terminal
bonnie runs     List and inspect durable runs
bonnie sandbox  Reclaim the sandboxes of terminal runs (prune)
bonnie version  Print the version
bonnie init my-agent --model anthropic/claude-sonnet-4-5
bonnie init .                      adopt this directory; never overwrites
bonnie init my-agent --tools       add a sample Go tool

bonnie dev my-agent                hot reload + the terminal interface
bonnie build my-agent              one static binary at ./my-agent
bonnie chat --addr :8080           talk to any running channel in a terminal

bonnie serve --addr :8080 --journal .bonnie \
             --model anthropic/claude-sonnet-4-5 \
             --sandbox docker --sandbox-deny-network

bonnie runs list --journal .bonnie --state waiting
bonnie runs show --journal .bonnie run-1
bonnie runs show --journal .bonnie run-1 --json | jq '.[] | select(.kind=="message")'

runs reads the journal directly, so it works while the server is stopped — which is exactly when you need it.

Storage

The journal is the durability seam. Two ship in the box:

runtime.NewMemoryJournal()            // tests and ephemeral runs
runtime.OpenSQLiteJournal(".bonnie")  // SQLite, one database for every run

SQLiteJournal writes <root>/journal.db through a pure-Go driver (modernc.org/sqlite), so BONNIE still builds and cross-compiles with CGO_ENABLED=0 and bonnie build still produces one static binary. The database runs in WAL mode and fsyncs every commit by default.

One record per row, so any SQLite client can read a run:

sqlite3 .bonnie/journal.db \
  "SELECT seq, role, text FROM records WHERE run_id = 'run-1' ORDER BY seq"

What the database buys over the JSONL files it replaced:

  • A step is atomic. A tool-calling step is one transaction, so the "torn single write" window a file append left is closed, not narrowed.
  • Concurrent writers are safe, not refused. SQLite serialises write transactions across processes and the (run_id, seq) primary key makes a reused sequence number a constraint violation. The per-run lock file, and the ErrRunOwnedElsewhere it produced, are gone.

Upgrading. A .bonnie that still holds runs/*.jsonl from an earlier BONNIE is imported on first open: records keep their sequence numbers, and each source file is renamed to <run>.jsonl.imported rather than deleted. The import is idempotent, so a crash halfway through costs a second read.

Bring your own by implementing seven methods:

type Journal interface {
	Append(ctx context.Context, rec Record) (seq int, err error)
	Replay(ctx context.Context, runID string) ([]Record, error)
	Checkpoint(ctx context.Context, runID string, state RunState) error
	State(ctx context.Context, runID string) (RunState, error)
	Runs(ctx context.Context, state RunState) ([]string, error)
	Persisted() bool
	Close() error
}

A table-driven conformance suite in runtime/journal_conformance_test.go runs against every implementation. Add yours to it and it inherits the whole suite.

Streaming

Subscribe to a run's events in-process:

events, unsubscribe := runner.Events().Subscribe("run-1", 0)
defer unsubscribe()

for ev := range events {
	fmt.Println(ev.Seq, ev.Type, ev.Text)
}

Or over HTTP, one JSON object per line:

curl -sN localhost:8080/runs/run-1/stream

Every event carries a monotonic seq. If a client drops, reconnect with the last one it saw and lose nothing:

curl -sN "localhost:8080/runs/run-1/stream?cursor=12"

Steer and cancel

runner.Steer("run-1", "actually, use eu-west-1")  // joins the running turn
runner.Cancel("run-1")                            // stops it

Cancelling keeps every completed step, so the run restores to a valid conversation and can be continued with Start.

Run states

pending → running → completed
                  → waiting    (parked for a human; resume with Resume)
                  → cancelled  (stopped by an operator; continue with Start)
                  → failed

How it works

BONNIE is built on four public Kit extension points. No fork, no patched SDK:

Need Kit API
Journal every message Options.SessionManager
Checkpoint each step Kit.OnStepFinish
Inject replayed context Kit.OnContextPrepare
Park for a human kit.ToolOutput{Halt, FinalValue}

Three properties are worth knowing, because they are the difference between a demo and something you can deploy:

  • Replay is lossless. Journalled messages keep their typed parts, so a resumed run knows which tools it called and what came back. It will not repeat a side effect it already performed.
  • A step commits atomically. A tool call and its result reach the journal as one write and one fsync (kit.StepAppender, adopted from Kit v0.106.0), so a crash cannot leave an unanswered tool call. If a torn step still reaches disk — from an older journal, a non-batching journal, or a short write — restore drops that incomplete step and records the repair.
  • Cancelling keeps finished work. Steps are persisted before the context is checked, so a cancelled turn loses only the step in flight.

Full detail, with the Kit citations, in docs/SPEC.md.

Limits

Stated plainly, because the failure modes are not obvious:

  • Sandboxing is opt-in. Without it, tool calls run as your process.
  • Docker is namespaces, not a kernel. Use microsandbox for hostile code.
  • microsandbox is verified on Linux/KVM only — not on macOS with Apple Silicon. Every network policy mode is enforced, but the policy is fixed at create time; reattaching under a different policy fails with ErrPolicyMismatch.
  • Sandbox egress is open unless you set a policy.
  • No auth verification on the HTTP channel. It carries a Principal; it does not check one. Authenticate in front of it. The chat channels are different: each verifies its platform's signature, and a channel without its credentials refuses to serve. That verifies the platform, not the person — a user ID inside a verified Slack event is Slack's word.
  • Run ownership is per host, and the journal no longer refuses a second writer. SQLite serialises write transactions and rejects a reused sequence number, so two processes writing one run cannot corrupt it. That is journal integrity, not turn coordination: two servers that both execute the same run still interleave the conversation. SQLite's locking also needs working POSIX locks, so a journal on a network filesystem is still unsafe.
  • Events are journal-anchored. The stream replays the journal past the in-memory backlog, so a reconnect — even after a restart — has no gap. Live-only deltas are the exception, marked as such.
  • Sandbox lifecycle is journalled, and reclaiming is manual. bonnie sandbox prune deletes the sandboxes of terminal runs; serve does not sweep them on its own yet.
  • The mark3labs modules are publicly fetchable. A scaffolded module runs go mod tidy and resolves bonnie and kit from the proxy; no go.work, no GOPRIVATE. Authoring an agent needs Go on your machine; the binary bonnie build produces needs nothing on the host.

Examples

Example Shows
examples/minimal one durable run, start to finish
examples/hitl-restart park, exit the process, resume
go run ./examples/minimal -text "What is a durable agent run?"

See examples/README.md for copy-pasteable commands.

Documentation

Document Purpose
docs/HANDOVER.md Picking up the project: state, pitfalls, what to do next
docs/SANDBOX.md Sandbox backends and the contracts an adapter must honour
docs/SPEC.md Specification: scope, verified Kit facts, known risks, invariants
docs/L2.md The agent tree: the default layout, configuration as code, init/dev/build, and the codegen contract
docs/TASKS.md Open work, and an archive of what shipped
docs/UPSTREAM.md The home for BONNIE's future asks of Kit; answered ones live in docs/archive/
CONTRIBUTING.md Boundary rule, workspace setup, commands
SECURITY.md Disclosure, and what v0.1.0 does not protect you from

Contributing

go build ./...
go test -race ./...
golangci-lint run

Or run the same loop with task check, and CI parity with task ci.

The live-model tests are behind a build tag and need a provider key:

go test -race -tags integration ./runtime ./sandbox

They skip, never fail, when no key is present. One rule matters above the rest: BONNIE uses the public Kit SDK only. See CONTRIBUTING.md.

License

MIT — see LICENSE.

Documentation

Overview

Package bonnie is the entry point of an authored agent tree.

A BONNIE agent is a directory of files whose meaning comes from their paths, and one Go file that starts it:

package main

import "github.com/mark3labs/bonnie"

func main() { bonnie.New().Serve() }

That is the whole default agent. Every slot in the tree has a framework default, and authoring the slot replaces it: instructions.md is the system prompt, workspace/ is the directory the agent's files live in, tools/ holds one directory per tool, and .bonnie holds the journal. Configuration that is not a file is code — an Option on New:

func main() {
	bonnie.New(
		bonnie.WithModel("anthropic/claude-sonnet-4-5"),
		bonnie.WithSandbox(sandbox.Docker()),
	).Serve()
}

There is no manifest file. Data-shaped settings live at their default paths, and everything else is a Go call, so a setting that does not exist is a compile error rather than a key that is accepted and ignored.

The tree's code — its tools — and the copies of its data files that a `bonnie build` binary carries are wired by codegen into bonnie_gen.go, which calls Register from its init. main.go never has to name them.

Index

Constants

View Source
const (
	// DefaultInstructions is the system prompt file, relative to the tree.
	DefaultInstructions = "instructions.md"

	// DefaultWorkspace is the directory the agent's files live in: the
	// working directory of the host's file tools without a sandbox, and the
	// seed mirrored into the sandbox with one.
	DefaultWorkspace = "workspace"

	// DefaultSkills is the tree's skills directory.
	DefaultSkills = "skills"

	// DefaultJournal is the directory the run journal is written to.
	DefaultJournal = ".bonnie"

	// DefaultAddr is the address the HTTP channel binds when none is given.
	DefaultAddr = ":8080"
)

The default layout of a scaffolded tree. These are the paths `bonnie init` writes, the paths codegen embeds, and the paths Agent.Run reads when no option overrides them. They are constants rather than five copies of a string literal, because the scaffold, the generator, the dev loop, and the runtime must agree on the answer: a rule written more than once is a rule one caller can honour while another misses it.

Variables

This section is empty.

Functions

func Register

func Register(t Tree)

Register hands the generated wiring to the runtime. The generated bonnie_gen.go calls it from init, before main runs.

It is the seam that keeps main.go short: adding a tool to the tree changes the generated file and nothing the author wrote.

Types

type Agent

type Agent struct {
	// contains filtered or unexported fields
}

Agent is a configured agent: the tree's defaults with the options applied over them. Build one with New, then Agent.Serve it.

Nothing is opened, bound, or read until it serves, so building an agent cannot fail and New returns no error. A setting that cannot apply — a network policy with no sandbox to enforce it, a model beside a host-supplied agent factory — is refused when serving starts, which is the first moment the whole configuration is known.

func New

func New(opts ...Option) *Agent

New builds an agent from the tree's default layout and the options.

func main() { bonnie.New().Serve() }

With no options that is a complete agent: instructions.md is the system prompt, workspace/ is the agent's root for files, .bonnie is the journal, the tools under tools/ are wired by codegen, and the HTTP channel is served on :8080. Each Option replaces one of those.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context) error

Run serves the agent until ctx ends, then drains in-flight turns and returns. It is Agent.Serve without the process: no flags, no signal handler, no exit — for a host that already owns those.

A run that parks holds no compute and lives in the journal, so stopping here is never destructive.

func (*Agent) Serve

func (a *Agent) Serve()

Serve runs the agent until the process is interrupted, then exits.

It owns the process, which is what makes a one-line main possible: it parses the operator flags a serving binary accepts, installs the signal handler, drains in-flight turns on SIGINT or SIGTERM, and exits non-zero after writing the error to stderr. A host that owns its own process calls Agent.Run instead.

The flags are -addr and -model, and each wins over the matching option, so an operator can move a built binary to another port or model without rebuilding it. `bonnie dev` starts a tree's binary with -addr, which is the whole contract between the dev loop and the child.

type Channel

type Channel interface {
	channel.Channel
	channel.Inbound
}

Channel is an inbound transport BONNIE can mount: the HTTP routes it needs and the inbound surface a run resolves through. Every adapter in channel/ is both.

type ChannelFunc

type ChannelFunc func(*runtime.Runner) (Channel, error)

ChannelFunc builds a channel for the runner that serves it. It is called once at start; an error stops the process before it listens.

type Option

type Option func(*config)

Option configures New. This is where a setting that is not a file in the tree lives: the model, a sandbox, an extra channel. A setting that does not exist is a compile error, which is the point.

func Quiet

func Quiet() Option

Quiet suppresses the startup banner. The no-sandbox warning is printed anyway: what runs unisolated must never be quieter than what does not.

func WithAddr

func WithAddr(addr string) Option

WithAddr binds the HTTP channel to addr instead of DefaultAddr.

func WithAgentFactory

func WithAgentFactory(f runtime.AgentFactory) Option

WithAgentFactory replaces the model-backed agent entirely with one the host builds itself. It is the escape hatch for a program that implements runtime.Agent — a test double, a router, a second framework — and wants BONNIE only for durability and transport.

It cannot be combined with the options that configure the agent BONNIE would have built (WithModel, WithSystemPrompt, WithSandbox, WithNetwork, WithTools, WithKit): the factory owns the agent, so those settings would be accepted and ignored. Agent.Run refuses instead, naming both.

What the factory owns, it owns completely: the tree's instructions and the tools codegen discovered do not reach it either. They are available through Registered for a host that wants them. The journal, the workspace, the channels, and the shutdown behaviour are unaffected — those are BONNIE's side of the boundary.

func WithChannel

func WithChannel(f ChannelFunc) Option

WithChannel mounts another inbound transport beside the HTTP channel.

func WithDiscord

func WithDiscord(cfg discord.Config) Option

WithDiscord mounts the Discord channel. DISCORD_BOT_TOKEN and DISCORD_PUBLIC_KEY come from the environment; see WithSlack.

func WithInstructions

func WithInstructions(path string) Option

WithInstructions reads the system prompt from path instead of DefaultInstructions. An empty path means the agent has no instructions file, which is how a host with no tree runs.

func WithJournal

func WithJournal(dir string) Option

WithJournal writes the run journal to dir instead of DefaultJournal.

func WithKit

func WithKit(opts ...kit.Option) Option

WithKit passes Kit options through to the agent, for settings BONNIE does not name itself.

func WithListener

func WithListener(ln net.Listener) Option

WithListener serves on an already-bound listener instead of dialling the configured address. Tests bind :0 with it and learn the port.

func WithModel

func WithModel(model string) Option

WithModel selects the model, as "provider/name". Without it, Kit's default applies.

func WithNetwork

func WithNetwork(p sandbox.NetworkPolicy) Option

WithNetwork constrains what the sandbox may reach. It needs a sandbox: a policy with nothing to enforce it is refused at startup, never stored and ignored.

func WithSandbox

func WithSandbox(p sandbox.Provider) Option

WithSandbox runs every tool call in p instead of in this process.

Without it, a model-chosen tool call has this process's files, network, and credentials, and Agent.Run says so at startup. See docs/SANDBOX.md.

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout is how long Agent.Run waits for in-flight turns to reach a checkpoint after a signal. A turn that is cut short still keeps its finished steps — the journal is what survives — but a clean stop is cheaper.

func WithSlack

func WithSlack(cfg slack.Config) Option

WithSlack mounts the Slack channel. Credentials come from the environment and never from code: SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET fill the config's empty fields, and a missing one is a startup error naming the variable. A webhook that does not verify its caller is a door with no lock.

func WithSystemPrompt

func WithSystemPrompt(prompt string) Option

WithSystemPrompt sets the system prompt directly, instead of reading the tree's instructions file. It wins over WithInstructions.

func WithTelegram

func WithTelegram(cfg telegram.Config) Option

WithTelegram mounts the Telegram channel. TELEGRAM_BOT_TOKEN and TELEGRAM_WEBHOOK_SECRET come from the environment; see WithSlack.

func WithTools

func WithTools(tools ...kit.Tool) Option

WithTools adds tools to the set the model may call, beside the tools codegen discovered under tools/ and Kit's core set.

func WithWorkspace

func WithWorkspace(dir string) Option

WithWorkspace roots the agent's files at dir instead of DefaultWorkspace. An empty dir means no workspace: the process's own directory stays the root, which is how a host with no tree runs.

The workspace is what keeps a model's write off the tree itself — the instructions, the journal, and the source beside them.

type Tree

type Tree struct {
	// Tools are the tools discovered under tools/, one per directory.
	Tools []kit.Tool

	// Instructions is the embedded copy of the tree's instructions file.
	Instructions string

	// Skills is the embedded copy of the tree's skills directory.
	//
	// Reserved: it is embedded so a later skill loader has it, and nothing
	// reads it today. It is stated here rather than implied, because a field
	// that quietly does nothing is the failure this package exists to avoid.
	Skills embed.FS

	// Workspace is the embedded copy of the tree's workspace seed files.
	// [Agent.Run] materialises them beside a built binary that has no tree, and
	// never overwrites a file that is already there.
	Workspace embed.FS
}

Tree is what codegen discovered in an agent tree: the code it wired and the data files it embedded. The generated bonnie_gen.go builds one and hands it to Register from its init, so main.go never names a tool or an embed.

A tree run from its source directory reads its data files from disk and uses the embedded copies only as a fallback. A binary from `bonnie build` has no tree beside it, so the embedded copies are all it has.

func Registered

func Registered() Tree

Registered returns what the generated file registered. It is the zero Tree when a tree has not been generated yet, which is what a hand-written main.go with no tools sees.

Call it at run time — inside main, or later. The generated file registers from init, and Go initialises every package-level variable before it runs any init function, so a package-level `var x = bonnie.Registered()` reads the empty tree.

Directories

Path Synopsis
Package agent implements BONNIE's L2 discovery: the authored agent tree.
Package agent implements BONNIE's L2 discovery: the authored agent tree.
Package channel defines BONNIE's inbound transport abstraction (L3).
Package channel defines BONNIE's inbound transport abstraction (L3).
chat
Package chat holds the plumbing every chat-platform channel shares: the journalled address map, per-run turn locks, the channel.SessionRef implementation, and the dispatch rule that sends a message to the right runner entry point.
Package chat holds the plumbing every chat-platform channel shares: the journalled address map, per-run turn locks, the channel.SessionRef implementation, and the dispatch rule that sends a message to the right runner entry point.
discord
Package discord is BONNIE's Discord inbound transport (L3).
Package discord is BONNIE's Discord inbound transport (L3).
http
Package http is BONNIE's HTTP inbound transport (L3).
Package http is BONNIE's HTTP inbound transport (L3).
slack
Package slack is BONNIE's Slack inbound transport (L3).
Package slack is BONNIE's Slack inbound transport (L3).
telegram
Package telegram is BONNIE's Telegram inbound transport (L3).
Package telegram is BONNIE's Telegram inbound transport (L3).
Package channeltest is the conformance suite for channel adapters, in the same spirit as the journal and sandbox suites.
Package channeltest is the conformance suite for channel adapters, in the same spirit as the journal and sandbox suites.
cmd
bonnie command
Command bonnie is the BONNIE developer CLI.
Command bonnie is the BONNIE developer CLI.
bonnie/tui
Package tui is the BONNIE terminal user interface.
Package tui is the BONNIE terminal user interface.
examples
hitl-restart command
Command hitl-restart is the headline demonstration: a run parks for a human, the process exits, and a completely new process finishes the run.
Command hitl-restart is the headline demonstration: a run parks for a human, the process exits, and a completely new process finishes the run.
minimal command
Command minimal starts one durable run and prints the answer.
Command minimal starts one durable run and prints the answer.
internal
treetest
Package treetest builds throwaway agent trees that compile against this checkout, for tests that need a real `go build` of a scaffolded module.
Package treetest builds throwaway agent trees that compile against this checkout, for tests that need a real `go build` of a scaffolded module.
Package runtime is BONNIE's durable execution layer (L1).
Package runtime is BONNIE's durable execution layer (L1).
Package sandbox gives a BONNIE run an isolated place to run tool calls.
Package sandbox gives a BONNIE run an isolated place to run tool calls.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL