Build durable AI agents in Go. Side effects that fire at most once. A pause you can clear beats a double-fire you can't.
One append-only journal gives four guarantees:
Side effects fire at most once, even across a crash, so they need not be idempotent.
Thousands of concurrent durable runs in one process. It is a library; there is no cluster to run.
A cryptographically verifiable audit trail: RFC 6962 Merkle proofs, checkable offline without trusting the vendor.
Provably convergent shared state: the convergence conditions are machine-checked.
All four come from the same journal, in a plain-Go library, for agents that move money, change records, or act under audit.
Ambient agents. An ambient agent runs unattended: it sleeps until a trigger (a schedule or an
event) wakes it, works over hours or days, and pauses to ask a human only when it needs judgment,
with nobody watching each step. Such an agent has to be safe to crash, safe to re-trigger, and
provable afterwards. Bide provides the durable lifecycle for this: durable Sleep/WaitUntil timers, a pluggable Waker for time- or event-driven wakeups, and
durable Interrupt/AnswerInterrupt for typed human-in-the-loop, all on the same journal. You bring the
trigger source and the oversight UI; the runtime keeps each run correct across sleeps, crashes and
node handoffs.
Status: working v0, live-verified end to end. Requires Go 1.27.
One journal, four guarantees
The agent loop is about 40 lines. The four guarantees come from the durable, append-only journal
underneath it.
1 · At most once (measured)
Temporal, DBOS, LangGraph, trpc-agent-go, ADK and eino resume by re-running: activities or steps
must be idempotent, so a non-idempotent side effect (a charge, an email, a shipment) can fire twice
across a crash. A crash-injection benchmark (chaos/, cross-SDK results in
benchmarks/) drives a non-idempotent charge through every crash point:
Each framework either never repeated the charge or charged again; the worst-case count depends on
how often a framework saves state, so it is not a ranking. Each competitor adapter passes a fairness test (a run with no crash charges exactly once, and where
the framework has a resume, it works), and LangGraph runs in its most durable documented mode. What
none of the benchmarked frameworks has: a durable attempt marker written before a non-idempotent
write, and halt-on-unknown-outcome on resume: if a write's result was never journaled, the run
stops for a human decision.
When the outcome is unknown, it stops
The hard case is a side effect whose call left the process but whose result never reached the
journal. The attempt marker lets a resumed run tell "never started" from "started, outcome
unknown", and the unknown case is resolved in a fixed order:
Which tier a tool lands in is set by its declared Safety: mark it read-only or idempotent and
an unknown outcome auto-retries; declare none of those and it
halts. Retry-safety is opt-in; without it, the default is to pause.
Most unknowns never reach a person: an idempotency key lets the provider dedupe a safe retry, and
for systems without one (email, internal services) a reconciler resolves the step from the record
it left (agent.ResolveHalt). A person is asked only when neither applies.
[!IMPORTANT]
When an action moves money, changes a record, or happens under audit and its outcome cannot be
known, the run stops. A human or a reconciler can clear a pause; a double charge cannot be taken
back.
2 · Durable execution as a library
Temporal provides durable execution with a server and a worker fleet. Here it comes from a store
adapter you already run (SQLite locally, Postgres in production). A hello-world imports only the
standard library: no Temporal, gRPC or vector database dependency (enforced by
architecture_test.go).
One process can keep many durable runs in flight at once. Agent work is I/O-bound (waiting on model and tool calls), which goroutines absorb
without a cluster. The cmd/bench harness measures it: 20,000 runs, 5,000 in
flight at a time, each blocking ~100ms on the model, finish in about half a second (~470ms) of
wall-clock on a 10-core Apple silicon Mac (measured at v0.7.0), and about one second on a standard 4-vCPU CI runner,
on a few thousand goroutines and tens of MB (go run ./cmd/bench -runs 20000 -concurrency 5000 -latency 50ms; reproducible with the Benchmark workflow). This measures throughput; per-call latency is the provider's. At high fan-out the limit is the
durable store's write throughput. Every concurrent run keeps all four guarantees. For reliability
under load there are per-attempt timeouts, retry with backoff that classifies transient and
terminal errors, hedged model calls (race a backup, take the first, for tail latency and provider
failover), and a rate limiter for model and tool calls
(middleware, docs/guides/reliability.md).
For high availability, any node resumes any run from the shared store, and competing drivers
coordinate through a per-run lease (agent.Lease): normally one process drives a run at a time,
and a crashed holder's lease expires so another node's agent.RecoverLoop takes it over. A holder
that stalls past its lease can wake still driving, but it cannot fire a side effect a second time:
at-most-once rests on the attempt claim, not on the lease. Tests cover concurrent-worker mutual exclusion, crash-and-takeover, and at-most-once under concurrent
drivers on the in-memory store (agent/ha_e2e_test.go), and on Postgres across real processes that
the tests kill, stall past their lease and restart (store/postgres/ha_multiproc_test.go; the
Postgres backend implements the lease with a DB-clock upsert).
3 · A verifiable audit trail from the same journal
The journal that makes resume safe is also the audit record, committed with the cryptography
Certificate Transparency uses (RFC 6962, checked
against the published reference vectors). A third party checks a proof without trusting you, your
database or your logs:
Inclusion proof: prove one specific action happened (this charge, this approval) in
O(log n), revealing no other record (only its position and the run's size). Selective
disclosure for an auditor.
Consistency proof: prove the history was only ever appended to, never rewritten or
reordered.
Signed tree head + continuous anchoring: AuditedStore signs a commitment per step and
publishes it out-of-band to an external transparency log, so tampering can be proven.
Who acted, under what authority: the same leaf can commit to the acting identity (actor, on
whose behalf, under which signed grant) and enforce delegated authority as a governed invariant,
so a proof shows who was authorized for what happened. Bring your own IdP; this
makes the authorized action provable, it does not replace authentication.
Produce a portable ProofBundle for one action (audit.ProveToolCall), or a whole-run EvidencePackage
(audit.Evidence) bundling every material action's proof into one file, and hand it to an auditor
who verifies it offline with bide-audit verify / verify-evidence or a stdlib-only verifier that
never imports the SDK. The same spine carries the rest of the accountability layer, all
verifiable offline: proof-carrying runs (one RunCertificate attesting a
whole run's policy compliance), signed capability grants with attenuating delegation, authority
earned from a clean audit trail, and governed k-of-n quorum. → docs/guides/audit.md
4 · Provably convergent shared state (gsm)
Many agents can change shared state concurrently, even with events that conflict and break
invariants, and every replica converges to the same valid state. The idea is convergence
by compensation: repair always terminates and commutes with events, so the order events replay in
cannot change the result. The theory, normalization
confluence, is an exact regime map
of governed concurrent state: in every regime, a machine-checked exact condition, a hardness result
showing no efficient one exists, or a gap stated in the open. The proofs are axiom-free and
CI-checked on Coq 8.18, 8.20 and Rocq 9.3
().
Checked at build time. Describe shared state as a registry of variables, invariants and
events; gsm's Build checks the convergence conditions or refuses with a concrete
counterexample.
Re-checked by the proof itself. Every registry machine gsm returns is re-certified in-process
by a checker generated from the Rocq proof; if it doesn't certify, there is no machine.
Federations too. For registries linked across teams or organizations, repair composes into a
unique normal form, and event order across registries is made safe by two local checks per edge
(C1 and C2) that gsm runs at build time.
Tested at scale. 10,000,000 governed agents driven through random, invariant-violating
orders, all converging to the same valid state, each with an audit proof that verifies offline
(a framework-level test: stub model, in-memory store; see
docs/testing/testing.md).
The claim is precise: order-independent convergence of the replay for a machine that meets the
conditions, on discrete, deterministic state, not "agents always agree". Details are in the
governance guide; the exact scope (causal delivery and Independent
pairs, delivery obligations, which checks are proof-generated versus Go, and gsm version history)
is in known limitations.
vs. durable-execution and agent runtimes
Bide
Temporal / DBOS
ADK · eino · trpc · langchaingo
Non-idempotent side effect on crash
At most once (halts on unknown outcome)
At least once; activities/steps must be idempotent
At least once; re-runs (measured 4–64×)
Deployment
Library + a DB you already run
Server + worker fleet
Library
Tamper-evident audit
RFC 6962 Merkle spine (same journal)
Not built in
None
Convergent shared state
Provable (gsm)[^gsm]
N/A
None
[^gsm]: The convergence theorem is machine-checked. gsm's Build checks each machine against its conditions and returns it only after the table oracle, Go generated from the proof, re-checks it in-process; for combinator rules inside its fragment and within a cost cap, the rules oracle re-checks it from the rules as well. A federation's own conditions, including the event-order checks C1 and C2, are checked by gsm's Go code. bide requires gsm v0.14.0; v0.13.0 added the C1 and C2 checks (v0.12.0 had neither), and v0.12.0 fixed the Build gap of v0.11.0 for guards and effects that read variables another event writes. Exact scope: known limitations.
Other details
The graph is optional. Write plain Go (if, for, functions); RenderMermaid and Topology derive the graph. If you prefer authoring topology, the plan builder does that and runs on the same runtime. See Graphs.
Claude's extended thinking works with tools. Thinking signatures are kept across turns, which the API requires when thinking and tool use are combined.
Tool schemas per provider. A tool's schema is generated once from its Go type and emitted in each provider's dialect, including OpenAI strict mode and Gemini's subset.
Model adapters. Native Claude and Gemini, plus any OpenAI-compatible endpoint (OpenAI, Ollama, DeepSeek, Groq, OpenRouter, vLLM, Azure, xAI) via WithBaseURL.
Failover across nodes. With Postgres, any node can resume any run. A per-run lease stops two nodes driving the same run, and agent.RecoverLoop picks up a crashed node's runs when its lease expires.
Graphs
In a graph-first framework the graph is what you author and what executes, with nodes, edges, a
state object, and sometimes a visual builder on top. In bide the same authoring surfaces exist, up
to a visual builder, as optional layers over a plain-Go journal.
A graph adds no expressive power. Anything a graph computes, ordinary control flow computes: a
computation graph is a control-flow graph, and sequence, selection, and iteration suffice to express
any of them. There is no agent behavior you can build as a node-and-edge graph that you cannot write
with if, for, and functions. What a graph adds is a first-class representation of the flow
that you can inspect, visualize, validate statically, and author outside the code. That is a
tooling layer, and it is not required to build agents.
So the substrate here is plain Go, and the guarantees (durability, at-most-once, the verifiable
trail) come from the journal. The graph exists as a derived view:
RenderMermaid reconstructs it from what actually ran.
To author a graph, use the plan package: a constrained, type-checked flow builder that compiles
to this runtime and inherits at-most-once and the audit trail. You wire typed nodes (Step, Tool, Model, Switch, fan-in Join, bounded
LoopBack) into a Flow, or author the same topology as declarative config (plan.Load) that a
higher layer such as a visual builder can emit.
An authored graph is a diagram; a derived graph is reconstructed from the journal, so it is
exactly what ran. The plan layer ties
the two together: Topology() and RenderMermaid() expose the declared shape, and Conform()
cryptographically checks that a run followed the topology it declared. The rule that keeps any such layer from forking the runtime is
that a new surface may add a way to author, never a way to execute: every layer lowers to the one
journal-backed runtime. See docs/guides/flows.md.
Three ways to author the same flow
The same order-triage flow, three ways. Plain Go is the default: write ordinary control flow, and name the steps the journal must make crash-safe.
// classify, then branch: rush orders reserve-then-finalize, the rest decline.
assess, _ := journal.Step(ctx, "order-42", "classify",
func(ctx context.Context) (Assessment, error) { return classify(order) },
agent.WithSafety(agent.Safety{ReadOnly: true})) // safe to re-run after a crash
var receipt Receipt
if assess.Rush {
res, _ := journal.Step(ctx, "order-42", "reserve", // a side effect: at most once
func(ctx context.Context) (Reservation, error) { return reserve(assess) })
receipt, _ = journal.Step(ctx, "order-42", "finalize",
func(ctx context.Context) (Receipt, error) { return finalize(res) })
} else {
receipt, _ = journal.Step(ctx, "order-42", "decline",
func(ctx context.Context) (Receipt, error) { return decline(assess) })
}
To make that flow an inspectable artifact, the plan builder wires typed nodes into a Flow that lowers to the same runtime:
flow, err := plan.Load[Order, Receipt](configBytes, reg) // same topology, same Digest()
All three lower to the one journal-backed runtime, so at-most-once, HA resume and the verifiable trail apply whichever you pick.
Ambient runs: durable sleep, wake, and interrupt
An ambient run does not sit in a synchronous chat loop. It sleeps, wakes on a trigger, and pauses for a human, and every
one of those transitions is a durable, at-most-once step on the journal, so the run survives crashes
and node handoffs between them.
Sleep until a deadline.Sleep/WaitUntil pause a run and journal its wake time, so the pause
outlives a restart. Re-invoking at the wake time resumes exactly once.
Wake on time or event. A pluggable Waker (in-process MemWaker by default) re-invokes a due
run; the trigger source is yours (an in-process loop, a cron, a queue, an inbound webhook), so the
same substrate drives both scheduled and event-driven agents. A wake that cannot be scheduled
fails the run (wrapping ErrStorage) rather than leave it asleep with nothing to wake it, and
RecoverLoop retries it.
Interrupt for a human, durably.Interrupt[T]/AnswerInterrupt pause a run at any point to request a
typed decision and resume with the human's answer as a journaled step (see
Human-in-the-loop). Approve/deny is the boolean special case.
You supply the trigger source and the oversight surface; the runtime keeps the run correct across
every sleep, wake, interrupt, crash, and handoff. Runnable in examples/signals (deliver an event
into a waiting run), examples/interrupt (human-in-the-loop pause/resume), and examples/recover
(durable resume). See the signals and ambient guide.
At most once, in code
// A tool that moves money is a write: not ReadOnly, not Idempotent.
charge := agent.MustFunc("charge_card", "Charge the customer", func(ctx context.Context, in ChargeArgs) (Receipt, error) { /* ... */ })
// If the process crashes after the charge fires but before its result is journaled,
// resume does NOT run it again: it returns *OutcomeUnknown so you confirm, not double-charge:
_, err := a.Run(ctx, runID, agent.UserText(input))
if halt, ok := errors.AsType[*agent.OutcomeUnknown](err); ok {
// halt.Op.ToolName == "charge_card": outcome unknown, a human decides, no double side effect.
}
Quickstart
Requires Go 1.27 (the core uses generic methods). If go version is older, upgrade or set
GOTOOLCHAIN=go1.27.0.
The core package is agent, imported from github.com/bide-ai/bide/agent
(as the block below shows). In a module of your own, install it and the SQLite store with
go get github.com/bide-ai/bide/agent@latest github.com/bide-ai/bide/store/sqlite@latest (or write the code and run go mod tidy).
No API key? The getting started guide runs the
same agent offline with a scripted model.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/bide-ai/bide/agent"
"github.com/bide-ai/bide/model/openai"
"github.com/bide-ai/bide/store/sqlite"
)
type WeatherArgs struct {
City string `json:"city" desc:"city name"`
}
type Weather struct {
TempF int `json:"temp_f"`
Sky string `json:"sky"`
}
func main() {
// Any OpenAI-compatible endpoint (here OpenRouter); swap the base URL for Ollama, etc.
model := openai.New(os.Getenv("OPENROUTER_API_KEY"),
openai.WithBaseURL("https://openrouter.ai/api/v1"),
openai.WithModel("openai/gpt-4o-mini"))
// A tool is a typed Go function; its schema is derived automatically.
weather := agent.MustFunc("get_weather", "Current weather for a city",
func(_ context.Context, in WeatherArgs) (Weather, error) {
return Weather{TempF: 68, Sky: "sunny"}, nil
}, agent.WithSafety(agent.Safety{ReadOnly: true}))
// Durable on-disk store: a crash mid-run resumes from here.
store, err := sqlite.Open("agent.db")
if err != nil {
log.Fatal(err)
}
defer store.Close()
j, err := agent.NewJournal(store)
if err != nil {
log.Fatal(err)
}
a, err := agent.New(
model,
j,
agent.WithTools(weather),
agent.WithSystemPrompt("You are a concise weather assistant."),
)
if err != nil {
log.Fatal(err)
}
out, err := a.Run(context.Background(), "run-1", agent.UserText("Weather in SF? Use the tool."))
if err != nil {
log.Fatal(err)
}
fmt.Println(out.Message.Text())
}
Run the live smoke example: OPENROUTER_API_KEY=sk-... go run github.com/bide-ai/bide/examples/smoke@latest
A Func tool decodes its arguments strictly, so the tool reads exactly what the model sent.
A call that is missing a required field (one the schema lists as required: not a pointer, and no
omitempty or omitzero in its json tag) or sends it as null (unless its schema admits any
value, as for any or json.RawMessage), uses a name that is not a field (an unknown name, or a
case variant such as "CITY" for city), repeats a name, has data after the object, or holds
invalid UTF-8 or an escaped lone surrogate is an ErrToolArgs error. That error goes back to the
model as the call's result, so the model can correct the call. encoding/json would fill in zero
values, match names case-insensitively, drop unknown names, and keep the last duplicate. Give each
argument field the json tag the model sees in the schema.
Run takes a Message input (text, or text and images) and per-run options, and returns a
Result: the final message, token usage for the whole run (including cache and sub-agents), the
model-turn count, and the wall-clock duration. Turns counts only the model turns this invocation
made, so a resumed run, or a finished one driven again, can show fewer turns than the run made
overall (Usage and Spend are the whole run's). The Result is non-nil on every error once the run
ID is valid: a pause, a halt, a failure, a saga's abort, a cancellation:
A run's first drive journals its input and the options its caller passed in run:start, and
every later drive runs under them, a recovery drive included: Resume(ctx, runID) needs
neither, a later drive may raise or lower a limit (WithMaxTurns, WithTokenBudget: journaled
as an amendment), and any other different setting is ErrConfig. WithSaga() runs the run as a
saga, and WithToolFilter(names...) restricts it to some of the agent's tools, enforced when each
call is dispatched. agent.Cancel(ctx, j, runID, reason) cancels a run (a saga is rolled back
first), and agent.Status(ctx, j, runID) reads its state from the journal.
Streaming
Run blocks and returns the final answer. To watch the agent work (token deltas, turn
boundaries, tool start/finish), use Stream. It drives the same loop as Run
(Stream(...).Result() returns what Run would), so durability, resume, and side-effect safety are identical:
stream := a.Stream(ctx, runID, agent.UserText(input))
for ev := range stream.Events() {
switch e := ev.(type) {
case agent.ModelEvent: // live token/reasoning/tool-call deltas
if d, ok := e.Event.(agent.TextDelta); ok {
fmt.Print(d.Text)
}
case agent.ToolStarted:
fmt.Printf("\n[calling %s]\n", e.Name)
case agent.ToolCompleted:
fmt.Printf("[%s done]\n", e.Name)
}
}
res, err := stream.Result() // what Run returns: the Result and an error (incl. a pause: *ApprovalPending, *OutcomeUnknown, ...)
Events: TurnStarted, ModelEvent (the token feed), TurnRestarted, AssistantTurn, ToolStarted /
ToolCompleted, ApprovalRequired, Finished. Range Events() for a UI then call Result(),
or call Result() alone to behave exactly like Run (it drains events for you). ToolStarted
fires immediately before a tool is called, so a call cancelled before it starts (and recorded as
not started) emits neither ToolStarted nor ToolCompleted.
Two things worth knowing, both consequences of durability:
Token deltas arrive below the middleware chain (Retry / Cost still see whole
assembled messages), and only on a fresh model call. One request of a turn streams at a
time. When a middleware such as Retry calls the model again after an attempt that already
streamed deltas, or the turn's response is not the one that streamed (a Hedge backup won, a
fallback or a cache answered), TurnRestarted marks those deltas as discarded, and the
response the turn records follows: clear the turn's text when it arrives. Middleware has no
streaming code for this; the agent does it.
On resume, the journaled transcript is re-emitted as AssistantTurn{Replayed: true} +
ToolCompleted before live progress, so a fresh UI reconstructs the whole story after a
crash, and a replayed turn produces no token deltas (it was already decided).
Stream takes the same options as Run (WithSaga() for a saga), and its RunStream.Result()
returns what Run would.
Typed output
RunTyped[T] returns a typed T instead of a free-form message. It injects a synthetic
final_answer tool whose JSON schema is derived from T (via the schema package) and steers
the model to call it once its work is done, so a tool-using agent can do its work and then
answer typed. It is provider-agnostic: it uses native tool calling, not a provider's JSON mode.
type Weather struct {
City string `json:"city"`
TempF int `json:"temp_f"`
}
w, _, err := a.RunTyped[Weather](ctx, runID, agent.UserText("weather in SF?"))
// w.City == "SF", w.TempF == 68
a.RunTyped[T](ctx, runID, input, opts...) is a Go 1.27 generic method; it returns the run's
Result too (its Output is the answer as journaled). A typed run journals
its output mode and T's schema in run:start, so resuming it through an untyped entry point, or
with another T, is ErrConfig; recovery resumes it with agent.ResumeTyped[T](a). The answer is the
arguments final_answer accepted, decoded strictly like any Func tool's (a loose call goes back
to the model to correct), as the tool received them after any tool middleware. The tool journals
them as its result, so the answer is resume-safe: a crash mid-run recovers the typed answer
from the log on resume. The first final_answer call the tool accepts ends the run.
Only if the model never makes one (it replies in plain JSON text instead) does RunTyped parse
the text of the run's final turn. T must be a JSON object (a struct, a pointer to one, or a map),
since providers take tool arguments only as an object; any other T is ErrConfig.
On OpenAI-compatible providers with strict structured outputs, RunTyped[T] with
agent.WithOutputMode(agent.OutputNative) uses the provider's native
JSON-schema response format instead of the tool (schema enforced provider-side, no tool
round-trip); the Anthropic adapter does not support it and returns ErrConfig, so use the default
tool mode there for provider-agnostic output.
Sampling
Generation controls are provider-neutral and set once; each adapter maps them onto its wire
format (and drops what it can't do, e.g. Anthropic has no seed):
a, err := agent.New(
model,
journal,
agent.WithTools(tools...),
agent.WithSampling(agent.Temperature(0), agent.MaxTokens(500), agent.TopP(0.9), agent.Seed(42)),
)
if err != nil {
panic(err)
}
Fields are optional: an unset field uses the provider default, so an explicit
Temperature(0) is distinct from "not specified." Request-level MaxTokens overrides an
adapter's construction-time default.
Prompt caching
An agent loop resends a large constant prefix (system prompt + tool schemas) every turn.
Anthropic prompt caching bills those repeats at the cache-read rate:
model := anthropic.New(key, anthropic.WithPromptCache())
This places cache_control breakpoints on the system block and the tool definitions. OpenAI
caches prefixes automatically (no flag needed). Either way, cache effectiveness surfaces in
agent.Usage (CacheReadTokens, served from cache, and CacheWriteTokens, written to it),
so cost accounting, tracing, and the run's token budget see the real numbers.
Sessions (multi-turn)
Run is one turn. A Session is a durable multi-turn conversation: each Send is a full agent
run (tools, resume, side-effect safety) seeded with the transcript so far, so the agent remembers
earlier turns.
s, _ := a.Session(ctx, "user-42") // reopens + rebuilds the transcript from the store
r1, err := s.Send(ctx, agent.UserText("what's the capital of France?"))
if err != nil {
panic(err)
}
r2, err := s.Send(ctx, agent.UserText("and its population?")) // sees turn 1 in context
if err != nil {
panic(err)
}
fmt.Println(r1.Message.Text(), r2.Message.Text())
The transcript is journaled turn-by-turn under the session id, so a restarted process
a.Session(ctx, "user-42") rebuilds it and continues. The id must not contain >. Turn N runs under "<id>>@turn/N" (its own
durable journal handles crash-resume within a turn); conversational memory is the question/answer
transcript: a turn's intermediate tool calls stay in that turn and don't leak into later ones. If
a turn pauses (approval / Interrupt), Send returns that error; resolve it and call Send again
with the same input to resume. Until then, Send with a different message returns ErrConfig: the
open turn belongs to its message. For inbound messages that may be redelivered, SendOnce(ctx, id, input) answers each message id once. Several handles on one session never lose a turn, record one
twice, or answer one message with another's reply. Over a store with leases (MemStore, SQLite,
Postgres), a turn is driven by one worker at a time under its run's lease, so its token budget
holds across workers; a second worker sent the same message meanwhile gets ErrTurnContended and
sends it again later. A turn resumed after a crash is seeded with the same transcript it started
with, even if other messages were answered in between. Send and SendOnce take a Message and
run options and return a Result. A turn whose run was cancelled (agent.Cancel) is closed: Send of its message
returns ErrRunCancelled, and the next message's Send records the turn closed, with no answer and
outside the transcript, and runs its own turn.
Auditability (tamper-evident journal)
The durable journal already records every step of a run. The audit package commits to that
history with a hash chain, so a run's execution is verifiable:
Any modify / insert / delete / reorder of a record changes the head. Security model: this
gives integrity unconditionally, and tamper-evidence when you anchor the head out-of-band (a chain
in the same DB an attacker controls can be rewritten and rehashed); see the package doc.
For selective disclosure, audit.Root / Prove / VerifyInclusion build an RFC 6962
(Certificate Transparency) Merkle tree, so you can prove one record is part of a committed run
via an O(log n) inclusion proof, without revealing the other records (e.g. show an auditor a
single charge happened, exposing no other customers or prompts). And ProveConsistency /
VerifyConsistency prove an earlier root is an append-only prefix of a later one: that history
was only appended, never rewritten or reordered (the transparency-log guarantee). The implementation
is checked against the published RFC 6962 test vectors.
SignTreeHead produces the CT-style Signed Tree Head (format bide.audit.sth.v5), {Kind, RunID, Size, Root, TimestampNanos} signed together with its scheme under any audit.Signer: Ed25519,
ML-DSA-65 (post-quantum), or a hybrid of both. That is the artifact you publish. Leaves commit to the
bytes the journal stores for each record, verbatim, so a proof carries those bytes (record_bytes) and
a record written by a newer release with fields this one does not know still verifies. The full flow: sign an STH, later disclose a single record with an
inclusion proof an auditor checks against the signed root, and prove append-only growth between two STHs.
See docs/guides/audit.md for the model, the API, and the end-to-end compliance flow.
Run viewer
bide-view opens a read-only page for one run, served on 127.0.0.1 from a SQLite or Postgres
journal or an exported one. It shows the run's timeline (model turns, tool calls and their Safety,
attempts, approvals), a halted operation with the ResolveHalt call that records its outcome, each
approval gate's k-of-n status and whether its signatures verify, a plan flow's path over its declared
topology, and whether the journal verifies against a signed tree head or an evidence package. It
never writes to the store. Install it with go install github.com/bide-ai/bide/cmd/bide-view@v0.13.0; see the
run viewer guide.
Before: the refund crashed after it fired, so the run is halted with its outcome unknown, and the page gives the ResolveHalt call.
After: ResolveHalt recorded the outcome and the run completed; the gate shows 2 of 3 approvers, and the evidence package verifies.
RAG & memory (bring your own)
Bide ships no vector store, embedder or memory backend: it gives you the interface, and you plug in
the store you already run. Implement one interface against your infra:
type Retriever interface {
Retrieve(ctx context.Context, query string, k int) ([]agent.Doc, error)
}
Then wire it in one of two ways:
// Agentic RAG: the model searches on demand:
a, err := agent.New(model, journal,
agent.WithTools(agent.MustRetrievalTool("search_kb", "Search the knowledge base.", myStore, 5)))
// Classic RAG: top-k auto-injected as context on each user turn:
a, err = agent.New(model, journal, agent.WithRetrieval(myStore, 5))
Conversational memory is already built in (Session); dynamic context goes through
WithSystemPromptFunc; this seam covers semantic / long-term memory. Concrete store adapters (if
ever needed) would be separate modules, never in the core. See
docs/guides/rag-memory.md.
Resume safety, in one table
agent.Safety{ReadOnly: true} // no side effects → always safe to re-run
agent.Safety{Idempotent: true} // safe to retry (dedupes downstream)
agent.Safety{} // a write → HALT on unknown outcome, don't double-fire
agent.WithApproval(agent.SingleApproval()) // not Safety: a tool option that pauses for human approval before executing
Before a non-idempotent side effect the loop records a durable attempt marker, so
resume can tell "never ran" (safe to run) from "ran and crashed" (halt).
dst_test.go is a deterministic simulation test: a
fault-injecting store crashes at every write point (and across hundreds of randomized
multi-crash schedules), and the harness asserts a non-idempotent side effect fires at most
once every time, with the run always ending completed or halted, never double-firing.
The harness is exported (chaos/) and pointed at other SDKs in benchmarks/. The measured result:
Bide maxFired=1 (PASS); trpc-agent-go maxFired=6; langchaingo maxFired=64 (both FAIL).
trpc's checkpoint/resume works (resuming a completed run is a no-op); its double-fire is the
documented LangGraph "nodes must be idempotent" window. langchaingo has no durability, so retries
re-run everything. Bide's attempt marker closes that window.
WithMaxTurns(n) caps model turns per run so a model that keeps calling tools can't loop forever:
hitting it returns ErrMaxTurns (which is errors.IsErrBudget). WithTokenBudget(n) caps the
tokens a run may use, cached input included: once the run has used n, it makes no further model
call and returns ErrBudgetExceeded. The budget covers the run's whole agent tree: its sub-agents'
model calls count against it, parallel ones included. Each call's usage is journaled with its turn,
so both limits are rebuilt from the journal and hold across a crash and resume.
Human-in-the-loop
Three forms. Approve/deny: a tool marked WithApproval(SingleApproval()) pauses before running; the
human decision is a bool:
_, err := a.Run(ctx, runID, agent.UserText(input))
if pend, ok := errors.AsType[*agent.ApprovalPending](err); ok {
// ... get a human decision ...
agent.Approve(ctx, journal, pend.RunID, pend.ToolUseID, true)
res, _ := a.Run(ctx, pend.RootRunID, agent.UserText(input))
var out agent.Message
if res != nil {
out = res.Message
} // resumes past the pause
}
Interrupt/resume: a tool pauses at an arbitrary point and resumes with a typed value
(generalizing the bool). Call agent.Interrupt[T] inside a retry-safe tool:
tool := agent.MustFunc("choose_plan", "pick a plan", func(ctx context.Context, in Options) (Plan, error) {
pick, err := agent.Interrupt[Plan](ctx, "plan", in) // pauses the run; in is shown to the human
if err != nil {
return Plan{}, err // *InterruptPending propagates out of Run
}
return pick, nil // on resume, pick is the human's typed answer
}, agent.WithSafety(agent.Safety{ReadOnly: true}))
_, err := a.Run(ctx, runID, agent.UserText(input))
if intr, ok := errors.AsType[*agent.InterruptPending](err); ok {
// ... show intr.Prompt, get a typed answer ...
journal.AnswerInterrupt(ctx, intr.RunID, intr.Name, chosenPlan)
res, _ := a.Run(ctx, intr.RootRunID, agent.UserText(input))
var out agent.Message
if res != nil {
out = res.Message
} // resumes; Interrupt now returns chosenPlan
}
Both are durable: the decision/value is a journaled step, so it survives a crash. Interrupt
must be in a retry-safe tool (ReadOnly/Idempotent): on resume the tool re-runs until the
interrupt resolves, so everything before the Interrupt call must be safe to repeat.
m-of-n approval: when one sign-off is not enough, require k signed decisions from a named set
of n approvers. Each approver signs the exact call (tool and arguments); the gate proceeds at k
approvals, denies once k is unreachable, and otherwise pauses with the running tally. A forged or
mistaken decision is ignored without locking its approver out:
refund := agent.MustFunc("refund", "refund the order", doRefund,
agent.WithApproval(&agent.ApprovalPolicy{Need: 2, Approvers: []string{"ops", "finance", "risk"}}))
a, err := agent.New(model, journal, agent.WithTools(refund), agent.WithApproverVerifiers(keysByApprover))
if err != nil {
panic(err)
}
// each approver, out of band, signs the paused call they were shown:
sig, _ := signer.Sign(agent.ApprovalDecisionBytes(pend.Subject(), "finance", true))
agent.SubmitDecision(ctx, journal, agent.Decision{RunID: pend.RunID, ToolUseID: pend.ToolUseID,
ApproverID: "finance", Approved: true, Alg: signer.Alg(), Signature: sig})
audit.ApprovalEvidence and audit.VerifyApprovals (or bide-audit verify-approvals) then prove
offline that k named approvers signed off on this exact call before it ran (each decision counts
only under the signature scheme journaled with it, which must be its approver key's), under the expected
policy, from evidence that cannot leave a decision out unnoticed. Each approver needs a key of their
own: a policy two of whose approvers resolve to one key is refused with ErrConfig, since whoever
holds that key could sign as both. See the approval guide; runnable across
separate processes in examples/approval.
Errors
Failures are classified with sentinel errors matched by errors.Is. Two tiers: a category (the coarse class) and a
condition (a specific cause) that wraps its category, so a match works at whichever level
you need:
_, err := a.Run(ctx, runID, agent.UserText(input))
switch {
case errors.Is(err, agent.ErrModel): // any provider fault (HTTP status, decode, stream)
backOffAndRetry()
case errors.Is(err, agent.ErrUnknownTool): // a specific condition (implies agent.ErrTool)
fixToolWiring()
case errors.Is(err, agent.ErrStorage): // durable-store I/O
alertOps()
}
Categories: ErrConfig, ErrModel, ErrTool, ErrStorage, ErrProtocol, ErrBudget.
Conditions (each wraps a category): ErrUnknownTool, ErrToolArgs, ErrToolOutcomeUnknown (wrap ErrTool),
ErrToolReinvoked, ErrInvalidApproval, ErrAlreadyDecided (wrap ErrConfig),
ErrNoRecordedOutput, ErrIncompleteResponse (wrap ErrModel), ErrTruncatedToolArgs (wraps
ErrProtocol), ErrStreamProtocol and ErrNegativeUsage (wrap ErrProtocol and ErrModel), ErrBudgetExceeded,
ErrMaxTurns (wrap ErrBudget). ErrToolNotCalled wraps no category: it marks a tool call known
never to have reached its tool (a tool middleware's denial wraps it). Provider adapters also return *RateLimited (HTTP 429, with a
RetryAfter hint) and *APIError (other non-2xx, with the StatusCode), both wrapping
ErrModel. Every error the
toolkit returns (including from the model, MCP, store, and governance adapters) carries a category,
so errors.Is is reliable across the whole surface.
The control-flow signals are richer than a category, so they stay concrete types. A paused or
halted run returns an agent.Pause, a sealed interface with exactly five kinds: *ApprovalPending
(approval needed), *InterruptPending (waiting for human input), *TimerPending (durable timer
pending), *SignalPending (waiting for an external signal or channel message), and
*OutcomeUnknown (unsafe to resume; its Cause is HaltCrashed or HaltContended; a plan flow's
node runs as a Step and halts as one, named by its node key node:<name>, and Flow.ResolveHalt
clears it). Test for one
with agent.IsPause(err), read it with agent.AsPause(err) (its Paused() names the run to answer
and the run to re-invoke), or match a kind with errors.As. The others are *SagaAborted (rolled
back) and *HaltTooYoung (from ResolveHalt, when WithMinHaltAge has not elapsed yet). A
paused or halted run is not a "failure" category; inspect the struct for its run, operation, or
compensation details. A run cancelled with agent.Cancel returns ErrRunCancelled (with a
Result), which carries no category: it is a terminal status, not a fault. Context cancellation
surfaces as the usual context.Canceled /
context.DeadlineExceeded, and a drive cancelled because its run lease was lost (agent.Lease) as
ErrLeaseLost; like cancellation, it carries no category.
Middleware & observability
Two independent func(Handler) Handler chains: one around the model
call (WithMiddleware) and each tool call (WithToolMiddleware). First added = outermost. Both are mutating and
short-circuiting: rewrite what goes in, transform what comes out, or return without calling
next.
var cost middleware.CostMeter
a, err := agent.New(model, journal,
agent.WithTools(tools...),
agent.WithTokenBudget(100_000), // per run, rebuilt from the journal on resume
agent.WithMiddleware(
middleware.Retry(3, middleware.WithBackoff(200*time.Millisecond, 10*time.Second)),
middleware.Cost(&cost, middleware.Rates{InputPer1M: 3, OutputPer1M: 15}),
),
agent.WithToolMiddleware(middleware.ToolLog(log.Printf), middleware.ToolCache(), middleware.ToolRetry(3)),
)
if err != nil {
panic(err)
}
// opt-in OTel gen_ai.* spans (provider and model from agent.ModelInfoOf); the core has no OTel dependency:
a, err = a.With(
agent.WithMiddleware(trace.Model(tracer)),
agent.WithToolMiddleware(trace.Tool(tracer)), // execute_tool span per call; nests across the sub-agent boundary
)
// ... after the run: cost.Snapshot() (answer and spend, in tokens and USD)
Retry does exponential backoff with jitter and honors a Retry-After on a provider 429 (the
adapter returns a typed *agent.RateLimited); Cost accumulates USD from token usage (incl.
cache-read/write) into a CostMeter you read after the run.
A model middleware is a func(next agent.ModelHandler) agent.ModelHandler. It receives an
agent.ModelCall (the Request, the target Model, RunID and Turn) and returns an
agent.ModelResponse (the Message, its Usage, and the Finish reason). The call is a value:
change a copy and pass it on (c := call; c.Model = backup is all Hedge does). Work that belongs
to each request actually sent, rather than to the call, goes in a hook, which runs once around every
request of every retried attempt and hedged target below it:
// Log every request a turn sends, with its number within the turn.
func LogRequests(next agent.ModelHandler) agent.ModelHandler {
return func(ctx context.Context, call agent.ModelCall) (agent.ModelResponse, error) {
return next(ctx, call.AddHook(agent.ModelCallHook{
After: func(_ context.Context, c agent.ModelCall, a agent.ModelAttempt) {
log.Printf("run %s turn %d request %d: %d tokens, err %v", c.RunID, c.Turn, c.Attempt(), a.Response.Usage.TotalTokens(), a.Err)
},
}))
}
}
Hooks are append-only, and the run's own spend accounting is not a hook, so no middleware can hide a
request from WithTokenBudget or Result.Spend. Outside an agent, agent.CallModel(ctx, model, req, mw...) sends one call through the same chain.
Because trace.Tool runs inside the loop, its span sits in the context handed to the tool, so
when a tool is itself a sub-agent, the sub-agent's run and its own spans nest as children. The
trace crosses the sub-agent boundary.
Tool middleware runs inside the durable step, so a short-circuit result (a ToolCache hit) is
journaled like any tool result, and so is a policy denial that wraps agent.ErrToolNotCalled; resume
replays it and never re-runs the middleware or the tool. A denial must wrap agent.ErrToolNotCalled:
one that does not leaves a side effect's outcome unknown, records nothing, and the run halts. ToolRetry and ToolCache act only on tools whose Safety allows it
(retry-safe, and ReadOnly, respectively), and the agent runs a tool that is not retry-safe at
most once per call whatever the middleware does. Write your own with the agent.ToolMiddleware signature:
// Deny a tool by policy: the tool never executes; the model sees the error and reacts.
// A denial must wrap agent.ErrToolNotCalled: without it the agent cannot tell the tool did
// not run, so a side effect's outcome is unknown and the run halts.
func RequireTag(tag string) agent.ToolMiddleware {
return func(next agent.ToolHandler) agent.ToolHandler {
return func(ctx context.Context, call agent.ToolCall) (json.RawMessage, error) {
if !authorized(ctx, tag) {
return nil, fmt.Errorf("tool %q denied: %w", call.Use.Name, agent.ErrToolNotCalled)
}
return next(ctx, call) // mutate call.Use.Args before, transform the result after
}
}
}
Modules
Bide is a multi-module repo: a dependency-light core (github.com/bide-ai/bide,
the loop, schema, middleware, model adapters, the plan flow builder, audit; deps are just x/sync and x/text) plus one
module per heavy adapter (mcptools, trace, store/sqlite, store/postgres, govern/redislog,
govern/sqlitelog, govern/postgreslog, codec/gcf) and the govern module, which carries gsm and stays v0.x until gsm
is stable. Import an adapter and you pull its dependency tree; import only the core
and you don't. A core-only consumer's external-module surface is 2, not 54. See
docs/reference/module-structure.md.
Architecture
Hexagonal: the core defines the ports (Model, Store, Tool,
Middleware); adapters plug in at the edges. Dependencies point inward; the core imports
no adapter and no infrastructure, guarded by architecture_test.go.
agent (root) durable loop · Journal/Store · Message/Part · Tool/Safety · middleware types · RenderMermaid
plan optional typed flow builder + declarative config; lowers to the loop (Topology · Conform)
model/anthropic native Claude (thinking + signatures)
model/openai any OpenAI-compatible endpoint
model/gemini native Gemini (generativelanguage / Vertex via WithBaseURL)
schema reflect Go types → inline JSON Schema + OpenAIStrict
middleware Retry, RateLimit, Cost, Hedge
trace opt-in OTel gen_ai.* spans
store/sqlite on-disk durable resume (single binary, no cluster)
store/postgres HA durable resume (any node resumes any run)
govern Tier-2: federated governed state + quorum for agents that must agree (gsm-backed; own module)
Federated governance (Tier-2)
The durable core keeps one agent's work crash-safe. The govern tier is for many independently
run agents that have to agree, across process, team, or organizational
boundaries, with no coordinator beyond the shared, totally ordered event log, and no single writer. It gives two verifiable forms of
agreement; in both, a party checks the outcome from public artifacts without trusting anyone else's
agent.
Agreement on shared state (convergence). Describe the shared state as a registry (variables +
invariants + events); gsm checks at build time that every interleaving of agent actions reaches
the same valid state, or refuses to build and shows you a counterexample (see
known limitations for its scope). Runtime is O(1) table
lookups; state is event-sourced and crash-recoverable. It scales from a single shared registry up
through federations (cross-boundary constraints: trees, multi-source DAGs with resolvers,
monotone cyclic meshes), composes via Embed, and can synthesize the compensation for
you (declare the rules, get a convergent governor, or a proof that none exists). Agents plug in
through FederatedEventTool, so an LLM tool call becomes a governed event.
Agreement on a decision (quorum). k-of-n named voters (each a model, provider, or principal)
cast a normalized decision; every vote is a journaled, at-most-once step that records who voted how,
and the k-of-n gate is a gsm invariant over the vote count, so "k agreed" is machine-checked over
every possible tally. bide-audit verify-quorum re-checks the tally and every vote from public
artifacts, reproducing the plurality rule without trusting the producer. The claim is precise: a
quorum proves that k voters agreed and lowers single-model risk; it does not certify the decision
is correct (correlated errors are not independence), and only normalized decisions can be quorumed,
not free-form prose.
gov, _ := govern.NewPersistent(ctx, machine, log, "order-42", machine.NewState())
tool := govern.EventTool(gov, govern.EventToolConfig{Name: "pay", Description: "mark the order paid", Event: "pay"})
// hand `tool` to the agent: concurrent agents sharing `gov` converge, durably.
Full guide, capability ladder, and the runnable demos (examples/govern/mesh, examples/govern/compose,
examples/govern/quorum) in the governance guide.
Guides
New here? Start with Getting started, use the docs index for the full map, and see Concepts for the vocabulary (journal, at-most-once, lease, Waker, gsm, ProofBundle). The precise durability guarantee is stated in the guarantee and its bounds in known limitations.
Authoring
Flows: the plan typed flow builder. Author topology (Step/Tool/Model/Switch/Join/LoopBack) that lowers to the same journal, then prove a run followed it (Conform). Runnable: examples/plan.
Durable steps: compose your own durable work: Step, Parallel/Task fan-in, sagas (WithSaga), and durable timers (Sleep/WaitUntil). Runnable: examples/parallel.
Reliability: per-attempt timeouts, classified retry, hedged model calls, rate limiting, and cost tracking, and how they compose. Runnable: examples/hedge.
Signals and ambient: external events into a run: durable timers and the Waker, human-in-the-loop (Interrupt/AnswerInterrupt), and durable signals (at-least-once in, exactly-once applied). Runnable: examples/signals, examples/interrupt.
Models: the Anthropic, OpenAI-compatible, and Gemini adapters: WithBaseURL, sampling, prompt caching, typed errors, and multimodal image input.
MCP: connect an MCP server as a runtime tool source, with side-effect-safe resume; a trusted server's tool annotations can mark tools safe to re-run.
Observability: OTel gen_ai spans in one line (trace.Instrument): span taxonomy, sub-agent nesting, token-to-cost, and the content-capture privacy default. Runnable: examples/observability.
Messaging: drive an agent from an inbound webhook (Slack, Telegram, SMS, Discord), redelivery-safe: a retried webhook replays instead of double-firing. Runnable: examples/webhook.
Run viewer: bide-view, a read-only local page for one run's journal: the timeline, a flow's path over its declared topology, halts, approval gates, and the audit verdict.
Debugging and recovery: deterministic replay (Replay), event reconstruction (ReplayEvents), Mermaid run diagrams, and crash recovery (Recover) that re-drives interrupted runs.
Accountability and governance
Audit: proof-carrying runs. A run ships one portable RunCertificate, checkable offline with bide-audit verify-run. Runnable: examples/govern/proof-carrying-run.
Delegation: signed capability grants a sub-agent can only narrow (Grant/SignGrant), verified offline (VerifyDelegationChain), plus authority earned from a clean trail. Runnable: examples/govern/delegation, examples/govern/authority.
Security model: the exact scope of the cryptographic guarantees (integrity, authenticity, tamper-evidence, non-repudiation, selective disclosure) and what is out of scope (confidentiality). Read before relying on the trail.
Governance: the Tier-2 governed-state substrate (gsm). Describe shared state as a registry, and Build() checks that every interleaving converges or returns a counterexample. Runnable: examples/govern/mesh, examples/govern/compose.
Human approval (human-in-the-loop): durable human sign-off before a tool runs, from 1-of-1 to signed m-of-n (ApprovalPolicy, SubmitDecision), with offline proof that k named approvers approved before the action (audit.ApprovalEvidence, audit.VerifyApprovals). Runnable: examples/approval.
Quorum: governed k-of-n model agreement (govern.Quorum), the tally anchored in the journal and re-checkable offline (bide-audit verify-quorum). Runnable: examples/govern/quorum.
Reference and internals
Extension points: the ports and adapters (Model, Store, Tool, Compensator, Retriever, Anchor, EventStore), with an implement-your-own-store walkthrough.
How bide is verified: no fix without a failing test, mutation checks, crash and cancellation sweeps, forced interleavings, conformance suites, TLA+ model checking of the coordination protocols, and what CI enforces.
Formal verification: the TLA+ models of bide's coordination protocols, checked with TLC on every pull request and with Apalache nightly (an inductive invariant proves the claim protocol's at-most-once rule at any depth, for two drivers, bounded attempts and claim ids), what each guarantees and covers, every bug they found or confirmed, and what they do not cover.
Testing and evidence: what is tested and how, the chaos crash-injection benchmark, differential oracles, RFC 6962 conformance, and the provable-versus-statistical boundary in the eval package.
Journal compaction (design note): compacting an unbounded journal without breaking the audit spine's inclusion and consistency proofs.
Package agenttest holds test doubles and helpers for code built on package agent: a scripted model that plays back the turns you give it, so an agent runs with no provider or API key (ScriptedModel, NewScriptedModel, ToolTurn, TextTurn, ErrorTurn); a Store that counts its round trips (CountingStore); and constructors that panic instead of returning an error (MemJournal, MustJournal, MustNew, Must), for tests whose setup cannot fail.
Package agenttest holds test doubles and helpers for code built on package agent: a scripted model that plays back the turns you give it, so an agent runs with no provider or API key (ScriptedModel, NewScriptedModel, ToolTurn, TextTurn, ErrorTurn); a Store that counts its round trips (CountingStore); and constructors that panic instead of returning an error (MemJournal, MustJournal, MustNew, Must), for tests whose setup cannot fail.
Package storetest checks that an agent.Store meets the requirements the journal builds on (A1 to A8, documented on agent.Store), that a Journal over it writes and checks the journal format header, and that records round-trip byte for byte.
Package storetest checks that an agent.Store meets the requirements the journal builds on (A1 to A8, documented on agent.Store), that a Journal over it writes and checks the journal format header, and that records round-trip byte for byte.
Package chaos is a deterministic crash-injection benchmark for agent runtimes: it drives a NON-IDEMPOTENT side effect through a fault schedule and checks it fires at most once.
Package chaos is a deterministic crash-injection benchmark for agent runtimes: it drives a NON-IDEMPOTENT side effect through a fault schedule and checks it fires at most once.
Command bench is a concurrency/scale load harness: it drives N durable agent runs concurrently in one process against a stub model (so it measures the FRAMEWORK's orchestration, journaling, and concurrency overhead, not LLM latency), and reports throughput, latency percentiles, peak goroutines, and memory.
Command bench is a concurrency/scale load harness: it drives N durable agent runs concurrently in one process against a stub model (so it measures the FRAMEWORK's orchestration, journaling, and concurrency overhead, not LLM latency), and reports throughput, latency percentiles, peak goroutines, and memory.
Command chaosbench runs the crash-injection benchmark: it drives a non-idempotent side effect through an exhaustive crash-point sweep plus randomized multi-crash schedules, and reports whether the side effect ever fired more than once.
Command chaosbench runs the crash-injection benchmark: it drives a non-idempotent side effect through an exhaustive crash-point sweep plus randomized multi-crash schedules, and reports whether the side effect ever fired more than once.
Command hedge shows the hedged-model middleware: race the primary model against a backup and take the first good answer, which cuts tail latency and fails over around a down provider.
Command hedge shows the hedged-model middleware: race the primary model against a backup and take the first good answer, which cuts tail latency and fails over around a down provider.
Command interrupt shows human-in-the-loop as a durable pause: a retry-safe tool calls agent.Interrupt to ask a human a typed question, Run returns *InterruptPending (the run has paused at the interrupt point), the caller records an answer with agent.Journal.AnswerInterrupt, and re-invoking Run with the SAME runID resumes past the interrupt with that answer.
Command interrupt shows human-in-the-loop as a durable pause: a retry-safe tool calls agent.Interrupt to ask a human a typed question, Run returns *InterruptPending (the run has paused at the interrupt point), the caller records an answer with agent.Journal.AnswerInterrupt, and re-invoking Run with the SAME runID resumes past the interrupt with that answer.
Command recover shows durable resume via named-step memoization: a run journals its model turns and tool results to the store, so re-invoking Run with the SAME runID against the SAME store reuses the recorded steps rather than re-running them.
Command recover shows durable resume via named-step memoization: a run journals its model turns and tool results to the store, so re-invoking Run with the SAME runID against the SAME store reuses the recorded steps rather than re-running them.
Command session shows a durable multi-turn conversation: each Session.Send is one full agent run seeded with the transcript so far, so the agent remembers earlier turns.
Command session shows a durable multi-turn conversation: each Session.Send is one full agent run seeded with the transcript so far, so the agent remembers earlier turns.
Command signals shows the externally-pushed durable pauses: Await for a single-shot signal delivered from outside the run with agent.Journal.Signal, AwaitFor with a durable timeout (the signal-vs-deadline race), and an ordered per-run channel consumed exactly-once with Enqueue / Receive / Ack.
Command signals shows the externally-pushed durable pauses: Await for a single-shot signal delivered from outside the run with agent.Journal.Signal, AwaitFor with a durable timeout (the signal-vs-deadline race), and an ordered per-run channel consumed exactly-once with Enqueue / Receive / Ack.
Command smoke runs the agent end-to-end against a live OpenAI-compatible endpoint (here, OpenRouter) to prove the loop + tool calling work against a real model.
Command smoke runs the agent end-to-end against a live OpenAI-compatible endpoint (here, OpenRouter) to prove the loop + tool calling work against a real model.
Command streaming shows Agent.Stream: range the lifecycle Events to render progress (token deltas, turn boundaries, tool start/finish) while the durable loop runs underneath, then call Result for the terminal answer, which is what Run returns.
Command streaming shows Agent.Stream: range the lifecycle Events to render progress (token deltas, turn boundaries, tool start/finish) while the durable loop runs underneath, then call Result for the terminal answer, which is what Run returns.
Command webhook shows how to drive an agent from an inbound messenger webhook (Slack, Telegram, Twilio, Meta, Discord) without shipping any messenger-specific code in the SDK.
Command webhook shows how to drive an agent from an inbound messenger webhook (Slack, Telegram, Twilio, Meta, Discord) without shipping any messenger-specific code in the SDK.
Package journalhook gives the packages of this module that write raw journal records (audit's leaves, plan's flow records) the journal's step functions without them being part of package agent's API.
Package journalhook gives the packages of this module that write raw journal records (audit's leaves, plan's flow records) the journal's step functions without them being part of package agent's API.
Package runview is a read-only, local web viewer for one run's journal: a plan flow's declared topology with the run's path drawn over it, or an agent run's timeline of model turns, tool calls, attempts, approvals and halts, with the audit verdict when a signed head or evidence package is given.
Package runview is a read-only, local web viewer for one run's journal: a plan flow's declared topology with the run's path drawn over it, or an agent run's timeline of model turns, tool calls, attempts, approvals and halts, with the audit verdict when a signed head or evidence package is given.
Package toolhook holds the hooks by which a tool wrapper in this module (audit's AttenuatingSubAgent) takes part in the agent's engine without exported API: the agent checks for these interfaces, and only packages of this module can name them.
Package toolhook holds the hooks by which a tool wrapper in this module (audit's AttenuatingSubAgent) takes part in the agent's engine without exported API: the agent checks for these interfaces, and only packages of this module can name them.
Command doccheck enforces bide's documentation policy over every module in the repository: every exported type, func, method, package-level const and var has a doc comment that starts with its name, and every package has a package comment ("Package name ..." outside package main).
Command doccheck enforces bide's documentation policy over every module in the repository: every exported type, func, method, package-level const and var has a doc comment that starts with its name, and every package has a package comment ("Package name ..." outside package main).
Command docsnip checks that every Go code block in bide's documentation compiles against the current code, so the docs cannot drift silently when an API changes.
Command docsnip checks that every Go code block in bide's documentation compiles against the current code, so the docs cannot drift silently when an API changes.
Command oldnames fails when a name the pre-1.0 API redesign removed (docs/design/api-v1.md, section 10.2) appears in bide's Go code, its godoc, or its documentation: the transitional names (RunMessage, Build, ResolveHaltRef, ...), the string entry points they replaced (RunSaga, RunResult, ...), the pause aliases, the Durable interface and its shims, the old Tool method set's SpecOf, the context decorators, the mcp package's old path, and agent.ScriptedModel.
Command oldnames fails when a name the pre-1.0 API redesign removed (docs/design/api-v1.md, section 10.2) appears in bide's Go code, its godoc, or its documentation: the transitional names (RunMessage, Build, ResolveHaltRef, ...), the string entry points they replaced (RunSaga, RunResult, ...), the pause aliases, the Durable interface and its shims, the old Tool method set's SpecOf, the context decorators, the mcp package's old path, and agent.ScriptedModel.
Package middleware provides composable func(Handler) Handler batteries for the model call — the "LiteLLM for Go" layer at the semantic level (handlers see messages, tool calls, and token usage, not raw bytes).
Package middleware provides composable func(Handler) Handler batteries for the model call — the "LiteLLM for Go" layer at the semantic level (handlers see messages, tool calls, and token usage, not raw bytes).
Package toolcfg checks a request's tools and tool choice against the rules the model providers enforce, so an adapter refuses a setup the provider would answer with a 400 before sending it, with an agent.ErrConfig that says what is wrong.
Package toolcfg checks a request's tools and tool choice against the rules the model providers enforce, so an adapter refuses a setup the provider would answer with a 400 before sending it, with an agent.ErrConfig that says what is wrong.
Package provider is the kit a model adapter is built from: HTTP error classification, SSE framing, a cap on a streamed reply, Retry-After parsing, and the tool-result codec.
Package provider is the kit a model adapter is built from: HTTP error classification, SSE framing, a cap on a streamed reply, Retry-After parsing, and the tool-result codec.
Package plan is rung-1 of the expression-surfaces stack: a handle-based flow builder that lowers to the journal-backed runtime and adds no new executor.
Package plan is rung-1 of the expression-surfaces stack: a handle-based flow builder that lowers to the journal-backed runtime and adds no new executor.
Package model is a test fixture: packages a/model and b/model both declare model.Req, a pair of distinct types whose reflect.Type.String() is the same ("model.Req"), so plan's digest tests can show the digest tells them apart by their package path.
Package model is a test fixture: packages a/model and b/model both declare model.Req, a pair of distinct types whose reflect.Type.String() is the same ("model.Req"), so plan's digest tests can show the digest tells them apart by their package path.
Package model is a test fixture: packages a/model and b/model both declare model.Req, a pair of distinct types whose reflect.Type.String() is the same ("model.Req"), so plan's digest tests can show the digest tells them apart by their package path.
Package model is a test fixture: packages a/model and b/model both declare model.Req, a pair of distinct types whose reflect.Type.String() is the same ("model.Req"), so plan's digest tests can show the digest tells them apart by their package path.