agent

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 38 Imported by: 0

Documentation

Overview

Package agent is the core of bide, a Go framework for AI agents that move money: a durability-first, Go-idiomatic toolkit whose side effects fire at most once, whose approvals are signed and counted by k-of-n, and whose every action is provable afterwards.

The agent loop journals every step to a Journal over a Store via named-step memoization and resumes after a crash — reusing recorded steps, re-running retry-safe tools, halting (rather than double-executing) a non-idempotent tool whose outcome is unknown, and pausing durably for human approval when a tool requires it. Orchestration is plain Go (Option B); the model call is wrapped by a func(Handler) Handler middleware chain. Reliability wrappers ship in the middleware package: per-attempt timeouts, classified retry with backoff, hedged model calls (middleware.Hedge), and rate limiting.

Scope: this resumes AROUND tool boundaries, not the internals of a single in-flight tool call. The precise promise is "survives crashes around tool calls, never double-fires an unsafe side effect" — not "mid-tool-call resume".

Example

Build an agent with one tool and run it. The scripted model calls the tool, then answers; a real adapter (model/anthropic, model/openai, model/gemini) takes its place in production.

package main

import (
	"context"
	"fmt"

	"github.com/bide-ai/bide/agent"
	"github.com/bide-ai/bide/agent/agenttest"
)

// WeatherIn is the arguments of the weather tool in the examples.
type WeatherIn struct {
	City string `json:"city"`
}

func main() {
	ctx := context.Background()
	weather := agenttest.Must(agent.Func("weather", "Current weather for a city.",
		func(_ context.Context, in WeatherIn) (string, error) { return "sunny in " + in.City, nil },
		agent.WithSafety(agent.Safety{ReadOnly: true})))

	model := agenttest.NewScriptedModel(
		agenttest.ToolTurn("call-1", "weather", `{"city":"Lisbon"}`),
		agenttest.TextTurn("It is sunny in Lisbon."),
	)
	j, err := agent.NewJournal(agent.NewMemStore())
	if err != nil {
		fmt.Println(err)
		return
	}
	a, err := agent.New(model, j, agent.WithTools(weather))
	if err != nil {
		fmt.Println(err)
		return
	}

	res, err := a.Run(ctx, "run-1", agent.UserText("What's the weather in Lisbon?"))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res.Message.Text())
}
Output:
It is sunny in Lisbon.

Index

Examples

Constants

View Source
const (
	MaxDecisionCertChain = 8
	MaxDecisionCertDER   = 16 << 10
)

MaxDecisionCertChain and MaxDecisionCertDER bound the certificate chain a decision carries: at most this many certificates, each at most this many bytes of DER. SubmitDecision refuses more (ErrConfig), and the identity verifier applies its own, usually tighter, limits.

View Source
const (
	ReasonNotEligible = "not an eligible approver"
	ReasonSuperseded  = "superseded by the approver's earlier valid decision"
	ReasonNoKey       = "no key for this approver"
	ReasonBadSig      = "signature does not verify for this call"
	ReasonSharedKey   = "the approver's signing key also serves another approver"
	ReasonNoKeyID     = "the approver's verifier reports no key identity"
	ReasonAlg         = "signed under another scheme than the approver's key"
	ReasonNoDecidedAt = "no signed decision time: signed under an earlier decision format than bide.approval.v3, which this version does not accept"
	ReasonDecidedAt   = "the signed decision time is not in canonical form (2006-01-02T15:04:05.000Z)"
	ReasonRequester   = "" /* 130-byte string literal not displayed */
	ReasonNoRequester = "the policy excludes the requester, and the run, or a run above it, records no requester principal in printable ASCII"
	ReasonLate        = "recorded after the gate's deadline (approval-deadline), or with no recorded time on a gate that has one"
	// ReasonAfterExpiry: the decision's write landed after the gate recorded its expiry, even if it
	// was stamped before the deadline. The expiry, recorded first, is the outcome.
	ReasonAfterExpiry = "not counted: written after the gate expired"
)

Reasons a decision record does not count.

View Source
const (
	ReasonNoCert       = "the approver is certificate-bound and the decision carries no usable certificate chain"
	ReasonCertChain    = "the certificate chain does not verify to exactly one chain under this gate's trust anchors and pinned intermediates"
	ReasonCertIdentity = "the certificate does not carry the approver's identity as its one identifier"
	ReasonCertUsage    = "the certificate is not usable for approving (key usage, extended key usage, policy, key type or CA flag)"
	ReasonCertWindow   = "the decision time, or the time it became provable, is outside the certificate's validity"
	// ReasonRevoked: a certificate of the chain has an effective revocation time at or before A(d).
	ReasonRevoked = "a certificate of the chain was revoked at or before the time the decision became provable"
	// ReasonStatusUnknown: no usable status covers a certificate of the chain.
	ReasonStatusUnknown = "no usable revocation status covers a certificate of the chain"
	// ReasonStatusStale: the status available is past nextUpdate or older than the policy allows.
	ReasonStatusStale = "the revocation status available is older than the policy allows"
	// ReasonNoCoveringHead: no signed tree head of the run covers the decision yet, so the time it
	// became provable is not known. Live, the gate anchors and retries; offline it is not verified.
	ReasonNoCoveringHead = "no signed tree head covers the decision yet, so when it became provable is not known"
	// ReasonHeadTimes: the head that first covers the decision is implicated in a contradiction among
	// the run's head times (a larger head stamped earlier than a smaller one, beyond the tolerance).
	ReasonHeadTimes = "the head that first covers the decision has a time contradicted by another head of the run"
)

Reasons a certificate-bound approver's decision does not count (docs/design/approver-identity.md). audit.CertError carries one of them with the precise cause, so a caller can both branch on the category and show the cause.

View Source
const (
	ApprovalIdentityFormat   = "bide.approval-identity.v2"
	ApprovalStatusFormat     = "bide.approval-status.v2"
	ApprovalExecHoldFormat   = "bide.approval-exec-hold.v2"
	ApprovalIdentityRulesVer = "v1"
	// EvaluatedLive is ApprovalIdentity.Evaluated of a record the live gate wrote.
	EvaluatedLive = "live"
	// MaxStatusInline is the largest status record (a CRL's DER) stored in the journal; above it the
	// record holds the metadata and the hash, and the bytes go to the policy's status archive.
	MaxStatusInline = 256 << 10
)

Formats of the identity records.

View Source
const (
	PauseAwaitingDecisions = "awaiting-decisions" // fewer valid decisions than needed (also the empty value)
	PauseAwaitingAnchor    = "awaiting-anchor"    // a decision that verifies has no covering signed head yet
	PauseAwaitingStatus    = "awaiting-status"    // revocation status is missing, stale or out of window
	PauseHeldUntilFinal    = "held-until-final"   // counted, only provisional, and the policy requires final to run
	PauseExecHold          = "exec-hold"          // the pre-execution status check holds the call
)

Pause phases (ApprovalWait.Pause): why a gate with a certificate-bound approver is waiting. The phase is a hint; a viewer decides from the authoritative records (result, hold, tally). A counted decision whose status is only provisional is not a pause: it executes, and its tier is in the identity record.

View Source
const (
	TierIndependentFinal = "independent-final" // status final, time from an independent source
	TierLogclockFinal    = "logclock-final"    // status final, time from the log signer's clock only
	TierProvisional      = "provisional"       // a later status could change it, the window is short, or the series has a gap
)

Identity tiers (IdentityDecision.Tier). A decision that is not counted does not appear.

View Source
const (
	BasisAtOrAfterDecision  = "at-or-after-decision" // status issued at or after the decision
	BasisLiveBeforeDecision = "live-before-decision" // a live count on status issued before the decision (provisional only)
)

Status bases (IdentityDecision.BasisKind).

View Source
const (
	TimeSourceLogSignerClock = "log-signer-clock"
	TimeSourceRFC3161        = "rfc3161"
	TimeSourceAnchorReceipt  = "anchor-receipt"
)

Time sources (IdentityDecision.TimeSource).

View Source
const DecisionFormat = "bide.approval.v3"

DecisionFormat names the signed decision format this version writes and accepts: the domain tag of ApprovalDecisionBytes without its newline. A gate's journaled tally records it.

View Source
const DecisionFormatCert = "bide.approval.v4"

DecisionFormatCert names the signed decision format of a certificate-bound approver: the v3 layout plus the SHA-256 of the signer's leaf certificate (ApprovalDecisionBytesCert). A bare-key approver's decision stays v3, and no verifier of one accepts v4.

View Source
const DefaultReceiver = "default"

DefaultReceiver is the name of the receiver WithNotifier's Notifier is, in Notification.Receiver and in WithNotifyRoute, WithNotifyApproverRoute and EscalationStep.

View Source
const ExpiredResult = `"tool call denied: the approval gate expired"`

ExpiredResult is the result an agent's model reads for a call whose approval gate expired: an error result, as for a denial.

View Source
const JournalFormat = "bide.journal.v1-dev"

JournalFormat is the journal format this version writes: the format every run's header names (see StepHeader). Before 1.0 it is one dev tag, "bide.journal.v1-dev", the same in every pre-release and never bumped per change: a pre-release journal is not promised to be readable by a later pre-release. It becomes "bide.journal.v1" at the 1.0 tag, so a 1.0 binary refuses every pre-release journal and none is read under rules it was not written for.

View Source
const MaxEscalationSteps = maxEscalationSteps

MaxEscalationSteps is the most steps an escalation chain may have.

View Source
const NotifyDigestTrail = "notify:@digest"

NotifyDigestTrail is the name of the trail the periodic digest uses; it belongs to no run.

View Source
const NotifyOpenTrail = "notify:@open"

NotifyOpenTrail is the prefix of the trail that lists the events whose all-clear may still be owed: a watch record before an outcome_unknown, approval_pending or recovery_failing event is observed, an unwatch record when nothing more is owed for it. It belongs to no run, so a sweep or a recovery pass finds the runs that need a scan, including a run that has completed since, without reading every finished run.

It is kept in one segment per UTC day, OpenSegment(t), so a read is bounded by the events listed now and the day's records, not by history. A writer that finds the newest segment is not today's appends seal to it (the append race makes the seal final: nothing is appended after it), then checkpoint, the listed set folded from it, as the first record of today's. The trails stay append-only and verifiable: a checkpoint must equal the fold of the segment it continues.

View Source
const OpenFirstYear = 2026

OpenFirstYear is the first year an open-trail index can exist: the year the open trail began. Every lookup, and every audit, looks at the years from it to the present, so none depends on a bound chosen for a deployment's age.

View Source
const PendingExecHoldReapproval = "" /* 136-byte string literal not displayed */

PendingExecHoldReapproval is ApprovalPending.Reason for a certificate-bound gate whose counted approvals were held before execution (ExecHold: a counted decision no longer holds): the call did not run, and it runs once approvers decide again and a new count passes (ApprovalIdentTallyStep of the next generation).

View Source
const PendingIdentReapproval = "" /* 181-byte string literal not displayed */

PendingIdentReapproval is ApprovalPending.Reason for a certificate-bound gate whose recorded tally is a plain-name one (approval-tally:<id>), counted without the approvers' certificates: the gate does not run the call on it, and asks again; the new count is recorded under ApprovalIdentTallyStep.

View Source
const PendingIdentityStatus = "" /* 215-byte string literal not displayed */

PendingIdentityStatus is ApprovalPending.Reason for a certificate-bound gate whose counted approvals cannot be shown to hold right now (the status of a certificate is missing or stale, or a decision no signed head covers): the call did not run and nothing authoritative was written. The same approvals run it once a fresh check passes; no approver needs to decide again.

View Source
const PendingReapproval = "approval re-required: the recorded approvals predate " + DecisionFormat + "; approvers sign again, then re-run"

PendingReapproval is ApprovalPending.Reason for a gate that passed under decisions signed before DecisionFormat (a run journaled by an earlier version): its call did not run, and it runs once approvers decide again under DecisionFormat with SubmitDecision.

View Source
const PendingSoDReapproval = "" /* 171-byte string literal not displayed */

PendingSoDReapproval is ApprovalPending.Reason for a gate whose recorded tally passed without excluding the requester (ApprovalPolicy.ExcludePrincipal) while its policy now excludes it: the call did not run, and it runs once enough approvers who are not requesters have decided.

View Source
const ReanchorMinInterval = 10 * time.Second

ReanchorMinInterval is the least time between two forced heads of one run: anchoring signs with the log key, which may be an HSM or KMS key with quotas and cost, so the gate rate-limits it.

View Source
const SaltSize = 32

SaltSize is the length of Record.Salt: 32 bytes (256 bits) from crypto/rand.

Variables

View Source
var (
	// ErrConfig is a programmer-facing misuse or misconfiguration, not a runtime fault.
	ErrConfig = errors.New("config")
	// ErrModel is a model-provider call that failed: HTTP status, decode, stream error.
	ErrModel = errors.New("model")
	// ErrTool is a tool that failed or was misused (unknown tool, bad arguments).
	ErrTool = errors.New("tool")
	// ErrStorage is a durable-store I/O failure: marshal, insert, query, history.
	ErrStorage = errors.New("storage")
	// ErrProtocol is malformed wire or journal data (bad log entry, unknown content part).
	ErrProtocol = errors.New("protocol")
	// ErrBudget is a middleware limit tripping (e.g. a token budget).
	ErrBudget = errors.New("budget")
)

The toolkit classifies failures with sentinel errors matched by errors.Is — the standard-library idiom (io.EOF, sql.ErrNoRows). There are two tiers:

  • Category sentinels (ErrModel, ErrTool, ErrStorage, ErrConfig, ErrProtocol, ErrBudget) — the coarse class of failure, for a broad branch or a metric label.
  • Condition sentinels (ErrUnknownTool, ErrToolArgs, …) — a specific cause. Each wraps its category, so errors.Is(err, ErrUnknownTool) and errors.Is(err, ErrTool) both hold.

Every error the toolkit returns wraps exactly one category (and usually the underlying cause too, via a second %w). Match the class you care about:

if errors.Is(err, agent.ErrModel)   { retryLater() }   // any model-provider fault
if errors.Is(err, agent.ErrUnknownTool) { … }          // one specific condition

The control-flow signals are richer than a category, so they stay concrete types matched with errors.As, not sentinels: *ApprovalPending (human approval needed), *OutcomeUnknown (unsafe to resume), *SagaAborted (transaction rolled back). context cancellation surfaces as the usual context.Canceled / context.DeadlineExceeded.

View Source
var (
	ErrUnknownTool = fmt.Errorf("unknown tool: %w", ErrTool)
	ErrToolArgs    = fmt.Errorf("invalid tool arguments: %w", ErrTool)
	// ErrToolOutcomeUnknown is a tool call that may or may not have taken effect: the tool
	// sent its request but lost the connection (or its deadline passed) before the answer
	// came back. A tool returns an error wrapping it instead of an ordinary failure when it
	// cannot tell. For a tool that is not retry-safe the agent records no result, as it does
	// for a cancelled call: the run stops with this error, and a resume halts (*OutcomeUnknown)
	// rather than run the side effect a second time. For a retry-safe tool the call is an
	// ordinary failure the model sees, since running it again does no harm.
	ErrToolOutcomeUnknown = fmt.Errorf("tool outcome unknown: %w", ErrTool)
	// ErrToolReinvoked is a tool middleware calling a tool that is not retry-safe a second
	// time for the same tool call. The agent refuses and does not run the tool again.
	ErrToolReinvoked    = fmt.Errorf("tool invoked again for one call: %w", ErrConfig)
	ErrNoRecordedOutput = fmt.Errorf("no recorded model output: %w", ErrModel)
	// ErrIncompleteResponse is a model stream that ended without a Finish event: the
	// response stopped partway through a turn, so what arrived is not the model's answer.
	ErrIncompleteResponse = fmt.Errorf("model response ended before the turn finished: %w", ErrModel)
	ErrTruncatedToolArgs  = fmt.Errorf("truncated tool-call arguments: %w", ErrProtocol)
	// ErrOutputTruncated is a model turn that ended at its output token limit (or the context
	// window): Finish reason FinishLength. What arrived is only the start of the model's answer,
	// so it is not journaled. It wraps ErrModel, so middleware.Retryable retries it; the same
	// request usually ends the same way, so raise the limit (MaxTokens) rather than rely on a retry.
	ErrOutputTruncated = fmt.Errorf("model output cut off at its token limit: %w", ErrModel)
	// ErrOutputFiltered is a model turn that a provider's safety or content filter stopped, or
	// that the model refused: Finish reason FinishFiltered. What arrived is not the model's whole
	// answer, so it is not journaled. It wraps ErrModel, so middleware.Retryable retries it.
	ErrOutputFiltered = fmt.Errorf("model output stopped by a content filter or refusal: %w", ErrModel)
	// ErrStreamProtocol is a model stream that broke its provider's event protocol, such as
	// content after the turn's end or an event after the Finish. What arrived is not a turn the
	// model finished, so it is not the answer. It wraps ErrModel as well as ErrProtocol: the
	// turn is not journaled and a fresh attempt can succeed, so a retry middleware retries it.
	ErrStreamProtocol = fmt.Errorf("model stream broke its protocol: %w (%w)", ErrProtocol, ErrModel)
	// ErrToolUseIDReused is a model turn whose tool call has no ID, repeats an ID from an
	// earlier turn of the conversation, repeats one within the turn, or has an ID that is not
	// valid UTF-8 (the journal cannot hold it). The loop keys each call's result and journal
	// step by its ID, so a reused ID would pass the call off as already done. Any other ID is
	// safe: the keys derived from it encode it (see ToolResultStep and SubRunID). It wraps
	// ErrModel as well as ErrProtocol: the fault is in the model's output, the turn is not
	// journaled, and a fresh attempt can issue a valid turn, so a retry middleware treats it as
	// retryable.
	ErrToolUseIDReused = fmt.Errorf("tool-use id missing, reused, or malformed: %w (%w)", ErrProtocol, ErrModel)
	// ErrNegativeUsage is token usage with a negative count, reported by a model or returned by
	// a middleware. The usage is rejected rather than lowering a run's totals and budget. It
	// wraps ErrModel as well as ErrProtocol.
	ErrNegativeUsage = fmt.Errorf("negative token usage: %w (%w)", ErrProtocol, ErrModel)
	// ErrQuotaExhausted is a provider refusing a call because the account's quota or credit
	// is used up (OpenAI insufficient_quota, Anthropic billing_error, HTTP 402, a Gemini daily
	// quota), as opposed to a momentary rate limit. Waiting a few seconds does not lift it, so
	// middleware.Retryable does not retry it. It arrives as an *APIError.
	ErrQuotaExhausted = fmt.Errorf("provider quota or credit exhausted: %w", ErrModel)
	// ErrResponseTooLarge is a streamed response with a line longer than the adapter reads, or
	// longer in all than its adapter's cap (see MaxSSELine and LimitResponse in model/provider). The same
	// request would produce it again, so middleware.Retryable does not retry it.
	ErrResponseTooLarge = fmt.Errorf("model response line too large: %w", ErrModel)
	ErrBudgetExceeded   = fmt.Errorf("budget exceeded: %w", ErrBudget)
	ErrMaxTurns         = fmt.Errorf("max turns exceeded: %w", ErrBudget)
	// ErrInvalidApproval is an approver decision rejected at submission by SubmitDecision's
	// WithDecisionCheck: no such tool call, an unknown approver, or a signature that does not
	// verify for this exact call.
	ErrInvalidApproval = fmt.Errorf("invalid approval: %w", ErrConfig)
	// ErrStatusUnavailable is a certificate-bound decision submitted (with WithDecisionCheck) while the
	// revocation status of its chain is missing or stale: the decision is not refused, the gate cannot
	// judge it yet, and nothing is written. It is transient (retry when status is current), not a
	// configuration error; the approvals intake answers 503 for it.
	ErrStatusUnavailable = errors.New("revocation status is not available")
	// ErrAlreadyDecided is a decision submitted (with WithDecisionCheck) by an approver whose
	// earlier valid decision on the same call already counts; the new one would be ignored.
	ErrAlreadyDecided = fmt.Errorf("approver already decided: %w", ErrConfig)
)

Condition sentinels — specific causes, each wrapping its category.

View Source
var ErrAlreadyResolved = fmt.Errorf("halt already has a different recorded outcome: %w", ErrConfig)

ErrAlreadyResolved is the condition of a resolution refused because the operation already has a different recorded outcome (see HaltAlreadyResolved). It wraps ErrConfig.

View Source
var ErrApprovalExpired = errors.New("approval expired")

ErrApprovalExpired is returned (wrapped) when an approval gate's deadline has passed: by Approve and SubmitDecision for a decision on such a gate (also ErrConfig: nothing is recorded), and by a plan flow for a node whose gate expired (also plan.ErrApprovalDenied). An agent's model reads an expired call as denied (ExpiredResult).

View Source
var ErrGateNeedsNewRun = errors.New("the gate needs a new run")

ErrGateNeedsNewRun is wrapped (with ErrConfig) by the error of a certificate-bound gate that cannot go on in place: its recorded count passed without excluding the requester and the policy now excludes them, so no re-ask can be named. The drive refuses; a reader of the gate (EvaluateGate) reports the same error as GateMisconfigured, and an operator cancels the run and starts the call again.

View Source
var ErrJournalVersion = fmt.Errorf("unsupported journal format: %w", ErrProtocol)

ErrJournalVersion is a run whose journal this version cannot read or write: its header names a journal format this version does not support, or the journal has no header first (written before the header existed, or by something other than a Journal). It wraps ErrProtocol. The error the journal returns is a *JournalVersionError, which wraps it.

View Source
var ErrLeaseLost = errors.New("run lease lost")

ErrLeaseLost ends a drive whose run lease was lost while it ran: the store reported the lease no longer held (it lapsed and another process took it), or no renewal succeeded before the renewal cutoff (see Lease). The drive's context is cancelled with it as the cause (context.Cause), and Lease returns the drive's error wrapped with it. It is a coordination signal, not a fault, so like context.Canceled it wraps no category: the run is not failed, it is another driver's to continue. Recover does not count it as a failure.

View Source
var ErrNoLiveAttempt = fmt.Errorf("operation has no live attempt to resolve: %w", ErrConfig)

ErrNoLiveAttempt is the condition of a resolution refused because the operation named has no live attempt marker: it never halted (it was never attempted, or its only attempts are recorded as not started and are re-attempted by the next drive). It wraps ErrConfig.

View Source
var ErrNotResumable = errors.New("run not resumable by this resumer")

ErrNotResumable is a Resumer's answer for a run it does not drive (another kind, a typed run whose schema is not its type's). ResumeAny tries the next Resumer on it, and Recover reports a run no Resumer drives once per process. Like ErrLeaseLost it wraps no category.

View Source
var ErrNotStarted = errors.New("run has no run:start record")

ErrNotStarted is a run with no run:start record: one never driven (a Signal sent to a mistyped run ID, say), or one whose first drive has not written it yet. Resume and Cancel refuse such a run, and Recover skips it and reports it once per process. It wraps no category: the run may be a race with its first drive, which a later call does not lose, rather than a configuration error.

View Source
var ErrNotifyPermanent = errors.New("agent: notification refused by the sink")

ErrNotifyPermanent marks a Notifier error as one a retry within the round cannot fix.

View Source
var ErrRunCancelled = errors.New("run cancelled")

ErrRunCancelled is the end of a run that was cancelled (see Cancel): the first of the run's end markers in journal order is run:cancelled. A drive of such a run returns it, with a Result, whatever input it is given, as a finished run returns its answer. It wraps no category: it is a terminal status, not a fault.

View Source
var ErrRunEnded = errors.New("run already ended")

ErrRunEnded is Cancel's answer for a run that is already over: its first end marker is run:complete or run:aborted, so there is nothing left to cancel. Like ErrRunCancelled it wraps no category.

View Source
var ErrToolNotCalled = errors.New("tool not called")

ErrToolNotCalled marks a tool call that is known never to have reached its tool, so its side effect cannot have happened. It is a condition with no category: it is wrapped beside the error that says why (a context error, a policy denial, ErrConfig).

A tool middleware that ends a call without calling next (a rate limiter that gives up, a policy that denies) returns an error wrapping it, so the agent records the call as a known failure. It must never be returned for a call that did call next: next may have reached the tool. A middleware reaches the tool only through next, never by calling the tool itself. The agent knows whether next reached the tool, and trusts it over this error; but for a call whose chain returned without entering next at all, an error that does not wrap ErrToolNotCalled leaves a side effect's outcome unknown, and the run halts for it rather than risk running it twice.

View Source
var ErrTrailConflict = errors.New("agent: notify trail position taken by a different record")

ErrTrailConflict is returned by NotifyTrail.Append when the position is already taken by a different record: another process won the append. The loser reloads, folds, and decides again.

View Source
var ErrTurnContended = errors.New("session turn driven by another holder")

ErrTurnContended is returned by a Session's Send, SendOnce, Send and SendOnce when another driver holds the lease on the turn's run (see Session): another worker, or, while a cancelled saga turn's rollback is in progress, another caller on the same session handle (see Session.Send). The turn's run was not driven and its answer was not recorded. Send the same message again later. Like ErrLeaseLost, it is in no category: it is neither a failure of the turn nor a pause of it.

Functions

func AbortedKey added in v0.16.0

func AbortedKey(runID string) string

AbortedKey is the event key of runID reaching run:aborted.

func Ack

func Ack(ctx context.Context, d *Journal, runID, channel, key string) error

Ack marks a message consumed so Receive advances past it. Idempotent (the first ack for a (runID, channel, key) wins) and journaled, so the advance survives a crash and a resume.

func ApprovalDeadlineStep added in v0.18.0

func ApprovalDeadlineStep(toolUseID string) string

ApprovalDeadlineStep is the journal name of the record that fixes the deadline of the gate on toolUseID (a tool call's ID, or a plan flow node's key).

func ApprovalDecisionBytes added in v0.4.0

func ApprovalDecisionBytes(s ApprovalSubject, approverID string, approved bool, decidedAt time.Time) []byte

ApprovalDecisionBytes returns the canonical bytes an approver signs and every verifier checks. decidedAt is when the approver decided, by their own clock; it is signed in FormatTimestamp's form, so pass the same time as Decision.DecidedAt. The layout, stable and reproducible outside Go:

"bide.approval.v3\n"
len32(RunID) RunID
len32(ToolUseID) ToolUseID
len32(ToolName) ToolName
SHA-256(canonical args), 32 bytes
len32(approverID) approverID
1 byte: 1 approved, 0 denied
len32(decidedAt) decidedAt

len32 is a 4-byte big-endian length, so no two distinct decisions encode to the same bytes. The canonical args are the arguments parsed as JSON and re-serialized with object keys sorted, no insignificant whitespace, no HTML escaping, and number literals kept verbatim; empty arguments are "{}", and bytes that are not a single JSON value are used as-is. The canonical form makes the signature independent of how the arguments were formatted in transit (a live model stream, the journal, an evidence file). decidedAt is RFC 3339 in UTC to the millisecond, 24 bytes, e.g. "2026-10-13T12:14:03.123Z" (FormatTimestamp); a verifier refuses a decision whose recorded time is in any other form (ParseTimestamp), so an instant has one signed spelling. A time outside the years 0001 to 9999 has no such form, and a decision signed with one never counts. docs/design/design-mofn-approval.md works one decision through to its exact bytes, for a verifier written in another language.

func ApprovalDecisionBytesCert added in v0.19.0

func ApprovalDecisionBytesCert(s ApprovalSubject, approverID string, approved bool, decidedAt time.Time, leafHash [32]byte) []byte

ApprovalDecisionBytesCert returns the canonical bytes a certificate-bound approver signs (bide.approval.v4): the layout of ApprovalDecisionBytes with the domain tag "bide.approval.v4\n" and, between the verdict byte and the decision time,

len32(leafHash) leafHash     (the 32-byte CertLeafHash of the signer's leaf certificate)

so a signature is valid only in the context of that certificate: another valid certificate for the same key, or a swapped-in one, cannot be attached to someone else's signature. decidedAt is signed in FormatTimestamp's form, as in v3.

func ApprovalExecHoldStep added in v0.19.0

func ApprovalExecHoldStep(toolUseID string, gen int) string

ApprovalExecHoldStep is the journal name of the ExecHold record of generation gen of toolUseID's gate.

func ApprovalIdentReaskKey added in v0.19.0

func ApprovalIdentReaskKey(runID, call string, gen int) string

ApprovalIdentReaskKey is the event key of the approval gate for call on runID asking again for generation gen of a certificate-bound gate: 0 over a plain-name tally it does not act on (PendingIdentReapproval), n >= 1 after an exec hold of generation n-1 (PendingExecHoldReapproval).

func ApprovalIdentReaskStep added in v0.19.0

func ApprovalIdentReaskStep(toolUseID string, gen int) string

ApprovalIdentReaskStep is the journal name of the pause note a certificate-bound gate writes when it asks again for generation gen: 0 over a plain-name tally it does not act on (PendingIdentReapproval), n >= 1 over the generation n-1 tally that an exec hold superseded (PendingExecHoldReapproval). Like ApprovalReaskStep's notes it holds an ApprovalWait with Reask and Supersedes set, is informational, and counts only if the journal proves it (ValidReaskNote).

func ApprovalIdentTallyStep added in v0.19.0

func ApprovalIdentTallyStep(toolUseID string, gen int) string

ApprovalIdentTallyStep is the journal name of the terminal tally of an identity-bound gate at generation gen: ApprovalTallyStep plus ":ident" for generation 0 and ":ident-reask:<gen>" for a gate re-asked after an exec hold. The tally is the commit point of a count: the status records and the identity record are written before it.

func ApprovalIdentityDigestStep added in v0.19.0

func ApprovalIdentityDigestStep(toolUseID string, gen int, digest string) string

ApprovalIdentityDigestStep is the name of the identity record of generation gen when the plain name already holds a record of a different tally (a crash between the record and the tally, then a different count): the record whose TallyDigest is the one the gate then writes is found under this name, digest being the first 16 hex digits of that digest.

func ApprovalIdentityStep added in v0.19.0

func ApprovalIdentityStep(toolUseID string, gen int) string

ApprovalIdentityStep is the journal name of the ApprovalIdentity record of toolUseID's gate at generation gen.

func ApprovalKey added in v0.16.0

func ApprovalKey(runID, call string, reminder int) string

ApprovalKey is the event key of the approval gate for call on runID. reminder is 0 for the first notification of the gate; a later reminder uses 1, 2, and so on.

func ApprovalPauseStep added in v0.19.0

func ApprovalPauseStep(toolUseID string, n int) string

ApprovalPauseStep is the journal name of the n-th phase note of the gate on toolUseID (an ApprovalWait with Pause set), n counting from 1 in the order the gate wrote them. It is informational and insert-once; the newest is the gate's phase (LatestPausePhase).

func ApprovalPendingStep added in v0.13.0

func ApprovalPendingStep(toolUseID string) string

ApprovalPendingStep is the journal name of the record that a run paused at the approval gate of the call toolUseID (see ApprovalWait).

func ApprovalReaskKey added in v0.17.0

func ApprovalReaskKey(runID, call string, sod bool) string

ApprovalReaskKey is the event key of the approval gate for call on runID asking again over a recorded tally it does not act on (ApprovalPending.Reason): with sod, because its policy now excludes the requester (PendingSoDReapproval); without, because the tally predates DecisionFormat (PendingReapproval). Approvers decide again, so it is an event of its own, not a repeat of the first ask (ApprovalKey).

func ApprovalReaskStep added in v0.17.0

func ApprovalReaskStep(toolUseID string, sod bool) string

ApprovalReaskStep is the journal name of the pause note a gate writes when it asks again over a recorded tally it does not act on: with sod, a tally that passed without excluding the requester under a policy that now excludes it (its outcome is recorded under ApprovalSoDRetallyStep); without, a tally that passed on decisions predating DecisionFormat (ApprovalRetallyStep). It holds an ApprovalWait with Reask and Supersedes set and the policy the gate asks again under, written only if absent, by every drive that pauses at the re-ask: the first writer wins, and a drive that crashed before writing it leaves it to the next.

The ApprovalPendingStep note is insert-once, so it keeps the policy of the first pause; a reader that has the journal but not the application's policy (bide view, bide approve, an audit) reads the newest note (LatestPauseNote) to see the gate as it now waits. Like the first note, it is informational: the gate never reads it.

func ApprovalRequestStep added in v0.14.0

func ApprovalRequestStep(toolUseID string) string

ApprovalRequestStep is the journal name of the request a plan flow node's approval gate records when the run first reaches it (see ApprovalRequest). toolUseID is the node's key.

func ApprovalRetallyStep added in v0.16.0

func ApprovalRetallyStep(toolUseID string) string

ApprovalRetallyStep is the journal name of the terminal tally a gate records when it re-asks for approval because the tally under ApprovalTallyStep passed on decisions that predate DecisionFormat (see ApprovalTally.Supersedes). When both are recorded, this one is the gate's outcome. The encoded ID has no ':', so the name never collides with another call's tally.

func ApprovalSoDRetallyStep added in v0.17.0

func ApprovalSoDRetallyStep(toolUseID string) string

ApprovalSoDRetallyStep is the journal name of the terminal tally a gate records when it re-asks for approval because the tally it would otherwise go by passed without excluding the requester, and the gate's policy now excludes it (ApprovalPolicy.ExcludePrincipal: a redeploy added the exclusion, or a drive still running the old policy recorded first). Its Supersedes names that tally. When it is recorded over that tally, it is the gate's outcome.

func ApprovalStatusStep added in v0.19.0

func ApprovalStatusStep(hash string) string

ApprovalStatusStep is the journal name of the ApprovalStatusBytes record whose DER hashes (SHA-256, lowercase hex) to hash. One record per run and hash, shared by every call of the run that relied on those bytes.

func ApprovalTallyStep added in v0.4.0

func ApprovalTallyStep(toolUseID string) string

ApprovalTallyStep is the journal name of the gate's terminal tally for toolUseID.

func Approve

func Approve(ctx context.Context, d *Journal, runID, toolUseID string, approved bool, opts ...ApproveOption) error

Approve durably records a human approve/deny decision for a tool call (HITL). It is idempotent: the first decision for a (runID, toolUseID) wins. After approving, re-run the agent with the pause's RootRunID and it resumes past the *ApprovalPending pause. The decision survives a crash because it's a journaled step like any other. For a plan flow node's gate (toolUseID a node key, in a flow's run) the run must have recorded the gate's request (it paused there): Approve refuses one with none (ErrConfig), and the gate honors only a decision that follows it; a decision recorded before the request under the same name is reported (ErrConfig), since it holds the gate's one decision and the gate cannot pass.

The record notes when it was written (Record.RecordedAt), by WithClock's clock, else the clock ctx carries (a run's), else the wall clock: the claim of the process that called Approve, not signed, set by the first decision and kept by the idempotent calls after it. It is for people and auditors; the gate never reads it. Approve takes no WithDecisionCheck, which belongs to SubmitDecision's signed decisions (ErrConfig).

Example

A tool gated by WithApproval pauses the run with *ApprovalPending before it runs. Approve records the decision, and calling Run again resumes the run past the pause.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/bide-ai/bide/agent"
	"github.com/bide-ai/bide/agent/agenttest"
)

// ChargeIn is the arguments of the charge tool in the examples.
type ChargeIn struct {
	Cents int `json:"cents"`
}

func main() {
	ctx := context.Background()
	refund := agenttest.Must(agent.Func("refund", "Refund a payment.",
		func(_ context.Context, in ChargeIn) (string, error) { return fmt.Sprintf("refunded %d", in.Cents), nil },
		agent.WithApproval(agent.SingleApproval())))
	model := agenttest.NewScriptedModel(
		agenttest.ToolTurn("call-1", "refund", `{"cents":500}`),
		agenttest.TextTurn("The refund is done."),
	)
	j := agenttest.MemJournal()
	a := agenttest.MustNew(model, j, agent.WithTools(refund))

	input := agent.UserText("Refund order 42.")
	_, err := a.Run(ctx, "run-1", input)
	var pending *agent.ApprovalPending
	if !errors.As(err, &pending) {
		fmt.Println("unexpected:", err)
		return
	}
	fmt.Printf("waiting for approval of %s %s\n", pending.ToolName, pending.Args)

	if err := agent.Approve(ctx, j, pending.RunID, pending.ToolUseID, true); err != nil {
		fmt.Println(err)
		return
	}
	res, err := a.Run(ctx, pending.RootRunID, input)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res.Message.Text())
}
Output:
waiting for approval of refund {"cents":500}
The refund is done.

func Await

func Await[T any](ctx context.Context, name string) (T, error)

Await blocks the current run until a single-shot signal named `name` is delivered, then returns its payload. Call it from inside a tool (the agent loop supplies the run context). On first encounter, with no signal recorded, it returns the zero T and a *SignalPending error that propagates out of Run, pausing the run durably. After Signal records a payload for the same name and Run is re-invoked, Await returns that payload and execution continues past this point. The payload survives a crash: it is a journaled step.

Await is the externally-pushed counterpart of Interrupt. Like Interrupt and Sleep it must be called from a retry-safe tool (Safety.ReadOnly or Idempotent): on resume the tool re-runs from the top until the await resolves, so everything before the Await call must be safe to repeat. Use distinct names for distinct awaits; each pauses and resolves independently.

func AwaitFor

func AwaitFor[T any](ctx context.Context, name string, d time.Duration) (T, bool, error)

AwaitFor blocks the current run until either a single-shot signal named `name` is delivered or the duration d elapses, whichever happens first. It returns (payload, true, nil) when the signal wins, or (zero, false, nil) when the timeout wins. Call it from inside a retry-safe tool (Safety.ReadOnly or Idempotent), like Await and Sleep: on resume the tool re-runs from the top until the await resolves, so everything before the AwaitFor call must be safe to repeat. A Waker that fails to schedule the deadline's wake fails the run as it does for Sleep.

It is Await composed with a durable timer. The deadline is journaled once, on the first encounter (at-most-once by name), as now()+d, so a resumed or crash-recovered run races against the same absolute instant rather than restarting the clock. While neither side has resolved, AwaitFor schedules a wake for the top-level run with the bound Waker (if any) and returns *SignalPending, pausing the run durably exactly like Await. The bool return is only meaningful when err is nil.

The outcome is journaled too, at-most-once by name, the first time either side wins. Every later evaluation of the same await (the tool re-runs because it pauses afterwards, or the process dies before the tool's result is recorded) returns that recorded outcome, so a signal delivered after the timeout won cannot flip the call to the signal branch.

func Cancel added in v0.10.0

func Cancel(ctx context.Context, j *Journal, runID, reason string, opts ...CancelOption) error

Cancel cancels runID's run (D1), recording reason. It takes no lease: it writes one record, and the run's drives act on it.

  • A run that is over is not cancelled: Cancel reads the run's end markers first, and a run whose first end marker (in journal order) is run:complete or run:aborted is ErrRunEnded, with nothing written. A run already cancelled is cancelled: Cancel returns nil and writes nothing, so a Cancel retried after a lost reply succeeds.
  • A run with no run:start record (one never driven, or a mistyped run ID) is ErrNotStarted, with nothing written.
  • A run that is not a saga gets the end marker run:cancelled {reason}. Recovery no longer lists it, and a drive checks for the marker when it starts, at every turn boundary, and once it has won a side effect's attempt claim, before calling the tool: it then starts nothing more and returns ErrRunCancelled. A call already past that check finishes and records its result. run:cancelled and the run's completion are different keys, so both can land; the first in journal order is the run's end for every reader, and Cancel reads the markers back after its write: if the run completed first, Cancel returns ErrRunEnded.
  • A saga (a run started with WithSaga) gets a rollback request, run:cancel-requested {reason}, which is not an end marker: recovery still lists the run, and the drive that sees the request (at the same checks) starts nothing more, rolls the run back, and then writes run:cancelled. Cancel returns once the request is durable; until the rollback finishes, Status reports the saga RunStarted. A saga whose run:complete lands before the request is seen is complete.

What "starts nothing more" covers, precisely: a side-effect call's claim won after the check saw the marker never fires (its attempt is recorded as not started). A retry-safe call (ReadOnly or Idempotent: no claim) is not checked one by one: a retry-safe call already dispatched in the current turn may still run, and the run stops at its next turn boundary or claim. A sub-run (a sub-agent's, a SubRunFor run, a delegation) reads its tree root's cancellation too, when it opens, at its turn boundaries and after every won claim, so a Cancel of the root stops the whole tree; it reads the root's markers from the root's store, also when its own agent journals to another. A plan flow's run checks it when the flow opens, at every node, and after every won claim.

Cancel's reads are not one atomic step: a drive can write run:complete between Cancel's first read and its write. The read-back after the write settles it: the first end marker in journal order is the run's end, and Cancel reports ErrRunEnded if it is not run:cancelled.

Cancel accepts any run ID but the empty one: a sub-run's or a session turn's run is a run too (see Session.Send for what a cancelled turn does to its session).

The run:cancelled record (and a saga's run:cancel-requested) notes when it was written (Record.RecordedAt), by WithClock's clock, else the clock ctx carries, else the wall clock. It is informational, for people and auditors: no drive, recovery pass or check reads it, and the first end marker in journal order is the run's end whatever the times say.

func Capability added in v0.8.0

func Capability[T any](store Store) (T, bool)

Capability returns store's implementation of the optional capability T (Lister, Leaser, or any other interface a store may implement beyond Store), looking through wrappers.

A store that wraps another store exposes the store it wraps by implementing

Unwrap() Store

Capability checks store itself first, then follows Unwrap until a store implements T, and reports false once a store has no Unwrap method or Unwrap returns nil. A wrapper therefore exposes exactly the capabilities of the store it wraps, with no forwarding methods to keep in step with new capabilities. A wrapper that must change what a capability means implements that capability itself, which takes precedence over the wrapped store's, as with errors.As. Only a wrapper that passes run IDs and names through unchanged may implement Unwrap (see Store). The engine follows Unwrap to the store beneath for what the process keeps per run.

func CertLeafHash added in v0.19.0

func CertLeafHash(der []byte) [32]byte

CertLeafHash is the SHA-256 of a leaf certificate's DER: what a v4 decision signs in place of the certificate itself.

func CertRecordVerifies added in v0.19.0

func CertRecordVerifies(iv CertIntakeVerifier, s ApprovalSubject, r Record) bool

CertRecordVerifies reports whether r is a valid decision of a certificate-bound approver for s as the intake judges (CertIntakeVerifier): its chain, identity, scheme and signature hold now and nothing in its chain is revoked. Status that is missing or stale does not make a record invalid. It is what an intake uses to tell a decision it already holds (a replay) from junk.

func DecisionStep added in v0.17.0

func DecisionStep(d Decision) string

DecisionStep returns the journal name SubmitDecision gives the record of d first: distinct for each distinct decision (call, approver, verdict, scheme and decision time), and the same for every resubmission of it, whatever its signature bytes, which is why that is a no-op. It is not a lookup key: a bad record may hold that name (the decision is then journaled under another), and a decision a version that named by signature wrote is under that name. To tell whether a decision is held, use HeldDecisionStep, ReportWritten, or WithDecisionCheck. A zero or non-canonical d.DecidedAt has no record (SubmitDecision refuses it); the name is then of FormatTimestamp's rendering of it.

func DigestKey added in v0.16.0

func DigestKey(bucket string) string

DigestKey is the event key of the digest for time bucket (an opaque, stable label).

func EncodeRecord added in v0.7.0

func EncodeRecord(r Record) ([]byte, error)

EncodeRecord returns the journal encoding of r: the bytes a store persists for it and the bytes an audit leaf commits to. Every store writes exactly these bytes, so a record's journal form does not depend on the backend, and every store hands back DecodeRecord of them, on the live path as on replay, so a resumed run rebuilds exactly the conversation the live run had.

The encoding is JSON with two properties that make it canonical:

  • It is a fixed point: decoding it with DecodeRecord and encoding the result again yields the same bytes. So a store holds exactly EncodeRecord(rec) for the record rec it hands back, and an audit leaf computed from a record read back from any store is the bytes that store persisted.
  • It keeps the content of every json.RawMessage (a tool's arguments and result, a step's value, a reconciler's evidence) byte for byte, except whitespace between JSON tokens, which is removed. Nothing is HTML-escaped: a tool result that says "<b>" is journaled as "<b>", not with the escapes json.Marshal writes for < and >, so an adapter that forwards the JSON text to a model sends what the tool returned. Key order, number spelling ("1.50", "1e20"), escapes already in the text, and even invalid UTF-8 inside a JSON string are kept as they are.

Insignificant whitespace is dropped because encoding/json compacts every raw value it embeds, and one spelling per JSON value is what an audit leaf should commit to. For the same reason the line and paragraph separators U+2028 and U+2029 are always written as their JSON escapes, which encoding/json emits in some builds but not others. In a Go string field (message text, a reasoning signature), invalid UTF-8 becomes U+FFFD, as encoding/json writes it; since the stores hand out only the decoded form, the live and replayed conversations agree.

func EscalationKey added in v0.17.0

func EscalationKey(key string, level int) string

EscalationKey is the event key of step level (from 1) of the escalation chain of the event with key.

func ExecHoldGen added in v0.19.0

func ExecHoldGen(toolUseID, name string) (int, bool)

ExecHoldGen parses the generation out of name, which must be ApprovalExecHoldStep of toolUseID; ok is false for any other name.

func ExpiredKey added in v0.18.0

func ExpiredKey(runID, call string) string

ExpiredKey is the event key of the expiry of the approval gate for call on runID (NotificationApprovalExpired).

func FormatTimestamp added in v0.16.0

func FormatTimestamp(t time.Time) string

FormatTimestamp returns t in the one form bide records times in (RunStart.StartedAt, a signed decision's decided_at, the recorded_at of a decision and of ApprovalWait): RFC 3339 in UTC, truncated to the millisecond, always 24 bytes and ending in "Z", e.g. "2026-10-13T12:14:03.123Z". A time outside the years 0001 to 9999 has no such form: what FormatTimestamp returns for it, ParseTimestamp refuses.

func GateReask added in v0.17.0

func GateReask(t ApprovalTally, excludePrincipal bool) string

GateReask returns why a gate asks again instead of acting on its recorded tally t, or "": t passed on decisions that predate DecisionFormat (PendingReapproval), or passed without excluding the requester while the policy now excludes it (PendingSoDReapproval). A reader that has no policy object, only the fact that the gate excludes the requester, asks with excludePrincipal.

func GateTallyValues added in v0.19.0

func GateTallyValues(recs []Record, toolUseID string) map[string]Record

GateTallyValues returns the StepValue records of recs that decide the gate of toolUseID, by name, first writer winning: the plain-name tallies (ApprovalTallyStep, ApprovalRetallyStep, ApprovalSoDRetallyStep), the identity tally chain and the exec holds.

func Generate

func Generate(ctx context.Context, m Model, req Request) (Message, Usage, error)

Generate is the convenience drain: stream and assemble in one call.

func HaltedKey added in v0.16.0

func HaltedKey(runID string, kind OpKind, opID string, attemptedMS int64) string

HaltedKey is the event key of an unknown-outcome halt of the operation opID of kind on runID. attemptedMS is the live attempt marker's time in Unix milliseconds (OutcomeUnknown.AttemptedAt; 0 if unknown), the same on every drive that reports the halt and different for an attempt made after the first was resolved.

func HeldDecisionStep added in v0.17.0

func HeldDecisionStep(ctx context.Context, store *Journal, d Decision, verifierFor ApproverVerifierFor) (name string, held, verified bool, err error)

HeldDecisionStep reports whether the journal holds a record of exactly the decision d (signature included) and the name of the record that does: under the identity name (DecisionStep), under the signature-derived name a bad record at the identity name pushes a decision to, or under the name a version that named by signature gave it. A record under another signature, or under another leaf certificate (or a chain where d has none), is not d, even when it is the same approver's decision of the same verdict and time: a randomized scheme's re-signing is found by SubmitDecision's WithDecisionCheck, which reads the signatures, not here.

held does not mean valid. It compares the record's fields and signature bytes and verifies nothing: a junk signature written without a check is held. With a verifierFor (the resolver the agent counts with), verified reports that the record verifies for the recorded call under the approver's key and, for a plan flow node's gate, was written after the gate's request, which is the only kind the gate counts. Without one verified is false. A flow gate's record written before the request is held, and never verified.

func IdentReaskGen added in v0.19.0

func IdentReaskGen(toolUseID, name string) (int, bool)

IdentReaskGen parses the generation out of name, which must be ApprovalIdentReaskStep of toolUseID; ok is false for any other name.

func IdentTallyGen added in v0.19.0

func IdentTallyGen(toolUseID, name string) (int, bool)

IdentTallyGen parses the generation out of name, which must be ApprovalIdentTallyStep of toolUseID; ok is false for any other name.

func IdentityTier added in v0.19.0

func IdentityTier(cc *CertCheck) string

IdentityTier maps a verifier's finding to the tier the identity record and the approvals intake report: only a final status is a final tier, and the time source says which (an independent source only when the finding names RFC 3161 or an anchor receipt; anything else is the log signer's clock); a live count or a short window is provisional. It is the one classifier: the gate's record and the intake's response both use it, so they cannot drift.

func Interrupt

func Interrupt[T any](ctx context.Context, name string, prompt any) (T, error)

Interrupt pauses the current run to request typed human input, identified by name. Call it from inside a tool (the agent loop supplies the run context). On first encounter it returns the zero T and an *InterruptPending error that propagates out of Run, pausing the run durably. After AnswerInterrupt records a value for the same name and Run is re-invoked, Interrupt returns that value and execution continues past this point. This generalizes approve/deny (a bool) to an arbitrary typed answer.

Interrupt must be called from a retry-safe tool (Safety.ReadOnly or Idempotent): on resume the tool re-runs from the top until the interrupt resolves, so everything before the Interrupt call must be safe to repeat. Use distinct names for multiple interrupt points; each pauses and resumes independently.

func IsApprovalDecision added in v0.4.0

func IsApprovalDecision(r Record, toolUseID string) bool

IsApprovalDecision reports whether r is an m-of-n approver decision (written by SubmitDecision) on toolUseID. The single-approver Approve record has no Approver and is not one.

func IsComplete

func IsComplete(ctx context.Context, store *Journal, runID string) (bool, error)

IsComplete reports whether runID completed: its first end marker in journal order is the completion marker the agent loop records at the end of a run (see runCompleteStep). A run whose run:cancelled or run:aborted precedes its run:complete is not complete: that marker is its end, as Status and every drive read it. A crash-recovery supervisor uses it to skip finished runs.

func IsDecisionStepName added in v0.17.0

func IsDecisionStepName(name, toolUseID, approverID string) bool

IsDecisionStepName reports whether name has the shape of the journal record name of a decision of approver on call: the call's approval prefix, the approver, and 64 lowercase hex digits. The identity name (DecisionStep), the signature-derived name a bad record at it pushes a decision to, and the name a version that named by signature gave a decision all have it. A client that is told a record name by an endpoint checks it with this before it shows it or writes it to disk.

func IsPause added in v0.9.0

func IsPause(err error) bool

IsPause reports whether err, or any error it wraps, is a Pause: the run stopped durably and is waiting, rather than failed.

func IsReservedStepName added in v0.8.0

func IsReservedStepName(name string) bool

IsReservedStepName reports whether name starts with a prefix the engine reserves for its own journal keys. Step and Parallel refuse such a name, and so does ResolveHalt for a step, except a plan flow node's key ("node:<name>"), which halts as a Step does.

func IsRunPrincipal added in v0.17.0

func IsRunPrincipal(r Record) bool

IsRunPrincipal reports whether r is a run's run:principal record (see RunPrincipal).

func IsSessionRun added in v0.8.0

func IsSessionRun(runID string) bool

IsSessionRun reports whether runID is a session's journal or one of its turn runs, which the session drives (see Session). A sub-agent called in a turn is IsSubRun instead.

func IsSubRun added in v0.8.0

func IsSubRun(runID string) bool

IsSubRun reports whether runID is a sub-agent's run (SubRunID), which its root run drives: its last '>' is followed by an encoded call ID, not by a session's mark.

func JournalEntry added in v0.7.0

func JournalEntry(name string, rec Record) ([]byte, error)

JournalEntry returns the bytes a store persists when it records rec as the step named name: rec with Name set to name and a fresh random Salt (see Record.Salt; a salt the engine drew for the record is kept), in its journal encoding (EncodeRecord). The journal records every new step through it, so every record carries a salt; the audit package refuses to commit a record without one. It errors only if the system's random source fails or rec cannot be encoded.

func LastSegment added in v0.17.0

func LastSegment(recs []NotifyRecord) string

LastSegment is the segment named by the last NotifySegment record of an open-trail index, "" if there is none.

func Lease

func Lease(ctx context.Context, store *Journal, runID string, drive func(context.Context) error, opts ...LeaseOption) (bool, error)

Lease runs drive under an exclusive, auto-renewed lease on runID, so a primary driver and a recoverer (or two workers) do not drive the same run at once. If the store implements Leaser and another holder currently leases the run, drive is NOT called and Lease returns (false, nil). If neither the store nor a store it wraps implements Leaser (see Capability), drive runs unconditionally. The bool reports whether drive ran; the error is the acquisition error (when false) or drive's own error (when true).

If the lease is lost while drive runs (see driveWithRenew for the renewal schedule), drive's context is cancelled with ErrLeaseLost as its cause (context.Cause), and a drive that then returns an error has it wrapped with ErrLeaseLost, so errors.Is(err, ErrLeaseLost) tells a lost lease from a shutdown or a genuine failure. A drive that returns nil succeeded regardless.

A primary driver wraps Agent.Run so a live run and a recoverer never both drive it, using the same lease the recoverer respects:

driven, err := agent.Lease(ctx, store, runID, func(ctx context.Context) error {
    _, err := ag.Run(ctx, runID, input)
    return err
}, agent.WithLeaseHolder("worker-1"))

func NextOnceKey added in v0.7.0

func NextOnceKey(ctx context.Context) string

NextOnceKey returns a key for the next exactly-once operation (an idempotent append, say) that the tool call running in ctx makes, or "" outside a tool call. The call's operations are numbered in the order they ask for keys, and the numbering starts again each time the call runs, so an operation keeps its key when the call runs again after a crash, and two operations of one call never share one.

The keys are scoped to one tool call: they dedupe the call running again (a resume after a crash, a retry-safe call a middleware retries), not the model calling the tool again. A retry the model makes, after a recorded error or a timeout, is a new call with a new tool-use ID, and gets new keys. Dedup across the model's retries needs a business key the tool derives from its arguments (an order id, a client request id the model passes), which the downstream dedupes on.

The numbering is stable only if the call asks for its keys in the same order each time it runs. Work a call does concurrently must therefore run in steps (Step, or Parallel's tasks): each step numbers its own operations, under a key that names the step, so concurrent steps keep their keys whatever order they run in.

func NotifyID added in v0.16.0

func NotifyID(key string) string

NotifyID is the stable delivery ID of an event: the hex SHA-256 of its key. It is the same on every attempt, in every process and after every restart.

func NotifyPending added in v0.16.0

func NotifyPending(ctx context.Context, j *Journal, n Notifier, trail NotifyTrail, opts ...NotifyOption) error

NotifyPending runs one catch-up sweep for a notifier: it reads every unfinished run in j and delivers, through n and trail as WithNotifier would, each event that needs a person and has not been delivered: a run waiting at an approval gate, and a run halted on an effect whose outcome is unknown. Call it at startup and on a timer, in an application that drives runs with Run alone and so has no RecoverLoop to find an event whose process died before it was recorded. With RecoverLoop or the sweep, an event is delivered at least once; with neither, only by the drive that saw it.

It reads each unfinished run's journal, so it costs a History per run: run it every few minutes, not every second. It needs a store that implements Lister. It reports, in the error it returns, any delivery that failed (those events are retried by the next sweep) and any run it could not read; it does not stop at the first.

When the trail signs and anchors its records (audit.NewAuditedEventStore beneath audit.NewNotifyTrail), the sweep also signs again the trail of each run it looked at that grew past its last signed head, within the anchor's timeout, so an outage of the anchor leaves an unattested stretch of trail only until the next sweep; a failure is reported to the handler of WithNotifyErrors and is not the sweep's error.

What it can see is narrower than a drive. A gate is found from the run's approval-pending record. A halt is found from an attempt marker that no not-started record voids and that has no result after WithNotifyHaltAge (default 5 minutes): it cannot tell a halted effect from one a process it cannot see is still running, so a call that runs longer than that is reported as halted, and it reports the cause as unknown. A run that was aborted, and recovery_failing, are raised by drives and by Recover and RecoverLoop, not by the sweep. Events are keyed as the drive keys them, so an event both see is delivered once.

func NotifyTrailName added in v0.16.0

func NotifyTrailName(runID string) string

NotifyTrailName is the name of runID's notification trail in a NotifyTrail.

func OpenIndex added in v0.17.0

func OpenIndex(year int) string

OpenIndex is the name of the open trail's index for a UTC year: one NotifySegment record per segment created, so the newest segment is found by reading the last record of the newest index (one or two loads, whatever the gaps and the age of the deployment), and the index is the size of a year of days at most.

func OpenSegment added in v0.17.0

func OpenSegment(t time.Time) string

OpenSegment is the name of the open-trail segment of the UTC day t is in.

func OverdueKey added in v0.16.0

func OverdueKey(key string) string

OverdueKey is the event key of the overdue notice for the event with key.

func ParseEscalationKey added in v0.17.0

func ParseEscalationKey(key string) (level int, orig string, ok bool)

ParseEscalationKey splits the key of an escalation step, "escalated:<level>:<key>" (see EscalationKey), into its level (from 1) and the key of the event it escalates. ok is false for any other key.

func ParseTimestamp added in v0.16.0

func ParseTimestamp(s string) (time.Time, error)

ParseTimestamp reads s, which must be in exactly the form FormatTimestamp writes: it parses s and refuses it unless formatting the result again gives s byte for byte. So it refuses another offset ("+00:00", "+02:00"), more or fewer fractional digits than three, a missing fraction, a lowercase "t" or "z", a leap second ("23:59:60.000Z"), and any date or time that does not exist. A signed decision time is checked with it, so one instant has one signed spelling.

func PromptDigest added in v0.9.0

func PromptDigest(msgs []Message) string

PromptDigest is the hex SHA-256 digest a model record journals of the system prompt a turn was sent: the text of every system message in msgs, in order, each length-prefixed under a domain tag, so no two different prompts share a digest. It is "" when msgs holds no system message.

func Recover

func Recover(ctx context.Context, store *Journal, resume Resumer, opts ...RecoverOption) (int, error)

Recover re-drives the runs that were in flight when the process died, in one pass. It enumerates the runs the store holds that are not over (via Lister, which filters out every run holding a terminal marker: run:complete, run:aborted or run:cancelled), and calls resume for each to push it forward. A run another driver finished after the listing is not resumed: holding the run's lease, Recover checks the terminal markers again and reads the run's run:start before it calls resume (over a Journal, four point reads per run it drives). It returns how many runs it re-drove and the joined genuine failures (nil if none).

The store must implement Lister; a store that cannot enumerate its runs (the base Store contract does not require it) yields an ErrConfig-wrapped error.

If the store also implements Leaser, Recover coordinates across processes: it claims an exclusive, renewed lease per run before driving it and skips a run another holder currently leases, so competing recoverers do not both re-drive the same run. A crash lets the lease expire (see WithLeaseTTL), and the next pass of another process takes the run over: Recover is one pass, so run RecoverLoop to keep passing. Without Leaser, Recover drives every enumerated run, which is safe under at-most-once memoization but redundant across processes.

A pause is a SUCCESS, not a failure. A re-driven run that is still waiting returns a Pause (*ApprovalPending, *InterruptPending, *TimerPending, *SignalPending, *OutcomeUnknown); Recover detects it with IsPause and does NOT record it as an error: it means "recovered, still waiting", and the run resumes later when its condition is met (a human approves, an interrupt is answered, a timer fires). Only a genuine error (a model or storage fault, a bad tool) is joined into the returned error. A run whose lease was lost mid-drive (ErrLeaseLost) is not joined either: another process holds it now and carries it on. Nor is a run the drive found cancelled (ErrRunCancelled), a saga's included once its cancellation's rollback finished: the run is over, as Cancel asked.

resume is deployment POLICY, not a mechanism the SDK can supply: it knows which agent drives a run and any Waker or clock it binds. Under the run's lease, after the terminal markers, Recover reads the run's run:start (one more point read) and hands it to resume, so resume needs no table of inputs: the run's input, saga flag and per-run options are journaled, and the run runs under them (see RunStart). ResumeAgent(a) is the Resumer of a's runs, ResumeTyped[T](a) that of its typed runs answering a T, and ResumeAny combines several (a deployment with two agents, say):

resume := agent.ResumeAny(agent.ResumeAgent(a, agent.WithWaker(w)), agent.ResumeTyped[Quote](b))

A deployment's own Resumer is any func(ctx, runID, start) error; it returns an error wrapping ErrNotResumable for a run it does not drive.

A run with no run:start (one never driven, such as a Signal sent to a mistyped run ID, or one whose first drive has not written it yet) is skipped, and reported once per process as ErrNotStarted. A run no Resumer drives (ErrNotResumable) is reported once per process too. What the process remembers is the report, not the skip: each pass reads run:start again, so a run that starts after a pass skipped it is recovered by a later pass. Neither counts as re-driven.

Recover skips a sub-agent's run (IsSubRun): its root run drives it, and re-running the root resumes it. It skips a session's journal and turn runs (IsSessionRun) too: a turn is seeded with the transcript before its message, which only the session holds, and only the session records its answer, so an unfinished turn resumes when its message is sent again (the same message through Send or Send, or the redelivered SendOnce or SendOnce). Recover re-drives every other incomplete run it enumerates. A nil resume is ErrConfig.

A run whose drive ends waiting on an approval or a halt resolution is re-signed, as RecoverLoop re-signs it: through the store's Reanchor method (audit.AuditedStore) or WithReanchor's function, which WithReanchor(nil) turns off. It writes nothing to the journal.

func RecoverLoop added in v0.8.0

func RecoverLoop(ctx context.Context, store *Journal, resume Resumer, opts ...RecoverLoopOption) error

RecoverLoop re-drives in-flight runs until ctx is done, so a run whose holder dies is taken over automatically: Recover is a single pass, and a run another holder leases at that moment is left for a later one. It runs two loops, each starting a pass every WithRecoverInterval (half the lease TTL by default):

  • The full pass enumerates the store's unfinished runs as Recover does and drives each one it can lease: halted runs (waiting on an approval, an interrupt, a timer or a signal, or with an outcome unknown), runs nobody leases (a plain Agent.Run whose process died) and runs whose holder died. It costs about six store round trips for each unfinished run it lists, halted runs included, and the next full pass does not start before this one has started all of its drives, so with many unfinished runs or a slow store a full pass can outlast the interval.
  • The lapsed loop, over a store that implements Leaser, enumerates only the unfinished runs whose lease has lapsed (RunFilter.LeaseLapsed) and drives them with slots of its own (WithRecoverLapsedConcurrency). A Leaser deletes a lease on release, so a lapsed lease is one its holder neither renewed nor released: the holder died or stalled. A halted run holds no lease between visits, so the lapsed loop neither visits the halted runs nor waits behind them. Each lapsed pass first deletes the lapsed leases no pass would take over, those of finished runs, of runs the store does not hold and of session and sub-agent runs (Leaser.ReapLeases), so a holder that died between its run's last write and its release does not leave a lease every later pass reads.

So a dead holder's run is picked up within about one interval of its lease expiring (the TTL after the holder's last renewal), however many halted runs the store holds, while the lapsed loop has a free slot and its listing is short: it waits for a slot only while it is already driving WithRecoverLapsedConcurrency lapsed runs. A run that holds no lease when its driver dies (one driven by a plain Agent.Run, not under Lease) is left to the full pass, and its pickup can take up to a full pass's length longer.

Runs are driven concurrently, up to WithRecoverConcurrency at once in the full pass (16 by default) and up to WithRecoverLapsedConcurrency in the lapsed loop (16 by default), so one long drive does not hold up the others; a run either loop is already driving is not started again by either. A pass that finds every slot of its loop busy waits for one, so each pass reaches every run it listed; the loop's next pass starts when this one has started all of its drives and the interval has elapsed. Genuine failures go to the WithRecoverErrors handler, and the run is retried on a later pass; pauses and lost leases are not failures (see Recover). A run that failed because its Waker could not schedule a wake (an error wrapping ErrStorage) recorded nothing for the sleeping call, so the next pass reaches the Sleep again and schedules again.

A run with no run:start, and a run no Resumer drives, are skipped and reported once per process, as Recover reports them (to the WithRecoverErrors handler); every pass reads run:start again.

A run whose drive ends waiting on an approval or a halt resolution is re-signed on every pass, when the journal's store has a Reanchor method (audit.AuditedStore, found through Unwrap) or WithReanchor names one: a decision another process wrote is then under a signed tree head within one pass. It reads that run's journal and writes nothing to it.

Run it once per process, for the life of the process, with the same resume Recover takes:

go func() {
    err := agent.RecoverLoop(ctx, store, resume,
        agent.WithLeaseHolder("worker-1"),
        agent.WithRecoverErrors(func(err error) { log.Print(err) }))
    // err is ctx's error once ctx is done
}()

A configuration error (a store that does not implement Lister, a non-positive TTL, interval or concurrency) is returned at once. Otherwise RecoverLoop returns ctx's error when ctx is done, after both loops have stopped and the drives they started (whose contexts derive from ctx) have returned.

func RecoveryKey added in v0.16.0

func RecoveryKey(runID string, episode int) string

RecoveryKey is the event key of runID's episode-th recovery failure (episode counts the recovered records before it, from zero).

func RedactURLs added in v0.8.0

func RedactURLs(s string) string

RedactURLs returns s with every URL in it redacted: its userinfo, each query parameter's value, and its fragment become REDACTED, and its scheme, host, path, and query parameter names are kept. It is the redaction the agent applies to a failed tool call's text; use ToolCall.ErrorText for a tool error, and this for other error text that can quote a URL.

func RenderMermaid

func RenderMermaid(ctx context.Context, d *Journal, runID string) (string, error)

RenderMermaid returns a Mermaid flowchart of a run's journaled steps — the "graph as derived OUTPUT" (Option B): you write plain Go, and the graph is rendered from what actually ran, not hand-authored. Feed it to the dev UI, a trace viewer, or a PR.

func ReportNotifyAck added in v0.16.0

func ReportNotifyAck(ctx context.Context, ack NotifyAck)

ReportNotifyAck is called by a Notifier, with the context it was given, to say how a delivery it is about to acknowledge was authenticated and answered. A call outside Notifier.Notify is a no-op.

func ResolveHalt

func ResolveHalt(ctx context.Context, store *Journal, ref HaltRef, out Outcome, opts ...ResolveOption) error

ResolveHalt is the sanctioned escape from an *OutcomeUnknown halt. After a non-retriable tool call or Step halted with an unknown outcome, an operator (or a reconciler) who has verified the real side effect out of band records it as the operation's result, so a re-run proceeds past the halt instead of halting again:

if halt, ok := errors.AsType[*agent.OutcomeUnknown](err); ok {
    // confirm out of band that the charge did go through
    err = agent.ResolveHalt(ctx, store, halt.Ref(), agent.Outcome{Result: "charged (operator-confirmed)"},
        agent.WithMinHaltAge(time.Minute))
    msg, err = a.Run(ctx, halt.RootRunID, input) // resumes past the halt
}

It is idempotent: the first result for an operation wins, so calling it twice or racing a concurrent driver records at most one. Resolving again with the same outcome returns nil; an operation that already has a different outcome (an earlier resolution, or the live driver's own result) is refused with *HaltAlreadyResolved, which reports the outcome that stands.

It refuses to resolve an effect a driver may still be running, whatever the halt's Cause:

  • with a store that leases runs (Leaser: MemStore, store/sqlite, store/postgres, found through a Journal and through wrappers that implement Unwrap() Store, see Capability), it takes the lease of the run the operation's driver leases (the root run, or a session turn's run for the turn and every sub-run inside it) for the resolution and returns *HaltInFlight while any driver holds it. Only drivers that lease the run (Lease, Recover, RecoverLoop, and a Session's turn) are seen; a plain Run holds no lease, which the claim below covers.
  • with a store that cannot (a custom store with no Leaser, or a wrapper that exposes none), it requires WithMinHaltAge, so the halt is resolved only once no driver can still be running it.

On either path, it then claims the attempt after the live one, under a claim of its own, before it records the outcome, and returns *HaltInFlight if a driver holds that claim already: a process that could not record that its claim never started may void the live attempt after the check and claim the next (a plain Run, which holds no lease, included), and its effect must not be overridden. The resolution's claim is journaled as an attempt marker of the operation. If recording the outcome then fails, the claim stays live (the outcome may have been recorded all the same), so the operation halts until it is resolved again; that resolution finds the resolution's attempt live and claims the one after it (with WithMinHaltAge, once that attempt is old enough).

WithoutLiveDriverCheck skips all of these, for an operator who knows no driver is running.

A plan flow's node runs as a Step under its node key, so its halt is OpRef{Kind: OpStep, ID: "node:<name>"} (or "node:iter:<n>:<name>" inside a loop), resolved like any Step's; the recorded Result is the node's output, as JSON of the node's output type.

It refuses (ErrNoLiveAttempt, which wraps ErrConfig) an operation with no live attempt marker: one that never halted, because it was never attempted or its attempts are recorded as not started. It refuses (ErrConfig) a ref with no valid Cause or Op.Kind, and an operation that only the other kind of operation attempted. WithMinHaltAge(d) refuses (*HaltTooYoung) a halt younger than d, measured from the live attempt's marker, so a reconciler cannot query and resolve before the provider's record has settled and thereby re-fire the effect. A HaltContended halt requires WithMinHaltAge: another driver may be running the effect, and resolving it while it is young could record an outcome that driver is about to contradict.

The recorded outcome notes when it was written (Record.RecordedAt), by WithClock's clock, else the wall clock (the one clock of the call: its claim marker and WithMinHaltAge use it too, and the context's clock is not read). It is the claim of the process that resolved the halt: the resolver signs nothing (ResolveHalt takes no key, and the runtime has no list of who may resolve), so the time is anchored only by the journal's Merkle trail once a signed head covers the record. It is informational: no drive, resume, replay or recovery pass reads it.

Deciding the true outcome is a human (or reconciler) judgment the runtime cannot make for you.

Example

A run halted with *OutcomeUnknown stays halted until someone who has checked the real outcome out of band records it with ResolveHalt. Calling Run again then continues past the halt, and the tool does not run a second time.

package main

import (
	"context"
	"errors"
	"fmt"
	"sync/atomic"

	"github.com/bide-ai/bide/agent"
	"github.com/bide-ai/bide/agent/agenttest"
)

// ChargeIn is the arguments of the charge tool in the examples.
type ChargeIn struct {
	Cents int `json:"cents"`
}

func main() {
	ctx := context.Background()
	store := &flakyStore{Store: agent.NewMemStore()}
	j := agenttest.MustJournal(store)

	var charges int
	charge := agenttest.Must(agent.Func("charge", "Charge the card.",
		func(context.Context, ChargeIn) (string, error) {
			charges++
			store.down.Store(true) // the charge went through, but its result is lost
			return "charged", nil
		}))
	model := agenttest.NewScriptedModel(
		agenttest.ToolTurn("call-1", "charge", `{"cents":500}`),
		agenttest.TextTurn("You have been charged."),
	)
	a := agenttest.MustNew(model, j, agent.WithTools(charge))

	input := agent.UserText("Charge me.")
	_, _ = a.Run(ctx, "run-1", input) // fails: the store is down
	store.down.Store(false)

	_, err := a.Run(ctx, "run-1", input)
	var halt *agent.OutcomeUnknown
	if !errors.As(err, &halt) {
		fmt.Println("unexpected:", err)
		return
	}
	fmt.Println("halted:", halt.Cause)

	// The payment provider confirms the charge went through: record that as the call's result.
	err = agent.ResolveHalt(ctx, j, halt.Ref(), agent.Outcome{Result: "charged (confirmed by the provider)"})
	if err != nil {
		fmt.Println(err)
		return
	}
	res, err := a.Run(ctx, halt.RootRunID, input)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res.Message.Text())
	fmt.Println("charges:", charges)
}

// flakyStore is a Store whose writes fail while down is set, as a lost database connection
// would.
type flakyStore struct {
	agent.Store
	down atomic.Bool
}

func (s *flakyStore) Insert(ctx context.Context, runID, name string, data []byte) (agent.Entry, bool, error) {
	if s.down.Load() {
		return agent.Entry{}, false, errors.New("store unavailable")
	}
	return s.Store.Insert(ctx, runID, name, data)
}

// Unwrap returns the wrapped store, so the journal finds its capabilities (its leases).
func (s *flakyStore) Unwrap() agent.Store { return s.Store }
Output:
halted: crashed
You have been charged.
charges: 1

func ResolvedKey added in v0.17.0

func ResolvedKey(key string) string

ResolvedKey is the event key of the all-clear for the event with key: the notification that it stopped being current. It has records of its own in the trail, like the overdue notice, and the delivery ID NotifyID(ResolvedKey(key)).

func RootRunID added in v0.17.0

func RootRunID(runID string) string

RootRunID returns the run that drives runID: the run itself, or for a sub-agent's run the root run to re-invoke to continue it (ApprovalPending.RootRunID for a gate inside a sub-agent). For a session's turn run, and the sub-runs inside it, it is the turn run, which the session drives.

func SameApprover added in v0.17.0

func SameApprover(a, b string) bool

SameApprover reports whether two ids name one approver: equal ignoring ASCII case, the rule Validate holds a policy's ids to, and the rule a gate that excludes the requester matches a requester to an approver id by. Approver ids are printable ASCII, so for them strings.ToLower is ASCII case folding; an id outside printable ASCII is equal only to itself.

func SameNotifyRecord added in v0.16.0

func SameNotifyRecord(a, b NotifyRecord) bool

SameNotifyRecord reports whether a and b are the same record: the same canonical JSON, so times compare by instant and not by location or monotonic clock reading.

func Sleep

func Sleep(ctx context.Context, name string, d time.Duration) error

Sleep pauses the current run until d has elapsed from the FIRST time this timer was reached, durably. Call it from inside a retry-safe tool (the agent loop supplies the run context). On the first encounter it journals the wake time (now + d) and returns *TimerPending, pausing the run; on a later resume it returns nil once the wake time has passed, and continues past this point. The wake time is fixed on the first call and memoized, so a resumed or crash-recovered run waits to the same absolute instant rather than restarting the clock. Use distinct names for distinct timers. Sleep requires a retry-safe tool (Safety.ReadOnly or Idempotent), like Interrupt.

With a Waker bound (the WithWaker option), Sleep schedules the wake before it pauses. If the Waker fails, Sleep returns an error wrapping ErrStorage instead of pausing: a pause with no wake scheduled could sleep forever. The run then fails and its tool call records nothing (the memoized wake time aside), so re-driving the run (RecoverLoop does, on its next pass) reaches this Sleep again and schedules again.

func SplitToKey added in v0.17.0

func SplitToKey(key string) (receiver, base string, ok bool)

SplitToKey splits the key of a fan-out delivery, "to:<receiver>:<key>" (see ToKey), into the receiver and the key of the event it delivers. ok is false for any other key.

func SubRunID added in v0.8.0

func SubRunID(parent, toolUseID string) string

SubRunID returns the run ID of the sub-agent run that the call toolUseID of run parent starts: parent, '>', then toolUseID encoded (see encodeID). It is RunScope inside that call.

func SubmitDecision added in v0.9.0

func SubmitDecision(ctx context.Context, store *Journal, d Decision, opts ...ApproveOption) error

SubmitDecision records d, one approver's signed decision on an m-of-n gated call. After enough decisions land, re-run the pause's RootRunID. (A single-approver gate uses Approve.)

The record carries d.DecidedAt, signed, and the time it was recorded (Record.RecordedAt), read from WithClock's clock, else the clock ctx carries (a run's), else the wall clock. Neither time affects the gate. A zero DecidedAt, or one with no canonical form, is ErrConfig, and so is an ApproverID that is not printable ASCII (see ApprovalPolicy.Validate), which no policy can name.

Each distinct decision is its own journal record; resubmitting the identical decision is a no-op: the same signature maps to the same record name, and with WithDecisionCheck the same approver, verdict, scheme and decided_at with a different signature (a randomized scheme such as ecdsa-p256 re-signs differently) is recognised as the same decision and written once. Without the check, a retry with a new signature adds a second record, which the gate ignores. The gate counts each approver's first valid decision and ignores the rest, so an invalid record never blocks a valid one. Without WithDecisionCheck, SubmitDecision does not verify: a decision that will not count is recorded and reported by the gate as ignored. For a plan flow node's gate (ToolUseID a node key, in a flow's run) the run must have recorded the gate's request: SubmitDecision refuses one with none (ErrConfig), and the gate counts only decisions after it. An identical decision recorded before the request (same name) is reported (ErrConfig): it cannot count; the approver re-signs at a new decided_at, which is a new decision that counts.

func SupersededTallies added in v0.17.0

func SupersededTallies(byName map[string]Record, outcome string) []string

SupersededTallies returns the names of the tallies that outcome, the gate's outcome (as RecordedTally returns it, among byName) supersedes, oldest first: the tally its Supersedes names, and that one's, and so on. Evidence for a re-approval tally carries them, since they decide that it is the outcome.

func TallyApprovals added in v0.4.0

func TallyApprovals(recs []Record, s ApprovalSubject, p ApprovalPolicy, verifierFor ApproverVerifierFor) (ApprovalTally, []DecisionCheck)

TallyApprovals is the m-of-n counting rule, shared by the gate and offline verification so the two cannot drift. Over recs, in journal order, it considers every decision record on s.ToolUseID (IsApprovalDecision). An approver's decision is their FIRST record that is valid: the approver is in p.Approvers, verifierFor resolves their key, the record carries a decision time in canonical form (ParseTimestamp), and the signature verifies over ApprovalDecisionBytes(s, approver, approved, decidedAt) under the scheme the record names (ApproverAlg), which must be the scheme of the approver's key. Records that are not valid never occupy an approver's place, so a forged or mistaken decision cannot block the approver's real one; later valid records from an approver who already decided are superseded.

A seat is a key, not an id. verifierFor is called once per approver, in policy order, and every record is checked with that one resolution. An approver whose verifier reports a key identity (ApproverVerifier.KeyIDs) that another approver's verifier also reports, or that reports none, never counts (ReasonSharedKey, ReasonNoKeyID), whichever of them signed first.

The decision time is checked only as signed bytes: its value never affects what counts, in what order, or the outcome, and the time a decision was journaled (RecordedAt) is not read. Journal order alone orders decisions.

Separation of duties: with p.ExcludePrincipal, the requesters are the principals the run's run:principal record among recs names (RecordedPrincipal, RunPrincipal.Requesters): the run's own and those of every run above it. An approver whose id equals any of them, ignoring ASCII case, never counts (ReasonRequester). When recs hold no run:principal, or one naming an empty or non-ASCII requester, no approver counts (ReasonNoRequester): the rule fails closed rather than count a decision it cannot tell apart from a requester's. A caller that passes only some of a run's records (offline verification, or a gate counting after a request) includes the run:principal.

It returns the tally and a classification of every record it considered. It does not validate p (see ApprovalPolicy.ValidateKeys, which the gate runs first and which refuses a policy this rule would count short); a nil verifierFor counts nothing.

func ToKey added in v0.17.0

func ToKey(receiver, key string) string

ToKey is the event key of key's delivery to receiver, for an event that goes to several receivers at once (an approval whose approvers are routed to different ones): each has a key, and so an ID, claim and acknowledgement, of its own.

func ToolResultStep added in v0.8.0

func ToolResultStep(toolUseID string) string

ToolResultStep is the key of the result of the call toolUseID: the record the loop, a saga rollback, or ResolveHalt writes when the call's outcome is known.

func ToolsDigest added in v0.9.0

func ToolsDigest(tools []ToolSpec) string

ToolsDigest is the hex SHA-256 digest a model record journals of the tool set a turn was sent: each tool's name, description and argument schema, length-prefixed under a domain tag, in the order of their names, so the digest does not depend on the order the tools were listed in. It is "" for no tools.

func ValidReaskNote added in v0.17.0

func ValidReaskNote(byName map[string]Record, toolUseID, name string, w ApprovalWait) bool

ValidReaskNote reports whether w, the content of the record named name (ApprovalReaskStep of toolUseID), is a re-ask note the journal proves: its Reask is the reason its name stands for, and its Supersedes names a recorded tally (among byName) that GateReask would ask again over for that reason, namely the call's tally for a format re-ask, and for a separation-of-duties re-ask the outcome so far (the format re-approval tally when the tally predates DecisionFormat and that one is recorded, else the tally) under a policy that excludes the requester. The gate writes a note only at such a point, so a note without it was not written by the gate.

func ValidReceiver added in v0.17.0

func ValidReceiver(name string) bool

ValidReceiver reports whether name is one a trail may record for a receiver other than the default: what WithNotifyReceiver accepts, and not DefaultReceiver, which a trail records as no name.

func ValidateRunID added in v0.10.0

func ValidateRunID(id string) error

ValidateRunID reports whether id may name a root run (Run, Resume, Stream, RunTyped): it is not empty and holds no '>', which the engine reserves for the run IDs of sub-agents and session turns. Any other string is a valid run ID: the journal's keys encode whatever they are given. A run ID that fails is ErrConfig, and the run entry points return it with a nil Result. (From a tool call's own context they also accept the programmatic sub-run IDs RunInfo.SubRunFor derives.)

func WaitUntil

func WaitUntil(ctx context.Context, name string, until time.Time) error

WaitUntil pauses the current run until the absolute time `until`, durably. It is the deadline form of Sleep: same semantics, but the wake time is the given instant rather than a relative delay.

Types

type APIError

type APIError struct {
	StatusCode int
	Body       string
	Message    string // the provider's error message
	Type       string // the provider's error type (OpenAI, Anthropic) or status (Gemini)
	Code       string // the provider's error code (OpenAI)
	Err        error
}

APIError is returned by a provider adapter for a non-2xx HTTP response that is not a rate limit (which uses RateLimited), and for an exhausted quota whatever its status (wrapping ErrQuotaExhausted). It carries the StatusCode so a retry classifier can tell a transient failure (5xx, 408) from a terminal one (most 4xx: auth, validation), the provider's error fields when the body held its error object, and the Body (cut to 8KB) for diagnostics. It wraps Err (typically ErrModel) so errors.Is(err, ErrModel) holds. StatusCode is 0 for an error the provider sent partway through a stream without one.

func (*APIError) Error

func (e *APIError) Error() string

Error describes the failure: the status, the provider's message and error code or type (or the Body when the provider sent no message), then Err.

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

Unwrap returns Err, so errors.Is(err, ErrModel) holds when Err wraps ErrModel.

type Agent

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

Agent binds a model, a tool set, a durable store, and a middleware chain.

Build an Agent with New and options, and derive a differently configured one with With, which copies it: an agent built that way is never changed, so it is safe for concurrent Run/Stream/Session calls and for concurrent With calls.

func New

func New(model Model, j *Journal, opts ...Option) (*Agent, error)

New constructs an Agent over model and the journal j, configured by opts. Every configuration problem is an error wrapping ErrConfig, returned here rather than at the first run:

  • a nil model, journal, option, tool, middleware or function argument;
  • two tools with one name, or a tool named "final_answer", which RunTyped reserves;
  • a tool whose input schema is not a JSON object, or that wraps another in a way the agent cannot honor (see SubAgent);
  • an invalid approval policy, an m-of-n policy with no WithApproverVerifiers, or one two of whose approvers resolve to one signing key (see ApprovalPolicy.ValidateKeys);
  • a negative limit (WithMaxTurns, WithTokenBudget, WithMaxConcurrency), a WithRetrieval k below 1, and a WithToolChoice that names no tool of the agent's or has an unknown mode.

Options apply in order, and for any setting the last one given wins. WithSystemPrompt and WithSystemPromptFunc fill one slot: the later of the two wins. Precedence for a setting is: a value the run was given (a run option; a sub-agent's run inherits its parent run's identity, Waker and clock), then the agent's, then the default.

The agent is immutable once built: With returns a configured copy and leaves the agent alone.

func (*Agent) FlushNotifications added in v0.16.0

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

FlushNotifications waits for the deliveries of a's notifier that are in flight to finish their round, and stops its worker; a later event starts it again. It returns ctx's error if ctx ends first. It does nothing for an agent with no WithNotifier. Events waiting out a backoff are not waited for: they are retried by the next process that drives or sweeps.

func (*Agent) Journal added in v0.10.0

func (a *Agent) Journal() *Journal

Journal returns the journal the agent writes through: the one New was given.

func (*Agent) Resume added in v0.11.0

func (a *Agent) Resume(ctx context.Context, runID string, opts ...RunOption) (*Result, error)

Resume drives runID's run on from its journal: the input, saga flag and options its first drive journaled (see RunStart), and the last limit amendment. opts may raise or lower its limits (WithMaxTurns, WithTokenBudget: journaled as an amendment) and set the deployment's values (WithWaker, WithClock, WithMaxConcurrency, the identity's Actor); any other setting that differs from the journaled one is ErrConfig. A run with no run:start is ErrNotStarted. A typed run is resumed with RunTyped or ResumeTyped, and a session turn by its session.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, runID string, input Message, opts ...RunOption) (*Result, error)

Run drives the agent to the end of runID's run, answering input under opts, and returns the run's Result. The first drive of a run journals input and opts in run:start, and every later drive runs under them (see RunStart): resume an unfinished run with Run and the same input (or Resume, which needs neither), and a finished run returns its recorded end, whatever it is passed.

The Result is non-nil whenever runID is valid (see ValidateRunID), whatever the error: a pause (*ApprovalPending, *InterruptPending, ...), a halt (*OutcomeUnknown), a failure, a saga's *SagaAborted, or ErrRunCancelled. Its Usage and Spend are the whole run's, sub-agents included; Turns and Duration are this call's. Message is the final answer, zero unless err is nil.

Example (Resume)

A run that fails partway resumes when Run is called again with the same run ID on the same journal: completed steps are replayed from the journal, so the tool does not run twice.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/bide-ai/bide/agent"
	"github.com/bide-ai/bide/agent/agenttest"
)

func main() {
	ctx := context.Background()
	var lookups int
	lookup := agenttest.Must(agent.Func("lookup", "Look up an order.",
		func(context.Context, struct{}) (string, error) { lookups++; return "order 42: shipped", nil },
		agent.WithSafety(agent.Safety{ReadOnly: true})))

	// The model is down for the second call of the first attempt.
	model := &outage{Model: agenttest.NewScriptedModel(
		agenttest.ToolTurn("call-1", "lookup", `{}`),
		agenttest.TextTurn("Your order has shipped."),
	), failAt: 2}
	j := agenttest.MemJournal()
	a := agenttest.MustNew(model, j, agent.WithTools(lookup))

	input := agent.UserText("Where is my order?")
	_, err := a.Run(ctx, "run-1", input)
	fmt.Println("first attempt failed on the model:", errors.Is(err, agent.ErrModel))

	res, err := a.Run(ctx, "run-1", input)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("resumed:", res.Message.Text())
	fmt.Println("lookups:", lookups)
}

// outage is a Model that fails its failAt'th call, as a provider outage would.
type outage struct {
	agent.Model
	calls, failAt int
}

func (m *outage) Stream(ctx context.Context, req agent.Request) (*agent.Stream, error) {
	if m.calls++; m.calls == m.failAt {
		return nil, fmt.Errorf("%w: provider unavailable", agent.ErrModel)
	}
	return m.Model.Stream(ctx, req)
}
Output:
first attempt failed on the model: true
resumed: Your order has shipped.
lookups: 1

func (*Agent) RunTyped added in v0.11.0

func (a *Agent) RunTyped[T any](ctx context.Context, runID string, input Message, opts ...RunOption) (T, *Result, error)

RunTyped runs the agent to the end of runID's run like Run, and returns its answer as a T instead of a free-form Message, with the run's Result (whose Output is the answer as journaled). It injects a synthetic final_answer tool whose JSON schema is derived from T (via the schema package) and instructs the model to call it once, with the structured answer, when its work is done. Real tools still run first, so a tool-using agent can do work and then answer typed. The first final_answer call the tool accepts ends the run: the model is not asked for another turn.

The answer is the arguments the final_answer tool accepted: what it received, after any tool middleware (a middleware that rewrites arguments rewrites the answer, as it would for any tool). The tool decodes them strictly as T (see Func: a missing required field, an unknown or case-variant name, a duplicate name, and so on are a tool error the model corrects) and journals them as its result, {"accepted": <arguments>}. RunTyped reads that JOURNALED result, not a value captured live, so it is resume-safe: on resume, or when the run already finished, the answer is recovered from the log without re-running anything. (A run journaled before final_answer recorded its arguments has the result {}; its answer is the model's arguments, decoded with encoding/json as the tool then accepted them.)

Only if the model never makes an accepted final_answer call (it answers in plain text instead) does RunTyped parse the text of the run's final turn (the message Run returns) as T, strictly too; a text that does not decode is ErrProtocol.

T must be a type whose schema is a JSON object (a struct, a pointer to one, or a map): the answer is a tool call's arguments, which providers take only as an object. Any other T is an ErrConfig before the run starts.

The output mode (WithOutputMode) is OutputTool by default: the final_answer tool collects the answer as above. OutputNative uses the provider's native structured output instead (a JSON-schema response format, Request.ResponseFormat, from T's schema), decoding the model's direct JSON output: prefer it on providers with strict structured outputs; an adapter that does not support response formats fails the run with ErrConfig before calling the model. The mode and T's schema (in full and as a digest) are journaled in run:start: driving a typed run through Run or Resume, or with another T or mode, is ErrConfig before any model call. Recovery resumes a typed run with ResumeTyped[T].

Example

ExampleAgent_RunTyped shows structured output: RunTyped returns a typed value, decoded from a schema-guided final_answer tool call (a real adapter streams the call the same way).

type Weather struct {
	City  string `json:"city"`
	TempF int    `json:"temp_f"`
}
m := &scriptModel{turns: [][]Emit{
	toolTurn("c1", "final_answer", `{"city":"SF","temp_f":68}`),
	textTurn("done"),
}}
a := mustNew(m, memJournal())

w, _, err := a.RunTyped[Weather](context.Background(), "run-1", UserText("weather in SF?"))
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Printf("%s is %d°F\n", w.City, w.TempF)
Output:
SF is 68°F

func (*Agent) Session

func (a *Agent) Session(ctx context.Context, id string, opts ...LeaseOption) (*Session, error)

Session opens (or reopens) a multi-turn conversation with the given id, rebuilding the transcript from the store so a restarted process continues where it left off.

opts set the lease each turn's run is driven under (WithLeaseHolder, WithLeaseTTL; see Lease): a non-positive TTL is ErrConfig. They have no effect over a store with no Leaser.

The id must not contain '>'. The session journals under "<id>>@session" and runs its turns under "<id>>@turn/<n>" and "<id>>@event/<encoded key>" (and their sub-agents under SubRunID(<turn run>, <call>)): everything up to the first '>' is the session id, so two sessions never share a run, and a root run ID may not contain '>', so no root run shares one with a session either.

func (*Agent) Stream

func (a *Agent) Stream(ctx context.Context, runID string, input Message, opts ...RunOption) *RunStream

Stream drives the agent like Run but returns a live RunStream: token deltas, turn boundaries and tool start and finish arrive as events while the durable loop runs, and RunStream.Result returns what Run would.

Example

ExampleAgent_Stream shows the streaming API: range Events for live progress, then call Final for the terminal answer. Run is the blocking equivalent of Final.

// A model that answers directly (a real adapter streams token deltas the same way).
m := &scriptModel{turns: [][]Emit{textTurn("hello world")}}
a := mustNew(m, memJournal())

stream := a.Stream(context.Background(), "run-1", UserText("hi"))
for ev := range stream.Events() {
	switch e := ev.(type) {
	case TurnStarted:
		fmt.Println("turn started")
	case ModelEvent:
		if d, ok := e.Event.(TextDelta); ok {
			fmt.Printf("delta: %q\n", d.Text)
		}
	case Finished:
		fmt.Println("finished")
	}
}
res, err := stream.Result()
if err != nil {
	fmt.Println("error:", err)
	return
}
answer := res.Message
fmt.Println("answer:", textOf(answer))
Output:
turn started
delta: "hello world"
finished
answer: hello world

func (*Agent) With added in v0.10.0

func (a *Agent) With(opts ...Option) (*Agent, error)

With returns a copy of a configured by opts, applied over a's configuration as New applies them: a setting given here replaces a's, and tools, middleware and retrievals given here are added after a's. The copy shares nothing mutable with a (its tool set, middleware lists and retrievals are its own, and settings are replaced, never written through), so the two may be used, and configured further, from different goroutines. a is never changed. A configuration problem is an error wrapping ErrConfig, as for New, including a tool whose name a already uses.

type AgentRunOption added in v0.10.0

type AgentRunOption interface {
	Option
	RunOption
}

AgentRunOption is a setting that applies to an agent (Option) and to one run (RunOption): WithMaxTurns, WithTokenBudget, WithSystemPrompt, WithSampling, WithToolChoice, WithWaker and WithIdentity.

func WithIdentity

func WithIdentity(id Identity) AgentRunOption

WithIdentity sets the acting identity of a run (see Identity): tool calls read it with IdentityFrom, and the runs they start inherit it. An identity the run's context already carries takes precedence, so a sub-agent built with WithIdentity runs as its parent's identity whenever the parent has one (attenuate the delegation to narrow it instead). An empty identity is ErrConfig.

func WithMaxTurns added in v0.10.0

func WithMaxTurns(n int) AgentRunOption

WithMaxTurns caps the model turns a run may take, so a model that keeps calling tools cannot loop forever. 0 means unbounded (the default); a negative n is ErrConfig. When the cap is reached the run returns ErrMaxTurns (category ErrBudget). The cap is per run (per Session.Send turn), not per session.

func WithSampling added in v0.10.0

func WithSampling(opts ...SamplingOption) AgentRunOption

WithSampling sets generation controls applied to every model call (Temperature, TopP, MaxTokens, Stop, Seed). Each control is its own setting: one this option does not set keeps the value an earlier option gave it. A nil control is ErrConfig.

func WithSystemPrompt added in v0.10.0

func WithSystemPrompt(s string) AgentRunOption

WithSystemPrompt sets a system message prepended to the conversation on every model turn. It fills the slot WithSystemPromptFunc fills: the later of the two wins. The message is configuration, not journal (see Agent.WithSystemPrompt).

func WithTokenBudget added in v0.10.0

func WithTokenBudget(max int) AgentRunOption

WithTokenBudget caps the tokens a run, and the runs its tool calls start, may use (see Agent.WithTokenBudget for how the budget is counted and enforced). 0 means unbounded (the default); a negative max is ErrConfig.

func WithToolChoice added in v0.10.0

func WithToolChoice(tc ToolChoice) AgentRunOption

WithToolChoice sets the tool-choice control applied to every model call (see ToolChoice and Agent.WithToolChoice). A mode that is not "", "auto", "none", "required" or "tool", mode "tool" with no Name, a Name with any other mode, and (for an agent) a forced tool the agent does not have are ErrConfig. Mode "required" on an agent with no tools is ErrConfig when the agent's model declares it needs one (ToolRules), at the run, since RunTyped supplies an answer tool. Mode "none" is enforced at dispatch too: a tool call the model makes anyway is refused with an error result the model reads, and its tool never runs (a typed run's answer tool excepted).

func WithWaker

func WithWaker(w Waker) AgentRunOption

WithWaker sets the Waker a run's durable Sleep registers its wake with (see Waker). A nil w is ErrConfig.

type Alg added in v0.9.0

type Alg string

Alg names a signature scheme: "ed25519", "ecdsa-p256", "ml-dsa-65", or "ed25519+ml-dsa-65". The audit package defines the schemes (audit.AlgEd25519, audit.AlgECDSAP256, audit.AlgMLDSA65, audit.AlgHybrid) and their signers and verifiers; the agent package only journals the name, with each approver decision, so a decision is checked under the scheme it was signed with.

const AlgCertificate Alg = "x509"

AlgCertificate is the Alg a certificate-bound verifier reports.

type AnswerOption added in v0.17.0

type AnswerOption interface {
	// contains filtered or unexported methods
}

AnswerOption configures AnswerInterrupt and Signal: WithClock, the clock that stamps the record's RecordedAt.

type ApprovalDeadline added in v0.18.0

type ApprovalDeadline struct {
	ToolUseID   string `json:"tool_use_id"`
	RequestedAt string `json:"requested_at"`
	ExpiresAt   string `json:"expires_at"`
	TimeoutMS   int64  `json:"timeout_ms"`
}

ApprovalDeadline is what the record named ApprovalDeadlineStep holds. The gate writes it once (insert-once), the first time it waits under a policy with a Timeout, and reads the deadline from it from then on. RequestedAt is when the request was recorded (a plan flow node's approval-request, or an agent call's approval-pending note, by their recorded_at, or the drive's clock when neither has one), and ExpiresAt is RequestedAt plus the timeout, both in FormatTimestamp's form. TimeoutMS is the timeout it was opened with.

func RecordedDeadline added in v0.18.0

func RecordedDeadline(recs []Record, toolUseID string) (d ApprovalDeadline, expiresAt time.Time, ok bool)

RecordedDeadline returns the deadline the gate on toolUseID recorded in recs (a run's records), and its parsed ExpiresAt, or ok=false when the gate has none.

type ApprovalIdentity added in v0.19.0

type ApprovalIdentity struct {
	Format      string               `json:"format"`
	ToolUseID   string               `json:"tool_use_id"`
	Generation  int                  `json:"generation"`
	Tally       string               `json:"tally"`        // the journal name of the tally this record belongs to
	TallyDigest string               `json:"tally_digest"` // SHA-256 of the exact JSON of that tally
	Evaluated   string               `json:"evaluated"`    // EvaluatedLive
	EvaluatedAt string               `json:"evaluated_at"` // the gate's clock, RFC 3339 with nanoseconds
	Policy      IdentityPolicyRecord `json:"policy"`
	// PolicyDigest is the SHA-256 of Policy's JSON, written without HTML escaping, with fields in
	// declaration order, map keys sorted and durations as time.Duration.String; it equals the pause
	// note's IdentityPolicyDigest.
	PolicyDigest string `json:"policy_digest"`
	// RosterDigest is the SHA-256 over the whole identity map in sorted approver order, each entry
	// len32(approver) approver len32(identifier) identifier.
	RosterDigest string             `json:"roster_digest"`
	Decisions    []IdentityDecision `json:"decisions"` // the counted certificate-bound decisions, in policy order
	Rules        string             `json:"rules"`
	Verifier     string             `json:"verifier"`                   // runtime.Version() of the verifier
	VerifierRev  string             `json:"verifier_rev,omitempty"`     // the build's vcs.revision, when known
	GoDebug      string             `json:"godebug_declared,omitempty"` // the x509 GODEBUG settings the operator DECLARED; not observed (the engine reads no environment)
	BuildGoDebug string             `json:"godebug_build,omitempty"`    // the build's baked-in DefaultGODEBUG (the toolchain's go line), from the build info
	RecordedAt   string             `json:"recorded_at,omitempty"`
}

ApprovalIdentity is what the record named ApprovalIdentityStep holds: the identity environment of a terminal count. It holds the policy in force, a digest of the identity roster, and for each counted certificate-bound decision the accepted chain, when and from what it became provable, its tier and basis, and the hashes of the status bytes relied on (ApprovalStatusBytes). The certificates are in the decision records. Decisions covers certificate-bound decisions only: the one-seat and requester checks that also involve bare-key decisions are covered by RosterDigest and the auditor's expected policy of record.

func IdentityRecordFor added in v0.19.0

func IdentityRecordFor(recs []Record, toolUseID string, gen int, tally Record) (ApprovalIdentity, bool)

IdentityRecordFor returns the identity record of generation gen that belongs to tally, the recorded tally of toolUseID's gate at that generation: the record whose TallyDigest equals the SHA-256 of tally's stored bytes, looked up under ApprovalIdentityStep first and then under ApprovalIdentityDigestStep (two drives racing, or a count that differed from an earlier attempt, can produce either). ok is false when none matches: a viewer or auditor then reports "incomplete identity data" and never a decided, verified gate.

type ApprovalPending added in v0.9.0

type ApprovalPending struct {
	RunRef
	ToolUseID string
	ToolName  string
	Args      json.RawMessage
	// Quorum is non-nil for an m-of-n gate: the running tally at the pause. Record
	// decisions with SubmitDecision, then re-run.
	Quorum *ApprovalTally
	// Reason says why the call waits when it is not its first ask: PendingReapproval when the gate
	// had passed on decisions that predate DecisionFormat and asks again, PendingSoDReapproval when
	// it had passed on approvals counted without excluding the requester and its policy now
	// excludes them. Empty otherwise.
	Reason string
}

ApprovalPending is returned by Agent.Run when a tool requiring human approval has no recorded decision yet. The run has paused durably; record a decision (Approve, or SubmitDecision for an m-of-n gate) against RunID, then re-run RootRunID to resume.

func (*ApprovalPending) Error added in v0.9.0

func (e *ApprovalPending) Error() string

Error names the run and the call awaiting approval.

func (*ApprovalPending) Subject added in v0.9.0

func (e *ApprovalPending) Subject() ApprovalSubject

Subject returns the approval subject of the paused call, which is what approvers sign:

sig, err := signer.Sign(agent.ApprovalDecisionBytes(pend.Subject(), "finance", true, decidedAt))

For a gate inside a sub-agent, RunID is the sub-run's id.

type ApprovalPolicy added in v0.4.0

type ApprovalPolicy struct {
	Need      int      `json:"need"`                // decisions required to proceed (k), 1 <= Need <= len(Approvers)
	Approvers []string `json:"approvers,omitempty"` // eligible approver ids, printable ASCII (see Validate); the bounded set (n)

	// ExcludePrincipal is separation of duties ("maker-checker"): the gate never counts a decision
	// by a requester of the run: the principal the run acts for (Identity.OnBehalfOf), or that of
	// any run above it, as its run:principal record names them (RunPrincipal.Requesters). An
	// approver id equal to a requester, ignoring ASCII case, fills no seat (ReasonRequester), and the
	// gate refuses with ErrConfig a run whose requesters it cannot tell (no run:principal, a run in
	// the chain with no OnBehalfOf, a requester that is not printable ASCII, a run:principal
	// journaled after the call's request) and a policy whose Need is unreachable once they are
	// excluded. It applies to m-of-n policies only: Validate refuses it on SingleApproval, whose
	// decision names no approver.
	ExcludePrincipal bool `json:"exclude_principal,omitempty"`

	// Timeout, when positive, is the gate's deadline: a call (or plan flow node) still waiting
	// Timeout after its request was recorded expires. The gate records the deadline once, when it
	// first waits (approval-deadline:<call>, see ApprovalDeadline), and from then on reads it from
	// that record, so a redeploy that changes Timeout does not move a gate already waiting. An
	// expiry is recorded as the gate's outcome, insert-once like a tally, so it is final: the call
	// never runs (it is denied, with ErrApprovalExpired), and a decision recorded after the
	// deadline never counts. Zero means no deadline. It must be a whole number of milliseconds,
	// as it is journaled (timeout_ms), and is never negative (Validate).
	Timeout time.Duration `json:"-"`
	// contains filtered or unexported fields
}

ApprovalPolicy declares a human approval gate. The policy SingleApproval returns is one decision, recorded with Approve and pausing as *ApprovalPending; only that value is, whatever its fields say, so a policy built by hand is never taken for it. Any other policy is an m-of-n gate: Need is k and the eligible approvers are the bounded set (n = len(Approvers)), whose signed decisions SubmitDecision records; Validate checks it, and refuses one with no approvers.

func SingleApproval added in v0.10.0

func SingleApproval() *ApprovalPolicy

SingleApproval returns the one-decision gate: a call pauses as *ApprovalPending until Approve records a decision for it. It is the only way to ask for that gate: an ApprovalPolicy literal, even {Need: 1} with no approvers, is an m-of-n policy, and one with no approvers is ErrConfig.

func (*ApprovalPolicy) Clone added in v0.10.0

func (p *ApprovalPolicy) Clone() *ApprovalPolicy

Clone returns a copy of p that shares nothing with it (a SingleApproval stays one), or nil for nil.

func (ApprovalPolicy) MarshalJSON added in v0.10.0

func (p ApprovalPolicy) MarshalJSON() ([]byte, error)

MarshalJSON encodes the policy as the journal records it: SingleApproval as {"single":true}, the bide protocol's wire form of the one-decision gate, so an offline reader sees the gate a call ran under without inferring it from a shape; any other policy as {"need":k,"approvers":[...]}, with "exclude_principal":true when ExcludePrincipal is set (omitted otherwise, so a policy without it encodes as it did before the field existed).

func (*ApprovalPolicy) UnmarshalJSON added in v0.10.0

func (p *ApprovalPolicy) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes what MarshalJSON encodes, strictly, as the bide protocol writes the gate: {"single":true}, with no other member, is SingleApproval; any other policy has need (and approvers, and optionally exclude_principal) and no single. "single" with any other value, beside any other member, a policy with no need, a duplicate or unknown member, or a value of the wrong type is ErrProtocol. It is the decoding for a policy being configured or received (a protocol input); the approval inside a stored Record is read leniently instead (see DecodeStoredRecord), so a journal a newer version wrote stays readable.

func (ApprovalPolicy) Validate added in v0.4.0

func (p ApprovalPolicy) Validate() error

Validate reports whether the policy is well formed: at least one approver, every approver id non-empty printable ASCII, no two ids the same approver, and 1 <= Need <= len(Approvers). A policy that fails is a configuration error; the gate refuses it with ErrConfig rather than guessing.

An approver id is printable ASCII, bytes 0x21 to 0x7E: letters, digits and punctuation, so an email address or a URI is a valid id, and a space, a control character or anything outside ASCII is not. An id outside that set could look like another (a Cyrillic "о" for a Latin "o", a fullwidth or accented spelling) and let one approver fill two seats. Ids are compared as exact bytes everywhere else (eligibility, the signed decision bytes, the verifier lookup), but a policy may not list two ids that differ only by ASCII case ("alice" and "Alice"): a person reading the policy, or a key lookup that folds case, takes them for one approver.

ExcludePrincipal is valid on an m-of-n policy and ErrConfig on SingleApproval. Whether Need stays reachable without the requester depends on the run (who requested it), so the gate checks that when it evaluates a call, not here: a policy whose Need equals the number of approvers can exclude the requester only for runs whose requester is not one of them.

func (ApprovalPolicy) ValidateKeys added in v0.9.0

func (p ApprovalPolicy) ValidateKeys(verifierFor ApproverVerifierFor) error

ValidateKeys reports whether the policy is well formed (Validate) and holds one seat per signing key under verifierFor: it resolves every approver once and refuses, with ErrConfig, a nil verifierFor, an approver whose verifier reports no key identity (an empty KeyIDs, or an empty entry), and two approvers whose verifiers report a common key identity. An approver the resolver does not know is not refused: its decisions cannot verify, so it fills no seat.

The gate runs this check on every evaluation, before it reads a recorded tally, so a resolver that changes between runs is checked again; TallyApprovals also never counts an approver whose key is shared, whatever resolver it is given. The check guards the tallies this version counts and records. A terminal tally already in the journal stays authoritative and is not recounted, so one recorded before this check existed is reused as recorded, even if it counted two approvers on one key. A caller that builds policies and resolvers ahead of a run can call ValidateKeys to refuse a bad pairing before any call pauses.

type ApprovalRequest added in v0.14.0

type ApprovalRequest struct {
	ToolUseID  string          `json:"tool_use_id"`
	ToolName   string          `json:"tool_name"`
	Args       json.RawMessage `json:"args"`
	RecordedAt string          `json:"recorded_at,omitempty"`
}

ApprovalRequest is what the record named ApprovalRequestStep holds: the gated plan flow node approvers decide on. It is the flow counterpart of the model turn that requests an agent's tool call, and it is what FindToolCall finds for the node's key: ToolUseID is the node's key, ToolName its name, and Args its input as JSON. It is recorded once, before the first pause, and a later drive whose input differs from it is refused (ErrConfig), so every decision is checked against the input the node then runs with. RecordedAt is when the request was recorded, by the drive's clock, in FormatTimestamp's form: when approvers are asked. It is informational (the gate orders decisions by journal position, never by it), and empty on a request an earlier version wrote.

type ApprovalRequired

type ApprovalRequired struct {
	ToolUseID string          `json:"tool_use_id"`
	Name      string          `json:"name"`
	Args      json.RawMessage `json:"args"`
	// Quorum is the running tally when the tool has an m-of-n approval policy (ToolSpec.Approval), so a
	// streaming UI can show progress ("1 of 2 approved") without waiting for Final. Nil for
	// a 1-of-1 gate.
	Quorum *ApprovalTally `json:"quorum,omitempty"`
}

ApprovalRequired fires immediately before the run pauses for a human decision on a tool that requires approval. The run then returns *ApprovalPending from Final; record a decision (Approve, or SubmitDecision for an m-of-n gate) and re-invoke to continue.

type ApprovalStatusBytes added in v0.19.0

type ApprovalStatusBytes struct {
	Format     string `json:"format"`
	Kind       string `json:"kind"` // "crl" or "ocsp"
	Issuer     string `json:"issuer"`
	Number     string `json:"number,omitempty"` // the CRLNumber, decimal
	ThisUpdate string `json:"this_update"`
	NextUpdate string `json:"next_update,omitempty"`
	Size       int    `json:"size"`          // len of the original DER
	DER        []byte `json:"der,omitempty"` // base64 in JSON; omitted above MaxStatusInline
}

ApprovalStatusBytes is what an ApprovalStatusStep record holds: one CRL (or OCSP response) a gate relied on. The metadata is informational (a reader parses DER and ignores the metadata where they differ); the writer checks that SHA-256 of DER is the record's name. A CRL larger than MaxStatusInline is recorded without DER, and its bytes go to the policy's status archive.

type ApprovalSubject added in v0.4.0

type ApprovalSubject struct {
	RunID     string
	ToolUseID string
	ToolName  string
	Args      json.RawMessage
}

ApprovalSubject is exactly what an approver decides on: one tool call, with its name and arguments, in one run. The decision bytes bind all of it, so a signature approves this call and no other: not another run, not another call, and not the same call id with different arguments.

type ApprovalTally added in v0.4.0

type ApprovalTally struct {
	Need       int      `json:"need"`                  // the policy's k
	Approvers  []string `json:"approvers"`             // the eligible set the gate enforced, in policy order
	Approved   int      `json:"approved"`              // approvers whose counted decision is an approval
	Denied     int      `json:"denied"`                // approvers whose counted decision is a denial
	ApprovedBy []string `json:"approved_by,omitempty"` // those approvers, in journal order
	DeniedBy   []string `json:"denied_by,omitempty"`   // those approvers, in journal order
	Pending    []string `json:"pending,omitempty"`     // eligible approvers with no valid decision yet who can still make one, in policy order
	// Excluded names the eligible approvers who fill no seat, in policy order: their verifier
	// shares a key identity with another approver's, reports none, or is a typed nil
	// (ReasonSharedKey, ReasonNoKeyID), or the policy excludes the requester and the approver is
	// the requester (ReasonRequester) or the run records no usable requester (ReasonNoRequester).
	// They never count, so Unreachable leaves them out. The gate refuses a policy with a key
	// problem, or with no usable requester, before counting, so a tally it journals names at most
	// the requester here.
	Excluded []string `json:"excluded,omitempty"`
	// ExcludePrincipal records that the policy excluded the requester (ApprovalPolicy.ExcludePrincipal),
	// and Requesters the requesters the count excluded: the principals the run's run:principal
	// names, those of the runs above it first and the run's own last (RunPrincipal.Requesters);
	// empty when it records none.
	ExcludePrincipal bool     `json:"exclude_principal,omitempty"`
	Requesters       []string `json:"requesters,omitempty"`
	// Records names every decision record on this call the gate read, valid or not, in
	// journal order. Evidence must disclose all of them, so an omitted decision is detectable.
	Records []string `json:"records,omitempty"`
	// DecisionFormat is the signed decision format the gate counted under (DecisionFormat), set on
	// the terminal tally it journals. A tally recorded by an earlier version has none; a gate
	// refuses to run a call on such a tally that passed (ErrConfig), since its approvals do not
	// verify under this format.
	DecisionFormat string `json:"decision_format,omitempty"`
	// Supersedes names the tally this count replaces: set when the gate re-asks for approval
	// because the tally recorded under ApprovalTallyStep passed on decisions that predate
	// DecisionFormat (the re-count is recorded under ApprovalRetallyStep once terminal), or because
	// the tally it would go by passed without excluding the requester and its policy now excludes it
	// (recorded under ApprovalSoDRetallyStep).
	Supersedes string `json:"supersedes,omitempty"`
	// Expired is set on the tally of a gate whose deadline passed with no terminal count: the
	// expiry, recorded as the gate's outcome. It does not pass, whatever the counts say. ExpiresAt
	// is the deadline the gate enforced (the approval-deadline record's), on an expiry and on any
	// tally of a gate with a deadline.
	Expired   bool   `json:"expired,omitempty"`
	ExpiresAt string `json:"expires_at,omitempty"`
	// contains filtered or unexported fields
}

ApprovalTally is the m-of-n gate's count for one call. At a pause it is the running tally (ApprovalPending.Quorum, ApprovalRequired.Quorum). At a terminal outcome the gate journals it under ApprovalTallyStep, before the tool runs, as the record of what it enforced and what it read.

func (ApprovalTally) Passed added in v0.4.0

func (t ApprovalTally) Passed() bool

Passed reports whether Need approvals are in.

func (ApprovalTally) Unreachable added in v0.4.0

func (t ApprovalTally) Unreachable() bool

Unreachable reports whether Need can no longer be reached: fewer approvers remain who have not denied, and are not Excluded, than approvals are required. Only valid denials count toward this, so an invalid record cannot force a denial.

type ApprovalWait added in v0.13.0

type ApprovalWait struct {
	ToolUseID  string   `json:"tool_use_id"`
	ToolName   string   `json:"tool_name"`
	Single     bool     `json:"single,omitempty"`      // a one-decision gate (SingleApproval): Need and Approvers are empty
	Need       int      `json:"need,omitempty"`        // the m-of-n gate's k
	Approvers  []string `json:"approvers,omitempty"`   // the m-of-n gate's eligible approvers, in policy order
	RecordedAt string   `json:"recorded_at,omitempty"` // when the run paused here, by the drive's clock
	// ExcludePrincipal: the m-of-n gate excludes the requester (ApprovalPolicy.ExcludePrincipal), and
	// Requesters are the requesters it excludes (RunPrincipal.Requesters).
	ExcludePrincipal bool     `json:"exclude_principal,omitempty"`
	Requesters       []string `json:"requesters,omitempty"`
	// Reask and Supersedes are set on a re-ask note (ApprovalReaskStep): why the gate asks again
	// (PendingReapproval, PendingSoDReapproval) and the recorded tally it does not act on. The
	// policy fields of such a note are the policy the gate asks again under.
	Reask      string `json:"reask,omitempty"`
	Supersedes string `json:"supersedes,omitempty"`
	// IdentityBound says the gate has a certificate-bound approver. IdentityApprovers lists them in
	// policy order, IdentityKinds is the SHA-256 of the sorted "approver=bare|cert" lines of the
	// eligible approvers joined by newlines, and IdentityPolicyDigest the SHA-256 of the certificate
	// policy in force (ApprovalIdentity.PolicyDigest). An auditor requires the ApprovalIdentity record
	// of a gate whose note says it was bound, and reports a missing one as stripped. A note written by
	// a process that predates the feature has none of these and looks like a bare-key gate: for a
	// gate the bank classifies as identity-required the auditor's expected policy of record is a
	// required input.
	IdentityBound        bool     `json:"identity_bound,omitempty"`
	IdentityApprovers    []string `json:"identity_approvers,omitempty"`
	IdentityKinds        string   `json:"identity_kinds,omitempty"`
	IdentityPolicyDigest string   `json:"identity_policy_digest,omitempty"`
	// Pause is why the gate waits (the Pause* constants; empty is PauseAwaitingDecisions), and
	// PauseDetail one sentence for a person. A newer note replaces an older one (LatestPauseNote).
	Pause       string `json:"pause,omitempty"`
	PauseDetail string `json:"pause_detail,omitempty"`
}

ApprovalWait is what the record named ApprovalPendingStep holds: that a run paused at a call's approval gate, the gate it waits on, and when (RecordedAt), which is when approvers are asked. The run writes it, as a StepValue and only if it is absent, when it first pauses at the gate, in the drive whose model turn made the call; a resume that pauses again writes nothing, and a record lost to a crash is not written later.

It is informational only, for a reader such as a run viewer, which can then show a gate before any decision on it is recorded. No part of the engine reads it to decide anything: not the gate (which counts decisions, see TallyApprovals), not a resume, not recovery. A failed write of it is not reported, since the pause is the same without it. Gate fields are the policy the run paused under; the call's result, or the gate's tally, records the policy the gate enforced. RecordedAt is when the record was written, by the drive's clock (WithClock, or the wall clock), in FormatTimestamp's form; empty on a record an earlier version wrote.

func LatestPausePhase added in v0.19.0

func LatestPausePhase(recs []Record, toolUseID string) (ApprovalWait, bool)

LatestPausePhase returns the newest phase note of toolUseID's gate among recs (ApprovalPauseStep), decoded, and ok=false when the gate has written none (its phase is then PauseAwaitingDecisions). A note is read only if its Pause is one of the Pause constants.

type ApproveOption added in v0.4.0

type ApproveOption interface {
	// contains filtered or unexported methods
}

ApproveOption configures SubmitDecision: WithDecisionCheck, WithClock for the clock that stamps the decision's RecordedAt, or ReportWritten.

func ReportWritten added in v0.17.0

func ReportWritten(wrote *bool) ApproveOption

ReportWritten makes SubmitDecision set *wrote to whether this call stored the decision's record: true when its Insert won the store's first-writer race, false when the journal already held the decision (an identical resubmission, another writer's record under the same name, a concurrent call that wrote it first, in this process or another). It is false when SubmitDecision returns an error. Of any number of concurrent submissions of byte-identical decisions (the same signature bytes, so the same record name) exactly one sees true, which is what a service that acts once per recorded decision needs. A scheme whose signatures are randomized (ECDSA from a KMS or an HSM) signs one decision into different bytes, each a record name of its own, and SubmitDecision's check that the decision is not already journaled is not one step with its insert: two such submissions racing both write, and both see true. The gate counts one and supersedes the other (the duplicate stays in the journal). wrote must not be shared between calls.

func WithDecisionCheck added in v0.4.0

func WithDecisionCheck(verifierFor ApproverVerifierFor) ApproveOption

WithDecisionCheck makes SubmitDecision check the decision before recording it, so an approver learns at submission time that it will not count: it reads the run, finds the call, and verifies the signature against that exact call with verifierFor. It returns ErrInvalidApproval (no such call, unknown approver, or a signature that does not verify) or ErrAlreadyDecided (the approver's earlier valid decision already counts), and records nothing. The check is for feedback; the gate re-verifies every record itself, so correctness never depends on it. Eligibility is the policy's, which SubmitDecision does not see.

type ApproverSignature added in v0.10.0

type ApproverSignature struct {
	Approver    string `json:"approver,omitempty"`     // the approver's id (Decision.ApproverID)
	ApproverAlg Alg    `json:"approver_alg,omitempty"` // the scheme Signature is under
	Signature   []byte `json:"signature,omitempty"`    // the approver's signature (Decision.Signature)
	// DecidedAt is when the approver decided, by the approver's clock, as they signed it
	// (Decision.DecidedAt, in FormatTimestamp's form): a claim the signature covers, so it cannot
	// be changed after signing. RecordedAt is when SubmitDecision journaled the decision, by the
	// clock of the process that called it (its WithClock, else its context's clock, else its wall
	// clock): that submitter's claim, not signed, and kept from the first submission of an
	// identical decision. The journal's Merkle trail anchors it once a signed tree head covers the
	// record, and that head's time bounds it. Both are for people and auditors: the counting rule checks
	// DecidedAt only as signed bytes, and nothing reads RecordedAt to decide anything.
	//
	// RecordedAt also appears alone, with no approver and no signature, on the other records that
	// resolve something a person was told about: a single approver's decision (Approve), the result
	// ResolveHalt injects, a run's end markers (run:complete, run:aborted, run:cancelled) and a
	// saga's rollback request (run:cancel-requested). It is then the claim of the process that
	// wrote the record, by the clock named at the writing call (WithClock on Approve, ResolveHalt,
	// Cancel, AnswerInterrupt and Signal), else the clock the context carries (a run's; ResolveHalt
	// reads WithClock, else the wall clock, never the context's), else the wall clock, and the same
	// rules hold: it is informational, set when the record is first written and kept from then on,
	// and no gate, claim, replay or recovery decision reads it. A record written before the time
	// existed has none, and Record.RecordedAt reports "" for it: a reader never supplies one. On
	// these records a clock with no canonical form (outside the years 0001 to 9999) records no
	// time, since they never fail for want of one.
	DecidedAt  string `json:"decided_at,omitempty"`
	RecordedAt string `json:"recorded_at,omitempty"`
	// ApproverCert is the leaf-first DER certificate chain of a certificate-bound approver (base64
	// in JSON), present only on a decision signed under bide.approval.v4: the signature binds the
	// leaf's SHA-256 (agent.ApprovalDecisionBytesCert), and the chain is stored so the journal is
	// self-contained and its Merkle trail covers it. Additive and optional; a reader that does not
	// know the member ignores it and, rebuilding v3 bytes, never counts the decision.
	ApproverCert [][]byte `json:"approver_cert,omitempty"`
}

ApproverSignature is the signed decision a StepApproval record written by SubmitDecision carries (see Record.ApproverSignature). A Record copy shares it with the original: never write through a record's ApproverSignature, assign the record a new one instead. A record decoded from its journal encoding carries one only when the encoding holds a non-empty member of it.

type ApproverVerifier added in v0.4.0

type ApproverVerifier interface {
	// Alg names the verifier's scheme. A decision counts only if it was journaled under this scheme.
	Alg() Alg
	// Verify reports whether sig is a valid signature over message.
	Verify(message, sig []byte) bool
	// KeyIDs identifies the signing keys behind Verify: one entry per public key whose private
	// key takes part in producing a signature Verify accepts. A single-key verifier reports one
	// entry; a verifier that accepts a signature by any of several keys (a rotation window)
	// reports one per key; a verifier that requires several signatures (audit.HybridVerifier)
	// reports one per component key.
	//
	// An entry identifies a key, not an approver: it is derived from the public key's bytes,
	// never from a configured name, so two verifiers over one key report the same entry however
	// they were built. The audit verifiers report the scheme and the hex SHA-256 of the public
	// key's encoding ("ed25519:3b6a27bc..."). Entries are compared as exact strings.
	//
	// The gate counts one seat per key: it refuses a policy two of whose approvers report a
	// common entry, or whose approver's verifier reports no entry or an empty one, with
	// ErrConfig (see ApprovalPolicy.ValidateKeys). Otherwise the holder of a shared key could
	// sign as each approver that key serves and meet a quorum alone. Enrolling one approver's
	// public key under a second approver makes the gate refuse the whole policy: it fails closed,
	// and no call under it runs until the keys are corrected.
	//
	// Trust boundary: a verifier is trusted code, like the resolver that returns it. The gate
	// takes its KeyIDs on faith; it cannot check that they name the keys Verify really accepts.
	// A verifier that under-reports its keys, or reports an identity that is not derived from a
	// key, defeats the check. Use the audit verifiers, or derive identities the same way.
	KeyIDs() []string
}

ApproverVerifier checks an approver's signature over the decision bytes under one scheme and names the signing keys behind it. audit.Ed25519Verifier, audit.ECDSAP256Verifier, audit.MLDSAVerifier and audit.HybridVerifier implement it, so a deployment reuses the existing verifiers with no new cryptography and no agent->audit import.

type ApproverVerifierFor added in v0.4.0

type ApproverVerifierFor func(approverID string) (ApproverVerifier, bool)

ApproverVerifierFor resolves the verifier for an approver id (the caller's PKI, mirroring audit's issuer lookup). It reports ok=false for an unknown approver; the counting rule treats that approver's decisions as unverifiable.

type AssistantTurn

type AssistantTurn struct {
	Message  Message `json:"message"`
	Replayed bool    `json:"replayed"`
}

AssistantTurn is the fully-assembled assistant message for a turn. Replayed is true when it was reconstructed from the journal on resume rather than produced by a live model call (in which case no ModelEvent deltas preceded it).

type CancelOption added in v0.17.0

type CancelOption interface {
	// contains filtered or unexported methods
}

CancelOption configures Cancel: WithClock, the clock that stamps the record's RecordedAt.

type CertCheck added in v0.19.0

type CertCheck struct {
	Reason string
	Detail string
	// Identity is the verified canonical identifier of the leaf's one subject alternative name.
	Identity string
	// KeyHash is the SHA-256 of the leaf's public key info: two counted decisions by different
	// approver ids with one key hash do not both count (ReasonSharedKey).
	KeyHash [32]byte
	// Anchored is when the decision became provable (A(d)), by the source Time names.
	Anchored time.Time
	Time     string
	// Tier ("final", "provisional", "no information"), Basis and StatusTime (S, RFC 3339) say what the
	// revocation status behind the decision is and rests on; only a "final" tier is a verified one.
	Tier       string
	Basis      string
	StatusTime string
	// Window is W', the compromise-reporting window the status rule left the decision (a duration string).
	Window string
	// Warnings are findings that do not change the outcome (a series gap against the declared period).
	Warnings []string
	// Revocation says which certificate of the chain is revoked, when Reason is ReasonRevoked and the
	// verifier can tell: it goes into the exec hold the pre-execution check writes.
	Revocation *CertRevocation
	// Chain is the SHA-256 (lowercase hex) of each certificate of the accepted chain, leaf first,
	// Anchor that of the trust anchor's SubjectPublicKeyInfo the chain ends at, and HeadSize the size
	// of the signed head that first covers the decision: what the gate records in ApprovalIdentity.
	Chain    []string
	Anchor   string
	HeadSize int
	// HeadRoot is the Merkle root of that head (hex) and HeadHash the SHA-256 of its signed canonical
	// encoding (hex), so an independent verifier can tell which head the time rests on.
	HeadRoot, HeadHash string
	// TimeEvidence is the SHA-256 (hex) of the RFC 3161 token or anchor receipt behind A(d); empty when
	// the time is the log signer's clock.
	TimeEvidence string
	// BasisKind is the status basis as a value a viewer can branch on: BasisAtOrAfterDecision or
	// BasisLiveBeforeDecision.
	BasisKind string
	// Evidence are the revocation status records (CRLs) the finding rests on, each once: the gate
	// stores them by hash (ApprovalStatusStep) so what it knew survives the loss of an archive.
	Evidence []ApprovalStatusBytes
}

CertCheck is a certificate-bound verifier's finding about one decision. Reason is empty when the decision verifies and counts; otherwise it is one of the Reason constants (a certificate or status reason, ReasonBadSig, ReasonAlg and so on) and Detail says precisely what failed. The other fields describe a decision that verified, for the verdict and the identity record.

type CertDecision added in v0.19.0

type CertDecision struct {
	// Subject is the call the gate counts for.
	Subject ApprovalSubject
	// Record is the decision record: approver, verdict, scheme, decided_at, signature and chain.
	Record Record
	// Journal is the run's journal from its first record, as Journal.History returns it, and Index the
	// position of Record in it: what a verifier needs to find the signed tree head that first covers
	// the record. Journal is nil when the caller does not hold the run's journal; the verifier then
	// cannot establish when the decision became provable and refuses it.
	Journal []Record
	Index   int
}

CertDecision is one decision record presented to a certificate-bound approver's verifier.

type CertDecisionVerifier added in v0.19.0

type CertDecisionVerifier interface {
	ApproverVerifier
	// VerifyCertDecision checks one decision record: that it carries a chain, that the chain is
	// accepted for this approver at the time the decision became provable, that the signature is over
	// the v4 bytes of this exact call under the leaf's key, and that revocation status allows it.
	VerifyCertDecision(cd CertDecision) CertCheck
}

CertDecisionVerifier is implemented by the verifier of a certificate-bound approver. It extends ApproverVerifier: Alg names the family ("x509"), Verify is false (a bare signature over v3 bytes is never a certificate-bound decision), and KeyIDs is the seat, "ident:" plus the approver's canonical identity, so the one-seat-per-key check (ValidateKeys) applies to a person, not to a key.

type CertExecVerifier added in v0.19.0

type CertExecVerifier interface {
	CertDecisionVerifier
	VerifyCertExec(cd CertDecision) CertCheck
}

CertExecVerifier is implemented by the verifier of a certificate-bound approver that can be asked, immediately before the call it authorizes executes, whether a counted decision still holds. VerifyCertExec is the counting check again at the current time with status no older than the gate's execution ages (audit.CertApprover.VerifyCertExec). A certificate-bound verifier that does not implement it holds every call it authorized: the gate fails closed.

type CertIntakeVerifier added in v0.19.0

type CertIntakeVerifier interface {
	CertDecisionVerifier
	VerifyCertIntake(cd CertDecision) CertCheck
}

CertIntakeVerifier is implemented by the verifier of a certificate-bound approver that can check a decision before it is written (audit.CertApprover). SubmitDecision with WithDecisionCheck calls it, for a decision that carries a chain, in place of the bare-key check: the chain is accepted for the approver at the time of the write, the signature verifies over the v4 bytes under the leaf's key, and status is usable now. A Reason of ReasonStatusUnknown or ReasonStatusStale means the status could not be judged (SubmitDecision returns ErrStatusUnavailable and writes nothing); any other Reason refuses the decision (ErrInvalidApproval). Nothing is written on any failure.

type CertRevocation added in v0.19.0

type CertRevocation struct {
	Serial     string // decimal serial of the revoked certificate
	E          string // its effective revocation time, RFC 3339 with nanoseconds
	ReasonCode int    // the CRL reason code
	CRL        string // SHA-256 (lowercase hex) of the CRL that lists it
}

CertRevocation describes the revocation behind a CertCheck whose Reason is ReasonRevoked.

type ClockOption added in v0.10.0

ClockOption is a clock that applies to an agent (Option), to one run (RunOption), to a halt resolution (ResolveOption), to a recorded approver decision (ApproveOption), and to a cancellation (CancelOption), and to an interrupt answer or signal (AnswerOption): WithClock.

func WithClock

func WithClock(now func() time.Time) ClockOption

WithClock sets the clock that reads "now": for a run (an agent's runs, or one run), the clock its durable timers (Sleep, WaitUntil, AwaitFor) measure against and that stamps the times it records (RunStart.StartedAt, ApprovalWait.RecordedAt, the end markers' RecordedAt); for ResolveHalt, the one clock of the call: WithMinHaltAge measures against it, and it stamps the claim marker and the outcome's RecordedAt (the context's clock is not read); for SubmitDecision, Approve and Cancel, the clock that stamps the record's RecordedAt. The default is time.Now. Deployments leave it unset; tests pass a controllable clock to advance time deterministically. A nil now is ErrConfig.

type Compensator

type Compensator interface {
	// Compensate undoes a completed call. args are the arguments the tool accepted (after tool
	// middleware; see CompensatedFunc); result
	// is what Call returned. Must be idempotent — on a crash mid-rollback it may re-run.
	Compensate(ctx context.Context, args, result json.RawMessage) error
}

Compensator is an optional interface a Tool implements to declare how to UNDO its side effect. In a saga run (WithSaga), if a step fails after earlier writes succeeded, the completed compensatable writes are rolled back in reverse order — automatically, and recursively through sub-agent trees.

This is the transactional / saga agent: "charged the card and booked the flight, then failed on the hotel → cleanly refund + cancel." It's the gsm compensation idea applied to the agent runtime — the journal records exactly which writes completed, so well-founded reverse compensation falls out of the durable substrate.

A saga step must be ATOMIC: it must not leave a partial external side effect before returning an error, because the failing step itself is not compensated (there's no recorded result to drive Compensate). Make forward steps all-or-nothing, or idempotent.

type ConcurrencyOption added in v0.10.0

type ConcurrencyOption interface {
	Option
	RunOption
	ParallelOption
}

ConcurrencyOption is a concurrency cap that applies to an agent's tool calls (Option), to one run's (RunOption), and to Parallel's tasks (ParallelOption): WithMaxConcurrency.

func WithMaxConcurrency added in v0.10.0

func WithMaxConcurrency(n int) ConcurrencyOption

WithMaxConcurrency caps how many tool calls of one turn run at once (for an agent or a run), or how many of Parallel's tasks do. 0 means unbounded (the default); 1 runs them one at a time; a negative n is ErrConfig.

type CountedDecision added in v0.19.0

type CountedDecision struct {
	Approver string
	Decision CertDecision
}

CountedDecision is one counted certificate-bound decision the execution check must re-verify.

type Decision added in v0.9.0

type Decision struct {
	RunID      string // the run whose journal holds the call: the pause's RunID
	ToolUseID  string
	ApproverID string // one of the policy's Approvers, exactly (printable ASCII; see ApprovalPolicy.Validate)
	Approved   bool
	DecidedAt  time.Time // when the approver decided, by their clock; signed
	Alg        Alg       // the signature scheme, e.g. audit.AlgEd25519
	Signature  []byte
	// Cert is the leaf-first DER certificate chain of a certificate-bound approver, whose Signature
	// is over ApprovalDecisionBytesCert with the CertLeafHash of Cert[0]. Empty for a bare-key
	// approver (the v3 bytes). SubmitDecision journals it (ApproverSignature.ApproverCert) and names
	// the record by the leaf hash; it does not verify it: that is the identity verifier's, and
	// WithDecisionCheck refuses a decision that carries one in this version.
	Cert [][]byte
}

Decision is one named approver's signed decision on a tool call gated by an m-of-n Approval policy. Signature is the approver's signature over ApprovalDecisionBytes(pend.Subject(), ApproverID, Approved, DecidedAt), where pend is the run's *ApprovalPending, under the scheme Alg names (the signer's Alg). Alg is journaled with the decision as Record.ApproverAlg, and the gate counts the decision only under a verifier of that scheme.

DecidedAt is when the approver decided, by the approver's clock, and is required. It is journaled in FormatTimestamp's form (to the millisecond, in UTC) as the record's DecidedAt, and the signature covers it, so it cannot be changed after signing. It is a claim by the approver, for people and auditors: the gate never reads its value (see TallyApprovals). An approver who sends a decision over the wire sends the time as text; ParseTimestamp reads it back.

type DecisionCheck added in v0.4.0

type DecisionCheck struct {
	Step     string // the record's journal name
	Approver string
	Approved bool
	Counted  bool   // this is the approver's decision: their first valid one
	Reason   string // why it did not count; empty when Counted
	// Cert is set for a decision of a certificate-bound approver, whatever its outcome: what the
	// verifier found. Nil for a bare-key approver's decision.
	Cert *CertCheck
}

DecisionCheck is how the counting rule classified one decision record.

type Describer added in v0.8.0

type Describer interface {
	Describe() ModelInfo
}

Describer is implemented by a Model that can say what it is. It is optional: the agent never requires it, and ModelInfoOf reports false for a Model that does not implement it.

type Doc

type Doc struct {
	ID       string         `json:"id,omitempty"`
	Text     string         `json:"text"`
	Score    float64        `json:"score,omitempty"`
	Metadata map[string]any `json:"metadata,omitempty"`
}

Doc is a document returned by a Retriever: its text plus optional id, similarity score, and metadata. This is the neutral shape the retrieval helpers speak; your Retriever maps your store's results onto it. A NaN or infinite Score (cosine similarity against a zero vector is 0/0) has no JSON encoding, so the retrieval helpers drop it (report 0).

type Emit

type Emit struct {
	Event Event
	Err   error
}

Emit is what an adapter pushes onto its event channel (event or terminal error).

type Entry added in v0.9.0

type Entry struct {
	// Seq is the entry's position: opaque, strictly increasing in commit order within the run
	// (see Store). It is not a count.
	Seq  int64
	Name string
	Data []byte
}

Entry is one stored entry of a run: its position, name and bytes.

type EscalationStep added in v0.17.0

type EscalationStep struct {
	// Receiver is the name of a receiver: DefaultReceiver or one given to WithNotifyReceiver.
	Receiver string
	// Deadline is the step's own deadline. It must be positive.
	Deadline time.Duration
}

EscalationStep is one step of an escalation chain: the receiver to tell, and how long it has, from the moment the step is observed, before the next step is due.

type Event

type Event interface {
	// contains filtered or unexported methods
}

Event is a normalized streamed model event. The stream/ package maps every provider wire format (OpenAI SSE / Anthropic typed / Gemini NDJSON / Bedrock binary) onto these.

type ExecFailure added in v0.19.0

type ExecFailure struct {
	Approver   string
	Reason     string // a Reason constant
	Detail     string
	Revocation *CertRevocation // set when Reason is ReasonRevoked and the verifier says which
	Anchored   string          // A(d), RFC 3339 with nanoseconds, when known
	StatusTime string
}

ExecFailure is a counted decision that does not hold.

type ExecHold added in v0.19.0

type ExecHold struct {
	Format       string   `json:"format"`
	ToolUseID    string   `json:"tool_use_id"`
	Generation   int      `json:"generation"`
	Reason       string   `json:"reason"` // ReasonRevoked, ReasonStatusStale, ReasonStatusUnknown, ReasonNoCoveringHead or ReasonSharedKey
	Detail       string   `json:"detail"`
	Approvers    []string `json:"approvers,omitempty"`
	CheckedAt    string   `json:"checked_at"`
	StatusTime   string   `json:"status_time,omitempty"`
	Serial       string   `json:"serial,omitempty"`      // decimal serial of the revoked certificate
	E            string   `json:"e,omitempty"`           // its effective revocation time
	ReasonCode   int      `json:"reason_code"`           // the CRL reason code, -1 if none
	CRLHash      string   `json:"crl_hash,omitempty"`    // SHA-256 of the CRL that lists it (an approval-status name)
	AnchoredAt   string   `json:"anchored_at,omitempty"` // A(d) of the decision
	PolicyDigest string   `json:"policy_digest,omitempty"`
}

ExecHold is what an ApprovalExecHoldStep record holds: the pre-execution status check found that the call may not run, with the evidence an operator decides on. Detail is our own words, never text taken from a certificate. Approvers lists every counted decision that no longer holds.

type ExecOutcome added in v0.19.0

type ExecOutcome int

ExecOutcome is what the pre-execution check decided.

const (
	// ExecProceed: every counted certificate-bound decision still holds; the call may run.
	ExecProceed ExecOutcome = iota
	// ExecWait: status is missing or stale, or the decision cannot be proven now (no covering head at hand).
	// The call does not run and nothing authoritative is written: the gate pauses with a named reason, and
	// the same tally executes when a fresh check passes.
	ExecWait
	// ExecReask: a counted decision no longer holds and cannot hold again (its certificate is revoked with
	// E <= A(d), or it no longer verifies for a reason that is not a wait). The call does not run; the gate
	// writes an exec hold and asks its approvers again.
	ExecReask
)

type ExecVerdict added in v0.19.0

type ExecVerdict struct {
	Outcome ExecOutcome
	Failed  []ExecFailure
	Checked int // the number of decisions re-verified
}

ExecVerdict is the result of CheckExec. Outcome is ExecReask if any decision must be held, else ExecWait if any waits, else ExecProceed. Failed lists every decision that does not hold, in the given order.

func CheckExec added in v0.19.0

func CheckExec(counted []CountedDecision, verifierFor ApproverVerifierFor) ExecVerdict

CheckExec re-verifies every counted decision through its approver's CertExecVerifier. It is pure: it reads the decisions' journal copies and the verifiers, and writes nothing. An approver whose verifier is absent or cannot do the check waits (fails closed). With no decisions to check there is nothing to hold: the caller passes only certificate-bound decisions.

type Finish

type Finish struct {
	Reason    FinishReason `json:"reason"`
	Raw       string       `json:"raw"` // the provider's own finish reason, as it sent it ("end_turn", "STOP"); empty if it sent none
	Usage     Usage        `json:"usage"`
	Discarded Usage        `json:"discarded"`
	// contains filtered or unexported fields
}

Finish ends a model turn. Reason says why the turn ended, in the neutral vocabulary below; an adapter maps its provider's reasons onto it where they enter, and keeps the provider's own value in Raw. A first-party adapter always sets Reason: a provider reason it does not know is passed through unchanged (Reason is then Raw), and a turn the provider ended without naming a reason is FinishStop with an empty Raw. A Model that does not know the reason may leave Reason empty, which counts as a natural stop.

The reason decides whether the turn is the model's answer. Stream.Message (and so every agent run) returns ErrOutputTruncated for FinishLength and ErrOutputFiltered for FinishFiltered rather than a message, since what arrived is only part of what the model would have said, and a run that recorded it would end with that part as its final answer. Any other reason is ErrStreamProtocol: the core does not guess what a provider's own word means.

The reason never decides whether tools run: the calls the turn carries do. A turn with calls runs them whatever its reason says (OpenAI reports stop under a forced tool_choice), and a FinishToolUse turn with no call is ErrStreamProtocol, since the calls it was for were lost.

Usage is what the turn's own response cost. Discarded is usage billed for responses the turn threw away (failed attempts a middleware retried, losing hedge targets); a live adapter leaves it zero, and a Model that replays a recorded turn reports the discarded spend it recorded there.

type FinishReason added in v0.8.0

type FinishReason string

FinishReason is why a model turn ended, in a neutral vocabulary every adapter maps its provider's reasons onto (see Finish). The set is closed: the core accepts only the constants below and the empty reason.

const (
	FinishStop     FinishReason = "stop"     // the model ended its answer (end of turn, a stop sequence)
	FinishToolUse  FinishReason = "tool_use" // the model ended its turn to call tools
	FinishLength   FinishReason = "length"   // the output hit its token limit (or the context window) and was cut off
	FinishFiltered FinishReason = "filtered" // a safety or content filter, or a refusal, stopped the output
)

The neutral finish reasons (see Finish).

type Finished

type Finished struct {
	Final Message `json:"final"`
}

Finished carries the terminal assistant answer — the same Message that Run returns and that Final reports. The event stream closes after this.

type FlowRef added in v0.9.0

type FlowRef struct {
	Name string `json:"name"`
}

FlowRef names the plan flow that drives a run of kind RunKindFlow.

type GateStanding added in v0.17.0

type GateStanding struct {
	Status GateStatus
	// Tally is the gate's recorded outcome (Recorded) or the count of the journal. For a recorded
	// outcome, Checks classify the decision records by a recount and Counted is cleared for a
	// decision the recorded tally does not name.
	Tally    ApprovalTally
	Recorded bool
	Checks   []DecisionCheck
	Reason   string // why a waiting gate asks again
	Err      error  // set when Status is GateMisconfigured
	// Ran: the call has a result. Denied: it was denied (a recorded denial, or the result of one).
	Ran, Denied bool
	// SingleDecided, SingleApproved: for GateSingle, a recorded unsigned decision and its verdict.
	SingleDecided, SingleApproved bool
	// Expired: the gate's recorded outcome is its expiry (its deadline passed with no terminal
	// count); Status is GateDecided and Denied is set. Deadline is the gate's recorded deadline
	// (approval-deadline:<call>), zero for a gate with none. A waiting gate past its Deadline is
	// still GateWaiting (only a drive records the expiry), and a decision on it is refused
	// (ErrApprovalExpired): a reader compares Deadline with its clock.
	Expired  bool
	Deadline time.Time
}

GateStanding is the result of EvaluateGate.

func EvaluateGate added in v0.17.0

func EvaluateGate(recs []Record, s ApprovalSubject, pol *ApprovalPolicy, verifierFor ApproverVerifierFor) GateStanding

EvaluateGate says where the approval gate stands on the call s in runID's journal recs, under the policy pol it is built with now (nil: the tool has no gate) and the approver verifiers, exactly as the agent's own gate reads it: the same function counts for both, so a service that tells approvers whether a call waits cannot disagree with the run. It records nothing.

The order is the loop's: a recorded result, a recorded denial (final, whatever the policy is now), the policy (no gate, a one-decision gate), then, for an m-of-n gate, the checks it makes on every evaluation (the policy valid, verifiers present, one seat per key, the requester excluded where the policy says so, a recorded tally readable), the recorded outcome it accepts or supersedes (GateReask), and the count.

recs are the run's records (Journal.History), s names the call (RunID, ToolUseID, ToolName, Args), and a call of a sub-agent is evaluated in the sub-run's journal. A plan flow node's gate is evaluated with EvaluateGateAfter.

func EvaluateGateAfter added in v0.17.0

func EvaluateGateAfter(recs []Record, s ApprovalSubject, pol *ApprovalPolicy, verifierFor ApproverVerifierFor, since string) GateStanding

EvaluateGateAfter is EvaluateGate for a plan flow node's gate (s.ToolUseID a node key), which counts only the decisions recorded after the gate's request, the step ApprovalRequestStep(key) names, and whose result is the key's own record. With since "" it is EvaluateGate.

type GateStatus added in v0.17.0

type GateStatus string

GateStatus is where an approval gate stands on one call, as the gate itself reads it.

const (
	// GateWaiting: an m-of-n gate with no recorded outcome. The run is paused, or will pause when it
	// reaches the call, and decisions are taken. Tally is the count of the journal (Passed or
	// Unreachable once the decisions suffice, though the gate has not recorded it yet), and Reason
	// says why the gate asks again after a recorded tally it does not accept (PendingReapproval,
	// PendingSoDReapproval).
	GateWaiting GateStatus = "waiting"
	// GateDecided: the call has an outcome the gate acts on: its result is recorded, or a recorded
	// denial (a human's Approve(false), or an m-of-n tally that did not pass), or the gate's recorded
	// terminal tally that it accepts under the policy now. Ran, Denied and Recorded say which.
	GateDecided GateStatus = "decided"
	// GateSingle: the policy is a one-decision gate (SingleApproval). It records an unsigned decision
	// (Approve), not signed per-approver ones, so there is no tally. SingleDecided and
	// SingleApproved say what is recorded.
	GateSingle GateStatus = "single"
	// GateUngated: no policy now (a redeploy removed the gate): the call runs without approval.
	GateUngated GateStatus = "ungated"
	// GateMisconfigured: the gate would refuse to evaluate, so the run fails at the call rather than
	// pauses: an invalid policy, approvers that share a key, no approver verifiers, a policy that
	// excludes the requester for a run that records none, a tally journaled by something that is not
	// this gate. Err is what the gate refuses with (it wraps ErrConfig, or ErrStorage for a tally
	// that does not read).
	GateMisconfigured GateStatus = "misconfigured"
)

type HaltAlreadyResolved added in v0.9.0

type HaltAlreadyResolved struct {
	RunID   string
	Op      OpRef
	Result  json.RawMessage // the recorded result
	IsError bool            // whether the recorded outcome is a failure
}

HaltAlreadyResolved is returned by ResolveHalt when the operation already has a recorded outcome that differs from the one given: an earlier resolution, or the result the live driver recorded. The recorded outcome stands; Result and IsError report it. Resolving again with the same outcome is not an error. It wraps ErrAlreadyResolved.

func (*HaltAlreadyResolved) Error added in v0.9.0

func (e *HaltAlreadyResolved) Error() string

Error names the operation and the outcome that stands.

func (*HaltAlreadyResolved) Unwrap added in v0.9.0

func (e *HaltAlreadyResolved) Unwrap() error

Unwrap returns ErrAlreadyResolved.

type HaltCause added in v0.9.0

type HaltCause string

HaltCause says why a run halted on an operation whose outcome is unknown.

const (
	// HaltCrashed: the operation's attempt marker is recorded with no result, and the halting
	// driver knows of no live claimant. That is what the halting driver saw, not proof: a
	// driver of the same run may still be running the effect (a Step that lost its claim with no
	// call of it in flight in this process, or a drive that read the journal after the claim,
	// cannot tell). ResolveHalt therefore checks for a live driver itself, whatever the cause.
	HaltCrashed HaltCause = "crashed"
	// HaltContended: another driver of the same run won the claim on the operation while this
	// one was running: a tool call's claim lost after this drive read no marker for it, or a
	// Step's claim lost to a call of the step in flight in this process. That driver owns the
	// effect and may be running it now.
	HaltContended HaltCause = "contended"
)

type HaltInFlight added in v0.9.0

type HaltInFlight struct {
	RunID     string // the run whose journal holds the operation
	RootRunID string // the run whose lease is held (the halted run's root, or its session turn's run)
	Op        OpRef
	// Attempt is the attempt marker key a driver holds, when that is why the resolution was
	// refused; empty when the lease is held.
	Attempt string
}

HaltInFlight is returned by ResolveHalt when a driver may be running the operation's effect right now, and its own result must win over a resolution: a driver holds the lease on the halted run's root (a session turn's run, for a turn and the sub-runs inside it), or (see ResolveHalt) a driver holds the claim on the attempt after the live one, having found the live one recorded as never started. Retry once the driver has finished (its lease released or expired, or its result recorded); a retry reads the run again.

func (*HaltInFlight) Error added in v0.9.0

func (e *HaltInFlight) Error() string

Error names the operation and what a driver holds.

type HaltRef added in v0.9.0

type HaltRef struct {
	RunID string
	Op    OpRef
	Cause HaltCause
}

HaltRef is what ResolveHalt needs to clear a halt: the run whose journal holds the operation, the operation, and why it halted. Take it from the halt with OutcomeUnknown.Ref.

Cause matters: a HaltContended halt may still be running in another driver, and ResolveHalt resolves it only once it is older than WithMinHaltAge. An operator building a HaltRef by hand from a journal listing must state the cause.

type HaltTooYoung added in v0.3.0

type HaltTooYoung struct {
	RunID     string
	ToolUseID string        // the operation's ID: a tool-use ID, or a step name
	Age       time.Duration // elapsed since the effect was attempted
	Min       time.Duration // the required minimum
}

HaltTooYoung is returned by ResolveHalt when WithMinHaltAge is set and the halt has not aged past the grace period yet. Wait and retry the resolution later.

func (*HaltTooYoung) Error added in v0.3.0

func (e *HaltTooYoung) Error() string

Error names the run and operation, the halt's age and the minimum it must reach.

type Identity

type Identity struct {
	// Actor is the acting agent instance: a deployment plus model version, e.g. "exec-agent@1.4.2".
	Actor string `json:"actor,omitempty"`
	// OnBehalfOf is the principal the agent acts for: a desk, an account, a user.
	OnBehalfOf string `json:"on_behalf_of,omitempty"`
	// AuthorityRef identifies the grant that authorizes this action: the digest or id of a signed
	// capability/authority record (see audit anchoring), so the leaf links to what permitted it.
	AuthorityRef string `json:"authority_ref,omitempty"`
}

Identity is the accountability binding for a run: which agent instance acted, on whose behalf, and under what authority. It is assigned by the DEPLOYMENT, not by the model: the operator sets it at run start from its own auth layer (an IdP, a signed capability grant, a service identity), and it rides the context so every governed action and every sub-agent call inherits it. The SDK anchors and proves the identity CLAIM (these fields, committed in the audit log); it does not authenticate the principal, which is the operator's IdP/PKI, and the attribution is only as strong as custody of whatever key signs the run's tree heads and grants.

The model has no inherent identity. "The agent's identity" is the identity the deployment binds to this run, so a compromised or misconfigured deployment attributes to whatever it claims. That boundary is deliberate and matches the model/governance split: prove the record and the granted authority, do not overclaim the intent behind them.

func IdentityFrom

func IdentityFrom(ctx context.Context) (Identity, bool)

IdentityFrom returns the acting identity bound to ctx, and whether one was set.

func (Identity) Empty

func (i Identity) Empty() bool

Empty reports whether the identity carries no attribution at all.

type IdentityDecision added in v0.19.0

type IdentityDecision struct {
	Approver   string   `json:"approver"`
	Step       string   `json:"step"`       // the decision record's journal name
	Identifier string   `json:"identifier"` // the verified canonical identifier; the seat is "ident:" plus it
	LeafHash   string   `json:"leaf_hash"`  // SHA-256 of the leaf certificate's DER
	Chain      []string `json:"chain"`      // SHA-256 of each certificate of the accepted chain, leaf first
	Anchor     string   `json:"anchor"`     // SHA-256 of the trust anchor's SubjectPublicKeyInfo the chain ends at
	// AnchoredAt is A(d); compare it to a recomputed time as an instant, not as a string.
	AnchoredAt   string   `json:"anchored_at"`
	TimeSource   string   `json:"time_source"`             // a TimeSource constant
	TimeEvidence string   `json:"time_evidence,omitempty"` // SHA-256 of the RFC 3161 token or anchor receipt; empty for the log clock
	HeadSize     int      `json:"head_size"`               // the covering head: size,
	HeadRoot     string   `json:"head_root"`               // its Merkle root,
	HeadHash     string   `json:"head_hash"`               // and the SHA-256 of its signed canonical encoding (audit.SignedTreeHead.Hash)
	Tier         string   `json:"tier"`                    // a Tier constant
	BasisKind    string   `json:"basis_kind"`              // a Basis constant; a viewer branches on this, never on Basis
	Basis        string   `json:"basis"`                   // display text
	StatusTime   string   `json:"status_time,omitempty"`   // S
	Window       string   `json:"window,omitempty"`        // W'
	Status       []string `json:"status,omitempty"`        // SHA-256 of the status bytes relied on (ApprovalStatusStep names)
	Warnings     []string `json:"warnings,omitempty"`
}

IdentityDecision is one counted certificate-bound decision in an ApprovalIdentity.

type IdentityDescriber added in v0.19.0

type IdentityDescriber interface {
	DescribeIdentity() IdentityPolicyRecord
	PolicyDigest() string
	RosterDigest() string
	// VerifierInfo is the revision the verifier was built from (vcs.revision) and the x509 GODEBUG
	// settings the operator declares it runs under (goDebug), and the build's baked-in default settings
	// (buildGoDebug, from the build info), for the record: the declaration is explicit configuration, never
	// read from the process environment by the engine, so the application compares it with its own
	// environment at startup (audit.CheckDeclaredGoDebug).
	VerifierInfo() (revision, goDebug, buildGoDebug string)
}

IdentityDescriber is implemented by the verifier of a certificate-bound approver that can describe the certificate policy in force, for the identity record a gate journals with its terminal count: DescribeIdentity is the policy and status values, PolicyDigest the SHA-256 (lowercase hex) of the JSON of that description (the value the pause note and the record both carry), and RosterDigest the SHA-256 over the whole identity map in sorted approver order, each entry len32(approver) approver len32(identifier) identifier.

type IdentityPolicyRecord added in v0.19.0

type IdentityPolicyRecord struct {
	EffectiveFrom           string            `json:"effective_from"`
	Anchors                 []string          `json:"anchors"`
	Intermediates           []string          `json:"intermediates"`
	Distrust                map[string]string `json:"distrust,omitempty"`
	EKU                     string            `json:"eku"`
	PolicyOID               string            `json:"policy_oid,omitempty"`
	IDKind                  string            `json:"id_kind"`
	Canonical               string            `json:"canonical"` // the canonicalizer's name and version
	AllowEmailProtectionEKU bool              `json:"allow_email_protection_eku,omitempty"`
	RequireNameConstraints  bool              `json:"require_name_constraints,omitempty"`
	RequirePolicyOnCAs      bool              `json:"require_policy_on_cas,omitempty"`
	RSA2048Until            string            `json:"rsa2048_until,omitempty"`
	MaxChain                int               `json:"max_chain"`
	MaxLeafLifetime         string            `json:"max_leaf_lifetime"`
	NotBeforeBackdate       string            `json:"not_before_backdate"`
	Skew                    string            `json:"skew"`
	AnchorGrace             string            `json:"anchor_grace"`
	ClockTolerance          string            `json:"clock_tolerance"`
	W                       string            `json:"w"`
	MinW                    string            `json:"min_w"`
	RoutineGrace            string            `json:"routine_grace,omitempty"`
	MaxStatusAge            string            `json:"max_status_age"`
	ExecStatusAge           string            `json:"exec_status_age"`
	ExecStatusAgeFor        map[string]string `json:"exec_status_age_for,omitempty"` // per issuing CA (issuer SPKI hash)
	MaxStatusAgeFor         map[string]string `json:"max_status_age_for,omitempty"`  // per issuing CA
	CAPeriod                string            `json:"ca_period"`
	CAPeriodFor             map[string]string `json:"ca_period_for,omitempty"`
	CAOverlapFor            map[string]string `json:"ca_overlap_for,omitempty"` // per issuing CA: the overlap the two-tier execution check allows
	CoverageTolerance       string            `json:"coverage_tolerance"`
	ThisUpdateSkew          string            `json:"this_update_skew"`
	ThisUpdateSkewFor       map[string]string `json:"this_update_skew_for,omitempty"`
	AllowHolds              bool              `json:"allow_holds,omitempty"`
	ConsecutiveNumbers      bool              `json:"consecutive_numbers,omitempty"`
	CountLive               bool              `json:"count_live,omitempty"`
	Retained                bool              `json:"retained,omitempty"`
	IndependentTime         bool              `json:"independent_time,omitempty"`
}

IdentityPolicyRecord is the certificate policy and status values in force when a gate counted, every one that reaches a verdict. Durations are time.Duration.String; times are RFC 3339 with nanoseconds; anchors and intermediates are named by the SHA-256 of their SubjectPublicKeyInfo. EffectiveFrom is the operator's change-record date (CertApprover.PolicyEffectiveFrom), informational: the auditor's expected policy of record dates the policy, not this field.

type IdentityRoster added in v0.19.0

type IdentityRoster interface {
	// HasIdentity reports whether the identity map names the approver id.
	HasIdentity(approverID string) bool
	// Identifier is the canonical identifier the identity map gives the approver id (what its certificate
	// must carry), for a client to check its own certificate before signing.
	Identifier(approverID string) (identifier string, ok bool)
	// RequesterIdentifier maps a requester principal (as the run records it) to the canonical identifier
	// of the person it names, so separation of duties can exclude the requester by identifier. ok is
	// false when the principal cannot be mapped.
	RequesterIdentifier(principal string) (identifier string, ok bool)
	// IdentityMappingConfigured reports whether the bank supplied the mapping from requesters to
	// identifiers. Without it the roster's default mapping (a principal equal to an approver id) only
	// repeats the exclusion by approver id, so a requester it cannot map is not worth a warning; with
	// it, a requester it cannot map is.
	IdentityMappingConfigured() bool
	// RosterDigest is the SHA-256 over the whole identity map (see CertApprover.RosterDigest). The
	// certificate-bound approvers of one gate must report one value: a gate has one identity map.
	RosterDigest() string
}

IdentityRoster is implemented by the verifier of a certificate-bound approver that knows the policy's identity map. A gate with a certificate-bound approver requires the map to name every approver of the policy, bare-key ones included: an approver outside it could be the same person as another, and one person would fill two seats.

type Image

type Image struct {
	Mime string `json:"mime,omitempty"` // MIME type for Data (required when Data is set), e.g. "image/jpeg"; ignored for URL
	Data []byte `json:"data,omitempty"` // raw image bytes; adapters base64-encode. Mutually exclusive with URL.
	URL  string `json:"url,omitempty"`  // hosted image URL. Mutually exclusive with Data.
}

Image is a provider-neutral image input. Exactly one of Data or URL is set (Data XOR URL). For raw bytes, set Data and Mime (e.g. "image/png"); the adapter base64-encodes it (Anthropic base64 source / OpenAI data URI). For a hosted image, set URL and leave Data nil; Mime is not required for URLs. This is an input-only part: models emit text, reasoning, and tool calls, never images, so nothing produces an Image on the response path.

func ImageData

func ImageData(mime string, data []byte) Image

ImageData builds an Image part from raw bytes with the given MIME type; the adapter base64-encodes the bytes onto the wire.

func ImageURL

func ImageURL(url string) Image

ImageURL builds an Image part referencing a hosted image by URL.

type InterruptPending added in v0.9.0

type InterruptPending struct {
	RunRef
	Name   string
	Prompt any // caller-defined payload for the human: a question, options, current state
}

InterruptPending is returned by Run when a tool called Interrupt and no answer has been recorded for that name yet. The run has paused durably at the interrupt point. Inspect Prompt to decide what to ask the human, record an answer with AnswerInterrupt (same RunID and Name), then re-invoke Run with RootRunID to continue.

func (*InterruptPending) Error added in v0.9.0

func (e *InterruptPending) Error() string

Error names the run and the interrupt point.

type Journal added in v0.9.0

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

Journal is the durable journal the engine writes through: it owns everything a run's records mean on top of a Store. It encodes each record (EncodeRecord) with a fresh salt, memoizes named steps, writes and checks the journal format header, runs the exclusive attempt claims that keep side effects at most once, and records a step's outcome even when its caller's context is cancelled. A Journal is safe for concurrent use, and any number of Journals may share a store, in one process or many: the store's single-winner Insert (A1) decides every race, and Journals in one process over the same store also share in-flight steps.

The header. Before its first write to a run a Journal inserts the header, a StepHeader record named "@journal" that names JournalFormat, and requires it to be the run's first entry. Every read checks that the first entry is a header naming a supported format; a run in another format, or with no header first, is refused with a *JournalVersionError before anything is read from it or written to it. A Journal remembers the runs it has checked (the 65536 most recently used), so a run is checked once per process, not on every step. Run IDs must never be reused after a run is deleted from its store: a Journal that remembers the deleted run writes into the new one with no header, which every reader refuses.

func NewJournal added in v0.9.0

func NewJournal(s Store, opts ...JournalOption) (*Journal, error)

NewJournal returns a Journal over s. A nil s is ErrConfig.

func (*Journal) AnswerInterrupt added in v0.11.0

func (j *Journal) AnswerInterrupt[T any](ctx context.Context, runID, name string, value T, opts ...AnswerOption) error

AnswerInterrupt records the typed value a paused run is waiting for at the interrupt point name (see Interrupt); then re-invoke Run with the pause's RootRunID to continue. It is idempotent: the first value for a (runID, name) wins. The value survives a crash: it is a journaled step.

The record notes when it was written (Record.RecordedAt), by WithClock's clock, else the clock ctx carries, else the wall clock: the claim of the process that answered, set by the first answer and kept by the idempotent calls after it. It is informational; nothing reads it.

func (*Journal) Enqueue added in v0.11.0

func (j *Journal) Enqueue[T any](ctx context.Context, runID, channel, key string, payload T) error

Enqueue appends a message to an ordered per-run channel, deduped by key: a redelivery with the same (runID, channel, key) is a no-op and the first payload wins (at-most-once intake over at-least-once transport). Safe to call from any process; the store's PK / ON CONFLICT is the cross-process dedup, exactly as for Signal. After enqueueing, re-invoke a run paused on the channel (*SignalPending) with the pause's RootRunID.

func (*Journal) Format added in v0.9.0

func (j *Journal) Format(ctx context.Context, runID string) (string, error)

Format returns the journal format runID's header names, "" for a run with no entries, and a *JournalVersionError for a run whose first entry is not a header. It reports a format this version does not support rather than refusing it.

func (*Journal) Get added in v0.9.0

func (j *Journal) Get(ctx context.Context, runID, name string) (Record, bool, error)

Get returns the record named name in runID's journal, if there is one. It reads the record first and then checks the run's header, so a read that races the run's first write never takes a record for one without a header: the header commits before any record. A run this version cannot read is a *JournalVersionError whether or not it holds the name; a run with no entries holds nothing.

func (*Journal) History added in v0.9.0

func (j *Journal) History(ctx context.Context, runID string) ([]Record, error)

History returns runID's records in journal order, the header first (see Records). A run with no entries has an empty history.

func (*Journal) Parallel added in v0.11.0

func (j *Journal) Parallel[T any](ctx context.Context, runID string, tasks []Task[T], opts ...ParallelOption) ([]T, error)

Parallel runs tasks concurrently, each as a durable named step (see Step), and returns their results in task order. It is the durable, auditable fan-in that compliance-style pipelines want: run several independent checks at once (sanctions, credit, fraud), each one crash-safe, each result committed to the journal and provable on its own, then aggregate.

All tasks run even if some fail, so the full set of results is preserved (a failed check does not hide the others). The returned error joins every task's error and is nil only if all succeeded. Succeeded tasks are memoized on a later resume. A failed task was not journaled: one marked retry-safe (Task.Safety) re-runs, and a side effect halts with *OutcomeUnknown, as Step does, since a failed effect may still have landed. Task names must be unique and not empty; a repeated or empty name is ErrConfig, as is a nil or invalid option. WithMaxConcurrency caps the tasks in flight; without it, each task runs on a goroutine of its own.

This is deliberately a thin primitive over the durable journal, not a graph engine: dynamic, model-driven routing stays in plain Go and sub-agents; Parallel covers the static fan-out/fan-in that a governed workflow's parallel-checks-then-decide stage is made of.

As with Step, each task result is journaled as JSON, so T must be JSON-serializable; a struct's unexported fields silently round-trip to their zero values.

func (*Journal) Reanchor added in v0.19.0

func (j *Journal) Reanchor(ctx context.Context, runID string) (anchored bool, err error)

Reanchor asks the journal's store, if it can (a Reanchorer such as audit.AuditedStore), to publish a signed head over runID's journal now, and reports whether it could. A store that signs on its own writes has usually done so already; this covers a record written under a context that was done, and a store whose head failed to publish. It is best effort for a caller that wants a decision it just wrote covered by a head before it answers.

func (*Journal) Records added in v0.9.0

func (j *Journal) Records(ctx context.Context, runID string) iter.Seq2[Record, error]

Records yields runID's records in journal order, the header first. It checks the header as it streams: a run whose first entry is not a header naming a supported format yields a *JournalVersionError and nothing else.

func (*Journal) Signal added in v0.11.0

func (j *Journal) Signal[T any](ctx context.Context, runID, name string, payload T, opts ...AnswerOption) error

Signal delivers a single-shot signal to a run, journaled at-most-once by name: a redelivery (a retried webhook, an at-least-once queue) is a no-op and the first payload wins. This turns at-least-once transport into exactly-once application to the run. Safe to call from any process; the store's primary-key / ON CONFLICT is the cross-process dedup.

Signal only records the payload. After delivering, re-invoke Run with the pause's RootRunID to resume the awaiting run: directly, or via a Waker scheduled at the current time.

The record notes when it was written (Record.RecordedAt), by WithClock's clock, else the clock ctx carries, else the wall clock; the first delivery sets it. It is informational; nothing reads it.

func (*Journal) Step added in v0.11.0

func (j *Journal) Step[T any](ctx context.Context, runID, name string, fn func(context.Context) (T, error), opts ...StepOption) (T, error)

Step runs fn as a named durable step and returns its typed result. On resume, a completed step returns its recorded result without re-running fn. This is the Option-B authoring primitive: write plain Go control flow, and name the operations that must survive a crash.

inv, err := j.Step(ctx, runID, "fetch-invoice", fetchInvoice, agent.WithSafety(agent.Safety{ReadOnly: true}))
res, err := j.Step(ctx, runID, "reserve", reserve) // a side effect: at most once

A step runs at most once, like a tool call. By default it is treated as a side effect: an attempt marker is journaled before fn runs, so if the process dies after fn's effect and before its result is recorded, the resumed step returns *OutcomeUnknown instead of running fn again. Clear it with ResolveHalt (the halt's Op is OpRef{Kind: OpStep, ID: name}) once the true outcome is known. A step that is safe to re-run declares it with WithSafety (ReadOnly or Idempotent); it then skips the marker and simply re-runs after a crash.

If fn returns an error, nothing is recorded but the marker: a side-effecting step whose fn failed halts on the next attempt too, since a failed call may still have taken effect.

A step that is not retry-safe must not pause: if fn returns a pause (Interrupt, Sleep, Await, a pending approval, a halt from a sub-agent it drives), Step returns an ErrConfig error instead, and the marker stays, so the step halts on the next attempt, since fn may have done something before it paused. Put the pause in a retry-safe step of its own, before or after the side effect.

T must be JSON-serializable: the result is marshaled into the journal, so a struct with unexported fields round-trips those fields to their zero values (encoding/json skips them) with no error reported. Return exported fields, a map, or a pointer whose fields are exported.

Inside the body of a plan flow node of the same run, the step is recorded under the node's key ("node:<node>:step:<name>", or "node:iter:<n>:<node>:step:<name>" in a loop body), so a loop body's step runs once per iteration, and name need be unique only within the node. Its halt names that key (OutcomeUnknown.Op.ID); resolve it with the halt's Ref.

name must not be empty, and must not start with a prefix the engine reserves for its own journal keys ("@", "run:", "tool:", "attempt:", "approval:", "signal:", and the rest; see IsReservedStepName): such a name is ErrConfig.

func (*Journal) Store added in v0.9.0

func (j *Journal) Store() Store

Store returns the store the journal writes to.

type JournalNotifyEvent added in v0.17.0

type JournalNotifyEvent struct {
	// Key is the event key a notifier gives the event (ApprovalKey, HaltedKey, AbortedKey).
	Key string
	// Kind is NotificationApprovalPending, NotificationOutcomeUnknown or NotificationRunAborted.
	Kind NotificationKind
	// RunID is the root run, whose notification trail holds the event (NotifyTrailName).
	RunID string
	// At is when the journal says the event began (the pause, the attempt, the abort); zero if the
	// journal does not say.
	At time.Time
	// Current reports whether the journal still shows the event unresolved: a gate with no
	// decision, an attempt with no result. An event that is not current was resolved since.
	Current bool
	// Approvers is the gate's approvers, in policy order; nil for a 1-of-1 gate and for other kinds.
	// A notifier routes an approval by them (NotifyRouting.Receivers).
	Approvers []string
	// ResolvedAt is, for an event that is not current, when what resolved it was journaled: a
	// gate's newest decision (signed or a single approver's) or the result agent.ResolveHalt
	// injected for a halt, by the recorded_at of that record (the writing process's clock, not
	// signed), so an all-clear can be judged late. Zero when the event is current, or when the
	// record that resolved it holds no time (written before records carried one, or a result the
	// halted driver itself recorded).
	ResolvedAt time.Time
}

JournalNotifyEvent is an event the journal of a run records that needs a person: a gate the run paused at, an effect whose outcome is unknown, a run that aborted. audit.VerifyNotifications compares these to a notification trail, so an event with no trail record is found.

func JournalNotifyEvents added in v0.17.0

func JournalNotifyEvents(runID string, recs []Record) []JournalNotifyEvent

JournalNotifyEvents returns the events recs, the history of runID, records that need a person, in journal order (gates, then halts in key order, then the abort). It reads the journal as the notifier's sweep does and with the same keys, so a notifier that saw the event wrote a trail record under the same Key. It differs from the sweep in what it keeps: a gate that was decided is returned with Current false, because the person was owed a notification while it was open, and a halt is returned whatever its age (the caller applies the age the notifier waits before reporting one, WithNotifyHaltAge).

A halt resolved by agent.ResolveHalt is returned with Current false and ResolvedAt the time its injected result records; a halt a later drive resolved by running the call is not a halt the journal shows. recovery_failing and overdue are not journal facts.

type JournalOption added in v0.9.0

type JournalOption interface {
	// contains filtered or unexported methods
}

JournalOption configures NewJournal.

type JournalVersionError added in v0.9.0

type JournalVersionError struct {
	RunID     string
	Found     string
	Supported []string
}

JournalVersionError is returned by a Journal asked to read or write a run whose journal format it does not support (see JournalFormat). Found is the format the run's header names, or "" for a journal with no header first (an unversioned journal). Supported lists the formats this version reads and writes. It wraps ErrJournalVersion. A journal refuses such a run before it writes to it, so an older binary never adds records to a run a newer one started.

func (*JournalVersionError) Error added in v0.9.0

func (e *JournalVersionError) Error() string

Error describes the run, the format found, and the formats supported.

func (*JournalVersionError) Unwrap added in v0.9.0

func (e *JournalVersionError) Unwrap() error

Unwrap returns ErrJournalVersion.

type LeaseControl added in v0.10.0

type LeaseControl interface {
	LeaseOption
	RecoverOption
	RecoverLoopOption
}

LeaseControl is a lease setting that applies to Lease (LeaseOption), Recover (RecoverOption) and RecoverLoop (RecoverLoopOption): WithLeaseHolder and WithLeaseTTL.

func WithLeaseHolder

func WithLeaseHolder(id string) LeaseControl

WithLeaseHolder names the worker that claims leases, as the leases table and logs show it. Defaults to a host/pid/random string. The name identifies, it does not grant: each Lease call (and each run Recover drives) claims under a token of its own derived from it, so two drivers sharing a name still exclude each other, and a restarted worker waits out its predecessor's lease (the TTL) like any other process rather than taking it over.

func WithLeaseTTL

func WithLeaseTTL(d time.Duration) LeaseControl

WithLeaseTTL sets how long an acquired lease is valid. Lease renews it while a run is driving, so a crash lets the lease expire after roughly this long and another process (RecoverLoop) takes over. Defaults to 30s. Set it well above the store's round-trip time and the longest pause you expect a process to take: renewal starts at half the TTL, and a drive that cannot renew within three quarters of it is cancelled with ErrLeaseLost (see Lease). It must be positive: Lease, Recover, RecoverLoop and Agent.Session return an ErrConfig error otherwise.

type LeaseOption added in v0.10.0

type LeaseOption interface {
	// contains filtered or unexported methods
}

LeaseOption configures Lease, and the lease Agent.Session drives each turn's run under.

type Leaser

type Leaser interface {
	// AcquireLease claims runID for holder until now+ttl. It returns true if granted (the run is
	// unleased or the live lease is already holder's, which renews it), false if another holder
	// currently holds a live lease. An expired lease is available to any holder.
	AcquireLease(ctx context.Context, runID, holder string, ttl time.Duration) (bool, error)
	// RenewLease extends holder's lease on runID, returning false if holder no longer holds it (it
	// expired or was taken). A driver whose work outlasts ttl renews to keep the lease.
	RenewLease(ctx context.Context, runID, holder string, ttl time.Duration) (bool, error)
	// ReleaseLease relinquishes runID if held by holder (a no-op otherwise) so another process can
	// take it immediately rather than waiting for expiry.
	ReleaseLease(ctx context.Context, runID, holder string) error
	// ReapLeases deletes every lapsed lease (see RunFilter.LeaseLapsed) whose run the store holds
	// no entry for, holds an entry named in ended (a finished run), or whose run ID contains '>'
	// (a session's run or a sub-agent's, see IsSessionRun and IsSubRun, which no recovery pass
	// takes over: the session, or the root run, resumes it), and returns how many it deleted. Each lease is deleted only if it is still lapsed when it is deleted, by the
	// comparison AcquireLease makes, so a lease granted or renewed meanwhile is kept: deleting a
	// lapsed lease changes nothing a holder can observe, since any holder may take it. A holder that
	// dies between its run's last write and its release leaves such a lease; RecoverLoop's lapsed
	// loop calls ReapLeases with the terminal markers on each pass, so these leases do not
	// accumulate and the lapsed listing does not grow with them. A session's turn run, leased by
	// its session (see Session), is the same: a holder that died mid-turn leaves a lapsed lease on
	// an unfinished run that no pass takes over and whose message may never be sent again.
	ReapLeases(ctx context.Context, ended []string) (int, error)
}

Leaser is the optional capability that coordinates driving a run across processes. Without it, several processes recovering against a shared store all re-drive the same in-flight runs. That is safe but wasteful: side effects stay at-most-once because each non-idempotent call is guarded by an exclusive attempt claim (ClaimAttempt), but the drivers duplicate model calls and a loser of a claim halts with OutcomeUnknown. A store that implements Leaser lets a driver take a time-bounded lease on a run so normally only the holder drives it; a dead holder's lease expires and another process takes over, which is the high-availability property. Recover uses it automatically when the store provides it. A lease is an efficiency and liveness mechanism, not the safety one: no lease can guarantee mutual exclusion against a holder that stalls past its TTL, which is why the claim, not the lease, is what keeps side effects at-most-once.

The Store port does not require leasing. MemStore's implementation is in-process (for tests and as the reference), SQLite's coordinates the processes sharing one database file, and Postgres's the nodes sharing one database, each with an atomic upsert over a leases table.

type Lister

type Lister interface {
	// Runs yields the IDs of the runs f admits, in ascending byte order. It yields an error at
	// most once, as its last element, and holds no connection or lock across a yield.
	Runs(ctx context.Context, f RunFilter) iter.Seq2[string, error]
}

Lister is the optional store capability a crash-recovery supervisor needs: it enumerates the runs a store holds, so Recover can find the ones to re-drive after a restart. A store opts in by implementing Runs; Recover finds it with Capability, so a wrapper that implements Unwrap keeps its inner store's Lister.

type MemNotifyTrail added in v0.16.0

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

MemNotifyTrail is an in-memory NotifyTrail, safe for concurrent use.

func NewMemNotifyTrail added in v0.16.0

func NewMemNotifyTrail() *MemNotifyTrail

NewMemNotifyTrail returns an empty in-memory trail store.

func (*MemNotifyTrail) Append added in v0.16.0

func (m *MemNotifyTrail) Append(_ context.Context, trail string, seq int, rec NotifyRecord) error

Append implements NotifyTrail.

func (*MemNotifyTrail) Load added in v0.16.0

func (m *MemNotifyTrail) Load(_ context.Context, trail string) ([]NotifyRecord, error)

Load implements NotifyTrail.

type MemStore

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

MemStore is an in-memory Store for tests and local development. It also implements Lister and Leaser (leases are in-process), so Recover and Lease work on it as on the SQL stores. SQLite (store/sqlite) is the on-disk default and store/postgres the high-availability backend.

Each run's entries get consecutive Seq values from 0, in insertion order, so MemStore meets the Store requirements trivially: an Insert is atomic under its mutex, and a read sees every entry inserted before it.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore returns an empty MemStore.

func (*MemStore) AcquireLease

func (m *MemStore) AcquireLease(_ context.Context, runID, holder string, ttl time.Duration) (bool, error)

AcquireLease implements Leaser.

func (*MemStore) Get added in v0.9.0

func (m *MemStore) Get(ctx context.Context, runID, name string) (Entry, bool, error)

Get implements Store.

func (*MemStore) Insert added in v0.9.0

func (m *MemStore) Insert(ctx context.Context, runID, name string, data []byte) (Entry, bool, error)

Insert implements Store.

func (*MemStore) Load added in v0.9.0

func (m *MemStore) Load(ctx context.Context, runID string, after int64) iter.Seq2[Entry, error]

Load implements Store. It takes the run's entries as of the call and yields them without holding the store's mutex, so the caller may write to the store inside the loop.

func (*MemStore) ReapLeases added in v0.10.0

func (m *MemStore) ReapLeases(_ context.Context, ended []string) (int, error)

ReapLeases implements Leaser.

func (*MemStore) ReleaseLease

func (m *MemStore) ReleaseLease(_ context.Context, runID, holder string) error

ReleaseLease implements Leaser.

func (*MemStore) RenewLease

func (m *MemStore) RenewLease(_ context.Context, runID, holder string, ttl time.Duration) (bool, error)

RenewLease implements Leaser.

func (*MemStore) Runs

func (m *MemStore) Runs(ctx context.Context, f RunFilter) iter.Seq2[string, error]

Runs implements Lister: the runs f admits, in ascending order.

type MemWaker

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

MemWaker is an in-process Waker: it holds pending timers and, when Fire is called with the current time (or via a Start ticker), resumes every run whose timer is due by calling the resume function it was built with. resume is how the deployment re-invokes a run, typically agent.Run(ctx, runID, savedInput); a resumed run whose wait is over proceeds, and one that sleeps again re-registers a new wake here. MemWaker is a reference and a local-dev default, not a durable scheduler: a process exit loses its in-memory timer set, so the wake times must also live in the durable journal (they do, Sleep journals them), and a restarted deployment rebuilds pending wakes by scanning runs, or an external scheduler owns the trigger.

func NewMemWaker

func NewMemWaker(resume func(ctx context.Context, runID string) error) *MemWaker

NewMemWaker builds an in-process waker that resumes due runs via resume.

func (*MemWaker) Fire

func (w *MemWaker) Fire(ctx context.Context, now time.Time) (int, error)

Fire resumes every run that has a timer due at or before now, each run once even if several of its timers are due, drops the fired timers, and returns how many runs it resumed. Resume callbacks run without the lock held, so a resumed run may register a new wake. Errors from resume are joined.

A resume that fails (the model provider or the store was briefly unavailable) leaves the run where it was, so its due timers are put back and the next Fire retries it; without that, one transient failure would leave the run asleep with nothing to wake it. A resume that ends in a Pause (IsPause: the run slept again, or awaits approval, an interrupt, a signal, or a halt resolution) is not retried: the run is waiting on something else, and waking it every tick would only spin.

func (*MemWaker) Schedule

func (w *MemWaker) Schedule(_ context.Context, wk Wake) error

Schedule registers a wake, idempotent per (w.RunID, w.Name): re-scheduling the same timer overwrites its fire time rather than adding a duplicate. Timers of the same name in different sub-runs of one root are distinct, and the root is resumed once when either is due. An empty RootRunID resumes RunID. An empty RunID is ErrConfig.

func (*MemWaker) Start

func (w *MemWaker) Start(ctx context.Context, every time.Duration, onError func(error)) <-chan struct{}

Start runs Fire on a ticker every `every` until ctx is cancelled, so sleeping runs wake on their own. Resume errors go to onError if non-nil. It is the turnkey local loop; a production deployment may prefer an external scheduler that owns the trigger and durable timer set.

No Fire starts after ctx is cancelled. The returned channel closes once the loop has stopped, after any Fire in flight when ctx was cancelled has returned. Wait on it before closing the store the resumed runs write to.

type Message

type Message struct {
	Role  Role
	Parts []Part
}

Message is one turn. Its content is a sequence of typed Parts (NOT a flat string), so reasoning/thinking parts (which Anthropic requires echoed back across turns), tool calls, tool results, and image inputs all survive round-trips intact. This is the provider-neutral type; adapters translate to/from provider wire formats.

func SystemText

func SystemText(s string) Message

SystemText returns a system message holding the text s.

func UserParts

func UserParts(parts ...Part) Message

UserParts builds a user message from mixed parts, e.g. a prompt and one or more images: UserParts(Text{"what is this?"}, ImageData("image/png", raw)). It is the multimodal counterpart to UserText.

func UserText

func UserText(s string) Message

UserText returns a user message holding the text s.

func (Message) MarshalJSON

func (m Message) MarshalJSON() ([]byte, error)

MarshalJSON encodes the message with each part tagged by its type, so UnmarshalJSON can restore the concrete Part types.

func (Message) Text

func (m Message) Text() string

Text returns the concatenation of the message's Text parts (ignoring reasoning, tool calls, and tool results). Empty if the message carries no text.

func (*Message) UnmarshalJSON

func (m *Message) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes a message written by MarshalJSON, restoring each part's concrete type. It fails on a part whose type tag it does not know.

type Middleware

type Middleware func(next ModelHandler) ModelHandler

Middleware wraps a ModelHandler: the net/http-style func(Handler) Handler chain, at the semantic layer (it sees messages, tool calls and token usage, not bytes). Batteries live in the middleware package (Retry, Hedge, RateLimit, Cost). A middleware passes on the call it received, or a copy of it with fields changed (c := call; c.Model = backup): the call carries the agent's per-turn state in unexported fields, and the agent's model handler refuses a ModelCall that was built from scratch rather than derived from the one the middleware received (ErrConfig).

type Model

type Model interface {
	Stream(ctx context.Context, req Request) (*Stream, error)
}

Model is the provider primitive. Streaming is FIRST-CLASS: Stream is the only method an adapter must implement. Generate is a convenience drain built on top — the opposite of frameworks that make blocking generation primary and bolt streaming on later.

func Replay

func Replay(ctx context.Context, source *Journal, runID string) (Model, error)

Replay returns a Model that re-emits the model outputs recorded in `source` for runID, in order, instead of calling a live LLM. Run an agent with it against a FRESH store to deterministically re-execute a past run:

  • time-travel debugging: step through exactly what happened, offline and free;
  • regression tests: capture a production run, replay it in CI (pairs with testing/synctest), assert behavior didn't drift;
  • evals over real traffic: the journal IS a golden dataset.

Because the journal captures every model output across the whole (possibly nested) tree, the replay is exact. Each turn ends with the Finish its record holds: the reason and the provider's raw reason the original turn journaled (a record written before they were journaled gets the reason its message implies, "tool_use" when it calls tools, else "stop"), and the replayed turn's record names the model its original record named (Record.Model), not the replaying Model. Spend is replayed too: a turn reports the usage the original turn discarded (failed attempts, losing hedge targets, and requests journaled in a late spend record after it) in its Finish's Discarded, beside its own Usage, and a model call that failed for good in the original fails again at the same point, reporting the same usage, so the replayed run's Result.Spend, journal (but for run:start's informational start time, which is the replay's own) and WithTokenBudget stops match the original's. A middleware in the replaying agent that retries such a failure moves on to the next recorded response instead. This is only possible because the durable substrate records a complete, replayable history in the first place.

type ModelAttempt added in v0.9.0

type ModelAttempt struct {
	// Response is what the request returned. When Err is set, only its Usage is meaningful: the
	// usage the request reported before it failed, which the provider may still bill.
	Response ModelResponse
	// Discarded is usage the request's model reported as billed for responses it threw away (see
	// Finish.Discarded): zero from a live adapter, the recorded discarded spend from Replay.
	Discarded Usage
	// Err is the request's error, or the agent's rejection of its response (a reused tool-use
	// id), or the error of a Before hook that kept it from being sent.
	Err error
}

ModelAttempt is how one request ended, as an After hook sees it.

type ModelCall added in v0.9.0

type ModelCall struct {
	// Request is what the call asks of the model.
	Request Request
	// Model is where the agent's model handler sends the call's requests. A middleware may
	// retarget a copy of the call (Hedge sends its backups this way).
	Model Model
	// RunID is the run the call belongs to, and Turn the model turn's sequence number within it
	// (0 for the first). Both are zero for a call made through CallModel.
	RunID string
	Turn  int
	// contains filtered or unexported fields
}

ModelCall is one model call as it passes through the middleware chain. It is a value: a middleware changes a copy and passes that on, so what one middleware changes is seen only by the handlers it calls, never by its caller or by a sibling (a Hedge's other targets).

Request.Messages and Request.Tools arrive at every handler slices.Clip'ped, so a handler that appends to them gets a new backing array and never writes into a slice another handler holds. The elements are shared: replace an element through a copy of the slice rather than assign into it.

func (ModelCall) AddHook added in v0.9.0

func (c ModelCall) AddHook(h ModelCallHook) ModelCall

AddHook returns a copy of c with h appended to its hooks, which the agent's model handler runs around every request it sends for the returned call and every call derived from it. Hooks are append-only: a middleware can add one, never remove or reorder those added outside it, and the agent's own spend accounting is not a hook at all, so no middleware can hide a request from the run's budget or Result.Spend.

func (ModelCall) Attempt added in v0.9.0

func (c ModelCall) Attempt() int

Attempt is the number of the request a hook is observing. The turn numbers each request when it reaches the agent's model handler, before its Before hooks run, from one counter for the whole turn: 1 for the first, 2 for the next, and so on, so the requests of every Retry attempt and every Hedge target are numbered apart and every hook of a request, Before and After, sees the same number. A request a Before hook refused keeps its number although it was not sent, so the numbers of the requests sent can have gaps. It is 0 on a call that has not reached the model handler (as a middleware receives it).

func (ModelCall) OnAnswer added in v0.9.0

func (c ModelCall) OnAnswer(key any, fn func(ctx context.Context, resp ModelResponse)) bool

OnAnswer registers fn to run once with the call's answer: the response the whole chain returns to the agent, after the agent's checks, once the journal holds it as the turn's record (for CallModel, which has no journal, once the call returns it). It runs after every request of the call has had its hooks, only when the call succeeds, and not at all when the turn's record is not written (the write failed, or another driver of the run recorded the turn first): fn runs once per recorded turn. Registrations are keyed: a key registered again for the same turn (a middleware inside a Hedge or a Retry registers once per target or attempt) keeps the first registration, so fn runs once per turn wherever the middleware sits; key must be comparable, and a pointer the middleware allocates is the usual choice. Cost counts answers this way.

It reports whether the call belongs to a turn. A call a middleware kept and passes on after its turn is over belongs to that turn: OnAnswer reports true and fn never runs, since the turn's answer is already decided. It reports false only for a call that belongs to no turn (a handler called directly, not through an agent or CallModel).

type ModelCallHook added in v0.8.0

type ModelCallHook struct {
	// Before runs before the request is sent, with the request's context and the call as it
	// reached the model handler (Attempt set). An error fails the request without sending it:
	// the hooks after it do not run, and the hooks before it get their After with that error.
	Before func(ctx context.Context, call ModelCall) error
	// After runs once the request has ended, successfully or not.
	After func(ctx context.Context, call ModelCall, a ModelAttempt)
}

ModelCallHook runs around each request the agent's model handler sends: every retried attempt and every hedged target, wherever the middleware that added it (ModelCall.AddHook) sits in the chain. Middleware whose effect belongs to requests actually sent, rather than to calls through it, uses one: RateLimit waits for a token in Before, Cost counts spend in After.

The hooks of a call run in the order they were added, so a middleware nearer the outside of the chain runs its Before first. Each Before runs once per request; a hook whose Before returned nil gets exactly one After for that request.

type ModelEvent

type ModelEvent struct {
	Event Event `json:"event"`
}

ModelEvent forwards one live model stream Event (TextDelta, ReasoningDelta, ToolCallDelta, Finish) from the current turn — the token-by-token feed. Emitted only for a FRESH model call; on durable replay the turn is reused from the journal and produces no deltas (an AssistantTurn with Replayed=true is emitted instead). When the turn's model call starts over (see TurnRestarted), the deltas before it are not part of the recorded turn.

type ModelHandler

type ModelHandler func(ctx context.Context, call ModelCall) (ModelResponse, error)

ModelHandler answers one model call: it returns the response the turn records. Middleware wraps it; the innermost handler, which the agent (or CallModel) supplies, sends each request to call.Model. On error the response carries at most the usage the failed request reported.

type ModelInfo added in v0.8.0

type ModelInfo struct {
	Provider       string `json:"provider,omitempty"`
	Model          string `json:"model,omitempty"`
	ResponseFormat bool   `json:"response_format,omitempty"`
}

ModelInfo identifies a Model. Provider names the API it calls ("anthropic", "openai", "gemini"), Model is the provider's model ID the requests name, and ResponseFormat reports whether the Model honours Request.ResponseFormat (a Model that does not fails a request that sets it, rather than drop the constraint).

func ModelInfoOf added in v0.8.0

func ModelInfoOf(m Model) (ModelInfo, bool)

ModelInfoOf describes m. It returns m's own Describe when m implements Describer, and otherwise follows an Unwrap() Model method, as a wrapping Model (middleware, a test double) offers, to the first Model in the chain that does. It reports false when no Model in the chain describes itself, when the chain ends in nil, or when it is longer than 64 links.

type ModelResponse added in v0.9.0

type ModelResponse struct {
	Message   Message
	Usage     Usage
	Finish    FinishReason
	RawFinish string // the provider's own finish reason, as it sent it; empty if it sent none
	// contains filtered or unexported fields
}

ModelResponse is the answer to a model call: the assistant message, the usage of the request that produced it, and why the turn ended. Finish is never empty in a response the agent records: an empty reason, which a Model may send when it does not know one, counts as FinishStop (see Finish). The loop decides whether to run tools from the message's tool calls, never from Finish (an OpenAI forced tool_choice reports stop alongside tool calls).

func CallModel added in v0.9.0

func CallModel(ctx context.Context, m Model, req Request, mw ...Middleware) (ModelResponse, error)

CallModel sends one model call outside an agent, through mw (first = outermost) and the same model handler an agent uses: hooks run around every request, Messages and Tools arrive clipped, and the response is checked as an agent checks it (usage, finish reason, tool-use ids). There is no journal, no stream and no run, so RunID and Turn are zero. It returns ErrConfig when m is nil.

type ModelTurn added in v0.10.0

type ModelTurn struct {
	// Finish is why the model turn ended, and RawFinish the provider's own reason as it sent it
	// (see FinishReason). Empty on a record written before they were journaled.
	Finish    FinishReason `json:"finish,omitempty"`
	RawFinish string       `json:"raw_finish,omitempty"`
	// Model identifies the model that answered the turn (see ModelInfoOf), for audit and for a
	// run whose turns a middleware sent to different providers. Nil when the model does not
	// describe itself or no request produced the response (a middleware built it).
	Model *ModelInfo `json:"model,omitempty"`
	// PromptDigest and ToolsDigest are digests of the system prompt and the tool set the turn was
	// sent (see the functions of the same names), so an auditor can tell which instructions and
	// which tools each answer was given, although agent-level defaults stay live across a
	// redeploy. Empty when the turn was sent no system prompt, or no tools.
	PromptDigest string `json:"prompt_digest,omitempty"`
	ToolsDigest  string `json:"tools_digest,omitempty"`
}

ModelTurn is a StepModel record's model-turn metadata (see Record.ModelTurn). A Record copy shares it with the original: never write through a record's ModelTurn, assign the record a new one instead. A record decoded from its journal encoding carries one only when the encoding holds a non-empty member of it.

type Notification added in v0.16.0

type Notification struct {
	// ID is the hex SHA-256 of Key: the same on every attempt, in every process and after every
	// restart. A sink that can drop duplicates should key on it.
	ID   string
	Key  string
	Kind NotificationKind
	// RunID is the root run, whose notification trail holds the event.
	RunID string
	// EventAt is when the run entered the state: the attempt marker's time for a halt, else the
	// time the event was first observed. Observed is when a notifier process first saw it. For
	// NotificationResolved, EventAt is when a notifier found the event no longer current (an upper
	// bound on when it ended) and Deadline is zero: a clear is never overdue.
	EventAt, Observed time.Time
	// Deadline is when the notification is overdue, if not acknowledged by then.
	Deadline time.Time
	// Attempt counts the delivery attempts this process has made for the event, from 1.
	Attempt int

	Halt      *NotifyHalt      // for NotificationOutcomeUnknown
	Approval  *NotifyApproval  // for NotificationApprovalPending
	Principal *Principal       // the run's principal, nil if it has none
	Clears    *NotifyClears    // for NotificationResolved
	Escalates *NotifyEscalates // for NotificationEscalated
	// Receiver is the name of the receiver this delivery is to: DefaultReceiver for WithNotifier's
	// Notifier, else a name given to WithNotifyReceiver.
	Receiver string
	// Reason is a code from a closed set, never error text: for NotificationRecoveryFailing the cause
	// class ("resume_error", "store_error", "not_resumable"), for NotificationOverdue "deadline", for
	// NotificationResolved why the event ended ("resolved", "decided", "expired", "ended", "recovered").
	Reason string
	// Err is the typed error that raised the event, for an in-process Notifier.
	Err error
}

Notification is one delivery attempt of an event to a Notifier. It holds structured fields only: no tool arguments, results, or error text travel in it, except Err, which is for an in-process Notifier and which a Notifier that sends the notification elsewhere must not forward by default.

type NotificationKind added in v0.16.0

type NotificationKind string

NotificationKind says what a Notification is about.

const (
	// NotificationOutcomeUnknown: a drive ended with *OutcomeUnknown.
	NotificationOutcomeUnknown NotificationKind = "outcome_unknown"
	// NotificationApprovalPending: a drive ended with *ApprovalPending.
	NotificationApprovalPending NotificationKind = "approval_pending"
	// NotificationRunAborted: the run reached the run:aborted end marker.
	NotificationRunAborted NotificationKind = "run_aborted"
	// NotificationRecoveryFailing: recovery has kept failing for a run for longer than the configured time.
	NotificationRecoveryFailing NotificationKind = "recovery_failing"
	// NotificationOverdue: an event was not acknowledged by its deadline.
	NotificationOverdue NotificationKind = "notification_overdue"
	// NotificationResolved: an outcome_unknown, approval_pending or recovery_failing event that a
	// receiver has, or may have, stopped being current (the halt was resolved, the gate decided, the
	// run ended, recovery recovered). Notification.Clears says which event, so a receiver can close
	// its incident; it is the all clear.
	NotificationResolved NotificationKind = "resolved"
	// NotificationApprovalExpired: an approval gate a receiver was, or may have been, told of
	// (approval_pending) expired: its deadline (ApprovalPolicy.Timeout) passed with no approval, so
	// the call will not run. It is raised when the pending approval's all-clear finds the expiry
	// (its reason is "expired"), and it is a fact: it never stops being current.
	NotificationApprovalExpired NotificationKind = "approval_expired"
	// NotificationEscalated: an outcome_unknown, approval_pending or recovery_failing event was not
	// acknowledged (an approval: not decided) by its deadline, and the next receiver of its
	// escalation chain is told. Notification.Escalates says which event and which step of the chain.
	NotificationEscalated NotificationKind = "escalated"
)

type Notifier added in v0.16.0

type Notifier interface {
	Notify(ctx context.Context, n Notification) error
}

Notifier delivers one Notification. Notify must return nil only once the sink has accepted it. It is called again for the same ID after an error, a crash or a lost reply, so a sink that can dedupe should use Notification.ID to do so. Return an error wrapping ErrNotifyPermanent for a failure that retrying within the same round cannot fix (a 4xx other than 429); the event is retried in a later round all the same, since a receiver's configuration can change.

Notify may be called from several goroutines at once, for different events (and for an event and the steps of its escalation chain), so an implementation must be safe for concurrent use. One event has at most one call in flight, except that a call abandoned at its time limit may still be running when the next is made.

type NotifierOption added in v0.16.0

type NotifierOption interface {
	Option
	RecoverOption
	RecoverLoopOption
}

NotifierOption is the result of WithNotifier: it configures an Agent (New, With), RecoverLoop and Recover.

func WithNotifier added in v0.16.0

func WithNotifier(n Notifier, trail NotifyTrail, opts ...NotifyOption) NotifierOption

WithNotifier tells n, at least once and with a stable ID, when a run needs a person: it halts on an unknown outcome, waits for approval, is aborted, or cannot be recovered. Delivery state is written to trail, which must be durable and shared by every process that drives runs for the guarantee to hold across restarts and processes (audit.NewNotifyTrail over a durable audit.EventStore); a nil trail is ErrConfig. The same value applies to an Agent, where the notification is raised at the end of every drive, and to RecoverLoop and Recover, where it also raises recovery_failing and covers a Resumer that drives no Agent with this option.

A delivery runs on a goroutine the notifier owns, so a slow sink never delays Run. Without RecoverLoop or a call of NotifyPending, an event whose process dies before the first record of it is not found again: the guarantee needs one of them. FlushNotifications waits for deliveries in flight.

type NotifyAck added in v0.16.0

type NotifyAck struct {
	Status int         // the sink's HTTP status, 0 if not HTTP
	Auth   *NotifyAuth // how the delivery was authenticated, nil if it was not
}

NotifyAck is what a Notifier reports about an accepted delivery, recorded in the trail beside the acknowledgement as audit evidence of how it was authenticated.

type NotifyApproval added in v0.16.0

type NotifyApproval struct {
	ToolUseID string
	ToolName  string
	// Need and Of are the gate's policy, k of n. Both are 0 for a 1-of-1 gate.
	Need, Of  int
	Approvers []string // the gate's eligible approvers; nil for a 1-of-1 gate
	Approved  int      // approvals counted when the event was observed
	// Excluded are the approvers who may not decide on this call: under separation of duties
	// (ApprovalPolicy.ExcludePrincipal), those who are requesters of the run. Their decisions never
	// count, so a notifier need not ask them.
	Excluded []string
	// Reask says why the gate asks again over a recorded tally it does not act on
	// (PendingReapproval, PendingSoDReapproval, PendingIdentReapproval, PendingExecHoldReapproval), or "" for the gate's first ask. A re-ask is an event
	// of its own (ApprovalReaskKey): approvers decide again.
	Reask string
	// contains filtered or unexported fields
}

NotifyApproval is the structured detail of a NotificationApprovalPending notification.

type NotifyAuth added in v0.16.0

type NotifyAuth struct {
	Signature    string `json:"signature,omitempty"`     // the scheme, "hmac-sha256", empty if unsigned
	SecretSlot   string `json:"secret_slot,omitempty"`   // "current" or "previous"; never the secret
	MTLS         bool   `json:"mtls,omitempty"`          // a client certificate was presented
	ClientSPKI   string `json:"client_spki,omitempty"`   // hex SHA-256 of the client certificate's SubjectPublicKeyInfo
	ServerPinned bool   `json:"server_pinned,omitempty"` // the receiver's key matched a pin
	CustomCA     bool   `json:"custom_ca,omitempty"`     // the receiver was verified against a custom CA bundle
	TLSVersion   string `json:"tls_version,omitempty"`   // the negotiated version, such as "1.3"
}

NotifyAuth records how a delivery was authenticated, next to the delivery it authenticated.

type NotifyClears added in v0.17.0

type NotifyClears struct {
	// Key and ID are the key and the delivery ID (NotifyID(Key)) of the event that stopped being
	// current, as that event's own notifications carry them.
	Key, ID string
	// Kind is the cleared event's kind.
	Kind NotificationKind
}

NotifyClears is the structured detail of a NotificationResolved notification: the event it ends. Event keys are never reused (a halt's key holds its attempt marker's time, a gate's its reminder number, a recovery failure's its episode), and an event that has stopped being current does not become current again, so the ID is final: a receiver that gets a clear for an ID may treat every delivery of that ID, before or after the clear, as superseded. That is the whole ordering rule; no sequence number is needed.

type NotifyEscalates added in v0.17.0

type NotifyEscalates struct {
	// Key and ID are the key and the delivery ID (NotifyID(Key)) of the event that was not
	// acknowledged or decided in time, as that event's own notifications carry them.
	Key, ID string
	// Kind is that event's kind.
	Kind NotificationKind
	// Level is the step of the escalation chain, from 1.
	Level int
}

NotifyEscalates is the structured detail of a NotificationEscalated notification: the event it escalates, and the step of the chain it is.

type NotifyEvent added in v0.16.0

type NotifyEvent struct {
	Key                 string
	Kind                string // the NotificationKind recorded at observation
	RunID               string
	EventAt             time.Time
	ObservedAt          time.Time
	DeadlineAt          time.Time
	OpRun, OpKind, OpID string // recorded at observation for a halt or an approval (see NotifyRecord)
	Reason              string // recorded at observation: for a resolved event, why the event it clears ended
	Receiver            string // recorded at observation: the receiver the event is to, "" for the default
	Level               int    // recorded at observation: the step of an escalated event's chain, 0 otherwise

	Claim   *NotifyRecord // the latest claimed record, nil if never claimed
	Rounds  int           // how many claimed records the event has
	Acked   *NotifyRecord
	Cleared *NotifyRecord
	Failing *NotifyRecord
	Overdue *NotifyRecord
}

NotifyEvent is the state of one event, folded from a notification trail.

func (*NotifyEvent) ClaimLive added in v0.16.0

func (e *NotifyEvent) ClaimLive(now time.Time) bool

ClaimLive reports whether some process holds a live claim at now. A claim lapses at its Until.

func (*NotifyEvent) Open added in v0.16.0

func (e *NotifyEvent) Open() bool

Open reports whether delivery of the event is still owed: it has been neither acknowledged nor cleared.

func (*NotifyEvent) OverdueDue added in v0.16.0

func (e *NotifyEvent) OverdueDue(now time.Time) bool

OverdueDue reports whether the event is open, past its deadline at now, and not yet marked overdue. An event observed with no deadline (a resolved event, which is never overdue) is not.

func (*NotifyEvent) ReceiverName added in v0.17.0

func (e *NotifyEvent) ReceiverName() string

ReceiverName is the name of the receiver the event is to: DefaultReceiver when none is recorded.

type NotifyHalt added in v0.16.0

type NotifyHalt struct {
	Op          OpRef
	Cause       HaltCause
	AttemptedAt time.Time
}

NotifyHalt is the structured detail of a NotificationOutcomeUnknown notification.

type NotifyOption added in v0.16.0

type NotifyOption interface {
	// contains filtered or unexported methods
}

NotifyOption configures the notifier WithNotifier and NotifyPending build.

func WithNotifyAbortedWithin added in v0.16.0

func WithNotifyAbortedWithin(d time.Duration) NotifyOption

WithNotifyAbortedWithin sets how recent an aborted run must be for NotifyPending to raise its run_aborted event (default 24 hours), by the time the run started, which is the only time its journal records. It stops the first sweep after a notifier is enabled from paging for every run that ever aborted. A run whose run:start records no time is not raised. d must be positive.

func WithNotifyApproverRoute added in v0.17.0

func WithNotifyApproverRoute(approver, receiver string) NotifyOption

WithNotifyApproverRoute sends the approval_pending event of a gate whose approvers include approver to the named receiver. A gate with approvers routed to several receivers is delivered to each of them, as a delivery of its own (key "to:<receiver>:<key>", its own ID, claim and acknowledgement), so that one receiver's acknowledgement is never taken for another's. An approver with no route is reached through the route of the kind (WithNotifyRoute(NotificationApprovalPending, ...), else the default receiver), which is delivered to as well whenever any approver of the gate has no route; a gate with none of its approvers routed, and a 1-of-1 gate (which names none), go to the route of the kind alone.

func WithNotifyClaimTTL added in v0.16.0

func WithNotifyClaimTTL(d time.Duration) NotifyOption

WithNotifyClaimTTL sets how long a process's claim on an event lasts (default 2 minutes). A crashed deliverer's claim lapses after it, which is how another process takes over, so it bounds the delay of a takeover. A round's calls and retries must fit inside the claim, so one call of a Notifier is limited to an eighth of it: choose a claim at least eight times the longest a sink takes to answer. d must be at least 30 seconds (a call limit of under 4 seconds).

func WithNotifyClock added in v0.16.0

func WithNotifyClock(now func() time.Time) NotifyOption

WithNotifyClock sets the clock the notifier reads: for claim and deadline times, backoff, and the age of an aborted run or a halt in a sweep. Default time.Now. A deployment whose runs are driven with WithClock (a test, a simulation) should give the notifier the same clock, because the journal's times (started_at, aborted_at, attempt markers) come from the drive's clock and the sweep compares them with the notifier's. The clock must advance: with a frozen clock, backoff and claim expiry never pass, so a failed delivery is never retried. A nil clock is ErrConfig.

func WithNotifyDeadline added in v0.16.0

func WithNotifyDeadline(d time.Duration) NotifyOption

WithNotifyDeadline sets how long after an event is observed it may go unacknowledged before it is overdue (default 5 minutes). Past it, an overdue record is written, a NotificationOverdue event is raised, and the original keeps being retried. d must be positive.

func WithNotifyErrors added in v0.16.0

func WithNotifyErrors(fn func(error)) NotifyOption

WithNotifyErrors sets the function that receives the notifier's own failures: a trail or store error, a delivery that has become failing, a trail position it kept losing. Without it they are dropped (delivery retries regardless). It may be called from several goroutines.

The handler hears of a failure while the notifier's round for that event is still in flight, so that a FlushNotifications waits for it: a process that flushes and exits has delivered every report. It follows that the handler must not itself call FlushNotifications (or RunNotifier.Flush, or anything that waits for the notifier to settle): it would wait for the round that is calling it. Such a flush returns an error wrapping ErrConfig at once instead of waiting. Keep the handler short (log, count, hand the error to a channel): a slow one holds the round's slot in the worker.

func WithNotifyEscalation added in v0.17.0

func WithNotifyEscalation(kind NotificationKind, steps ...EscalationStep) NotifyOption

WithNotifyEscalation sets the escalation chain of events of kind: NotificationOutcomeUnknown, NotificationApprovalPending or NotificationRecoveryFailing. When such an event has not been acknowledged by its deadline (WithNotifyDeadline), or, for an approval, has not been decided by then, the first step's receiver is told in a NotificationEscalated event of its own, with its own claim and acknowledgement in the trail and its own deadline, the step's. When that deadline passes with the event still unacknowledged (an approval: undecided) the next step is told, up to the last. An event that stops being current stops the chain, and the receivers already told get its all-clear. The original keeps being delivered. 1 to 4 steps.

Escalation runs in a process that is alive at the deadline: a RecoverLoop, a long-lived Agent or a periodic NotifyPending finds the event again in the trail and continues the chain.

func WithNotifyFailingAfter added in v0.16.0

func WithNotifyFailingAfter(n int) NotifyOption

WithNotifyFailingAfter sets how many failed delivery attempts make a delivery failing (default 8). A round makes up to 3 attempts, so the count is taken in rounds, ceil(n/3), and from the trail: rounds that claimed the event and never acknowledged it, whichever process ran them and however they ended (a crash included). A failing record is written, once, and the failure is reported to the handler of WithNotifyErrors. Delivery continues. n must be at least 1.

func WithNotifyHaltAge added in v0.16.0

func WithNotifyHaltAge(d time.Duration) NotifyOption

WithNotifyHaltAge sets how old an attempt marker with no result must be before NotifyPending reports it as a halt (default 5 minutes). A sweep reads the journal and cannot tell a halted effect from one still running in a process it cannot see, so it waits: set it longer than your longest tool call or Step, or a slow call is reported as halted. A marker with no recorded time is not reported. It does not apply to a drive, which knows. d must be positive.

func WithNotifyHolder added in v0.16.0

func WithNotifyHolder(id string) NotifyOption

WithNotifyHolder names this process in the claims it writes (default host, pid and a random number). Two processes must not share a name.

func WithNotifyReceiver added in v0.17.0

func WithNotifyReceiver(name string, n Notifier) NotifyOption

WithNotifyReceiver names a receiver besides the one WithNotifier is given (DefaultReceiver), for WithNotifyRoute, WithNotifyApproverRoute and WithNotifyEscalation. A name is 1 to 32 lower-case letters, digits, '_' or '-', and each receiver is a Notifier of its own (typically a webhook notifier with its own URL, secret and relay). Every receiver has its own claim, acknowledgement, retries and overdue in the notification trail.

func WithNotifyRecoveryFailingAfter added in v0.16.0

func WithNotifyRecoveryFailingAfter(d time.Duration) NotifyOption

WithNotifyRecoveryFailingAfter sets how long recovery must keep failing for a run before a NotificationRecoveryFailing event is raised (default 15 minutes). The time runs from the first failing pass, recorded in the trail, so it counts across restarts and processes. d must be positive.

func WithNotifyRetry added in v0.16.0

func WithNotifyRetry(base, max time.Duration) NotifyOption

WithNotifyRetry sets the backoff between rounds of a failing delivery: base, doubling per failure, up to max (defaults 1 second and 15 minutes). A delivery is retried for as long as the process runs; this only paces it. Both must be positive, and base at most max.

func WithNotifyRoute added in v0.17.0

func WithNotifyRoute(kind NotificationKind, receiver string) NotifyOption

WithNotifyRoute sends events of kind to the named receiver instead of the default one. kind is one a run raises: NotificationOutcomeUnknown, NotificationApprovalPending, NotificationApprovalExpired, NotificationRunAborted or NotificationRecoveryFailing. The all-clear of an event goes to the receiver the event went to; an overdue notice too, unless WithOverdueNotifier says otherwise.

func WithNotifyUpgradeWindow added in v0.17.0

func WithNotifyUpgradeWindow(d time.Duration) NotifyOption

WithNotifyUpgradeWindow sets how long after the first backfill pass the second runs (default 24 hours). A rolling upgrade leaves processes of the previous version delivering events they do not list on the open trail; every scan of an unfinished run lists such an event, and the second pass lists what no scan reached (a run such a process saw to completion). Set it to the time your upgrade takes to reach every process, a week for a staged rollout across regions: an event a previous-version process delivers after the second pass, in a run that ends before a new-version scan reaches it, stays unlisted, and `bide audit notifications` reports it. d must be positive.

func WithOverdueNotifier added in v0.16.0

func WithOverdueNotifier(n Notifier) NotifyOption

WithOverdueNotifier sends NotificationOverdue events to n instead of the main Notifier, for the usual case where the main sink being down is why the event is overdue.

type NotifyRecord added in v0.16.0

type NotifyRecord struct {
	Kind  NotifyRecordKind `json:"kind"`
	Key   string           `json:"key,omitempty"` // the event key; empty for failing_since and recovered
	RunID string           `json:"run_id,omitempty"`
	At    time.Time        `json:"at"`
	// ClockAt is, on an open-trail record only, the writer's own clock reading when At was moved
	// later than it to fit the record's segment (see openAt): At is never earlier than the clock, so
	// a time moved only ever makes a delivery look later. Zero when At is the clock's reading.
	ClockAt time.Time `json:"clock_at,omitzero"`

	// observed
	EventKind string `json:"event_kind,omitempty"` // the NotificationKind of the event
	// Receiver is the receiver the event is to (see WithNotifyReceiver); empty for the default one.
	Receiver string `json:"receiver,omitempty"`
	// Level is the step of the escalation chain of an escalated event, from 1.
	Level      int       `json:"level,omitempty"`
	EventAt    time.Time `json:"event_at,omitzero"` // when the run entered the state, from the journal
	DeadlineAt time.Time `json:"deadline_at,omitzero"`

	// claimed
	Holder string    `json:"holder,omitempty"`
	Until  time.Time `json:"until,omitzero"`

	// acked
	FirstSentAt time.Time   `json:"first_sent_at,omitzero"`
	Attempts    int         `json:"attempts,omitempty"`
	Status      int         `json:"status,omitempty"` // the sink's HTTP status
	Auth        *NotifyAuth `json:"auth,omitempty"`

	// failing
	Failures int `json:"failures,omitempty"`

	// overdue
	Latency time.Duration `json:"latency,omitempty"` // At minus ObservedAt

	// observed, for a halt or an approval event: the operation or gate the key names, so that whether
	// the event is still current can be checked from the record when an ID in the key was too long to
	// encode (it is a digest). OpRun is the run whose journal holds the operation or gate, OpKind
	// "tool" or "step" for a halt and "gate" for an approval, OpID the operation's or call's ID. A
	// record written before these fields has none; the key is read instead.
	OpRun  string `json:"op_run,omitempty"`
	OpKind string `json:"op_kind,omitempty"`
	OpID   string `json:"op_id,omitempty"`

	// checkpoint: the events listed, and whether the backfill is done. seal: the next segment.
	Watching   []Watched `json:"watching,omitempty"`
	Backfilled bool      `json:"backfilled,omitempty"`
	// Backfills is how many backfill passes the carried state holds (at most two), and BackfillAt
	// when the last one was recorded: a second pass, a day after the first, catches the events an
	// older process delivered without listing them during a rolling upgrade.
	Backfills  int       `json:"backfills,omitempty"`
	BackfillAt time.Time `json:"backfill_at,omitzero"`
	// BackfillWindow is, on the first backfill marker and on every checkpoint after it, the upgrade
	// window the first pass recorded: the second pass is due no sooner than BackfillAt of the first
	// plus it, and the verifier holds the trail to that.
	BackfillWindow time.Duration `json:"backfill_window,omitempty"`
	Next           string        `json:"next,omitempty"`

	// cleared: "resolved", "decided", "ended" or "recovered". observed, for a resolved event: the same
	// code, why the event it clears ended. failing_since: a class of cause, never error text.
	Reason string `json:"reason,omitempty"`
}

NotifyRecord is one record of a notification trail. Which fields are set depends on Kind (see the constants); every record names its time in At, by the clock of the process that wrote it. A record is a leaf of the trail's tamper-evident log, so it holds no secret, no tool arguments and no error text.

NotifyRecord is an agent.RunEvent only so the audit package can commit it as an event leaf; a run's stream never emits it.

func (NotifyRecord) Normalized added in v0.16.0

func (r NotifyRecord) Normalized() NotifyRecord

Normalized returns r with every time in UTC and stripped of its monotonic clock reading, the form a trail stores and compares, so two records of one instant are equal whatever location they were written in.

type NotifyRecordKind added in v0.16.0

type NotifyRecordKind string

NotifyRecordKind says what a NotifyRecord records.

const (
	// NotifyObserved: a notifier process first saw the event. Written once per event.
	NotifyObserved NotifyRecordKind = "observed"
	// NotifyClaimed: a process claimed the event for one delivery round, until Until.
	NotifyClaimed NotifyRecordKind = "claimed"
	// NotifyAcked: the sink acknowledged the event. Ends delivery.
	NotifyAcked NotifyRecordKind = "acked"
	// NotifyFailing: delivery has failed Failures times. Written once; delivery continues.
	NotifyFailing NotifyRecordKind = "failing"
	// NotifyOverdue: the event was not acknowledged by its deadline. Written once; delivery continues.
	NotifyOverdue NotifyRecordKind = "overdue"
	// NotifyCleared: the event stopped being current (resolved, decided, or the run ended).
	NotifyCleared NotifyRecordKind = "cleared"
	// NotifyFailingSince: a recovery pass failed and no failure episode was open for the run.
	NotifyFailingSince NotifyRecordKind = "failing_since"
	// NotifyRecovered: a recovery pass succeeded while a failure episode was open.
	NotifyRecovered NotifyRecordKind = "recovered"
	// NotifyWatch: an event that can stop being current is about to be observed; written to the
	// NotifyOpenTrail, not to a run's trail. Idempotent per key while the key is watched.
	NotifyWatch NotifyRecordKind = "watch"
	// NotifyUnwatch: nothing more is owed for the watched event (its all-clear was acknowledged, or
	// it was cleared before any delivery was attempted, or it was never observed); written to the
	// NotifyOpenTrail.
	NotifyUnwatch NotifyRecordKind = "unwatch"
	// NotifyCheckpoint: the first record of every open-trail segment but the first: the events
	// listed when the previous segment was sealed, carried forward (Watching, Backfilled), so a read
	// starts at the newest segment.
	NotifyCheckpoint NotifyRecordKind = "checkpoint"
	// NotifySeal: the last record of an open-trail segment; Next names the segment that continues it.
	NotifySeal NotifyRecordKind = "seal"
	// NotifySegment: one record of the open trail's index, naming (Next) a segment that was created.
	NotifySegment NotifyRecordKind = "segment"
	// NotifyBackfilled: the events observed before the open trail existed were listed (once).
	NotifyBackfilled NotifyRecordKind = "backfilled"
)

type NotifyRouting added in v0.17.0

type NotifyRouting struct {
	// Routes is the receiver by kind (WithNotifyRoute); a kind not named goes to DefaultReceiver.
	Routes map[NotificationKind]string `json:"routes,omitempty"`
	// ApproverRoutes is the receiver by approver (WithNotifyApproverRoute).
	ApproverRoutes map[string]string `json:"approver_routes,omitempty"`
	// Escalation is, for each kind with a chain (WithNotifyEscalation), the receiver of each step.
	Escalation map[NotificationKind][]string `json:"escalation,omitempty"`
}

NotifyRouting is the routing of a notifier: which receiver each kind of event goes to, which receivers an approval goes to by its approvers, and the receivers of each escalation chain. It is the part of the notifier's configuration an auditor needs to say which deliveries an event is owed (audit.NotifyVerifyInput.Routing), and the notifier routes by the same method (Receivers), so the two cannot differ. Receivers are named as DefaultReceiver or as given to WithNotifyReceiver.

func RoutingOf added in v0.17.0

func RoutingOf(opts ...NotifyOption) (NotifyRouting, error)

RoutingOf returns the routing the options give a notifier: build it from the same NotifyOptions you pass to WithNotifier or NotifyPending, and write it (as JSON) for the auditor. Options that are not about routing are accepted and ignored.

func (NotifyRouting) ApproverRouted added in v0.17.0

func (r NotifyRouting) ApproverRouted() bool

ApproverRouted reports whether routing depends on a gate's approvers, so that a gate's receivers cannot be known without them.

func (NotifyRouting) Receivers added in v0.17.0

func (r NotifyRouting) Receivers(kind NotificationKind, approvers []string) []string

Receivers returns the receivers an event of kind is delivered to, sorted: for an approval whose approvers are routed, the receiver of each (one delivery each), and, when any approver of the gate has no route, the kind's route as well (else DefaultReceiver), through which that approver is reached; for any other event, or an approval none of whose approvers is routed, the kind's route, else DefaultReceiver. approvers is the gate's approvers, nil for a 1-of-1 gate.

type NotifyState added in v0.16.0

type NotifyState struct {
	Events map[string]*NotifyEvent
	Keys   []string // event keys in the order they were observed

	// FailingSince is the open recovery failure episode of the run, nil if none.
	FailingSince *NotifyRecord
	// Episodes counts closed recovery failure episodes (recovered records that closed one).
	Episodes int
}

NotifyState is a notification trail folded to what a notifier needs to decide its next step.

func FoldNotify added in v0.16.0

func FoldNotify(recs []NotifyRecord) NotifyState

FoldNotify folds a trail, in append order, to its state. It is a pure function of recs, and tolerant of a trail it did not write: a record that does not apply is ignored (a claim for an event never observed, a second observation of a key, any record after an event is acknowledged or cleared, a recovered with no open episode). The first terminal record (acked or cleared) ends an event, and the later one is ignored. Verification of a trail is audit's job, not the fold's.

func (NotifyState) ByReceiver added in v0.17.0

func (s NotifyState) ByReceiver() map[string][]string

ByReceiver groups the keys of the events in the trail by the receiver they are to (DefaultReceiver for the default one), each in the order observed: the per-receiver view of a run's deliveries, whose acknowledgement an auditor reads from Events[key].

func (*NotifyState) RecoveryFailingDue added in v0.16.0

func (s *NotifyState) RecoveryFailingDue(now time.Time, d time.Duration) bool

RecoveryFailingDue reports whether the open recovery failure episode has lasted longer than d at now, so a recovery_failing event is due. It is false with no open episode.

type NotifyTrail added in v0.16.0

type NotifyTrail interface {
	// Load returns trail's records in append order, nil if it has none.
	Load(ctx context.Context, trail string) ([]NotifyRecord, error)
	// Append stores rec at position seq (from 0) of trail. seq equal to the trail's length appends.
	// A seq below it holding an equal record is a no-op (nil): the caller's own earlier append
	// landed although it saw an error. A seq below it holding a different record is
	// ErrTrailConflict. A seq above the length is an error, not a conflict.
	Append(ctx context.Context, trail string, seq int, rec NotifyRecord) error
}

NotifyTrail is the port for the notification trail: a named, append-only sequence of NotifyRecords with insert-if-absent at a position. audit.NewNotifyTrail implements it over an audit.EventStore, so the trail is salted, Merkle-committed and verifiable by bide audit; package agent cannot import audit. MemNotifyTrail is the in-memory implementation for tests and local development.

The position is what makes a claim atomic: a process loads the trail, decides from the fold, and appends at len(loaded). If any other process appended meanwhile, the position is taken, the append returns ErrTrailConflict, and the decision is made again from a fresh load.

type NotifyTrailAnchorer added in v0.17.0

type NotifyTrailAnchorer interface {
	AnchorTrails(ctx context.Context, trails []string) error
}

NotifyTrailAnchorer is an optional capability of a NotifyTrail: one whose records are signed and anchored as they are written (audit.NotifyTrail over an audit.AuditedEventStore) can also be asked to catch up. The notifier calls AnchorTrails with the trails of the runs a NotifyPending sweep or a recovery pass looked at, so a trail that grew while the anchor was unreachable, or whose writer died before it anchored, is signed again without waiting for a new event. It is best effort: an error is reported to the handler of WithNotifyErrors and never affects a delivery, and it must be bounded (it is called inline by the sweep and the recovery pass). A NotifyTrail that does not implement it is simply not asked.

type OpKind added in v0.9.0

type OpKind string

OpKind is the kind of operation a halt is on.

const (
	// OpTool is a tool call; OpRef.ID is its tool-use ID.
	OpTool OpKind = "tool"
	// OpStep is a Step; OpRef.ID is the step name.
	OpStep OpKind = "step"
)

type OpRef added in v0.9.0

type OpRef struct {
	Kind     OpKind
	ID       string
	ToolName string
}

OpRef names an operation in a run's journal: a tool call (ID is the tool-use ID and ToolName the tool's name) or a Step (ID is the step name, ToolName is empty).

type OpenState added in v0.17.0

type OpenState struct {
	Watched        []Watched     // the events listed, in the order they were
	Backfilled     bool          // the events observed before the open trail existed were listed
	Backfills      int           // the backfill passes recorded, in this segment and the ones before it
	BackfillAt     time.Time     // when the last one was recorded
	BackfillWindow time.Duration // the window the first pass recorded
	Sealed         string        // the segment that continues this one, "" if it is not sealed
}

OpenState is one open-trail segment folded.

func FoldOpen added in v0.17.0

func FoldOpen(recs []NotifyRecord) OpenState

FoldOpen folds an open-trail segment, in append order. A checkpoint is read only as the first record; a watch for a key already listed is ignored; an unwatch removes the key, and a later watch lists it again; nothing after a seal counts; a record of another kind is ignored.

type Option added in v0.10.0

type Option interface {
	// contains filtered or unexported methods
}

Option configures an Agent: New applies options to a new agent, and With to a copy of one. New an []Option to choose options conditionally; an AgentRunOption fits in one as well.

func WithApproverVerifiers added in v0.10.0

func WithApproverVerifiers(fn ApproverVerifierFor) Option

WithApproverVerifiers sets how the m-of-n approval gate resolves an approver id to the verifier of its decision signatures. An agent with any tool whose ToolSpec.Approval is an m-of-n policy needs it: New and With refuse such an agent without one, and refuse a policy two of whose approvers it resolves to one signing key (ApprovalPolicy.ValidateKeys). The gate checks again on every evaluation, since fn is a function. A nil fn is ErrConfig.

func WithMiddleware added in v0.10.0

func WithMiddleware(mw ...Middleware) Option

WithMiddleware appends model-call middleware, after any the agent has; the first one added is the outermost. A nil middleware is ErrConfig.

func WithOptions added in v0.10.0

func WithOptions(opts ...Option) Option

WithOptions groups options into one, applied in order where it appears.

func WithRetrieval

func WithRetrieval(r Retriever, k int, opts ...RetrievalOption) Option

WithRetrieval adds classic retrieval-augmented generation: for each run, the agent retrieves the top-k documents for the run's user message, as a journaled engine step, and adds them to every model request of the run as a user message placed just before that user message (after the system prompt and any earlier turns). Every model middleware sees the request with the documents in it.

The step is read-only, so a crash before it is recorded retrieves again; once recorded, every later model call of the run (after a crash and resume, or from a second driver) is given the documents it recorded, and the journal holds what the model was shown: retrieved text is durable content, stored like a tool result. The documents are data the operator does not control, so they are not given system authority, and each is written as one line of JSON, so no document can forge another entry.

The retrieval runs before the model middleware chain, so model middleware neither retries it nor prevents it:

  • A retrieval error fails the model call, and nothing is recorded, so the next drive retrieves again. A model middleware such as middleware.Retry does not retry it; WithRetrievalRetry does. Have the Retriever return (nil, nil) to degrade to no context instead.
  • A model middleware that refuses the call (a policy gate, a spend cap, a rate limiter) runs after the retrieval: the query has been sent to the Retriever and the documents journaled. A policy that must keep a query from the Retriever wraps the Retriever (see RetrieverFunc).

A Retriever that returns more than k documents is cut to its first k. Several WithRetrieval options retrieve in the order given, and their blocks appear in that order. A nil Retriever, a k below 1, or an invalid option is ErrConfig.

func WithSystemPromptFunc added in v0.10.0

func WithSystemPromptFunc(fn func(ctx context.Context, run RunInfo) (string, error)) Option

WithSystemPromptFunc sets a system message computed for each drive of a run (each Run, Stream or resume), from the run's context and its RunInfo, so it can carry dynamic context (the date, the tenant, state read from elsewhere). It fills the slot WithSystemPrompt fills: the later of the two wins. Its result is not journaled: a run resumed later is sent what fn returns then. Context that the run's later turns must see unchanged belongs in the input, which is journaled (see RunStart), or in a tool result.

fn is called once per drive, just before the drive's first model request, and only if the drive sends one: reading back a finished run, or a resume whose pending tool calls pause or halt before the next turn, does not call it, so an outage of the prompt's source does not block them. A resumed turn's pending tool calls run before it. An error from fn fails the drive, wrapped with the run's ID, and the model is not called. A nil fn is ErrConfig.

func WithToolErrorRedactor added in v0.10.0

func WithToolErrorRedactor(fn func(tool string, err error) string) Option

WithToolErrorRedactor sets the text recorded for a tool call that fails (see Agent.WithToolErrorRedactor for what it covers). A nil fn is ErrConfig.

func WithToolMiddleware added in v0.10.0

func WithToolMiddleware(mw ...ToolMiddleware) Option

WithToolMiddleware appends tool-call middleware, after any the agent has; the first one added is the outermost (see ToolMiddleware). A nil middleware is ErrConfig.

func WithTools added in v0.10.0

func WithTools(tools ...Tool) Option

WithTools registers tools with the agent. Each tool's spec is read once, here, and every decision about its calls is made from that copy. A nil tool, two tools with one name (including a name the agent already has), a name the agent's model's declared tool-name rule does not match (ToolRules), the name "final_answer", a non-object input schema, an invalid approval policy, and a wrapper the agent cannot honor are ErrConfig.

type Outcome added in v0.9.0

type Outcome struct {
	Result   any
	IsError  bool
	Evidence any
}

Outcome is the verified outcome of a halted operation, recorded by ResolveHalt as the operation's result. Result is the value an actual call would have returned (JSON-marshalled); IsError marks an outcome the model should read as a failure (in a saga, a failed step, which rolls the saga back). A non-nil Evidence marks the resolution reconciled from evidence and stores Evidence (JSON-marshalled) beside the result, as WithEvidence does.

type OutcomeUnknown added in v0.9.0

type OutcomeUnknown struct {
	RunRef
	Op OpRef
	// AttemptedAt is when the effect was attempted (the live attempt marker's timestamp), zero
	// if unknown (including a marker stamped zero or negative, see markerTime). A reconciler uses
	// it to honor a grace period before resolving (see WithMinHaltAge) so it does not query the
	// provider before its record has settled.
	AttemptedAt time.Time
	Cause       HaltCause
}

OutcomeUnknown is returned when a run cannot safely proceed past an operation whose outcome is unknown: a non-retriable tool call or Step was attempted (its marker is recorded) but no result was. The run stops for confirmation rather than risk a double side effect (a double charge). Cause says why: HaltCrashed when the marker was already there with no live claimant known (the driver that claimed it most likely died mid-effect), HaltContended when another driver of the same run won the claim while this one ran (for example one that took over after this node's lease lapsed); that driver owns the effect, and once it records the result, re-running proceeds normally.

Clear it with ResolveHalt(ctx, store, halt.Ref(), outcome), then re-run RootRunID.

func (*OutcomeUnknown) Error added in v0.9.0

func (e *OutcomeUnknown) Error() string

Error names the operation whose outcome is unknown, and says when another driver holds its claim.

func (*OutcomeUnknown) Ref added in v0.9.0

func (e *OutcomeUnknown) Ref() HaltRef

Ref returns the reference ResolveHalt takes to clear this halt.

type OutputMode added in v0.10.0

type OutputMode string

OutputMode is how a typed run (RunTyped) collects its answer: OutputTool (the default) through a final_answer tool call whose arguments are the answer, or OutputNative through the provider's native structured-output constraint (a JSON-schema response format).

const (
	// OutputTool collects a typed run's answer as the arguments of a final_answer tool call (the
	// default; see RunTyped).
	OutputTool OutputMode = "tool"
	// OutputNative collects a typed run's answer through the provider's native structured output
	// (WithOutputMode(OutputNative)).
	OutputNative OutputMode = "native"
)

type ParallelOption added in v0.10.0

type ParallelOption interface {
	// contains filtered or unexported methods
}

ParallelOption configures Parallel.

type Part

type Part interface {
	// contains filtered or unexported methods
}

Part is a single piece of message content. Concrete parts: Text, Reasoning, ToolUse, ToolResult, Image.

type Pause added in v0.9.0

type Pause interface {
	error
	// Paused names the paused run: RunID is the journal the pause lives in (a sub-agent's
	// sub-run for a pause raised inside a sub-agent) and RootRunID is the run to re-invoke.
	Paused() RunRef
	// contains filtered or unexported methods
}

Pause is the error a run returns when it has stopped durably and is waiting on something outside it: a human's approval (*ApprovalPending), an answer to an interrupt (*InterruptPending), a signal or a channel message (*SignalPending), a durable timer (*TimerPending), or a verdict on a side effect whose outcome is unknown (*OutcomeUnknown). A pause is not a failure: the run's journal is intact, and once the condition is met the run is re-invoked with Paused().RootRunID and carries on.

The set is sealed: only this package's five types implement Pause, so a type switch over them is exhaustive. Test for a pause with IsPause and read it with AsPause, which see through wrapping.

func AsPause added in v0.9.0

func AsPause(err error) (Pause, bool)

AsPause returns the first Pause in err's chain, if there is one.

type Principal added in v0.10.0

type Principal struct {
	OnBehalfOf   string `json:"on_behalf_of,omitempty"`
	AuthorityRef string `json:"authority_ref,omitempty"`
}

Principal is the part of a run's Identity that is journaled: on whose behalf it acts and under what authority. It is restored on every later drive, and a drive that passes another is ErrConfig. The Actor is not journaled: it is the deployment that drives the run now.

RunStart.Ancestors are the principals of the runs above a run started inside another run's drive (a sub-agent's, a delegation's, a SubRunFor run's, or a plan flow's run started from a tool), root first, as that drive bound them: the parent's ancestors and the parent's own principal (empty for a run that has none). They are recorded only when one of them, or the run's own principal, is set, and also in the run's run:principal (see RunPrincipal); the journaled value is authoritative, and later drives are not compared with it.

type RateLimited

type RateLimited struct {
	RetryAfter time.Duration
	Message    string // the provider's error message, if it sent one
	Err        error
}

RateLimited is returned by a provider adapter when it is rate limited (HTTP 429 that is not an exhausted quota; see ClassifyHTTPError). It carries an optional RetryAfter hint from the Retry-After response header or the provider's own retry delay (0 means no hint was provided), and the provider's error message. It wraps the caller-supplied Err (typically wrapping ErrModel) so errors.Is(err, ErrModel) holds.

func (*RateLimited) Error

func (e *RateLimited) Error() string

Error describes the rate limit: the retry hint and the provider's message when present, then Err.

func (*RateLimited) Unwrap

func (e *RateLimited) Unwrap() error

Unwrap returns Err, so errors.Is(err, ErrModel) holds when Err wraps ErrModel.

type ReanchorOption added in v0.17.0

type ReanchorOption interface {
	RecoverOption
	RecoverLoopOption
}

ReanchorOption is a setting for the re-signing of runs that wait on a person: it applies to Recover (RecoverOption) and RecoverLoop (RecoverLoopOption): WithReanchor and WithReanchorTimeout.

func WithReanchor added in v0.17.0

func WithReanchor(fn func(ctx context.Context, runID string) error) ReanchorOption

WithReanchor sets the function Recover and RecoverLoop call to sign the journal of a run they found waiting on a decision or a halt resolution: a run whose drive ended at an ApprovalPending or an OutcomeUnknown (several, when parallel sub-agents wait together). A decision or a halt resolution recorded by another process (a CLI writing the database, an approver's tool) is a journal write the application's AuditedStore did not make, so no signed tree head covers it until the application next writes to that run, which for a rejection may be never. Calling fn for each such run on every pass bounds that gap to one pass, for a run the pass drives.

By default both call the Reanchor method of the journal's store, or of the first store beneath it through Unwrap that has one (Reanchorer): an AuditedStore needs no option, and a Recover or RecoverLoop over one re-signs unless told not to. A fn given here replaces that; a nil fn turns re-signing off. fn is called only for a run this pass drove (it holds the run's lease), at most once per run per pass, and only for the runs the pass already enumerated as unfinished: it adds no listing and no scan. fn must honor its context: each call gets a deadline of WithReanchorTimeout (half the lease TTL by default), so a hung anchor cannot hold a run's lease and a concurrency slot for longer. An error it returns (the deadline's included) goes to WithRecoverErrors, wrapping ErrStorage, and the next pass tries again. fn must be safe for concurrent use, as runs are driven concurrently. A drive that ends with several waiting runs (parallel sub-agents) re-signs them one after another under one lease, so its worst case is the number of runs times the timeout; set WithReanchorTimeout with that in mind.

func WithReanchorTimeout added in v0.17.0

func WithReanchorTimeout(d time.Duration) ReanchorOption

WithReanchorTimeout bounds each re-sign (see WithReanchor): the context fn gets expires after d. It must be positive. The default is half the lease TTL.

type Reanchorer added in v0.17.0

type Reanchorer interface {
	Reanchor(ctx context.Context, runID string) error
}

Reanchorer is the optional capability of a store that signs and publishes a run's tree head on its own writes and can do so again on demand: audit.AuditedStore. Reanchor signs and publishes a head over runID's journal as it is now, if no published head covers it, and writes nothing to the journal. Recover and RecoverLoop call it for a run that is waiting on a person (see WithReanchor).

type Reasoning

type Reasoning struct {
	Text      string `json:"text"`
	Signature string `json:"signature,omitempty"`
	Redacted  string `json:"redacted,omitempty"`
}

Reasoning is model thinking. Signature is the provider's opaque token that must be echoed back on later turns (Anthropic extended thinking); dropping it corrupts the conversation — which is exactly why Content can't be a flat string.

Redacted holds reasoning the provider returned encrypted (Anthropic redacted_thinking data), which must also go back unchanged; Text and Signature are empty when it is set. Each thinking block is its own Reasoning part, in the order the model produced them.

type ReasoningDelta

type ReasoningDelta struct {
	Text      string `json:"text"`
	Signature string `json:"signature"` // opaque provider token to echo back on later turns (Anthropic thinking)
	Redacted  string `json:"redacted"`  // encrypted reasoning to echo back unchanged (Anthropic redacted_thinking)
}

ReasoningDelta is a fragment of model thinking. A Signature ends the thinking block it belongs to, so the next ReasoningDelta starts a new Reasoning part. Redacted carries a whole encrypted block (Anthropic redacted_thinking), which becomes a Reasoning part of its own.

type Received

type Received[T any] struct {
	Key     string
	Payload T
}

Received is one message pulled from a channel.

func Receive

func Receive[T any](ctx context.Context, channel string) (Received[T], error)

Receive returns the oldest not-yet-acked message on channel, in delivery order. If the channel has no unacked message it returns *SignalPending and the run pauses durably. The run must Ack(channel, msg.Key) after it has durably handled the message; until then Receive keeps returning the SAME message, which is what makes processing replay-safe and exactly-once. Call from a retry-safe tool (Safety.ReadOnly or Idempotent), like Await.

type Record

type Record struct {
	Name    string   `json:"name"`
	Kind    StepKind `json:"kind"`
	Message *Message `json:"message,omitempty"` // StepModel
	// Usage is token usage. On a StepModel record, the call's. On a StepToolResult or StepSagaFail
	// record, the Usage of the runs the tool call started (sub-agents): their recorded responses.
	Usage *Usage `json:"usage,omitempty"`
	// DiscardedUsage is billed usage no recorded response carries. On a StepModel record, the
	// turn's other model requests: failed attempts a middleware retried, losing hedge targets. On
	// a StepValue record named "@spend/<id>", a model call that failed for good; on one named
	// "@spend-late/<id>", requests that ended after their turn was recorded, or whose turn another
	// driver recorded. On a
	// StepToolResult or StepSagaFail record, the rest of the Spend of the runs the tool call
	// started. Nil when there was none. WithTokenBudget counts it.
	DiscardedUsage *Usage `json:"discarded_usage,omitempty"`
	// ModelTurn is the model-turn metadata of a StepModel record: why the turn ended, the model that
	// answered it, and digests of the prompt and tools it was sent. Nil on every other kind, and on
	// a StepModel record that carries none. Read it through the nil-safe accessors (Record.Finish,
	// RawFinish, Model, PromptDigest, ToolsDigest). It sits behind a pointer to keep Record small
	// (see TestRecord_Size); its fields are journaled in place, as members of the record. A copy of
	// the record shares it: do not write through it.
	*ModelTurn
	ToolUseID string          `json:"tool_use_id,omitempty"` // StepToolResult
	Result    json.RawMessage `json:"result,omitempty"`      // StepToolResult / StepValue
	IsError   bool            `json:"is_error,omitempty"`    // StepToolResult
	Approved  bool            `json:"approved,omitempty"`    // StepApproval
	// ApproverSignature is the signed decision of a StepApproval record written by SubmitDecision:
	// who decided and their signature. It also carries the informational recorded_at alone, with no
	// signed decision, on the records that note when they were journaled (see
	// ApproverSignature.RecordedAt). Nil on every other record. Read it through the nil-safe
	// accessors (Record.Approver, ApproverAlg, Signature, DecidedAt, RecordedAt). It sits behind a
	// pointer to keep Record small (see TestRecord_Size); its fields are journaled in place, as
	// members of the record. A copy of the record shares it: do not write through it.
	*ApproverSignature
	// AttemptedAt is the Unix-millis wall-clock time an attempt marker (StepAttempt) was
	// written, i.e. just before a non-retriable side effect fired. It is set once and read
	// back verbatim on replay, so it stays deterministic. Zero (and omitted) on every
	// other kind; a reconciler uses it to honor a grace period before resolving a halt.
	AttemptedAt int64 `json:"attempted_at,omitempty"`
	// Reconciled marks a StepToolResult that ResolveHalt injected from verified evidence
	// rather than one the tool produced by running. It lets a later reader (and bide audit)
	// tell a reconciled outcome from a clean one at a glance.
	Reconciled bool `json:"reconciled,omitempty"`
	// OutcomeUnknown marks a StepSagaFail record for a step whose outcome is unknown: a
	// retry-safe step that failed with ErrToolOutcomeUnknown, or returned an error after its
	// deadline. It may have committed, so a rollback reports it (SagaAborted.UnknownOutcome)
	// rather than take it for a step that changed nothing.
	OutcomeUnknown bool `json:"outcome_unknown,omitempty"`
	// Redacted marks a record whose stored bytes a redaction replaced with a tombstone, the
	// reserved form {"redacted":{"leaf_hash":"<hex>","at_ms":<ms>}}: the hex audit leaf hash of
	// the bytes it replaced and the Unix-millis time of the redaction. Only Name is meaningful on
	// such a record, and Raw returns the tombstone. It is never journaled: the tombstone is what the
	// store holds. A run holding a redacted record is over and cannot be driven again.
	Redacted bool `json:"-"`

	// Safety is the Safety of the tool a StepToolResult or StepSagaFail record's call ran under
	// (or, for a denied call, would have run under), and Approval its approval gate, nil for an
	// ungated tool. A saga rollback reads Safety: it skips a call that ran ReadOnly (it changed
	// nothing) and treats any other completed call as a write, whatever the tool is declared as
	// by the time the rollback runs. Safety is nil on a result that ResolveHalt injected, which
	// is a write (only a call that was not retry-safe halts), and on every other kind.
	Safety   *Safety         `json:"safety,omitempty"`
	Approval *ApprovalPolicy `json:"approval,omitempty"`
	// Evidence is what a reconciler read to decide the outcome (a queried provider record,
	// a message id, a log line). It is carried on the reconciled result and signed with it,
	// so the verdict and its basis live in the journal beside the outcome.
	Evidence json.RawMessage `json:"evidence,omitempty"`
	// Format is the journal format a StepHeader record names (see JournalFormat). Empty on every
	// other kind.
	Format string `json:"format,omitempty"`
	// contains filtered or unexported fields
}

Record is one durably-recorded, named event. The ordered sequence of Records for a run is its full, replayable history; resume rebuilds state by replaying them. Name is the stable identity used for memoization (see Journal.Get).

func ClaimAttempt added in v0.5.0

func ClaimAttempt(ctx context.Context, j *Journal, runID, name string, rec Record) (won bool, got Record, err error)

ClaimAttempt writes the attempt marker named name as an exclusive claim and reports whether the caller won it. It stamps rec with a fresh random claim id and inserts it; because a journal records a name at most once, exactly one driver's marker is stored, and a driver won only if the marker it gets back carries its own claim (see Record.ClaimID). A driver that loses must not run the side effect the marker guards: another driver owns it and may be running it right now.

This makes at-most-once independent of leasing. A lease cannot guarantee mutual exclusion (a holder stalled past its TTL wakes still believing it holds the lease), but two drivers that overlap still cannot both win the claim, since the store's atomic insert decides the winner. With a single driver the claim is always won, so a run with no contention behaves exactly as before.

Every claim is made with a fresh id. If the marker cannot be written, the claim records that its attempt did not start, and if even that record cannot be written, the process remembers it and writes it again at the next claim of the same name, which then loses to the marker its earlier claim may have left: a caller that claims one name only (rather than numbered re-attempts, as Step and tool calls do) halts on it, which is safe.

func DecodeRecord added in v0.7.0

func DecodeRecord(b []byte) (Record, error)

DecodeRecord decodes a record from its journal encoding (see EncodeRecord) into an independent copy that shares no memory with b. The record's Raw is a copy of b.

func DecodeStoredRecord added in v0.8.0

func DecodeStoredRecord(runID, name string, b []byte) (Record, error)

DecodeStoredRecord decodes the record a store holds for the step name of run runID (see DecodeRecord) and checks that the record carries that name. Every record is journaled with the name it is stored under (JournalEntry), and the engine finds records by the name inside them (IsComplete, the approval tally, a signal, an attempt marker), so a row whose record names another step (a row edited or copied in the backing store) would be read as that step: a row under "x" holding a record named run:complete would mark an unfinished run complete. Such a row is ErrStorage, naming the run and the key, like any other stored record that does not decode: the store's contents are wrong, whatever wrote them. Fields this version does not know still decode, so a journal a newer version wrote stays readable; that includes members of the record's approval policy, which a stored record reads leniently (ApprovalPolicy's own UnmarshalJSON is strict, for a policy being configured or received).

A redaction tombstone (see Record.Redacted) decodes as a record that carries only its name.

The journal reads every record back through it.

func LatestPauseNote added in v0.17.0

func LatestPauseNote(byName map[string]Record, toolUseID string) (r Record, ok bool)

LatestPauseNote returns the newest pause note on toolUseID among records keyed by name: the separation-of-duties re-ask note (ApprovalReaskStep with sod), else the format re-ask note, else the first pause note (ApprovalPendingStep). A gate re-asks for the format before it re-asks for separation of duties, so this order is journal order. A re-ask note counts only if the journal proves it (ValidReaskNote): the note is insert-once by name, so a journal writer without approver keys could pre-insert one, and it is not read as the gate's ask unless a recorded tally the gate would ask again over is there. byName must hold the call's tallies as well as its notes. ok is false when there is none.

func RecordedTally added in v0.16.0

func RecordedTally(byName map[string]Record, toolUseID string) (r Record, ok bool)

RecordedTally returns the record of the gate's outcome on toolUseID among records keyed by name. It starts from the tally (ApprovalTallyStep); over one that passed on decisions predating DecisionFormat, the re-approval tally (ApprovalRetallyStep) is the outcome when recorded; and over an outcome so far that passed without excluding the requester, the separation-of-duties re-approval tally (ApprovalSoDRetallyStep) is the outcome when it is recorded, excluded the requester, and supersedes exactly that outcome (its Supersedes names it). Those are the only cases a gate writes them, so a re-approval tally beside any other tally is not the gate's outcome and is ignored. ok is false when there is no tally.

func (Record) Approver added in v0.4.0

func (r Record) Approver() string

Approver returns the approver of a signed decision (ApproverSignature.Approver), or "" when the record carries none.

func (Record) ApproverAlg added in v0.9.0

func (r Record) ApproverAlg() Alg

ApproverAlg returns the scheme a signed decision is under (ApproverSignature.ApproverAlg), or "" when the record carries none.

func (Record) ApproverCert added in v0.19.0

func (r Record) ApproverCert() [][]byte

ApproverCert returns the certificate chain a certificate-bound decision carries (ApproverSignature.ApproverCert), leaf first, or nil when the record carries none. The slices are the record's own, as the field was.

func (Record) ClaimID added in v0.9.0

func (r Record) ClaimID() string

ClaimID returns the random id of the driver that wrote this attempt marker or not-started record (see ClaimAttempt), or "" for any other record. The journal sets it when it records the marker, and it is read back verbatim.

func (Record) DecidedAt added in v0.16.0

func (r Record) DecidedAt() string

DecidedAt returns the signed decision time of a signed decision (ApproverSignature.DecidedAt), or "" when the record carries none.

func (Record) Finish added in v0.9.0

func (r Record) Finish() FinishReason

Finish returns why the model turn ended (ModelTurn.Finish), or "" when the record carries no model-turn metadata.

func (Record) MarshalJSON added in v0.9.0

func (r Record) MarshalJSON() ([]byte, error)

MarshalJSON returns the record's JSON encoding, claim and salt included, without HTML escaping (see EncodeRecord for the canonical form a store persists).

func (Record) Model added in v0.9.0

func (r Record) Model() *ModelInfo

Model returns the model that answered the turn (ModelTurn.Model), or nil when the record carries no model-turn metadata.

func (Record) PromptDigest added in v0.9.0

func (r Record) PromptDigest() string

PromptDigest returns the digest of the system prompt the turn was sent (ModelTurn.PromptDigest), or "" when the record carries no model-turn metadata.

func (Record) Raw added in v0.9.0

func (r Record) Raw() []byte

Raw returns the bytes the store holds for the record, verbatim, for a record read back from a journal (Journal.Get, Journal.History, or Journal.Records). It is nil for a record built in memory. The returned slice is a copy.

func (Record) RawFinish added in v0.9.0

func (r Record) RawFinish() string

RawFinish returns the provider's own finish reason (ModelTurn.RawFinish), or "" when the record carries no model-turn metadata.

func (Record) RecordedAt added in v0.16.0

func (r Record) RecordedAt() string

RecordedAt returns when a signed decision was journaled (ApproverSignature.RecordedAt), or "" when the record carries none.

func (Record) Salt added in v0.7.0

func (r Record) Salt() []byte

Salt returns the record's salt: SaltSize random bytes the journal sets when it first records the record (see JournalEntry), replacing any salt the step returned (the engine draws the salt of its own model records itself, just as randomly, to tell its record from another writer's). It is persisted with the record and read back verbatim, so it is stable across replay and across stores. It has no meaning to the run; the audit trail needs it. An audit leaf commits to the record's journal encoding, salt included, and an inclusion proof for one record carries its neighbours' leaf hashes, so without the salt anyone holding a proof could confirm a guessed neighbour (an approval, a small tool result) by hashing it. The salt is disclosed only with its own record. The returned slice is a copy, nil for a record built in memory.

func (Record) Signature added in v0.4.0

func (r Record) Signature() []byte

Signature returns the signature of a signed decision (ApproverSignature.Signature), or nil when the record carries none. The slice is the record's own, as the field was.

func (Record) ToolsDigest added in v0.9.0

func (r Record) ToolsDigest() string

ToolsDigest returns the digest of the tool set the turn was sent (ModelTurn.ToolsDigest), or "" when the record carries no model-turn metadata.

func (*Record) UnmarshalJSON added in v0.9.0

func (r *Record) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes a record from its JSON encoding, claim and salt included.

type RecoverLoopOption added in v0.10.0

type RecoverLoopOption interface {
	// contains filtered or unexported methods
}

RecoverLoopOption configures RecoverLoop. Lease options apply only when the store implements Leaser.

func WithRecoverConcurrency added in v0.8.0

func WithRecoverConcurrency(n int) RecoverLoopOption

WithRecoverConcurrency caps how many runs RecoverLoop's full pass drives at once. Defaults to 16. It must be at least 1. The lapsed loop has slots of its own (see WithRecoverLapsedConcurrency).

func WithRecoverErrors added in v0.8.0

func WithRecoverErrors(fn func(error)) RecoverLoopOption

WithRecoverErrors sets the function RecoverLoop hands each genuine failure to: a store error while enumerating runs or acquiring a lease, or a drive that failed. Pauses and lost leases are not failures and are not reported. Without it, RecoverLoop drops failures (the next pass retries the run). It may be called from several goroutines, one call at a time.

func WithRecoverInterval added in v0.8.0

func WithRecoverInterval(d time.Duration) RecoverLoopOption

WithRecoverInterval sets how often RecoverLoop starts a recovery pass. Defaults to half the lease TTL, so a dead holder's run is taken over within about 1.5 TTLs of its last renewal. It must be positive.

func WithRecoverLapsedConcurrency added in v0.10.0

func WithRecoverLapsedConcurrency(n int) RecoverLoopOption

WithRecoverLapsedConcurrency caps how many runs RecoverLoop's lapsed loop drives at once: the runs whose lease lapsed, taken over from a holder that died or stalled (see RecoverLoop). Defaults to 16. It must be at least 1. It applies only over a store that implements Leaser. These slots are separate from WithRecoverConcurrency's, so a worker drives up to the sum of the two at once.

type RecoverOption

type RecoverOption interface {
	// contains filtered or unexported methods
}

RecoverOption configures Recover. Lease options apply only when the store implements Leaser.

type Request

type Request struct {
	Messages       []Message
	Tools          []ToolSpec
	Sampling       Sampling        // generation controls; zero value = provider/model defaults
	ResponseFormat *ResponseFormat // nil = free-form; set = constrain output to a JSON schema
	ToolChoice     *ToolChoice     // nil = provider default (auto); see ToolChoice
}

Request is a single model call. Tools are the specs of the tools the model may call, sorted by name; an adapter sends each one's Name, Description and Input schema. The agent sends each request its own copy of the slice, but the specs' schema bytes are shared: treat them as read-only.

type ResolveOption added in v0.3.0

type ResolveOption interface {
	// contains filtered or unexported methods
}

ResolveOption configures ResolveHalt.

func WithEvidence added in v0.3.0

func WithEvidence(v any) ResolveOption

WithEvidence records the resolution as reconciled from verified evidence rather than an operator's bare assertion: it marks the injected result Reconciled and stores v (JSON-marshalled) as its Evidence, signed alongside the outcome. Use it so a later reader can tell a reconciled step from a clean one and re-check the basis of the verdict. Outcome.Evidence does the same; giving both is ErrConfig.

func WithMinHaltAge added in v0.3.0

func WithMinHaltAge(d time.Duration) ResolveOption

WithMinHaltAge refuses to resolve a halt younger than d, measured from the live attempt marker's AttemptedAt to now (an attempt recorded as never started does not count). A reconciler passes this so it cannot resolve before the provider's record has had time to settle (a sent-message id can appear seconds after the send): resolving too early reads "absent" and re-fires the very side effect the halt exists to prevent. A HaltContended halt requires it, and so does any halt on a store that cannot lease runs (no Leaser), unless WithoutLiveDriverCheck is given. d <= 0 skips the check. When d > 0 but no attempt timestamp is found, resolution errors rather than resolve blind; a marker whose AttemptedAt is zero or negative carries none (a negative value is not one the engine writes). Returns *HaltTooYoung when the halt has not aged enough, including a marker stamped in the future, so the caller waits and retries later.

Where d stands in for the live-driver check (a HaltContended halt, or a store with no Leaser), it must cover the tool's real worst-case run time, not its ToolSpec.Timeout: a result the tool returns after its deadline is still recorded, and a tool that ignores cancellation can run past the timeout, so a halt resolved at the timeout can race a call still running. On a store that leases runs, the lease check that ResolveHalt makes first is not affected by this.

func WithoutLiveDriverCheck added in v0.9.0

func WithoutLiveDriverCheck() ResolveOption

WithoutLiveDriverCheck skips ResolveHalt's check that no driver may still be running the halted operation: the lease probe on a store that leases runs, and the WithMinHaltAge requirement on one that does not. With it, a resolution can race a live driver's own result. Pass it only when you know no driver of the run is running, for example with every worker stopped.

type ResponseFormat

type ResponseFormat struct {
	Name   string
	Schema json.RawMessage
}

ResponseFormat asks the provider to constrain the model's output to a named JSON schema (OpenAI structured outputs / "json_schema" response format). An adapter that does not support it (Anthropic) fails the request with ErrConfig rather than drop the constraint. Name is the schema name; Schema is provider-neutral JSON Schema (as from the schema package). See OutputNative.

type Result

type Result struct {
	// Message is the final assistant answer. It is zero unless the run returned no error.
	Message Message

	// Output is a typed run's answer as its journal holds it (RunTyped): the arguments the
	// final_answer tool accepted, or the model's native structured output. It is nil for an
	// untyped run, and for a typed run that returned an error.
	Output json.RawMessage

	// Usage is the sum of the usage of the model responses the run recorded, one per turn
	// (input, output, and cache tokens). It is the whole run's, read from the journal: the same
	// whether the run finished in one invocation or was resumed after a crash, and the same again
	// when a finished run is re-entered.
	Usage Usage

	// Spend is every token the run's model requests used: Usage plus the usage of requests whose
	// responses were discarded (failed attempts a middleware retried, losing hedge targets) and of
	// model calls that failed. It is what the provider bills, and what WithTokenBudget counts.
	// Like Usage it is the whole run's, from the journal (see Record.DiscardedUsage). A request
	// still in flight when an invocation ends (a hedge loser, a request a middleware left
	// running) is waited for, for at most two seconds and not past the run's context, and counted;
	// one that runs longer than that is not in Spend.
	Spend Usage

	// Turns is the number of live model turns this invocation executed. Turns replayed from
	// the journal are not counted: unlike Usage and Spend, it describes this invocation.
	Turns int

	// Duration is the wall-clock elapsed time for the run (from entry to return).
	Duration time.Duration

	// RunID echoes the run identifier the run was driven under.
	RunID string
}

Result is what a run entry point returns (Run, Resume, RunStream.Result, RunTyped, Session.Send): the final answer and the run's telemetry. Each returns a non-nil Result whenever the run ID is valid, whatever the error: a pause, a halt, a failure, a saga's abort, or a cancellation.

type Resumer added in v0.10.0

type Resumer func(ctx context.Context, runID string, start RunStart) error

Resumer drives one run a recovery pass found unfinished: runID, started as start (its run:start, read under the run's lease). It returns an error wrapping ErrNotResumable for a run it does not drive, so ResumeAny can try the next, and Recover reports the run once per process rather than as a failure on every pass. A Resumer passes no per-run option: the run runs under the options its run:start journaled (see RunStart). ResumeAgent and ResumeTyped return the Resumers of an agent's runs; a deployment's own Resumer is any func with this signature (one that drives a plan flow's runs reads start.Flow and start.Input and calls the flow's Run).

func ResumeAgent added in v0.10.0

func ResumeAgent(a *Agent, opts ...RunOption) Resumer

ResumeAgent returns the Resumer that drives a's runs: those of kind agent that are not typed, sagas included (the run's journal says which). It drives each under the options its run:start journaled (Resume's semantics), with opts setting only the deployment's values: WithWaker, WithClock, WithMaxConcurrency and an identity's Actor. A journaled setting in opts (a limit, the system prompt, sampling, tool choice, a tool filter, saga, an output mode, a principal) is ErrConfig on every call, since a recovery drive must not change a run's options. Any other run is ErrNotResumable: a typed run (ResumeTyped), a flow's (plan), and a session turn's, which only its session drives (it is seeded with the session's transcript, and only the session records the turn), so Recover never hands it one. So is a run whose run:start records no kind and no typed start (one an earlier version journaled): the record does not say whether a plain run or a typed one started it, so ResumeAgent does not guess; a deployment that knows drives it with a Resumer of its own (after ResumeAgent in ResumeAny, say).

func ResumeAny added in v0.10.0

func ResumeAny(rs ...Resumer) Resumer

ResumeAny returns the Resumer that hands a run to each of rs in turn, in order, until one returns anything that does not wrap ErrNotResumable (nil included), and returns that. If every Resumer returns ErrNotResumable (or rs is empty), it returns an error wrapping ErrNotResumable. A nil Resumer it reaches is ErrConfig.

func ResumeTyped added in v0.10.0

func ResumeTyped[T any](a *Agent, opts ...RunOption) Resumer

ResumeTyped returns the Resumer that drives a's typed runs whose answer type is T: the runs of kind agent whose run:start journals T's schema digest, in their journaled output mode. opts are as for ResumeAgent. Any other run is ErrNotResumable.

type RetrievalOption added in v0.8.0

type RetrievalOption interface {
	// contains filtered or unexported methods
}

RetrievalOption configures one WithRetrieval: WithRetrievalRetry.

func WithRetrievalRetry added in v0.10.0

func WithRetrievalRetry(n int, base, max time.Duration) RetrievalOption

WithRetrievalRetry retries a failed Retrieve up to n more times within the drive's retrieval step, sleeping between attempts with exponential backoff and full jitter: a random duration up to base, then up to twice that, and so on, capped at max. Use it for a store with transient failures (a timeout, a 503).

WithRetrieval retrieves as an engine step before the model middleware chain runs, so a model middleware such as middleware.Retry does not retry a retrieval: this option is how a retrieval is retried. Only the final outcome is recorded: the documents of the attempt that succeeded, or, once every attempt failed, nothing, and the drive fails with the last error. A cancelled context ends the retries, and an error wrapping ErrConfig is not retried (the same request fails the same way). n, base or max below 0, or max below base, is ErrConfig.

type Retriever

type Retriever interface {
	Retrieve(ctx context.Context, query string, k int) ([]Doc, error)
}

Retriever is the bring-your-own-RAG port: given a query, return the top-k relevant documents from YOUR store (pgvector, Pinecone, a file index — anything). Bide ships no vector store and no embedder; you implement Retrieve against infrastructure you already run, and wire it in with RetrievalTool (agentic — the model searches on demand) or WithRetrieval (classic — top-k auto-injected as context on each user turn). Sessions already give conversational memory; this is the seam for semantic / long-term memory.

Retrieve must be safe for concurrent use: parallel tool calls in one turn, concurrent runs on one Agent, and sub-agents sharing a Retriever call it at once.

type RetrieverFunc added in v0.10.0

type RetrieverFunc func(ctx context.Context, query string, k int) ([]Doc, error)

RetrieverFunc adapts a function to a Retriever. It is also how a policy wraps a Retriever: a WithRetrieval step runs before the model middleware chain, so a check that must keep a query from the store (a tenant's data policy, a spend cap) goes in a RetrieverFunc around it, which returns (nil, nil) to retrieve nothing or an error to fail the drive.

Example

ExampleRetrieverFunc gates a WithRetrieval with a policy. The retrieval runs before the model middleware chain, so a policy that must keep a query from the store wraps the Retriever: here a tenant whose data may not be searched retrieves nothing, and the run goes on without context.

var store Retriever = RetrieverFunc(func(context.Context, string, int) ([]Doc, error) {
	return []Doc{{Text: "the private record"}}, nil
})
mayRetrieve := func(ctx context.Context) bool { return false } // your policy: the tenant, a spend cap
gated := RetrieverFunc(func(ctx context.Context, query string, k int) ([]Doc, error) {
	if !mayRetrieve(ctx) {
		return nil, nil // or an error, to fail the drive
	}
	return store.Retrieve(ctx, query, k)
})

a, err := New(NewScriptedModel(TextTurn("answered without the record")), memJournal(),
	WithRetrieval(gated, 3))
if err != nil {
	fmt.Println(err)
	return
}
res, err := a.Run(context.Background(), "gated-1", UserText("what does the record say?"))
var msg Message
if res != nil {
	msg = res.Message
}
fmt.Println(msg.Text(), err)
Output:
answered without the record <nil>

func (RetrieverFunc) Retrieve added in v0.10.0

func (f RetrieverFunc) Retrieve(ctx context.Context, query string, k int) ([]Doc, error)

Retrieve calls f.

type Role

type Role string

Role identifies the author of a Message.

const (
	RoleSystem    Role = "system"
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleTool      Role = "tool"
)

The roles of a Message: the system prompt, the user, the model, and a tool result.

type RunEvent added in v0.11.0

type RunEvent interface {
	// contains filtered or unexported methods
}

RunEvent is a lifecycle event emitted by Agent.Stream as the run loop advances: turn boundaries, live model token deltas, and tool start/finish. It is the SEMANTIC-layer companion to the model's byte-level Event — a UI ranges over these to render progress while the durable loop runs underneath.

The terminal answer and any error are NOT events: they come from RunStream.Final (mirroring how the model Stream yields Events but returns the assembled Message separately). A run that pauses for approval or halts on an unsafe resume emits the relevant lifecycle event and then surfaces the *ApprovalPending / *OutcomeUnknown via Final, exactly as Run returns it.

func ProjectEvents added in v0.7.0

func ProjectEvents(recs []Record) (events []RunEvent, sources []int)

ProjectEvents returns the events ReplayEvents returns for a journal holding recs, and for each event the index in recs of the record it projects: each event comes from exactly one record. The audit package uses the index to salt a projected event from its record's salt.

func ReplayEvents

func ReplayEvents(ctx context.Context, store *Journal, runID string) ([]RunEvent, error)

ReplayEvents returns the semantic lifecycle events implied by runID's DURABLE journal: the same AssistantTurn and ToolCompleted events Agent.Stream re-emits when it resumes from that journal, in persisted order. Because the sequence is a pure function of the recorded steps, it — and any commitment built over it (see audit.EventLogFromJournal) — is identical before and after a crash, which is what makes it a resume-stable audit artifact.

It reconstructs from the journal alone, without re-running the model or tools. Only journaled facts are reproduced: assembled assistant turns (StepModel) and completed tool calls with their results (StepToolResult). Live-loop-only signals — token-level ModelEvent deltas, TurnStarted, TurnRestarted, ToolStarted, and the terminal Finished — are not journaled and so are not part of the durable projection; the durable content is the turns and tool results.

type RunFilter added in v0.9.0

type RunFilter struct {
	// After is a cursor: only run IDs greater than it (in byte order) are yielded.
	After string
	// Prefix admits only run IDs that start with it: a tenant or namespace, by the convention
	// that a deployment prefixes its run IDs.
	Prefix string
	// ExcludeHolding drops every run that holds an entry with any of these names. Recover passes
	// the terminal markers (run:complete, run:aborted, run:cancelled).
	ExcludeHolding []string
	// LeaseLapsed admits only runs whose lease has lapsed: the store holds a lease on the run (see
	// Leaser) and its expiry has passed, by the same comparison AcquireLease makes, so a run it
	// admits is one AcquireLease would grant to another holder at that moment. A run with no lease
	// (never leased, or released) or a live one is not admitted. A Leaser deletes a lease on
	// release, so a lapsed lease is one its holder neither renewed nor released: the holder died or
	// stalled. RecoverLoop's lapsed loop sets it. A Lister whose store does not implement Leaser
	// admits no run under it. A custom store that implements both Lister and Leaser must implement
	// it (storetest's Lister_LeaseLapsed checks it): one that ignored it would hand RecoverLoop's
	// lapsed loop every unfinished run on every pass, doubling the cost of recovery, and halted runs
	// would again delay the takeover of a dead holder's run.
	LeaseLapsed bool
}

RunFilter selects the runs a Lister yields. A SQL store evaluates it in its query, so a recovery pass over many finished runs reads none of them.

func (RunFilter) Admits added in v0.9.0

func (f RunFilter) Admits(runID string, holds func(name string) bool, lapsed func() bool) bool

Admits reports whether f admits runID, given holds, which reports whether the run holds an entry with a given name, and lapsed, which reports whether the run's lease has lapsed (see LeaseLapsed; it is called only when f.LeaseLapsed is set). It is the filter's meaning, for a store that evaluates it in Go.

type RunInfo added in v0.10.0

type RunInfo struct {
	// RunID is the run's ID: the run whose tool call this is.
	RunID string
	// RootRunID is the top-level run of the agent tree the run belongs to: RunID itself for a run
	// started directly, and its root's for a sub-agent's run.
	RootRunID string
	// ToolUseID is the ID of the tool call being executed, as the model issued it; "" outside a
	// tool call (in a WithSystemPromptFunc function, for one).
	ToolUseID string
	// Saga reports whether the run is a saga, whose failure rolls back its completed calls. A
	// sub-agent called from one runs as a saga too, so the whole tree rolls back together.
	Saga bool
}

RunInfo describes the run some code runs in: a tool call's (RunInfoFrom), or the run a WithSystemPromptFunc function is computing the prompt for.

func RunInfoFrom added in v0.10.0

func RunInfoFrom(ctx context.Context) (RunInfo, bool)

RunInfoFrom returns the RunInfo of the tool call whose context ctx is (or derives from), and false outside one. The agent loop sets it for every call, and for a saga's rollback re-runs.

func (RunInfo) SubRunFor added in v0.10.0

func (r RunInfo) SubRunFor(name string) string

SubRunFor returns the run ID of the programmatic sub-run name, for code that starts a run of another agent itself (where SubAgent would start it for a tool call): "<scope>>step:<name>", the name encoded as SubRunID encodes a tool-use ID, where scope is the tool call's sub-run ID (SubRunID(RunID, ToolUseID)) inside a tool call, and RunID outside one. The ID is stable, so a resumed call that starts the sub-run again resumes it, and distinct, so two calls (or two names in one call) never share a journal. The sub-run is part of the run's tree like a sub-agent's: Recover leaves it to its root (IsSubRun), a saga's rollback walks it after the call that started it (declare the agent it runs with, WithSubRuns, so the rollback can compensate its writes), and a run given this ID from the tool call's context is accepted although it contains '>'. Run refuses the ID (ErrConfig) for an empty name, once the tool call has returned (start the sub-run within the call, not from a goroutine that outlives it), and, in a saga, for a name longer than 96 bytes once escaped or for an agent that journals to another store than the run (a saga's tree shares one store). Start it with WithSaga to keep it a saga: whether a call is in one is its own run's flag; a plain run started from a saga's call still links its own programmatic sub-runs, since the saga's rollback walks it.

type RunKind added in v0.9.0

type RunKind string

RunKind is what drives a run, as its run:start records it (see RunStart).

const (
	// RunKindAgent is a run an Agent drives (Run, Stream, a sub-agent, a session turn).
	// A run:start journaled before v0.10.0 may record no kind (an agent run's did not; a flow's
	// always recorded its kind): any agent entry point may drive it (a plain run, a session turn,
	// a typed run), since the record does not say which started it. Every run started now records
	// its kind.
	RunKindAgent RunKind = "agent"
	// RunKindFlow is a run a plan flow drives (plan.Flow.Run). RunStart.Flow names the flow.
	RunKindFlow RunKind = "flow"
	// RunKindSessionTurn is a Session turn's run (Session.Send, Session.SendOnce).
	// RunStart.Session names the session. Only the session drives it: it is seeded with the
	// session's transcript, and only the session records the turn (see Recover).
	RunKindSessionTurn RunKind = "session_turn"
)

type RunNotifier added in v0.16.0

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

RunNotifier tells a notifier when a run driven outside an Agent ends needing a person. A plan flow uses one (see plan.Builder.WithNotifier); code that drives its own runs can too. It is the WithNotifier option bound to a journal, and safe for concurrent use.

func NewRunNotifier added in v0.16.0

func NewRunNotifier(j *Journal, o NotifierOption) (*RunNotifier, error)

NewRunNotifier binds o, a WithNotifier option, to j, the journal the runs it will be told about are in. A nil o, or a nil j, is ErrConfig.

func (*RunNotifier) DriveEnd added in v0.16.0

func (r *RunNotifier) DriveEnd(runID string, err error)

DriveEnd tells the notifier that a drive of runID ended with err: it queues the event err raises (a halt, a pending approval, an aborted saga), if any, and returns at once. A drive that ended with another error or none raises nothing.

func (*RunNotifier) Flush added in v0.16.0

func (r *RunNotifier) Flush(ctx context.Context) error

Flush waits for the deliveries in flight to finish their round and stops the worker, as Agent.FlushNotifications does.

type RunOption added in v0.10.0

type RunOption interface {
	// contains filtered or unexported methods
}

RunOption configures one run (Run, Resume, Stream, RunTyped, Session.Send). It is the scope of the settings a caller may choose per run, which take precedence over the agent's (see New). The settings a run's first drive is given are journaled in its run:start and hold for every later drive (see RunStart); the clock, the Waker, the concurrency cap and the identity's Actor are the deployment's, and are not journaled.

func WithOutputMode added in v0.10.0

func WithOutputMode(m OutputMode) RunOption

WithOutputMode sets how a typed run (RunTyped) collects its answer: OutputTool (the default) or OutputNative (see OutputMode). It is journaled with the run's typed start, and a later drive that passes another mode is ErrConfig. Any other mode, and the option on an untyped run, is ErrConfig.

func WithSaga added in v0.10.0

func WithSaga() RunOption

WithSaga runs the run as a saga: a step that fails after earlier writes succeeded rolls the run back, compensating the completed writes in reverse order (recursing into sub-agent trees), and the run returns *SagaAborted. It is journaled (run:start's saga flag): a later drive of the run is a saga whether or not it passes WithSaga, and a drive that passes it for a run that is not a saga is ErrConfig. Cancel on a saga rolls it back (see Cancel).

func WithToolFilter added in v0.10.0

func WithToolFilter(names ...string) RunOption

WithToolFilter restricts a run to the named tools of its agent: the model is offered only those, and a call naming any other tool (a model naming a tool it was not offered, or a turn replayed from the journal) is refused at dispatch, with an error result recorded that the model reads, and the tool never runs. The filter is journaled (run:start's tools) and enforced against the journaled filter by every drive, a recovery drive included; a later drive that passes another filter is ErrConfig. RunTyped's answer tool is always offered. No name, an empty name, or (at the run's first drive) a name the agent has no tool for is ErrConfig.

type RunPrincipal added in v0.17.0

type RunPrincipal struct {
	Principal Principal   `json:"principal"`
	Ancestors []Principal `json:"ancestors,omitempty"`
}

RunPrincipal is what the run:principal record holds: the principal a run acts for, and the principals of the runs above it, root first. It holds nothing else (not the run's input), so evidence can prove who requested a run on its own: audit.ApprovalEvidence discloses it for an approval gate that excludes the requester (ApprovalPolicy.ExcludePrincipal), in place of the run's run:start.

The run's first drive writes it, before run:start and only if it is absent, when the run has a principal (its Identity's OnBehalfOf or AuthorityRef) or was started inside another run's drive whose chain names one. Principal is the same as RunStart.Principal (empty when the run has none), and Ancestors the same as RunStart.Ancestors: a drive that finds a run:principal another drive wrote with other values is refused with ErrConfig. A run an earlier version started has none until a drive under this version first loads it.

func RecordedPrincipal added in v0.17.0

func RecordedPrincipal(recs []Record) (p RunPrincipal, ok bool)

RecordedPrincipal returns the run:principal record among recs (see RunPrincipal), read strictly: no duplicate or case-variant name at any depth (names a later version adds are allowed), so the record names one principal to every reader, the gate, an auditor and a verifier in another language alike. ok is false when recs hold none, when it does not read strictly, and when recs hold two that differ.

func (RunPrincipal) Matches added in v0.17.0

func (p RunPrincipal) Matches(start RunStart) bool

Matches reports whether p names the principal and the ancestors start does (RunStart.Principal, read as an empty principal when nil, and RunStart.Ancestors): the agreement a drive and the gate hold run:principal and run:start to.

func (RunPrincipal) Requesters added in v0.17.0

func (p RunPrincipal) Requesters() []string

Requesters is every requester p names: the OnBehalfOf of each ancestor, root first, then the run's own. An entry is empty for a run that records no OnBehalfOf. An approval policy with ExcludePrincipal counts none of them.

type RunRef added in v0.9.0

type RunRef struct {
	RunID     string
	RootRunID string
}

RunRef names a paused run. RunID is the journal the pause lives in: record the answer against it (Approve, AnswerInterrupt, Signal, ResolveHalt). RootRunID is the top-level run to re-invoke to continue; it differs from RunID when the pause comes from inside a sub-agent, whose journal is RunID.

func (RunRef) Paused added in v0.9.0

func (r RunRef) Paused() RunRef

Paused returns r. Every pause type embeds a RunRef and so reports it through Pause.

type RunSettings added in v0.10.0

type RunSettings struct {
	MaxTurns     *int        `json:"max_turns,omitempty"`
	TokenBudget  *int        `json:"token_budget,omitempty"`
	SystemPrompt *string     `json:"system_prompt,omitempty"`
	Sampling     *Sampling   `json:"sampling,omitempty"`
	ToolChoice   *ToolChoice `json:"tool_choice,omitempty"`
}

RunSettings are the per-run values a run's first caller chose (RunOptions), as run:start journals them. A nil field is a setting the caller left alone: the agent's live value applies.

type RunStart added in v0.8.0

type RunStart struct {
	Input     Message                    `json:"input"`
	Saga      bool                       `json:"saga,omitempty"`
	Kind      RunKind                    `json:"kind,omitempty"`
	Session   *SessionRef                `json:"session,omitempty"`
	Flow      *FlowRef                   `json:"flow,omitempty"`
	Typed     *TypedStart                `json:"typed,omitempty"`
	Settings  RunSettings                `json:"settings,omitzero"`
	Principal *Principal                 `json:"principal,omitempty"`
	Ancestors []Principal                `json:"ancestors,omitempty"`
	Tools     []string                   `json:"tools,omitempty"`
	Ext       map[string]json.RawMessage `json:"ext,omitempty"`
	StartedAt string                     `json:"started_at,omitempty"`
}

RunStart is how a run was started, as its first drive records it in run:start: the input it answers (for a Session turn, the turn's message), whether it runs as a saga, what drives it, and the per-run options its first caller chose. A run's model turns and tool calls answer that input under those options, so every later drive is held to them (the journaling rule):

  • A drive that passes no option for a setting runs under the journaled value, or, where none was journaled, the agent's live value. A recovery drive passes none.
  • A later drive that passes another turn limit or token budget (WithMaxTurns, WithTokenBudget) amends it: the amendment is journaled as run:limits:<n> and binds the drives that load the run after it.
  • A later drive that passes any other setting that differs from the journaled one (the input, saga, tool filter, system prompt, sampling, tool choice, output mode, typed schema or principal) is ErrConfig, before any model call.

A finished run returns its recorded end: a completed run returns its answer only to a drive with the input it answered (another input is ErrConfig, since the answer is not that input's), and a cancelled or aborted run returns its end whatever it is passed. A run whose earlier drives predate this record gets it on its first drive under this version, with what that drive is given.

Kind says what drives the run. A plan flow's run records RunKindFlow, its flow's name in Flow, and its input as JSON text in Input's text; resuming it with another input (compared as canonical JSON, so the input decoded and encoded again resumes it), under another flow's name, or driving it as an agent run (or an agent run as a flow) is ErrConfig. The flow's topology is held by its own record, flow:digest, not here.

Input is journaled as a JSON string when it is a user message of one text part (as earlier versions wrote it), and as a message otherwise (an image input, say).

StartedAt is when the run's first drive wrote the record, by the drive's clock (WithClock, or the wall clock): UTC, RFC 3339 with milliseconds ("2026-10-13T01:11:47.123Z"). It is informational, for people and tools that list runs (bide view): no drive, resume, recovery pass or at-most-once check reads it, and a drive is never held to it. A run:start written by an earlier version has none, and a value that is not RFC 3339 is kept as it is; Started reads it.

func RecordedStart added in v0.8.0

func RecordedStart(ctx context.Context, d *Journal, runID string) (RunStart, bool, error)

RecordedStart returns how runID was started (see RunStart), and ok=false for a run whose journal holds no such record: one never driven, or one not driven since before the record existed. A session's turn run (IsSessionRun) records its message, but only the session can drive it (it seeds the turn with the transcript before that message), so Recover never hands one to its callback. A recovery callback uses it to re-drive a run with its own input and entry point:

start, ok, err := agent.RecordedStart(ctx, store, runID)
if err != nil {
    return err
}
if !ok {
    start = startFor(runID) // the deployment's own record, for a run not driven under this version
}
if start.Kind == agent.RunKindFlow {
    return driveFlow(ctx, start.Flow.Name, runID, start.Input) // the flow's Run, with the input decoded
}
_, err = a.Run(ctx, runID, start.Input) // a saga's later drives run as the saga

func (RunStart) MarshalJSON added in v0.10.0

func (s RunStart) MarshalJSON() ([]byte, error)

MarshalJSON writes s's journal form: the input as a JSON string when it is a user message of one text part, and as a message otherwise.

func (RunStart) Started added in v0.15.0

func (s RunStart) Started() (time.Time, bool)

Started is StartedAt as a time, and false when the record holds none or one that is not RFC 3339 (see RunStart).

func (*RunStart) UnmarshalJSON added in v0.10.0

func (s *RunStart) UnmarshalJSON(b []byte) error

UnmarshalJSON reads s's journal form (see MarshalJSON).

type RunState added in v0.10.0

type RunState string

RunState is a run's state as Status reads it from the run's journal.

const (
	// RunNotStarted is a run with no run:start record.
	RunNotStarted RunState = "not_started"
	// RunStarted is a run with a run:start record and no end marker: running, paused, halted, or
	// stopped at a limit. Pauses are not journaled, so a paused run is RunStarted.
	RunStarted RunState = "started"
	// RunCompleted is a run whose first end marker is run:complete.
	RunCompleted RunState = "completed"
	// RunAborted is a saga whose first end marker is run:aborted: a step failed and its rollback
	// finished.
	RunAborted RunState = "aborted"
	// RunCancelled is a run whose first end marker is run:cancelled.
	RunCancelled RunState = "cancelled"
)

type RunStatus added in v0.10.0

type RunStatus struct {
	State RunState
	// Terminal is the text the run's first end marker records: the reason given to Cancel for a
	// cancelled run, the failure that aborted a saga; empty otherwise.
	Terminal string
	// Records is the number of records the run's journal held when Status read it, the journal
	// header included.
	Records int
	// EndedAt is when the run's first end marker was written (its Record.RecordedAt; for a
	// run:aborted that has none, the aborted_at it records), in FormatTimestamp's form, by the
	// clock of the process that wrote it. It is "" for a run with no end marker, and for one whose
	// end marker records no time (written before they did): never a time Status supplies. It is
	// informational: nothing that drives, resumes or recovers a run reads it.
	EndedAt string
}

RunStatus is a run's state, as Status reads it.

func Status added in v0.10.0

func Status(ctx context.Context, j *Journal, runID string) (RunStatus, error)

Status reads runID's state from its journal (D8), with one Load: a prefix of the journal, so the state is the run's at one instant. A run with no run:start record is RunNotStarted. The first end marker in journal order is the run's end (RunCompleted, RunAborted, RunCancelled), with the text it records in Terminal. A run with none is RunStarted, whether it is running, paused, halted, or stopped at a limit: pauses are not journaled. A saga whose cancellation's rollback has not finished (it holds run:cancel-requested and no end marker) is RunStarted.

For a session turn's run, RunCompleted says the run finished, not that the session recorded the turn: a turn whose run completed is answered once its session records it (Session.History, Session.Turns), which the next send of its message does.

type RunStream added in v0.11.0

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

RunStream is a live view of a running agent: range Events for progress, then call Result for the run's Result and error, as Run returns them. Stream and Run share one loop.

A stream is consumed once. Either range Events fully (Result then returns immediately) or call Result directly (it drains and discards events); calling Result after breaking out of Events early drains whatever remains. Cancel ctx to abandon a run without draining: pending emits then unblock on ctx.Done rather than leaking.

func (*RunStream) Events added in v0.11.0

func (as *RunStream) Events() iter.Seq[RunEvent]

Events returns a range-over-func iterator over lifecycle events until the run ends. Per-event errors do not occur here (a mid-stream model error aborts the run and is reported by Final); the iterator simply ends when the run stops producing events.

func (*RunStream) Result added in v0.11.0

func (as *RunStream) Result() (*Result, error)

Result drains any un-consumed events and returns the run's Result and error, as Run returns them: the Result is non-nil whenever the run ID is valid, whatever the error (including a *ApprovalPending or an *OutcomeUnknown).

type Safety

type Safety struct {
	// ReadOnly: no external side effects, so always safe to re-run.
	ReadOnly bool `json:"read_only,omitempty"`
	// Idempotent: mutates state but is safe to retry, because a repeat is a no-op downstream.
	// The tool keeps that promise itself. NextOnceKey dedupes the same call running again (a
	// resume, a middleware retry); it does not dedupe the model calling the tool again, which
	// is a new call with new keys. A tool whose repeats the model may make (after a recorded
	// error or timeout) derives a business key from its arguments for the downstream to dedupe.
	Idempotent bool `json:"idempotent,omitempty"`
}

Safety declares how a tool call may be retried: when a run resumes after a crash and the call's outcome is unknown (it was invoked, but no result was journaled), and when a tool middleware retries or caches it. It is plain data: comparable, and journaled with each call's result. The zero value is a side effect, which runs at most once per call and halts a resume whose outcome is unknown. For a trusted MCP server, its tool annotations (readOnlyHint / idempotentHint) map onto it (see mcp.TrustAnnotations). Whether a call waits for a human is not Safety but ToolSpec.Approval (see WithApproval).

Example

A tool's Safety decides what a resume does with a call that was attempted but has no recorded result (the process died, or lost its store, mid-call). A ReadOnly tool is run again; a side effect, the default, halts the run with *OutcomeUnknown rather than risk running it twice.

package main

import (
	"context"
	"errors"
	"fmt"
	"sync/atomic"

	"github.com/bide-ai/bide/agent"
	"github.com/bide-ai/bide/agent/agenttest"
)

func main() {
	ctx := context.Background()
	store := &flakyStore{Store: agent.NewMemStore()}
	j := agenttest.MustJournal(store)

	var reads, charges int
	read := agenttest.Must(agent.Func("balance", "Read the balance.",
		func(context.Context, struct{}) (int, error) {
			reads++
			store.down.Store(reads == 1) // the first call's result is lost
			return 100, nil
		},
		agent.WithSafety(agent.Safety{ReadOnly: true})))
	charge := agenttest.Must(agent.Func("charge", "Charge the card.",
		func(context.Context, struct{}) (string, error) {
			charges++
			store.down.Store(true) // the charge went through, but its result is lost
			return "charged", nil
		})) // no WithSafety: a side effect

	for _, tool := range []agent.Tool{read, charge} {
		name := tool.Spec().Name
		model := agenttest.NewScriptedModel(
			agenttest.ToolTurn("call-1", name, `{}`),
			agenttest.TextTurn("done"),
		)
		a := agenttest.MustNew(model, j, agent.WithTools(tool))
		runID := "run-" + name
		_, _ = a.Run(ctx, runID, agent.UserText("go")) // fails: the store is down
		store.down.Store(false)

		_, err := a.Run(ctx, runID, agent.UserText("go"))
		var halt *agent.OutcomeUnknown
		switch {
		case errors.As(err, &halt):
			fmt.Printf("%s: halted on %s\n", name, halt.Op.ID)
		case err != nil:
			fmt.Printf("%s: %v\n", name, err)
		default:
			fmt.Printf("%s: finished\n", name)
		}
	}
	fmt.Println("reads:", reads, "charges:", charges)
}

// flakyStore is a Store whose writes fail while down is set, as a lost database connection
// would.
type flakyStore struct {
	agent.Store
	down atomic.Bool
}

func (s *flakyStore) Insert(ctx context.Context, runID, name string, data []byte) (agent.Entry, bool, error) {
	if s.down.Load() {
		return agent.Entry{}, false, errors.New("store unavailable")
	}
	return s.Store.Insert(ctx, runID, name, data)
}

// Unwrap returns the wrapped store, so the journal finds its capabilities (its leases).
func (s *flakyStore) Unwrap() agent.Store { return s.Store }
Output:
balance: finished
charge: halted on call-1
reads: 2 charges: 1

func (Safety) RetrySafe added in v0.6.0

func (s Safety) RetrySafe() bool

RetrySafe reports whether a call to the tool may run more than once for one tool call: it is ReadOnly or Idempotent. It is the test the agent applies on resume, and the one tool middleware applies before retrying a call (see ToolCall).

type SafetyOption added in v0.10.0

type SafetyOption interface {
	ToolOption
	StepOption
}

SafetyOption is a Safety that applies to a tool (ToolOption) and to a step (StepOption): WithSafety.

func WithSafety added in v0.10.0

func WithSafety(s Safety) SafetyOption

WithSafety declares how safe a tool's calls, or a step, are to re-run. For a tool built by Func or CompensatedFunc it sets the tool's Safety (the default is a side effect); SubAgent refuses it, since a sub-agent call re-enters its sub-run, whose own calls carry their safety. For a Step, a step that is RetrySafe (ReadOnly or Idempotent) re-runs after a crash, and any other step halts (the default is a side effect).

type SagaAborted

type SagaAborted struct {
	RunID          string
	Cause          error
	Compensated    []string
	Uncompensated  []string
	UnknownOutcome []string
	CompensateErr  error
}

SagaAborted is returned by a saga run (WithSaga) when a step failed and the transaction was rolled back. Compensated lists tools whose side effects were undone (reverse of execution, including sub-agent trees). Uncompensated lists the writes the rollback did not undo: a completed write with no compensator, a call whose outcome is unknown (the rollback halted on it, with a *OutcomeUnknown in CompensateErr, whether or not it has a compensator), or a call whose tool is no longer registered. They are side effects that may need manual cleanup. CompensateErr is non-nil if the rollback stopped (a compensator failed, or an outcome is unknown), so writes before it remain uncompensated. A programmatic sub-run whose declared agent (WithSubRuns) could not be used is listed as "<tool> (sub-run \"<name>\": <reason>)", next to its writes.

UnknownOutcome lists the saga steps that failed with an unknown outcome, the failure that aborted the saga included: a retry-safe step that returned ErrToolOutcomeUnknown, or an error after its WithTimeout deadline, may have committed before it was cut off. The rollback does not run their compensators (it has no result to compensate, and the effect may not have happened), and it does not list them as Uncompensated writes; it reports them here, so they are never silent. Check each against its downstream system.

func (*SagaAborted) Error

func (e *SagaAborted) Error() string

Error describes the abort: the cause, the compensated steps, and any uncompensated writes, unknown outcomes and rollback error.

func (*SagaAborted) Unwrap

func (e *SagaAborted) Unwrap() error

Unwrap returns Cause, the failure that aborted the saga.

type Sampling

type Sampling struct {
	Temperature *float64 `json:"temperature,omitempty"` // 0..2 (OpenAI) / 0..1 (Anthropic); determinism at 0
	TopP        *float64 `json:"top_p,omitempty"`       // nucleus sampling
	MaxTokens   *int     `json:"max_tokens,omitempty"`  // cap on generated tokens; overrides the adapter's construction default
	Stop        []string `json:"stop,omitempty"`        // stop sequences
	Seed        *int64   `json:"seed,omitempty"`        // best-effort determinism (OpenAI; ignored where unsupported)
}

Sampling holds provider-neutral generation controls. Pointer fields distinguish "unset" (nil → use the provider/model default) from an explicit value — so a deliberate Temperature of 0 is not confused with "not specified". Each adapter maps the set fields onto its wire format and ignores those it doesn't support (e.g. Anthropic has no Seed). Set it once with Agent.WithSampling.

type SamplingOption

type SamplingOption func(*Sampling)

SamplingOption sets one field of the Sampling config; see Temperature, TopP, MaxTokens, Stop, Seed.

func MaxTokens

func MaxTokens(v int) SamplingOption

MaxTokens caps generated tokens, overriding the model adapter's construction default.

func Seed

func Seed(v int64) SamplingOption

Seed sets a best-effort determinism seed (honored by providers that support it).

func Stop

func Stop(seqs ...string) SamplingOption

Stop sets stop sequences.

func Temperature

func Temperature(v float64) SamplingOption

Temperature sets the sampling temperature (0 = most deterministic).

func TopP

func TopP(v float64) SamplingOption

TopP sets nucleus-sampling top-p.

type Session

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

Session is a durable multi-turn conversation. Each Send is one full agent run (tools, resume, side-effect safety and all), seeded with the transcript so far — so the agent remembers earlier turns. The transcript is journaled turn-by-turn in the session's journal, so a Session reloaded (after a restart) from the same store resumes the conversation.

Layering: a turn runs under its own run ID ("<id>>@turn/<n>" for Send, "<id>>@event/<key>" for SendOnce, the key encoded), whose durable journal handles crash resume WITHIN the turn; the session's journal, "<id>>@session", records which message started each Send turn and each completed turn's (input, answer) so the transcript can be rebuilt. A root run ID may not contain '>', so no run started with Run shares a journal with a session, and IsSessionRun tells a session's run IDs apart from every other. Recover skips them: a turn is seeded with the transcript before it, which only the session holds, and its answer is recorded only by the session, so an unfinished turn resumes when its message is sent again (the same Send, or the redelivered SendOnce). Intermediate tool calls stay in the turn's journal and are NOT carried into later turns — the conversational memory is the question/answer transcript, not every tool call. Each turn also journals the transcript it started from (as "from/<turn run>"), so a turn resumed after a crash is seeded with exactly the turns its earlier model calls saw, even if other messages were answered in between.

A turn belongs to the message that started it: while a Send turn is unfinished, Send with a different message is ErrConfig rather than resuming that turn. Several handles on one session (a stale handle, or two workers) never lose a turn, record one twice, or answer one message with another's reply; a handle that finds the journal moved on reloads it, and one that sees another message's turn open reads the journal again before it refuses.

One driver at a time: when the store implements Leaser (MemStore, store/sqlite and store/postgres do, found through a Journal and through wrappers, see Capability), a turn's run is driven under its lease, as Lease drives a run, so two workers given one message do not both drive its turn. The run loads its journal only once it holds the lease, and the lease is held until the run returns, so the drive loads every model call an earlier drive journaled and WithTokenBudget's bound holds across workers. A Send or SendOnce whose turn run another driver holds does not drive it: if the run has finished (the holder has not released the lease yet), its recorded answer is returned and the turn recorded, as a drive of it would; otherwise it returns at once with an error wrapping ErrTurnContended, having driven nothing and recorded no answer. That includes a second caller on the same handle while a saga turn's rollback is in progress (see Send). Send the same message again later, and it returns the recorded answer, or resumes the turn if the other driver stopped short of it. A drive whose lease is lost returns an error wrapping ErrLeaseLost (see Lease). Over a store with no Leaser, two drivers of one turn still never record it twice, but each counts only the spend it has seen (see KNOWN-LIMITATIONS).

A Session is safe for concurrent use: callers sharing one handle behave as callers on separate handles do, and turns of different messages run in parallel.

func (*Session) History

func (s *Session) History() []Message

History returns a copy of the conversation transcript so far (alternating user and final-assistant messages).

func (*Session) Send

func (s *Session) Send(ctx context.Context, input Message, opts ...RunOption) (*Result, error)

Send runs one conversation turn: the agent answers input (which may carry images) with the full prior transcript in context, under opts, the turn run's options (journaled in its run:start, as Run journals them), and the turn is journaled. It returns the turn's Result, whose Message is the assistant's answer: non-nil whatever the error once the turn has its run (an invalid option, or a refusal to start the turn, such as another message's open turn, has none).

If the turn pauses (a tool needs approval or Interrupt) or fails, Send returns that error (*ApprovalPending / *InterruptPending / ...) and does NOT advance the transcript; resolve it (Approve / AnswerInterrupt) and call Send again with the SAME input to resume that turn. Until then, Send with a different input is ErrConfig: the open turn belongs to its message.

A turn whose run was cancelled (see Cancel) is closed: Send of its message (the same text) returns ErrRunCancelled, and the next Send of another message records the turn closed (with no answer, and outside the transcript) and runs its own turn. Cancel of a saga turn's run writes only its rollback request: the next Send of another message drives that rollback and then closes the turn. Over a store with a Leaser the rollback is driven under the turn's lease and without holding the handle's mutex, so the call gets ErrTurnContended while another driver holds the turn, and while the rollback is in progress any other caller gets ErrTurnContended: another worker, or a second caller on this same handle. Over a store with no Leaser the handle's mutex is held across the rollback, and a second caller on the handle waits for it.

func (*Session) SendOnce added in v0.6.0

func (s *Session) SendOnce(ctx context.Context, key string, input Message, opts ...RunOption) (*Result, error)

SendOnce runs one conversation turn for an inbound message identified by key (an event or message id), at most once per key. A key whose turn already completed returns that turn's answer without running anything, so a redelivered message never opens a second turn, even if the process died after the turn was recorded and before the caller replied. A key whose turn was interrupted resumes that same turn: it runs under its own journal, "<session id>>@event/<key>" (the key encoded, so any key is allowed), so a different message arriving in between gets its own turn. It returns the turn's Result, as Send does. Reusing a key with a different input is ErrConfig, whether the key's turn has finished or is still open: the turn's run records the message it answers (see RunStart).

func (*Session) Turns

func (s *Session) Turns() int

Turns returns the number of completed turns.

type SessionRef added in v0.10.0

type SessionRef struct {
	ID   string `json:"id"`
	Turn *int   `json:"turn,omitempty"`
	Key  string `json:"key,omitempty"`
}

SessionRef names the session a session turn's run belongs to (RunKindSessionTurn): the session's ID, and the turn's index for a Send turn or its key for a SendOnce turn.

type SignalPending added in v0.9.0

type SignalPending struct {
	RunRef
	Name string
}

SignalPending is returned by Run when a tool called Await, AwaitFor or Receive and nothing has been delivered under Name yet (for Receive, Name is the channel). The run has paused durably at the await point. Deliver with Signal (or Enqueue for a channel), then re-invoke Run with RootRunID to continue. It is the externally-pushed dual of InterruptPending: Interrupt asks a human and resumes with their answer; Await waits for an event an outside system delivers.

func (*SignalPending) Error added in v0.9.0

func (e *SignalPending) Error() string

Error names the run and the signal or channel it awaits.

type StatusArchiver added in v0.19.0

type StatusArchiver interface {
	ArchiveStatus(ctx context.Context, hash string, der []byte) error
}

StatusArchiver is implemented by the verifier of a certificate-bound approver that has a status archive: ArchiveStatus stores the bytes of a CRL larger than MaxStatusInline under its hash, so what the gate relied on survives although the journal holds only its hash. A policy without one cannot count on a CRL that large.

type StepKind

type StepKind string

StepKind records what a journal entry represents.

const (
	// StepModel records an assistant message from the model.
	StepModel StepKind = "model"
	// StepToolResult records a completed tool call and its result.
	StepToolResult StepKind = "tool_result"
	// StepValue records the result of a user-authored durable step (see Journal.Step).
	StepValue StepKind = "value"
	// StepSignal records an external event delivered into a run (see Journal.Signal and Await).
	StepSignal StepKind = "signal"
	// StepApproval records a durable human decision to approve or deny a tool call.
	StepApproval StepKind = "approval"
	// StepAttempt is the marker a driver claims exclusively before a non-retriable side effect.
	// It says the effect may have fired: a resume halts on a marker with no result (see
	// attempt.go).
	StepAttempt StepKind = "attempt"
	// StepSagaFail records that a saga step failed, which durably triggers the saga's rollback.
	StepSagaFail StepKind = "saga_fail"
	// StepNotStarted records that the attempt whose marker it names never called its side
	// effect, written by the driver that claimed it (see attempt.go). That attempt then no
	// longer halts a resume, and the effect is re-attempted under a new marker.
	StepNotStarted StepKind = "not_started"
	// StepHeader is the journal header: the first record of every run's journal, named "@journal",
	// whose Format names the journal format the run is written in (see JournalFormat). The
	// journal writes it before a run's first record and checks it before reading any other.
	StepHeader StepKind = "header"
)

type StepOption added in v0.6.0

type StepOption interface {
	// contains filtered or unexported methods
}

StepOption configures Step.

type Store added in v0.9.0

type Store interface {
	// Insert stores data under (runID, name) if no entry has that name, and returns the stored
	// entry (the caller's, or the one already there) and whether this call stored it.
	Insert(ctx context.Context, runID, name string, data []byte) (Entry, bool, error)
	// Get returns runID's entry named name, if there is one.
	Get(ctx context.Context, runID, name string) (Entry, bool, error)
	// Load yields runID's entries whose Seq is greater than after (-1 for all of them), in
	// ascending Seq. It yields an error at most once, as its last element.
	Load(ctx context.Context, runID string, after int64) iter.Seq2[Entry, error]
}

Store is the storage port: an append-only set of named entries per run. It stores bytes and knows nothing of what they mean; the Journal above it encodes records, memoizes steps, writes the format header and runs attempt claims. A backend implements Store and passes the storetest suite (agent/storetest), which checks every requirement below.

Requirements (A1 to A8):

  • A1, unique names and a linearizable insert: a run holds at most one entry per name. Concurrent Inserts of one name, from any number of processes, have exactly one winner, and every caller and every later reader sees the winner's bytes. Which entry a (runID, name) names does not depend on the context a method is called with: a store that scopes keys by a tenant carried in the context breaks the journal (the Journal remembers runs by run ID), so a tenant belongs in the run ID (see RunFilter.Prefix).
  • A2, commit-ordered, prefix-closed visibility: Seq is an opaque int64, strictly increasing in commit order within a run. Every read of a run returns a prefix of the run's final order: no entry ever becomes visible with a lower Seq than an entry already visible. Gaps in Seq are allowed. Nothing reads Seq as a count; positions are computed when a run is read, in Load order.
  • A3, durable before return: Insert reports inserted only after the entry is committed. On an error the entry is either absent or complete, and the same bytes may be inserted again.
  • A4, read-your-writes and monotone visibility: once visible, an entry stays visible with the same bytes and position (the one exception is a redaction, which keeps the position).
  • A5, byte fidelity: Data comes back exactly as inserted, and the caller may keep or modify the slices it passes and receives. That includes every record's salt: the engine tells its own model record from another driver's by the salt it drew (see JournalEntry), so a store that rewrites or drops salt bytes makes it take its own record for another's.
  • A6, immutable: the port has no update or delete. Only a redaction may replace an entry's Data, with a tombstone (see Record.Redacted), and only in a run that is over.
  • A7, context: every method honors ctx. Recording a step's outcome after ctx is cancelled is the Journal's job, not the store's.
  • A8, iterator hygiene: breaking out of Load's iterator releases every resource it holds, and a store holds no connection, transaction or lock across a yield, so a caller may Insert into the run inside its Load loop without deadlocking.

A store that wraps another store may implement

Unwrap() Store

so Capability finds the capabilities of the store it wraps (Lister, Leaser), but only if it passes run IDs and names through unchanged. A wrapper that rewrites keys (a tenant prefix, say) must not implement Unwrap, and implements each capability itself; storetest.CheckWrapper checks this.

type Stream

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

Stream is a live model response. Range over Events() for a streaming UI, OR call Message() to drain it into the fully-assembled assistant Message — not both (a Stream is consumed once).

A consumer that stops early releases the producer: breaking out of Events() does it, and so does Close. Cancelling the context passed to Model.Stream also releases it.

func NewStream

func NewStream(ch <-chan Emit) *Stream

NewStream wraps an event channel the caller fills and closes. It suits producers that buffer their whole response up front. A producer that sends as it reads (a network adapter) should use NewStreamFunc, so it is released when the consumer stops reading; a goroutine blocked sending on ch is not.

func NewStreamFunc added in v0.6.0

func NewStreamFunc(ctx context.Context, produce func(send func(Emit) bool)) *Stream

NewStreamFunc runs produce in its own goroutine and returns the Stream it feeds. produce delivers each event through send, which blocks until the consumer takes it and returns false once the consumer has stopped reading (it broke out of Events or called Close) or ctx is cancelled; produce should then return promptly, releasing whatever it holds, such as a response body. The stream ends when produce returns.

If ctx cancels an event the consumer has not yet taken, the stream ends with ctx's error, so a consumer never mistakes a response cut short by cancellation for a complete one.

func (*Stream) Close added in v0.6.0

func (s *Stream) Close()

Close tells the producer the consumer will read no further events, so it can stop and release its resources. It is safe to call more than once, and after the stream ends.

func (*Stream) Events

func (s *Stream) Events() iter.Seq2[Event, error]

Events returns a range-over-func iterator (Go 1.23+) over streamed events. Breaking out of the loop closes the stream. A stream that ends without a Finish event yields ErrIncompleteResponse last: the response stopped partway through the turn. A Finish is the turn's last event: an event after it yields ErrStreamProtocol and ends the stream.

func (*Stream) Message

func (s *Stream) Message() (Message, Usage, error)

Message drains the stream and returns the assembled assistant Message + usage. When the stream fails, the usage is what it reported before failing, which the provider may still bill.

type Task

type Task[T any] struct {
	Name string
	Fn   func(context.Context) (T, error)
	// Safety declares whether the task may re-run after a crash, as WithSafety does for a Step.
	// The zero value treats the task as a side effect: it runs at most once, and a crash after
	// its effect halts the resumed call with *OutcomeUnknown. Mark a check or a lookup ReadOnly.
	Safety Safety
}

Task is one unit of durable parallel work: a named step returning a T. Name is the durable memoization key within the run, so it must be unique across the tasks in one Parallel call. Because each task is a journaled step, a resumed run returns a completed task's recorded result without re-running it, and that result is an independently provable record (see audit.ProveStep).

type Text

type Text struct {
	Text string `json:"text"`
}

Text is plain assistant/user text.

type TextDelta

type TextDelta struct {
	Text string `json:"text"`
}

TextDelta is a fragment of the model's text answer.

type TimerPending added in v0.9.0

type TimerPending struct {
	RunRef
	Name   string
	FireAt time.Time
}

TimerPending is returned by Run when a tool called Sleep or WaitUntil and the wake time has not yet passed. The run has paused durably at the timer: its wake time is journaled, so the pause survives a restart. Re-invoke Run with RootRunID at or after FireAt to resume (a Waker does this automatically; otherwise the deployment re-invokes on its own schedule).

func (*TimerPending) Error added in v0.9.0

func (e *TimerPending) Error() string

Error names the run, the timer, and its wake time.

type Tool

type Tool interface {
	// Spec describes the tool (see ToolSpec).
	Spec() ToolSpec
	// Call runs the tool on the arguments the model sent, and returns its result as JSON.
	Call(ctx context.Context, args json.RawMessage) (json.RawMessage, error)
}

Tool is an action the agent can take. The interface is untyped (json.RawMessage) so heterogeneous tools share one type, including runtime MCP tools whose schema is only known at runtime and can't be a Go struct. Native tools get compile-time typing via Func (below); that's the primary ergonomic.

A tool describes itself with a ToolSpec: its name, what the model is shown, how a call may be retried, whether it waits for approval, and how long a call may take. The agent reads the spec once, when the tool is registered, and decides every call from that copy.

func CompensatedFunc

func CompensatedFunc[In, Out any](
	name, description string,
	do func(context.Context, In) (Out, error),
	undo func(context.Context, In, Out) error,
	opts ...ToolOption,
) (Tool, error)

CompensatedFunc is a typed tool that declares both its forward action and its compensator. undo receives the same typed input and the output do produced: the call's recorded arguments decoded as the forward call decoded them, strictly (see Func). A record written before tool arguments decoded strictly may hold arguments only encoding/json accepts; the forward call of that time decoded them with encoding/json, so compensation does too, and undoes the value that call acted on. Arguments neither decodes are ErrProtocol.

The arguments are the ones the tool accepted. When a tool middleware changes a compensable call's arguments in a saga, the arguments the tool receives are journaled before it runs (under a key of their own), and compensation undoes those; otherwise the model's arguments are the ones the tool received. A journal written before accepted arguments were journaled has no such record, so compensation there falls back to the model's arguments. A middleware that rewrites a retry-safe compensable call's arguments must rewrite them the same way every time: the first record is kept when the call runs again.

opts set the rest of the tool's spec, as for Func, which also gives its errors.

func Func

func Func[In, Out any](name, description string, fn func(context.Context, In) (Out, error), opts ...ToolOption) (Tool, error)

Func wraps a typed Go function into a Tool. In is decoded from the args strictly, so the tool reads exactly what the model sent: a missing required field (one schema.For lists as required), null for a required field whose schema does not admit null, an unknown name or a case variant of a field's name, a duplicate name, data after the value, invalid UTF-8, or an escaped lone surrogate is ErrToolArgs, which goes back to the model as a tool error to correct. Empty args are the empty object. The return value is JSON-encoded. This is the compile-time-typed ergonomic: change In and the handler won't compile. The Tool interface itself stays untyped so a map of mixed tools (and runtime MCP tools) works.

opts set the rest of the tool's spec: WithSafety (the default is a side effect, Safety{}), WithApproval, WithTimeout, WithTitle and WithOutputSchema. WithSubRuns declares the agents the tool runs programmatic sub-runs with, for a saga's rollback.

Func returns an error wrapping ErrConfig if schema.For cannot describe In (such a type, a field reached through an embedded pointer to an unexported struct, could never be decoded from a call's arguments, so the tool would fail every call) or an option is invalid. MustFunc panics instead, for a tool built at init.

func MustCompensatedFunc added in v0.11.0

func MustCompensatedFunc[In, Out any](
	name, description string,
	do func(context.Context, In) (Out, error),
	undo func(context.Context, In, Out) error,
	opts ...ToolOption,
) Tool

MustCompensatedFunc is CompensatedFunc for a tool built at init: it panics with CompensatedFunc's error.

func MustFunc added in v0.11.0

func MustFunc[In, Out any](name, description string, fn func(context.Context, In) (Out, error), opts ...ToolOption) Tool

MustFunc is Func for a tool built at init: it panics with Func's error.

func MustRetrievalTool added in v0.11.0

func MustRetrievalTool(name, description string, r Retriever, k int, opts ...ToolOption) Tool

MustRetrievalTool is RetrievalTool for a tool built at init: it panics with RetrievalTool's error.

func MustSubAgent added in v0.11.0

func MustSubAgent(name, description string, sub *Agent, opts ...ToolOption) Tool

MustSubAgent is SubAgent for a tool built at init: it panics with SubAgent's error.

func RetrievalTool

func RetrievalTool(name, description string, r Retriever, k int, opts ...ToolOption) (Tool, error)

RetrievalTool exposes a Retriever as a tool the model can call to search on demand (agentic RAG): the model decides when to retrieve and with what query. It returns the top-k documents: a Retriever that returns more is cut to its first k. It is read-only (retry-safe) unless WithSafety says otherwise, and takes the other tool options (WithApproval, WithTimeout, WithTitle, WithOutputSchema) as Func does. name is what the model calls it by, so an agent that searches several stores gives each RetrievalTool its own; description is what the model reads to decide when to call it, such as what the store holds. An empty name, a nil r, a k below 1 and an invalid option are each an error wrapping ErrConfig; MustRetrievalTool panics with it instead.

Example

ExampleRetrievalTool shows bring-your-own-RAG: implement Retriever against your store, wire it in as a tool (or with WithRetrieval), and the loop handles the rest.

// Your store, behind the Retriever port (here a trivial in-memory one).
docs := map[string]string{"france": "Paris is the capital of France."}
r := RetrieverFunc(func(_ context.Context, query string, _ int) ([]Doc, error) {
	if d, ok := docs[strings.ToLower(query)]; ok {
		return []Doc{{Text: d}}, nil
	}
	return nil, nil
})

tool := MustRetrievalTool("retrieve", "Search the knowledge base.", r, 1)
out, _ := tool.Call(context.Background(), json.RawMessage(`{"query":"france"}`))
fmt.Println(string(out))
Output:
[{"text":"Paris is the capital of France."}]

func SubAgent

func SubAgent(name, description string, sub *Agent, opts ...ToolOption) (Tool, error)

SubAgent wraps an agent as a Tool so a parent can delegate to it — the multi-agent primitive, as a durable tree rather than a fragile handoff.

The sub-run journals under a hierarchical, deterministic ID (SubRunID(parentRunID, toolUseID)), so give `sub` the SAME journal as the parent for a unified journal. Then a crash ANYWHERE in the tree resumes the whole tree precisely: completed sub-agents are reused, the in-flight one resumes from its own journal, and OutcomeUnknown / ApprovalPending from deep in the tree propagate up (approve, re-run the root, and it resumes down the path).

opts set the rest of the tool's spec: WithApproval, so the parent waits for a human before it delegates, WithTitle and WithOutputSchema. SubAgent refuses WithSubRuns, whose sub-run is its own, and WithSafety, since a sub-agent call re-enters its sub-run, whose own calls carry their safety, and WithTimeout: a deadline would cut the sub-run off mid-call and record the delegation as failed while the outcome of the sub-run's own call is unknown, so the model, told the delegation failed, could delegate again and repeat a side effect that may already have happened. Bound the sub-agent's tools instead; each of them then follows the timeout rule (see WithTimeout) inside the sub-run's own journal. A nil sub, and a refused or invalid option, is an error wrapping ErrConfig; MustSubAgent panics with it instead.

This is what the incumbents can't do: ADK/agenticenv can't recover sub-agents across a restart, and Eino doesn't unify nested state into the parent checkpoint.

type ToolCall added in v0.10.0

type ToolCall struct {
	Use   ToolUse
	Spec  ToolSpec
	RunID string // the run whose model made the call
	// contains filtered or unexported fields
}

ToolCall is one tool call as tool middleware sees it: the ToolUse the model sent (ID, Name, Args), the Spec of the tool it names, and the run it belongs to. A middleware reads Spec to tell a side effect from a read before it retries, caches, or skips a call, and may pass next a copy with different Use.Args. It passes next the ToolCall it was given (or a copy): a call whose Use.Name or Use.ID a middleware changed, or a ToolCall it built itself, fails with ErrConfig and the tool is not called. Spec is the zero ToolSpec (a side effect) for a call that names no registered tool.

Spec informs middleware only. The agent decides from its own copy of the registered tool's spec: a middleware that changes Spec changes nothing the agent enforces.

func (ToolCall) ErrorText added in v0.10.0

func (c ToolCall) ErrorText(err error) string

ErrorText returns the text the agent journals, and sends to the model, for this call failing with err: the text chosen by the agent's WithToolErrorRedactor, if it has one, and otherwise err's own text, in either case with every URL in it redacted. A tool middleware that records a failed call's error text (a trace span, a log line) uses it to record no more than the journal holds. A ToolCall the middleware built itself, rather than one the agent passed it, has no redactor: its text is err's own, URLs redacted.

type ToolCallDelta

type ToolCallDelta struct {
	Index        int             `json:"index"`
	ID           string          `json:"id"`
	Name         string          `json:"name"`
	ArgsFragment json.RawMessage `json:"args_fragment"`
	Signature    string          `json:"signature"` // opaque provider token for this call, kept on ToolUse.Signature (Gemini thoughtSignature)
}

ToolCallDelta is an incremental fragment of a tool call, keyed by Index. Fragments are concatenated and gated by json.Valid before use (the UTF-8/partial-JSON safety the research flagged as universally missing).

type ToolChoice

type ToolChoice struct {
	Mode string `json:"mode,omitempty"` // "", "auto", "none", "required", or "tool"
	Name string `json:"name,omitempty"` // tool name to force; only meaningful when Mode == "tool"
}

ToolChoice controls whether and how the model may call tools on a request. Mode is one of "" or "auto" (model decides, the default), "none" (never call a tool), "required" (must call some tool), or "tool" (must call the specific tool named in Name). A nil *ToolChoice on a Request means the provider default (auto).

CAVEAT: in the multi-turn agent loop, forcing "required" or "tool" on EVERY turn prevents the model from ever emitting a final text answer, so the loop never terminates. Those two modes are intended for single-turn or typed/structured calls where exactly one tool round-trip is expected. Use "auto" (the default) for the loop.

type ToolCompleted

type ToolCompleted struct {
	ToolUseID string          `json:"tool_use_id"`
	Name      string          `json:"name"`
	Result    json.RawMessage `json:"result"`
	IsError   bool            `json:"is_error"`
}

ToolCompleted carries a tool call's result. Emitted for live executions, for a human-denied call, and for each result reconstructed from the journal on resume.

type ToolHandler

type ToolHandler func(ctx context.Context, call ToolCall) (json.RawMessage, error)

ToolHandler executes one tool call and returns the raw JSON result or an error. It is the tool-side analogue of ModelHandler.

type ToolMiddleware

type ToolMiddleware func(ToolHandler) ToolHandler

ToolMiddleware wraps a ToolHandler, the same func(Handler) Handler idiom as model Middleware, but around TOOL execution. First added = outermost. A middleware can:

  • observe/log/trace a call (its name, args, duration, error);
  • MUTATE the outgoing args (validate, redact, inject defaults) by calling next with a copy of the ToolCall whose Use.Args differ;
  • transform the result before it is journaled (turning a side effect's success into an error halts the run: see below);
  • SHORT-CIRCUIT: return a result (a cache hit) or an error (a policy denial) WITHOUT calling next, so the tool never runs. A denial MUST wrap ErrToolNotCalled (fmt.Errorf("denied: %w", agent.ErrToolNotCalled)): without it, a side effect's outcome is unknown and the run halts (see below).

A middleware that may call next more than once (a retry) or not at all (a cache) must check the call's Spec.Safety first: repeating or skipping a side effect is not its call to make. The agent enforces the first half itself: a tool that is not RetrySafe runs at most once per tool call, and a second call to next for it returns ErrToolReinvoked without running the tool.

A middleware reaches the tool only through next: never by calling the tool itself, and never by leaving next running after it returns (the agent refuses an invocation of next that comes after the chain returned). When it ends a call without calling next (a denial, a rate limiter that gives up), it MUST return an error wrapping ErrToolNotCalled, and it returns that error only then. The agent needs positive proof that a side effect was not called: a chain that returns an error without calling next, and without ErrToolNotCalled, leaves the side effect's outcome unknown, and the run halts for it rather than risk running it twice. It needs positive proof that a side effect failed, too: a middleware may transform a result, but one that turns a side effect's success into an error (or returns another error for a call whose tool did not itself fail) makes the outcome unknown, and the run halts for it, so the model is never told a fired side effect failed. A retry-safe tool's error is recorded as a failure the model sees, except in a saga for one that changes state (Idempotent, not ReadOnly): there it is recorded with an unknown outcome and reported in SagaAborted.UnknownOutcome, since the rollback would otherwise skip it as a step that made no change.

A result needs positive proof as well: a chain that returns a result while any invocation of the call's tool is still running in this process (a next the middleware left running, a sibling invocation, or one an earlier, cancelled drive left behind) has an unknown outcome, since that invocation's effect may land after anything the run records next, a saga's compensation included. A side effect halts; a retry-safe saga write is reported in SagaAborted.UnknownOutcome and never compensated. Once the chain has returned, no invocation of next begins the tool, even for a retry-safe tool an earlier invocation already reached. A saga rollback that re-runs a retry-safe step to learn the result to compensate applies the same rules, and takes as the result only an answer that reached the tool: a re-run whose outcome is unknown, or that a middleware answered itself, is reported, not compensated, and the rollback goes on. So is a re-run the agent's call guard refused (audit: the delegation's grant has expired), which the rollback recognises by the error next returned. A middleware that returns its own error in place of next's, rather than wrapping it (%w), hides that refusal: the rollback then stops at the step with the middleware's error, and the saga's next drive meets the same refusal. Return next's error, or wrap it, so the error chain (errors.Is, errors.As) still reaches it.

The chain runs INSIDE the durable, memoized step, so a short-circuit result or a transformed result is what gets journaled: resume replays it and never re-runs the middleware or the tool. A tool's Timeout bounds the whole chain. Batteries live in the middleware/ package (ToolLog, ToolCache, ToolRetry).

type ToolOption added in v0.10.0

type ToolOption interface {
	// contains filtered or unexported methods
}

ToolOption configures a tool built by Func, CompensatedFunc or SubAgent.

func WithApproval added in v0.10.0

func WithApproval(p *ApprovalPolicy) ToolOption

WithApproval gates every call to the tool on human approval before it runs: p is SingleApproval() for one decision (Approve), or an m-of-n policy (SubmitDecision, and the agent's WithApproverVerifiers). A nil or invalid policy is ErrConfig. The policy is copied.

func WithOutputSchema added in v0.10.0

func WithOutputSchema(schema json.RawMessage) ToolOption

WithOutputSchema declares the JSON Schema of the tool's result (ToolSpec.Output). It must be a JSON object (ErrConfig otherwise). The schema is copied.

func WithSubRuns added in v0.10.0

func WithSubRuns(agentFor func(name string) *Agent) ToolOption

WithSubRuns declares the agents that run the tool's programmatic sub-runs: agentFor(name) is the agent the tool runs RunInfo.SubRunFor(name) with. A saga's rollback walks those sub-runs as it walks a sub-agent's (SubAgent): for each sub-run a call of the tool started, latest first, it compensates the sub-run's completed writes with agentFor(name)'s compensators, after the call itself. agentFor must return the agent (or one with the same tools) for every name the tool starts a sub-run under, also after a restart, since the rollback may run in a later process.

A sub-run in a saga's tree must journal to the run's store, where the rollback reads it: Run refuses (ErrConfig) one whose agent journals elsewhere. The call's own compensation runs before its sub-runs are walked. A sub-run is linked when it starts, within the call; one that is still running when the call returns (held by a goroutine the call left behind) may still be writing while the rollback walks it, so a write it makes after the walk passed it is not undone.

Without it, or when agentFor returns nil for a sub-run, the rollback cannot undo the sub-run's writes: it reports each one in SagaAborted.Uncompensated (and stops for a human at one whose outcome is unknown), as it does for a write with no compensator. An agent it cannot use (one on another store, or a panic in agentFor) is reported the same way, and the call is listed too, with the reason (see SagaAborted). A nil agentFor is ErrConfig, and SubAgent, whose sub-run is its own, refuses the option.

func WithTimeout added in v0.10.0

func WithTimeout(d time.Duration) ToolOption

WithTimeout bounds each call to the tool by d: the call runs under a context with that deadline, on top of the run's. It bounds only a tool that honors its context; the agent still waits for the call to return. A call that returns a result is recorded even if the deadline has passed, since a known outcome is never discarded. A call that returns an error after the deadline (judged by the deadline itself) has an unknown outcome, as if it had failed with ErrToolOutcomeUnknown: a side effect records nothing, and a resume halts for its outcome, while a retry-safe tool records the error for the model, which may call it again as a new call (see NextOnceKey), and in a saga is reported in SagaAborted.UnknownOutcome. Only a call that reached the tool is judged so: one a tool middleware ended first, even at the deadline, never ran the tool and fails as a known error. d must be positive (ErrConfig otherwise).

func WithTitle added in v0.10.0

func WithTitle(title string) ToolOption

WithTitle sets the tool's human-readable display name (ToolSpec.Title).

type ToolResult

type ToolResult struct {
	ToolUseID string          `json:"tool_use_id"`
	Result    json.RawMessage `json:"result,omitempty"`
	IsError   bool            `json:"is_error,omitempty"`
}

ToolResult carries a tool's output back to the model (tool turn).

type ToolRules added in v0.10.0

type ToolRules interface {
	// ToolNameRule is the pattern every tool name must match; nil declares none.
	ToolNameRule() *regexp.Regexp
	// RequiresToolsForRequired reports whether a request with tool choice "required" must
	// declare at least one tool.
	RequiresToolsForRequired() bool
}

ToolRules is an optional interface a Model implements to declare the tool setups its provider refuses, so New and With refuse them when the agent is built instead of every run failing. A Model that does not implement it (itself or through an Unwrap() Model chain, as for ModelInfoOf) declares nothing, and New checks nothing of the kind for it.

type ToolSpec added in v0.10.0

type ToolSpec struct {
	// Name is the name the model calls the tool by. It must be unique among an agent's tools.
	Name string
	// Title is a human-readable display name (an MCP tool's title). Models are not shown it.
	Title string
	// Description tells the model what the tool does and when to call it.
	Description string
	// Input is the provider-neutral JSON Schema of the arguments, an object schema. The schema/
	// package emits each provider's dialect from it.
	Input json.RawMessage
	// Output is the JSON Schema of the result, when the tool declares one (an MCP tool's
	// outputSchema). Models are not shown it; it documents the result for hosts and auditors.
	Output json.RawMessage
	// Safety declares how a call may be retried (see Safety). It is journaled with each result.
	Safety Safety
	// Approval, when non-nil, pauses each call for human approval before the tool runs: one
	// decision (SingleApproval) or an m-of-n gate over named approvers. It is journaled with
	// each result.
	Approval *ApprovalPolicy
	// Timeout, when positive, bounds each call: the call runs under a context with this
	// deadline. A result the tool returns is recorded even if the deadline has passed; an error
	// it returns after the deadline has an unknown outcome (see WithTimeout).
	Timeout time.Duration
}

ToolSpec is everything the agent knows about a tool: what the model is shown (Name, Description, Input), how a call may be retried (Safety), whether it waits for a human first (Approval), and how long a call may take (Timeout). The agent reads a tool's spec once, when the tool is registered, and decides every call from that copy.

type ToolStarted

type ToolStarted struct {
	ToolUseID string          `json:"tool_use_id"`
	Name      string          `json:"name"`
	Args      json.RawMessage `json:"args"`
}

ToolStarted fires when a tool call begins executing: after any approval gate and, for non-idempotent tools, after the durable attempt marker is written, immediately before the tool is called. A call that does not start (cancelled after its attempt marker and before the call, and recorded as not started) emits neither ToolStarted nor ToolCompleted.

type ToolUse

type ToolUse struct {
	ID        string          `json:"id"`
	Name      string          `json:"name"`
	Args      json.RawMessage `json:"args,omitempty"`
	Signature string          `json:"signature,omitempty"`
}

ToolUse is the model's request to call a tool (assistant turn). Signature is an opaque provider token bound to this call that must be sent back with it on later turns (a Gemini thoughtSignature); it is journaled with the call, and adapters that have no such token leave it empty and ignore it.

func FindToolCall added in v0.4.0

func FindToolCall(recs []Record, toolUseID string) (idx int, call ToolUse, ok bool)

FindToolCall returns the journal index of the record that requested toolUseID, and the call itself (its name and arguments), or ok=false if no record requested it. The request of an agent's tool call is the model turn that made it; the request of a gated plan flow node (whose toolUseID is the node's key) is its ApprovalRequest, and the call's name and arguments are the node's name and input. The first request in journal order is returned.

type TurnRestarted added in v0.8.0

type TurnRestarted struct {
	Seq int `json:"seq"`
}

TurnRestarted fires when the model call for turn Seq starts over after an attempt that had already streamed ModelEvent deltas: a middleware such as Retry called the model again after that attempt failed, or delivered a different response in its place. The deltas received since TurnStarted{Seq} (or the previous TurnRestarted) belong to the discarded attempt and are not part of the recorded turn, so a consumer rendering the turn should clear them. Not emitted when the discarded attempt streamed nothing.

type TurnStarted

type TurnStarted struct {
	Seq int `json:"seq"`
}

TurnStarted marks the beginning of a fresh model turn (Seq is the model-call sequence number within the run). Not emitted for turns replayed from the journal.

type TypedStart added in v0.10.0

type TypedStart struct {
	Mode         OutputMode      `json:"mode"`
	SchemaDigest string          `json:"schema_digest"`
	Schema       json.RawMessage `json:"schema"`
}

TypedStart is how a typed run (RunTyped) was started: its output mode and the JSON schema of its answer type, in full and as a digest. Resuming a typed run through an untyped entry point, or with another answer type, is ErrConfig before any model call.

type Usage

type Usage struct {
	// InputTokens is input tokens neither read from nor written to the prompt cache.
	InputTokens  int `json:"input_tokens,omitempty"`
	OutputTokens int `json:"output_tokens,omitempty"`
	// CacheReadTokens is input tokens served from the provider's prompt cache (billed at
	// a discount). CacheWriteTokens is input tokens written to the cache on this call
	// (Anthropic cache creation; 0 for providers that cache implicitly, like OpenAI).
	CacheReadTokens  int `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
}

Usage is token accounting for a call; middleware turns it into cost. The four counts are disjoint, whatever the provider: each input token is counted in exactly one of InputTokens, CacheReadTokens, or CacheWriteTokens, so each is billed once at its own rate and TotalInputTokens is the whole prompt. Providers differ on the wire (Anthropic reports uncached input separately; OpenAI and Gemini report a prompt total that includes cached tokens), and adapters normalize to this form.

func (Usage) TotalInputTokens added in v0.6.0

func (u Usage) TotalInputTokens() int

TotalInputTokens is every input token the call processed, cached or not.

func (Usage) TotalTokens added in v0.6.0

func (u Usage) TotalTokens() int

TotalTokens is every token the call processed: all input plus output.

func (Usage) Validate added in v0.8.0

func (u Usage) Validate() error

Validate reports an error wrapping ErrNegativeUsage when any count in u is negative.

type Wake added in v0.9.0

type Wake struct {
	RunID     string
	RootRunID string
	Name      string
	FireAt    time.Time
}

Wake is one scheduled wake: re-invoke RootRunID at or after FireAt. RunID and Name identify the timer (a sub-agent's timer lives in its sub-run, RunID); the top-level run is what a deployment re-invokes, since re-running it re-enters any sub-agent down to the timer.

type Waker

type Waker interface {
	// Schedule registers w, idempotent per (w.RunID, w.Name): scheduling the same timer again
	// replaces its fire time. An error means the wake may not be registered; the run then fails
	// with an error wrapping ErrStorage, records nothing, and schedules again when re-driven.
	Schedule(ctx context.Context, w Wake) error
}

Waker is the pluggable trigger that re-invokes a sleeping run when its durable timer is due. Sleep registers a wake with the run's Waker (WithWaker); the Waker later calls back to resume the run. The SDK provides the durable, at-most-once timer and its resume safety; what re-invokes the run at the wake time is deployment policy (an in-process loop, a cron, a queue), exactly as the inbound trigger for an event-driven run is (see docs/guides/messaging.md). MemWaker is the reference in-process implementation.

type Watched added in v0.17.0

type Watched struct {
	Key   string    `json:"key"`
	RunID string    `json:"run_id"`
	At    time.Time `json:"at"` // when it was listed
}

Watched is an event on the open trail whose all-clear may still be owed.

func FoldWatch added in v0.17.0

func FoldWatch(recs []NotifyRecord) []Watched

FoldWatch is the events FoldOpen lists.

Directories

Path Synopsis
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.

Jump to

Keyboard shortcuts

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