ledger

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 27 Imported by: 0

Documentation

Overview

Package agenttools exposes in-process workflow tools for the agent surface. Tools call the shared workflow ledger for reads and an injected Engine for mutations. This package must not import controller/agents/skills so the tools package can import it without an import cycle.

Package ledger provides durable persistence for workflow runs. It implements the workflow repository contract with SQLite projections following the established storage migration pattern.

Phase 2 deliverable — ledger, isolated worktree, and lifecycle.

Projection and catch-up for the plan/task ledger. The projection is derived state: it is rebuilt from the durable event log of each plan run, so a fresh Store instance over the same storage backend sees identical state after a restart, and incremental catch-up keeps several instances coherent.

Package tasks provides a generic, durable plan and task ledger (D8).

One mechanism serves many scopes: sessions, workflow steps, agents, workflows and runs all store plans and task statuses through the same API. The engine stores, transitions and queries; consumers define their own status vocabulary. Statuses are OPAQUE strings: this package never interprets them, only validates non-empty and journals transitions.

Durability: every mutation appends one event to a shared storage.Store (the same primitive the workflow ledger builds on). The in-memory projection is rebuilt from the event log on catch-up, so state survives restarts and each mutation is atomic with its journal entry.

Index

Constants

View Source
const (
	ToolWorkflowRun      = "workflow_run"
	ToolWorkflowStatus   = "workflow_status"
	ToolWorkflowEvents   = "workflow_events"
	ToolWorkflowInspect  = "workflow_inspect"
	ToolWorkflowListRuns = "workflow_list_runs"
	ToolWorkflowDeliver  = "workflow_deliver"
	ToolWorkflowCancel   = "workflow_cancel"
	ToolWorkflowDelete   = "workflow_delete"
)

Tool names are model-facing and project/language-generic (rule 60).

View Source
const (
	DefaultStatusBudgetBytes  = 256 << 10
	DefaultEventsBudgetBytes  = 256 << 10
	DefaultInspectBudgetBytes = 512 << 10
	DefaultListBudgetBytes    = 128 << 10
	DefaultRunBudgetBytes     = 16 << 10
	DefaultDeliverBudgetBytes = 32 << 10
	DefaultCancelBudgetBytes  = 16 << 10
	DefaultDeleteBudgetBytes  = 16 << 10
	DefaultEventsPageSize     = 50
	DefaultListRunsPageSize   = 50

	// DefaultInspectPageBytes is the default page size for workflow_inspect
	// output text (INV-AG-25): one page of redacted, rune-safe text.
	DefaultInspectPageBytes = 64 << 10
	// MaxPageableBytes is the total artifact size beyond which
	// workflow_inspect refuses to page output at all (clear refusal).
	MaxPageableBytes = 8 << 20
)

Result budgets bound tool JSON (INV-AG-25). Framing stays inside the budget.

View Source
const (
	ScopeSession  = "session"
	ScopeStep     = "step"
	ScopeAgent    = "agent"
	ScopeWorkflow = "workflow"
	ScopeRun      = "run"
)

Scope types bind a plan or a task to one engine entity. The type and ID are opaque; consumers choose their own vocabulary.

View Source
const DefaultClaimLease = 2 * time.Minute

DefaultClaimLease is how long a run execution claim is considered fresh after its last refresh. Every claim heartbeat in the codebase derives from it (controller and delivery both refresh every Lease/3), so a live holder never appears stale and a dead one is detected after at most one lease. Two minutes means a hard-killed session's runs are recoverable within ~2m (plus one recovery scan); graceful shutdowns release claims instantly. The tradeoff: a process frozen for longer than the lease could be taken over by another session — the 3-heartbeats-per-lease ratio keeps that window at the refresh cadence.

View Source
const DefaultEventPageSize = 200

DefaultEventPageSize is the page size when ListEvents is called without a limit. It bounds one CLI listing without forcing callers to page.

View Source
const MaxEventSummaryBytes = 512

MaxEventSummaryBytes bounds one event summary for operator display.

View Source
const MaxEvidenceBytes = 16 << 10

MaxEvidenceBytes bounds persisted evidence-selection metadata.

View Source
const SnapshotSchemaVersion = 1

SnapshotSchemaVersion is the version of the immutable run snapshot wire format.

View Source
const WorkflowsDir = workspace.Namespace + "/workflows"

WorkflowsDir is the workspace-relative path that holds workflow TOML files.

Variables

View Source
var (
	ErrDuplicate         = ledgercore.ErrDuplicate
	ErrNotFound          = ledgercore.ErrNotFound
	ErrConflict          = ledgercore.ErrConflict
	ErrInvalidTransition = ledgercore.ErrInvalidTransition
	ErrClaimHeld         = ledgercore.ErrClaimHeld
	ErrClaimNotHeld      = ledgercore.ErrClaimNotHeld
	ErrClosed            = ledgercore.ErrClosed
	ErrContentNotFound   = ledgercore.ErrContentNotFound
)

Sentinel errors returned by Repository methods.

View Source
var (
	// ErrPlanNotFound reports a plan ref that has no stored plan.
	ErrPlanNotFound = errors.New("plan not found")
	// ErrTaskNotFound reports a task that has no record under its plan.
	ErrTaskNotFound = errors.New("task not found")
	// ErrInvalidScope reports a scope type outside the supported set.
	ErrInvalidScope = errors.New("invalid scope type")
	// ErrEmptyStatus reports an empty status where a non-empty one is required.
	ErrEmptyStatus = errors.New("status must not be empty")
	// ErrInvalidPlan reports a plan record that cannot be stored (empty ref).
	ErrInvalidPlan = errors.New("invalid plan")
	// ErrInvalidTask reports a task record that cannot be created (empty id or plan ref).
	ErrInvalidTask = errors.New("invalid task")
	// ErrTaskDuplicate reports a record that already exists with different content.
	ErrTaskDuplicate = errors.New("duplicate record")
	// ErrTaskConflict reports a logical key taken by a concurrent writer.
	ErrTaskConflict = errors.New("state conflict")
)

Sentinel errors returned by Store methods.

View Source
var ErrRepoUnset = errRepoUnset("workflow ledger is not configured for this session")

ErrRepoUnset is returned when tools register before a ledger is wired.

Functions

func AllToolNames

func AllToolNames() []string

AllToolNames returns the eight Phase 7 workflow tool names in stable order.

func ContextWithClaimHolder

func ContextWithClaimHolder(ctx context.Context, holder string) context.Context

ContextWithClaimHolder binds one controller's immutable claim holder to its writes.

func ContextWithPanelChildPrincipal

func ContextWithPanelChildPrincipal(ctx context.Context, workflowRunID string) context.Context

ContextWithPanelChildPrincipal replaces the caller for every panel child coordinator operation. It never inherits a host caller's authority scope.

func ContextWithRunID

func ContextWithRunID(ctx context.Context, runID string) context.Context

ContextWithRunID binds a workflow mutation to its run.

func DigestHex

func DigestHex(data []byte) string

DigestHex returns the lowercase hex SHA-256 of data (shared content-hash helper).

func EventID

func EventID(runID, kind string, parts ...string) string

EventID mints the DETERMINISTIC event ID for a logical key:

"wfe:" + hex(runID) + ":" + kind + ":" + hex(part) for each dynamic part.

Every dynamic part is hex-encoded so the mapping is injective regardless of caller-controlled characters (step IDs, loop names, keys may contain ':'). The store's events.id PRIMARY KEY is therefore the uniqueness constraint for every logical key: a second writer appending the same key gets ErrDuplicate (identical payload -> success; different payload -> ErrConflict).

func FinalizePanelTaskSpec

func FinalizePanelTaskSpec(spec *PanelTaskSpec)

FinalizePanelTaskSpec records the fingerprint of the durable work fields.

func HasWorkflows

func HasWorkflows(root string) bool

HasWorkflows reports whether root contains a workflows directory that can hold workflow definitions. The check is presence-only: an empty directory still enables tool registration so authors can add workflows later in the same session without restarting.

func InputDigest

func InputDigest(inputs map[string]string) string

InputDigest returns the digest of the canonical input JSON object.

func InvocationRunID

func InvocationRunID(key string) string

InvocationRunID returns the stable workflow run ID for a caller key.

func IsDeletableRunStatus

func IsDeletableRunStatus(s RunStatus) bool

IsDeletableRunStatus reports whether a settled run may be deleted from the ledger: every terminal status plus delivery_pending (the explicit operator choice not to deliver). Active statuses (pending, running, waiting_approval) are not deletable — cancel the run first.

func IsResumableRunStatus

func IsResumableRunStatus(s RunStatus) bool

IsResumableRunStatus reports whether the status means the run was interrupted and can be resumed: pending, running, waiting_approval. delivery_pending is a deliberate terminal-like pause, not an interruption.

func IsTerminalAttemptStatus

func IsTerminalAttemptStatus(s AttemptStatus) bool

IsTerminalAttemptStatus reports whether the attempt status is terminal.

func IsTerminalRunStatus

func IsTerminalRunStatus(s RunStatus) bool

IsTerminalRunStatus reports whether the status is terminal (no outgoing transitions).

func IsTerminalStepID

func IsTerminalStepID(stepID string) bool

IsTerminalStepID reports whether a step ID is one of the reserved terminal steps from the workflow contract ("success", "failure"). A route to either means the workflow is done even if the run status CAS was not recorded.

func MarshalSnapshot

func MarshalSnapshot(s Snapshot) ([]byte, error)

MarshalSnapshot serializes the snapshot to its canonical JSON form. The output bytes are the durable artifact: the snapshot digest is computed over them, and resume must reproduce them byte-identically.

func PanelChildIDs

func PanelChildIDs(workflowRunID, attemptID, childID string) (runID, taskID string)

PanelChildIDs returns deterministic coordinator identifiers for one child. Each identifier uses the coordinator's canonical run ID encoding.

func PanelChildPrincipal

func PanelChildPrincipal(workflowRunID string) runtime.Caller

PanelChildPrincipal derives the only principal panel child operations use.

func RunIDFromContext

func RunIDFromContext(ctx context.Context) (string, bool)

RunIDFromContext returns the workflow run bound to a mutation context.

func SnapshotDigest

func SnapshotDigest(data []byte) string

SnapshotDigest returns the hex SHA-256 of the canonical snapshot bytes.

func ValidAttemptTransition

func ValidAttemptTransition(from, to AttemptStatus) bool

ValidAttemptTransition reports whether an attempt may move from one status to another. Edges: pending->running; running->succeeded|failed|timed_out|canceled|interrupted. All of succeeded/failed/timed_out/canceled/interrupted are terminal for the attempt record.

func ValidRunTransition

func ValidRunTransition(from, to RunStatus) bool

ValidRunTransition reports whether a run may move from one status to another. Edges: pending->running; running->waiting_approval|delivery_pending|succeeded|failed|canceled|timed_out; waiting_approval->running|failed|canceled|timed_out; delivery_pending->succeeded|delivery_failed|failed|running. Repair edges: delivery_pending->running and delivery_failed->running return a run whose delivery failed for a repairable reason to the step the workflow names in delivery.on_failure. Delivery runs after the success terminal, outside the step graph, so without these a failed delivery had no route back and the run stopped with all of its work done. Recovery carve-out: delivery_failed->delivery_pending re-opens a refused run for re-eligibility (the delivery retry path CASes it back to delivery_pending before re-attempting), and delivery_failed->delivery_failed is a defensive self-loop so a still-refused re-eligibility can settle without an invalid transition. Every other terminal status (succeeded/failed/canceled/timed_out) has no outgoing edges.

func ValidScopeType

func ValidScopeType(t string) bool

ValidScopeType reports whether t is one of the supported scope types.

Types

type AgentSnapshot

type AgentSnapshot struct {
	Digest       string `json:"digest"`
	Version      int    `json:"version,omitempty"`
	ProviderName string `json:"provider_name,omitempty"`
	Model        string `json:"model,omitempty"`
}

AgentSnapshot pins an agent definition and its effective provider binding.

type ApprovalRecord

type ApprovalRecord struct {
	ApprovalID   string     `json:"approval_id"`
	RunID        string     `json:"run_id"`
	StepID       string     `json:"step_id"`
	Status       string     `json:"status"` // pending | approved | rejected
	Actor        string     `json:"actor,omitempty"`
	Reason       string     `json:"reason,omitempty"`
	EvidenceJSON []byte     `json:"evidence_json,omitempty"`
	CreatedAt    time.Time  `json:"created_at"`
	ResolvedAt   *time.Time `json:"resolved_at,omitempty"`
}

ApprovalRecord records one human-gate request and its resolution.

func (ApprovalRecord) Clone

func (a ApprovalRecord) Clone() ApprovalRecord

Clone returns a deep copy.

type ApprovalView

type ApprovalView struct {
	ApprovalID string `json:"approval_id"`
	Step       string `json:"step"`
	Status     string `json:"status"`
	Actor      string `json:"actor,omitempty"`
	Reason     string `json:"reason,omitempty"`
}

ApprovalView is one human-gate approval summary.

type AttemptOutcome

type AttemptOutcome struct {
	Status           AttemptStatus
	CoordinatorRunID string
	TaskID           string
	OutputRef        string
	OutputDigest     string
	ErrorRef         string
	ToStepID         string
	TransitionIndex  int
	MatchDigest      string
	DecisionJSON     []byte
	EvidenceJSON     []byte
}

AttemptOutcome is the terminal result of one attempt, recorded atomically with the attempt's status change (ONE event per mutation). The route fields (ToStepID, TransitionIndex, MatchDigest, DecisionJSON) carry the transition decision computed from snapshotted typed evidence before the completion is persisted; they are empty for interrupted/canceled/timed_out completions.

type AttemptStatus

type AttemptStatus string
const (
	AttemptStatusPending     AttemptStatus = "pending"
	AttemptStatusRunning     AttemptStatus = "running"
	AttemptStatusSucceeded   AttemptStatus = "succeeded"
	AttemptStatusFailed      AttemptStatus = "failed"
	AttemptStatusTimedOut    AttemptStatus = "timed_out"
	AttemptStatusCanceled    AttemptStatus = "canceled"
	AttemptStatusInterrupted AttemptStatus = "interrupted"
)

type AttemptView

type AttemptView struct {
	Step             string `json:"step"`
	Attempt          int    `json:"attempt"`
	Status           string `json:"status"`
	ToStep           string `json:"to_step,omitempty"`
	OutputDigest     string `json:"output_digest,omitempty"`
	OutputRef        string `json:"output_ref,omitempty"`
	ErrorRef         string `json:"error_ref,omitempty"`
	CoordinatorRunID string `json:"coordinator_run_id,omitempty"`
	TaskID           string `json:"task_id,omitempty"`
	Verdict          string `json:"verdict,omitempty"`
	MatchDigest      string `json:"match_digest,omitempty"`
	StartedAt        string `json:"started_at,omitempty"`
	FinishedAt       string `json:"finished_at,omitempty"`
	ElapsedSeconds   int64  `json:"elapsed_seconds,omitempty"`
	// LastHeartbeatAt is the latest liveness observation for a RUNNING
	// attempt, RFC3339 UTC, or empty when none was recorded.
	LastHeartbeatAt string `json:"last_heartbeat_at,omitempty"`
	// LastHeartbeatStalenessSeconds is the seconds elapsed since the latest
	// heartbeat, or 0 when none was recorded (or the clock is skewed).
	LastHeartbeatStalenessSeconds int64 `json:"last_heartbeat_staleness_seconds,omitempty"`
}

AttemptView summarises one numbered step attempt.

type CancelResult

type CancelResult struct {
	RunID  string `json:"run_id"`
	Status string `json:"status"`
}

CancelResult is the response from workflow_cancel.

type DeleteResult

type DeleteResult struct {
	RunID   string `json:"run_id"`
	Status  string `json:"status"`
	Deleted bool   `json:"deleted"`
}

DeleteResult is the response from workflow_delete. Status is the run's status BEFORE deletion; Deleted is always true on success (an error is returned otherwise), so the tool output is self-documenting for the agent.

type DeliverResult

type DeliverResult struct {
	RunID   string `json:"run_id"`
	Status  string `json:"status"`
	URL     string `json:"url,omitempty"`
	Mode    string `json:"mode,omitempty"`
	Refused bool   `json:"refused,omitempty"`
	Reason  string `json:"reason,omitempty"`
}

DeliverResult is the response from workflow_deliver.

type DeliveryRecord

type DeliveryRecord struct {
	RunID          string `json:"run_id"`
	IdempotencyKey string `json:"idempotency_key"`
	Mode           string `json:"mode"`
	BaseRef        string `json:"base_ref"`
	HeadRef        string `json:"head_ref,omitempty"`
	CommitSHA      string `json:"commit_sha,omitempty"`
	TreeSHA        string `json:"tree_sha,omitempty"`
	Provider       string `json:"provider,omitempty"`
	RemoteID       string `json:"remote_id,omitempty"`
	URL            string `json:"url,omitempty"`
	Status         string `json:"status"`
	ErrorRef       string `json:"error_ref,omitempty"`
	DiffRef        string `json:"diff_ref,omitempty"`
	// DeferredFiles is the host-computed split decision of a deferred-split
	// delivery (spec-auto-split-oversized-prs.md §5.2, revised per §10): a
	// JSON-encoded array of workspace-relative paths whose edits ship in a
	// separate follow-up commit on DeferredBranchName, never on the pushed
	// branch. It is recorded on the pending stage record BEFORE the delivered
	// commit is created, so a crash or transient failure mid-split can be
	// resumed by delivery.resumeDeliveryCommitSplit instead of the retry
	// committing or adopting the deferred scope onto the pushed branch
	// (commitWorktreeFollowUp/adoptOwnFollowUpCommit never see a split state).
	// Empty means no split attempt.
	DeferredFiles string `json:"deferred_files,omitempty"`
	// StackRemainingCommits is the count of commits still on the delivered
	// branch after the one that was pushed (git rev-list --count, a derived
	// integer, never an LLM-authored claim), set when a diff-size repair
	// commits a review-sized slice plus deferred scope as trailing commits on
	// the same branch (spec-auto-split-oversized-prs.md §5.2-5.3). Zero means
	// no split: nothing downstream changes from a chunk that delivered
	// cleanly. The stack driver reads this to admit the trailing commits as
	// follow-up chunk runs stacked on this one.
	StackRemainingCommits int       `json:"stack_remaining_commits,omitempty"`
	UpdatedAt             time.Time `json:"updated_at"`
}

DeliveryRecord records the retry-safe publish lifecycle for one run.

func (DeliveryRecord) Clone

func (d DeliveryRecord) Clone() DeliveryRecord

Clone returns a deep copy.

type DeliverySnapshot

type DeliverySnapshot struct {
	Mode     string `json:"mode"`
	Provider string `json:"provider"`
	Base     string `json:"base,omitempty"`
}

type DeliveryView

type DeliveryView struct {
	IdempotencyKey string `json:"idempotency_key"`
	Status         string `json:"status"`
	Mode           string `json:"mode,omitempty"`
	URL            string `json:"url,omitempty"`
	CommitSHA      string `json:"commit_sha,omitempty"`
	ErrorRef       string `json:"error_ref,omitempty"`
	// ErrorText carries the resolved failure hint for a failed delivery, so
	// the run status surfaces why delivery is pending without an extra lookup.
	ErrorText string `json:"error_text,omitempty"`
}

DeliveryView is one delivery record summary.

type Engine

type Engine interface {
	// Start admits a run and advances it in a background goroutine.
	// It returns as soon as the run ID is durable (non-blocking).
	Start(ctx context.Context, req StartRequest) (StartResult, error)
	// Cancel settles a non-terminal run to canceled (idempotent).
	Cancel(ctx context.Context, runID string) (CancelResult, error)
	// Deliver publishes a delivery_pending run when allow_publish is true.
	Deliver(ctx context.Context, runID string, allowPublish bool) (DeliverResult, error)
	// Delete removes a run from the durable ledger. Settled runs (terminal or
	// delivery_pending) are always deletable; with force, a non-terminal run
	// (pending/running/waiting_approval) is deletable too — the crash-recovery
	// override for runs stranded by a dead executor. A fresh claim held by a
	// live executor is refused either way; only an expired lease is taken over.
	Delete(ctx context.Context, runID string, force bool) (DeleteResult, error)
}

Engine performs mutating workflow operations. Reads use Repository only.

type EventRecord

type EventRecord struct {
	ID        string
	Kind      string
	Sequence  int
	CreatedAt time.Time
	Summary   string
}

EventRecord is one workflow run event with a bounded, human-safe summary. The summary never contains raw payloads: agent output is content-addressed (refs/digests only), the run_created payload never echoes the snapshot JSON, approval reasons are truncated to the summary bound, and wf_attempt_prompt summaries are REF-ONLY (attempt_id + prompt_ref), never the prompt body.

type EventView

type EventView struct {
	Seq       int    `json:"seq"`
	Timestamp string `json:"timestamp"`
	Kind      string `json:"kind"`
	Detail    string `json:"detail"`
}

EventView is one audit-trail entry for workflow_events.

type EventsPage

type EventsPage struct {
	RunID  string      `json:"run_id"`
	Events []EventView `json:"events"`
	Limit  int         `json:"limit"`
	Offset int         `json:"offset"`
	Count  int         `json:"count"`
}

EventsPage is a paged audit trail.

type InspectView

type InspectView struct {
	RunID             string          `json:"run_id"`
	Step              string          `json:"step"`
	Attempt           int             `json:"attempt"`
	Status            string          `json:"status"`
	CoordinatorRunID  string          `json:"coordinator_run_id,omitempty"`
	TaskID            string          `json:"task_id,omitempty"`
	Output            any             `json:"output,omitempty"`
	OutputRef         string          `json:"output_ref,omitempty"`
	OutputDigest      string          `json:"output_digest,omitempty"`
	OutputText        string          `json:"output_text,omitempty"`
	OutputBytes       int             `json:"output_bytes,omitempty"`
	OutputOffset      int             `json:"output_offset,omitempty"`
	OutputNextOffset  int             `json:"output_next_offset,omitempty"`
	ErrorRef          string          `json:"error_ref,omitempty"`
	ErrorText         string          `json:"error_text,omitempty"`
	EvidenceSelection any             `json:"evidence_selection,omitempty"`
	Transition        *TransitionView `json:"transition,omitempty"`
	StartedAt         string          `json:"started_at,omitempty"`
	FinishedAt        string          `json:"finished_at,omitempty"`
	ElapsedSeconds    int64           `json:"elapsed_seconds,omitempty"`
}

InspectView is the Level-2 step attempt detail for workflow_inspect. The OutputText/OutputBytes/OutputOffset/OutputNextOffset fields page a large artifact: OutputText is one redacted, rune-safe text page (DefaultInspectPageBytes), OutputBytes is the total artifact size (metadata only), OutputOffset is this page's raw-byte offset, and OutputNextOffset is the next page's offset (0 when exhausted). Artifacts larger than MaxPageableBytes are refused outright.

type ListRunsView

type ListRunsView struct {
	Runs   []RunListItem `json:"runs"`
	Limit  int           `json:"limit"`
	Offset int           `json:"offset"`
	Count  int           `json:"count"`
}

ListRunsView lists active and historical runs.

type LoopCounter

type LoopCounter struct {
	RunID      string `json:"run_id"`
	LoopName   string `json:"loop_name"`
	Iterations int    `json:"iterations"`
}

type LoopView

type LoopView struct {
	Name       string `json:"name"`
	Iterations int    `json:"iterations"`
}

LoopView is one named loop counter.

type PanelBindingSnapshot

type PanelBindingSnapshot struct {
	StepID         string `json:"step_id"`
	MemberID       string `json:"member_id"`
	AgentName      string `json:"agent_name"`
	AgentDigest    string `json:"agent_digest"`
	ProviderName   string `json:"provider_name"`
	Model          string `json:"model"`
	SkillDigest    string `json:"skill_digest"`
	TemplateDigest string `json:"template_digest"`
	SchemaDigest   string `json:"schema_digest"`
}

PanelBindingSnapshot pins one static panel member. The key in Snapshot is always <step-id>/<member-id> so one agent name may safely use many bindings.

type PanelCoordinator

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

PanelCoordinator binds every child operation to persisted panel state. It does not execute panel fan-out or aggregation.

func NewPanelCoordinator

func NewPanelCoordinator(workflowRunID string, inner coordinator.Coordinator, repo Repository) PanelCoordinator

func (PanelCoordinator) CancelMember

func (p PanelCoordinator) CancelMember(ctx context.Context, attemptID, memberID string, handle *coordinator.RunHandle) error

func (PanelCoordinator) CancelOrTombstoneMember

func (p PanelCoordinator) CancelOrTombstoneMember(ctx context.Context, attemptID, memberID string) (bool, error)

CancelOrTombstoneMember cancels an admitted member's coordinator child or tombstones one that was never admitted, under the cancel_pending phase (D15). It returns terminal=true once the child is safely known terminal (including a member that was never dispatched, or one that already finished on its own). A non-nil error means the child's terminal state could not be safely verified right now (an ambiguous recovered claim, or a running task with no verifiable live owner) and cancellation must not proceed to a false "canceled" outcome for this member.

func (PanelCoordinator) CancelOrTombstoneSynthesis

func (p PanelCoordinator) CancelOrTombstoneSynthesis(ctx context.Context, attemptID string) (bool, error)

CancelOrTombstoneSynthesis mirrors CancelOrTombstoneMember for the synthesis child. If no synthesis phase-intent exists yet, there is no child to cancel or tombstone: it returns terminal=true immediately.

func (PanelCoordinator) CancelSynthesis

func (p PanelCoordinator) CancelSynthesis(ctx context.Context, attemptID string, handle *coordinator.RunHandle) error

func (PanelCoordinator) EnsureMember

func (p PanelCoordinator) EnsureMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)

func (PanelCoordinator) EnsureRemoteMember

func (p PanelCoordinator) EnsureRemoteMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)

EnsureRemoteMember joins an already remote member without taking it over. A caller that loses the remote state receives ErrWaitOnlyJoinLost and must acquire a local actor permit before a normal ensure.

func (PanelCoordinator) EnsureSynthesis

func (p PanelCoordinator) EnsureSynthesis(ctx context.Context, attemptID string) (*coordinator.RunHandle, error)

func (PanelCoordinator) EnsureTerminalMember

func (p PanelCoordinator) EnsureTerminalMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)

func (PanelCoordinator) EnsureTerminalSynthesis

func (p PanelCoordinator) EnsureTerminalSynthesis(ctx context.Context, attemptID string) (*coordinator.RunHandle, error)

func (PanelCoordinator) JoinMember

func (p PanelCoordinator) JoinMember(ctx context.Context, attemptID, memberID string, handle *coordinator.RunHandle) (*coordinator.RunResult, error)

func (PanelCoordinator) JoinSynthesis

func (p PanelCoordinator) JoinSynthesis(ctx context.Context, attemptID string, handle *coordinator.RunHandle) (*coordinator.RunResult, error)

func (PanelCoordinator) MemberNeedsActorPermit

func (p PanelCoordinator) MemberNeedsActorPermit(ctx context.Context, attemptID, memberID string) (bool, error)

MemberNeedsActorPermit checks whether member admission can create a local actor. Existing remote and terminal children only need a wait-only join.

func (PanelCoordinator) ResumeMember

func (p PanelCoordinator) ResumeMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)

func (PanelCoordinator) ResumeSynthesis

func (p PanelCoordinator) ResumeSynthesis(ctx context.Context, attemptID string) (*coordinator.RunHandle, error)

type PanelExecution

type PanelExecution struct {
	Members         []PanelMemberExecution   `json:"members"`
	SynthesisRunID  string                   `json:"synthesis_run_id"`
	SynthesisTaskID string                   `json:"synthesis_task_id"`
	Synthesis       *PanelSynthesisExecution `json:"synthesis,omitempty"`
	Phase           PanelPhase               `json:"phase"`
}

PanelExecution records all panel child identities and phase state.

type PanelMemberExecution

type PanelMemberExecution struct {
	MemberID         string        `json:"member_id"`
	CoordinatorRunID string        `json:"coordinator_run_id"`
	TaskID           string        `json:"task_id"`
	Work             PanelTaskSpec `json:"work"`
	Order            int           `json:"order"`
}

PanelMemberExecution records one member identity and its exact work.

type PanelPhase

type PanelPhase string

PanelPhase identifies the durable phase of a panel step attempt.

const (
	PanelPhaseMembersAdmitted   PanelPhase = "members_admitted"
	PanelPhaseSynthesisAdmitted PanelPhase = "synthesis_admitted"
	PanelPhaseCancelPending     PanelPhase = "cancel_pending"
)

type PanelSynthesisExecution

type PanelSynthesisExecution struct {
	Work PanelTaskSpec `json:"work"`
}

PanelSynthesisExecution records exact synthesis work after its phase intent.

type PanelTaskSpec

type PanelTaskSpec struct {
	TaskName                      string               `json:"task_name"`
	DependsOn                     []string             `json:"depends_on"`
	InputRef                      string               `json:"input_ref"`
	InputDigest                   string               `json:"input_digest"`
	InputSchemaRef                string               `json:"input_schema_ref"`
	InputSchemaDigest             string               `json:"input_schema_digest"`
	Budget                        int                  `json:"budget"`
	Scope                         string               `json:"scope"`
	AgentName                     string               `json:"agent_name"`
	AgentDigest                   string               `json:"agent_digest"`
	Skill                         string               `json:"skill"`
	Provider                      string               `json:"provider"`
	Model                         string               `json:"model"`
	OutputSchemaDigest            string               `json:"output_schema_digest"`
	OutputSchemaRef               string               `json:"output_schema_ref"`
	Timeout                       time.Duration        `json:"timeout"`
	DeadlineAt                    time.Time            `json:"deadline_at"`
	WorkLimits                    PanelWorkLimits      `json:"work_limits"`
	Policy                        ledgercore.RunPolicy `json:"policy"`
	WorkFingerprint               string               `json:"work_fingerprint"`
	CoordinatorRequestFingerprint string               `json:"coordinator_request_fingerprint"`
}

PanelTaskSpec records exact, non-authority data for one admitted child task.

func (PanelTaskSpec) Validate

func (s PanelTaskSpec) Validate() error

Validate checks that a durable panel task has the fields needed to rebuild work.

type PanelWorkLimits

type PanelWorkLimits = runtime.WorkLimits

PanelWorkLimits is the durable form of runtime work limits.

type Plan

type Plan struct {
	ID         string    `json:"id"`
	Scope      Scope     `json:"scope"`
	Schema     string    `json:"schema,omitempty"`
	PayloadRef string    `json:"payload_ref,omitempty"`
	CreatedAt  time.Time `json:"created_at,omitempty"`
}

Plan is a durable engine-ledger artifact. The ref returned by StorePlan is the plan ID; ReadBackPlan(planRef) reads the record back by that ref. The payload itself lives in content-addressed storage under PayloadRef; this package treats the ref as opaque metadata.

func (Plan) Clone

func (p Plan) Clone() Plan

Clone returns a defensive copy.

type Projection

type Projection struct {
	// Run is the current run snapshot. ActiveStepID is the DERIVED active
	// step (see below); all other fields replay the wf_run_created payload
	// plus status changes. nil when no wf events exist for the run.
	Run *RunSnapshot
	// SnapshotJSON is the canonical snapshot blob from wf_run_created.
	SnapshotJSON []byte
	// Attempts is ordered by event sequence.
	Attempts []StepAttempt
	// Transitions is derived from completed attempts that carried a route,
	// ordered by event sequence.
	Transitions  []TransitionRecord
	LoopCounters []LoopCounter
	Approvals    []ApprovalRecord
	Deliveries   []DeliveryRecord
	// ActiveStepID is the transition target of the NEWEST step-bearing event:
	// a completion's to_step_id, else an attempt's step_id, else the initial
	// step from wf_run_created. Loop/approval/delivery/status events carry no
	// step and are skipped. When the newest target is a reserved terminal step
	// ("success"/"failure") the workflow is done even if the run status CAS
	// was never recorded.
	ActiveStepID string
	// HasRun reports whether any wf_run_created event was seen.
	HasRun bool
}

Projection is the rebuilt in-memory state of one workflow run.

func RebuildProjection

func RebuildProjection(events []storage.Event) (Projection, error)

RebuildProjection deterministically replays wf events in store order (sorted by RowID, then Sequence) into a Projection. Unknown kinds are ignored (foreign coordinator events). All timestamps come from event payloads — never derived at read time. Returns an error only for undecodable payloads of known kinds.

type RecoveredRun

type RecoveredRun struct {
	RunID          string
	WorkflowName   string
	Status         RunStatus
	WasInterrupted bool
	CreatedAt      time.Time
}

RecoveredRun summarises one workflow run for the startup recovery report.

type RecoveryPlan

type RecoveryPlan struct {
	// Run is the current run snapshot.
	Run RunSnapshot
	// AttemptsInFlight are recorded attempts that never reached a terminal
	// status. Each names its stored CoordinatorRunID/TaskID: the caller must
	// JOIN those coordinator runs (query their recorded outcome) before
	// dispatching anything — a recorded attempt is never re-dispatched.
	AttemptsInFlight []StepAttempt
	// NextAttemptNo is the next fresh attempt number for the active step
	// (max recorded attempt_no for that step + 1).
	NextAttemptNo int
	// Terminal is true when the run cannot resume: it is already terminal, or
	// its derived active step is a reserved terminal step (success/failure)
	// even though the status CAS was not recorded.
	Terminal bool
	// TerminalStatus is the status the caller should record when Terminal is
	// true (succeeded for "success", failed for "failure", else the run's
	// current status).
	TerminalStatus RunStatus
	// Reason is a human-readable explanation.
	Reason string
}

RecoveryPlan is the pure, ledger-typed encoding of what resuming a run requires. It is computed from ledger state alone — no coordinator, compiler, matcher or definition imports. Joining the stored coordinator run and re-matching evidence are caller (controller, Phase 4) responsibilities that consume this plan.

func PlanResume

func PlanResume(ctx context.Context, repo Repository, runID string) (RecoveryPlan, error)

PlanResume derives, purely from repository state, what resuming runID requires. Rules: (1) every recorded attempt with a non-terminal status is returned in AttemptsInFlight to be JOINED, never re-dispatched; (2) a run whose derived active step is a reserved terminal step is Terminal even without a recorded status CAS; (3) a terminal run yields Terminal; (4) a delivery_pending run is settled (Terminal) with TerminalStatus delivery_pending — never succeeded, so the resume path cannot CAS it to succeeded and skip delivery. Returns ErrNotFound if the run is absent.

type RefSnapshot

type RefSnapshot struct {
	Digest  string `json:"digest"`
	Version int    `json:"version,omitempty"`
	Bytes   []byte `json:"bytes,omitempty"`
}

RefSnapshot pins one schema, template, or verifier by content digest. Bytes stores bounded content for templates and schemas.

type RepoFactory

type RepoFactory func(ctx context.Context) (Repository, func(), error)

RepoFactory opens a workflow ledger repository. The closer releases resources.

type Repository

type Repository interface {
	// CreateRun admits a run: persists the run snapshot (typed fields + the
	// canonical snapshot JSON) and records the wf_run_created event. Returns
	// ErrDuplicate if the run already exists, ErrInvalidTransition if the
	// snapshot status is not pending.
	CreateRun(ctx context.Context, snap RunSnapshot, snapshotJSON []byte) error

	// GetRun returns the current run snapshot with the DERIVED active step
	// (see Projection.ActiveStepID). Returns ErrNotFound if absent.
	GetRun(ctx context.Context, runID string) (RunSnapshot, error)

	// ListRuns returns bounded snapshots, optionally filtered by status.
	ListRuns(ctx context.Context, status ...RunStatus) ([]RunSnapshot, error)

	// GetRunSnapshot returns the canonical snapshot JSON stored at admission.
	// Returns ErrNotFound if absent.
	GetRunSnapshot(ctx context.Context, runID string) ([]byte, error)

	// CompareAndSetRunStatus atomically transitions the run status, bumping
	// the run version. Returns ErrConflict on version mismatch, ErrInvalidTransition
	// on an illegal edge. finishedAt is persisted when the new status is terminal.
	CompareAndSetRunStatus(ctx context.Context, runID string, expectedVersion uint64, status RunStatus, finishedAt *time.Time) error

	// CreateStepAttempt records a fresh numbered attempt for a step. The
	// (runID, stepID, attemptNo) triple is unique: a second create for the
	// same triple never appends a second event (ErrDuplicate in-process, or
	// ErrConflict when a concurrent writer took the deterministic event ID).
	CreateStepAttempt(ctx context.Context, attempt StepAttempt) error

	// GetStepAttempt returns one attempt. Returns ErrNotFound if absent.
	GetStepAttempt(ctx context.Context, runID, attemptID string) (StepAttempt, error)

	// ListStepAttempts returns the run's attempts ordered by event sequence.
	ListStepAttempts(ctx context.Context, runID string) ([]StepAttempt, error)

	// CompleteStepAttempt atomically records an attempt's terminal outcome
	// (status + optional route/output evidence in ONE event) under CAS on the
	// attempt version. Returns ErrConflict on version mismatch, ErrInvalidTransition
	// for a non-terminal outcome status or an illegal status edge.
	CompleteStepAttempt(ctx context.Context, runID, attemptID string, expectedVersion uint64, outcome AttemptOutcome) error

	// RecordStepAttemptOutcome records a fresh numbered attempt and its
	// TERMINAL outcome in ONE durable wf_attempt_completed event: the attempt
	// is never observable in a non-terminal state. It mirrors
	// CreateStepAttempt's (runID, stepID, attemptNo) triple and AttemptID
	// uniqueness (ErrDuplicate for a taken key) and CompleteStepAttempt's
	// outcome rules (ErrInvalidTransition for a non-terminal outcome status or
	// an illegal status edge, including the no-route-on-interrupted/canceled/
	// timed_out rule and the MaxEvidenceBytes cap). The recorded attempt
	// carries Version 1 and StartedAt == FinishedAt == the append instant.
	// Returns ErrNotFound if the run is absent.
	RecordStepAttemptOutcome(ctx context.Context, attempt StepAttempt, outcome AttemptOutcome) error

	// CompareAndSetPanelPhase records one claim-fenced panel phase intent.
	CompareAndSetPanelPhase(ctx context.Context, runID string, attemptID string, expectedVersion uint64, from PanelPhase, to PanelPhase, synthesis *PanelSynthesisExecution) error

	// SetStepAttemptPrompt records the content-addressed prompt reference for
	// one attempt (the prompt body lives in content-addressed storage and is
	// looked up via PromptRef; the event log never carries prompt text). The
	// attempt may still be Running — the prompt is persisted at dispatch time,
	// before completion — and its status/version are never changed. Setting the
	// same promptRef twice is an idempotent no-op. Returns ErrNotFound if the
	// run or attempt is absent; ErrConflict if the attempt already carries a
	// prompt ref different from promptRef (attempts are immutable after
	// dispatch) or a concurrent writer took the deterministic event ID with a
	// different payload.
	SetStepAttemptPrompt(ctx context.Context, runID, attemptID, promptRef string) error

	// SetStepAttemptExecution durably records the child identity used by the
	// current execution of an attempt, together with the reason for a
	// re-dispatch when one exists (a transient retry records the provider
	// error text that triggered it; an initial dispatch records none). It is
	// idempotent for the same identity.
	SetStepAttemptExecution(ctx context.Context, runID, attemptID, coordinatorRunID, taskID, reason string) error

	// SetStepAttemptHeartbeat durably records one liveness observation for a
	// RUNNING attempt. Each call appends a DISTINCT wf_attempt_heartbeat event
	// (the event ID embeds the heartbeat timestamp), so successive ticks never
	// conflict and a retried append of the same heartbeat is an idempotent
	// no-op (nil). The attempt's status/version are never changed. Returns
	// ErrNotFound if the run or attempt is absent.
	SetStepAttemptHeartbeat(ctx context.Context, runID, attemptID string, heartbeatAt time.Time) error

	// ListTransitions returns the route decisions derived from completed
	// attempts, ordered by event sequence.
	ListTransitions(ctx context.Context, runID string) ([]TransitionRecord, error)

	// IncrementLoopCounter mints the next iteration number for a named loop
	// under the run claim, after catch-up. Counters are derived state: the
	// returned number is persisted via a wf_loop_incremented event and rebuilt
	// on reopen. Returns ErrNotFound if the run is absent.
	IncrementLoopCounter(ctx context.Context, runID, loopName string) (int, error)

	// GetLoopCounters returns the run's derived loop counters.
	GetLoopCounters(ctx context.Context, runID string) ([]LoopCounter, error)

	// CreateApproval records a pending human-gate request (provisional).
	CreateApproval(ctx context.Context, a ApprovalRecord) error

	// ResolveApproval resolves a pending approval to approved or rejected.
	ResolveApproval(ctx context.Context, runID, approvalID, actor, status, reason string) error

	// ListApprovals returns the run's approval records.
	ListApprovals(ctx context.Context, runID string) ([]ApprovalRecord, error)

	// UpsertDelivery records a delivery attempt keyed by idempotency key.
	UpsertDelivery(ctx context.Context, d DeliveryRecord) error

	// GetDeliveryByIdempotencyKey returns the delivery record for a key.
	// Returns ErrNotFound if absent.
	GetDeliveryByIdempotencyKey(ctx context.Context, key string) (DeliveryRecord, error)

	// ListDeliveries returns the run's delivery records.
	ListDeliveries(ctx context.Context, runID string) ([]DeliveryRecord, error)

	// ListEvents returns the run's audit trail, ordered by event sequence,
	// paged (limit <= 0 means DefaultEventPageSize, offset skips events).
	// Summaries are bounded and never contain raw payloads. Unknown kinds
	// and undecodable payloads are skipped. Returns ErrNotFound when the
	// run is absent.
	ListEvents(ctx context.Context, runID string, limit, offset int) ([]EventRecord, error)

	// DeleteRun removes a settled run's durable record: the wf_run_deleted
	// tombstone plus every prior event and the run's claim are removed from
	// the store, and the in-memory projection is dropped. Shared
	// content-addressed blobs are never deleted. Returns ErrNotFound when
	// the run has no record (never created or already deleted). The caller
	// must hold the execution lock and a claim (or otherwise guarantee no
	// concurrent writer) before calling.
	DeleteRun(ctx context.Context, runID string) error

	// RecordRunResumed appends the wf_run_resumed audit event for a run that
	// is being resumed (crash recovery, operator resume, or controller
	// re-entry). It mutates no run state; the event is purely observational.
	// Returns ErrNotFound when the run is absent.
	RecordRunResumed(ctx context.Context, runID string) error

	// ClaimRun acquires the exclusive execution claim on a run. Returns
	// ErrClaimHeld if another holder owns it. Same-holder refresh succeeds.
	ClaimRun(ctx context.Context, runID, holder string) error

	// RefreshRunClaim refreshes the claim's acquired_at ONLY when holder
	// already owns the claim row. It never inserts a missing row: a holder
	// whose claim is gone returns ErrClaimNotHeld. Run execution heartbeats
	// use this so a displaced or expired holder is treated as lost instead of
	// reclaiming itself (F2).
	RefreshRunClaim(ctx context.Context, runID, holder string) error

	// TakeoverRunClaim atomically replaces any existing claim with holder.
	TakeoverRunClaim(ctx context.Context, runID, holder string) error
	TakeoverExpiredRunClaim(ctx context.Context, runID, holder string, maxAge time.Duration) error

	// ReleaseRun releases the claim; only the current holder may. Returns
	// ErrClaimNotHeld otherwise.
	ReleaseRun(ctx context.Context, runID, holder string) error

	// ClearRunClaim force-releases any claim regardless of holder (explicit
	// operator force-release for stale claims; Recover clears claims only on
	// terminal runs).
	ClearRunClaim(ctx context.Context, runID string) error

	// GetRunClaim reads the run's current execution claim as a read-only
	// liveness probe: the holder and the claim's last acquired_at (the
	// holder's heartbeat refreshes it). ok=false means the run has no claim
	// or the backend cannot expose claims; err is reserved for backend
	// failures. It never mutates claim state.
	GetRunClaim(ctx context.Context, runID string) (holder string, acquiredAt time.Time, ok bool, err error)

	// StoreContent persists bytes under a content-addressed reference
	// (shared content store; idempotent).
	StoreContent(ctx context.Context, ref string, data []byte) error

	// LoadContent retrieves stored bytes. Returns ErrContentNotFound if absent.
	LoadContent(ctx context.Context, ref string) ([]byte, error)

	// Recover brings the projection up to date, classifies every run, and
	// clears stale claims on terminal runs only. It mutates no run status.
	Recover(ctx context.Context) ([]RecoveredRun, error)
}

Repository is the durable storage boundary for workflow runs. Implementations must be concurrency-safe and return defensive copies.

Concurrency contract: mutations are serialized per run. When a caller holds an execution claim (ClaimRun), only that holder can mutate the run. The repository enforces intra-process serialization and claim fencing. CAS methods take the caller's observed version and fail with ErrConflict when the recorded version has moved.

func UnsetRepoFactory

func UnsetRepoFactory(context.Context) (Repository, func(), error)

UnsetRepoFactory is used when tools register before a ledger is available.

type RunListItem

type RunListItem struct {
	RunID     string `json:"run_id"`
	Workflow  string `json:"workflow"`
	Status    string `json:"status"`
	Age       string `json:"age,omitempty"`
	StartedAt string `json:"started_at,omitempty"`
	// ActiveStep is the run's current step id, empty for a terminal run or
	// one that hasn't started its first step yet.
	ActiveStep string `json:"active_step,omitempty"`
	// LastHeartbeatAt mirrors AttemptView's field of the same name, for the
	// active step's newest attempt - RFC3339 UTC, empty when the run is
	// terminal, has no active step, or that attempt hasn't heartbeated yet.
	// A caller rendering a live list (e.g. a desktop app's run list) needs
	// this without a second per-run round trip through workflow_status.
	LastHeartbeatAt string `json:"last_heartbeat_at,omitempty"`
	// DeliveryClaimHeld and DeliveryClaimAt mirror the TUI's own delivery
	// liveness surface (internal/cli/workflow_run_dialog.go's
	// workflowRunDeliveryClaim / workflowDeliveryClaimLine): only populated
	// for a DELIVERY_PENDING run. Held=true with a fresh ClaimAt means a
	// delivery attempt is actually in flight right now; Held=true with a
	// stale ClaimAt means one crashed mid-publish; Held=false means the run
	// is simply parked waiting for someone to call workflow_deliver. Without
	// this a caller has no way to distinguish "actively delivering" from
	// "waiting indefinitely" for a DELIVERY_PENDING run - LastHeartbeatAt
	// freezes once the run's last step attempt finishes and says nothing
	// about delivery activity.
	DeliveryClaimHeld bool   `json:"delivery_claim_held,omitempty"`
	DeliveryClaimAt   string `json:"delivery_claim_at,omitempty"`
}

RunListItem is one row from workflow_list_runs.

type RunSnapshot

type RunSnapshot struct {
	RunID            string     `json:"run_id"`
	InvocationKey    string     `json:"invocation_key,omitempty"`
	WorkflowName     string     `json:"workflow_name"`
	WorkflowDigest   string     `json:"workflow_digest"`
	SnapshotDigest   string     `json:"snapshot_digest"`
	InputDigest      string     `json:"input_digest"`
	Status           RunStatus  `json:"status"`
	ActiveStepID     string     `json:"active_step_id"`
	BaseRef          string     `json:"base_ref,omitempty"`
	BaseCommit       string     `json:"base_commit,omitempty"`
	OriginBaseCommit string     `json:"origin_base_commit,omitempty"`
	WorktreeName     string     `json:"worktree_name,omitempty"`
	RemoteURL        string     `json:"remote_url,omitempty"`
	Version          uint64     `json:"version"`
	StartedAt        time.Time  `json:"started_at"`
	DeadlineAt       *time.Time `json:"deadline_at,omitempty"`
	FinishedAt       *time.Time `json:"finished_at,omitempty"`
}

func (RunSnapshot) Clone

func (s RunSnapshot) Clone() RunSnapshot

Clone returns a deep copy.

type RunStatus

type RunStatus string
const (
	RunStatusPending         RunStatus = "pending"
	RunStatusRunning         RunStatus = "running"
	RunStatusWaitingApproval RunStatus = "waiting_approval"
	RunStatusDeliveryPending RunStatus = "delivery_pending"
	RunStatusSucceeded       RunStatus = "succeeded"
	RunStatusFailed          RunStatus = "failed"
	RunStatusCanceled        RunStatus = "canceled"
	RunStatusTimedOut        RunStatus = "timed_out"
	RunStatusDeliveryFailed  RunStatus = "delivery_failed"
)

type Scope

type Scope struct {
	Type string `json:"type"`
	ID   string `json:"id"`
}

Scope identifies one engine entity: a session, a workflow step, an agent, a workflow, or a run.

type Service

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

Service is the in-process host for the eight workflow tools. Read methods use only ledger projections. Mutating methods call Engine.

func NewService

func NewService(opts ServiceOptions) (*Service, error)

NewService builds a Service from options.

func (*Service) Cancel

func (s *Service) Cancel(ctx context.Context, runID string) (CancelResult, error)

Cancel settles a non-terminal run to canceled.

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, runID string, force bool) (DeleteResult, error)

Delete removes a run from the ledger. force is the crash-recovery override that also permits non-terminal (pending/running/waiting_approval) runs stranded by a dead executor; a live claim is refused either way.

func (*Service) Deliver

func (s *Service) Deliver(ctx context.Context, runID string, allowPublish bool) (DeliverResult, error)

Deliver publishes a delivery_pending run when allow_publish is true.

func (*Service) Engine

func (s *Service) Engine() Engine

Engine returns the mutating engine, or nil when none is configured. The dialog surfaces use it to route cancel/resume/deliver/delete through the session engine instance so in-process controllers started by this session can be stopped before the fenced ledger settlement.

func (*Service) Events

func (s *Service) Events(ctx context.Context, runID string, limit, offset int) (EventsPage, error)

Events returns a paged audit trail from the ledger.

func (*Service) Inspect

func (s *Service) Inspect(ctx context.Context, runID, step string, attemptNo, offset, limit int) (InspectView, error)

Inspect returns one step attempt's validated output and route decision. offset/limit page large output artifacts: limit 0 means the default page size; both must be >= 0 (the tool schema enforces the same minimum). The final view is also guarded by the inspect result budget: a page that would marshal over it is halved once and rebuilt before returning (bounded; the tool's encodeJSON remains the outer fail-closed guard).

func (*Service) ListRuns

func (s *Service) ListRuns(ctx context.Context, statusFilter string, limit, offset int) (ListRunsView, error)

ListRuns lists active and historical runs with optional status filter.

func (*Service) Run

func (s *Service) Run(ctx context.Context, req StartRequest) (StartResult, error)

Run starts or resumes a workflow run without waiting for terminal state.

func (*Service) SetEngine

func (s *Service) SetEngine(engine Engine)

SetEngine replaces the mutating engine (e.g. after session dispatcher attach).

func (*Service) Status

func (s *Service) Status(ctx context.Context, runID string) (StatusView, error)

Status returns a deep run overview from ledger projections only.

type ServiceOptions

type ServiceOptions struct {
	// Engine performs run/cancel/deliver. Nil refuses mutating calls.
	Engine Engine
	// Repo opens the workflow ledger. Required for read tools.
	Repo RepoFactory
	// Optional result budget overrides (bytes). Zero keeps package defaults.
	StatusBudgetBytes  int
	EventsBudgetBytes  int
	InspectBudgetBytes int
	ListBudgetBytes    int
}

ServiceOptions configures a Service.

type Snapshot

type Snapshot struct {
	SchemaVersion    int    `json:"schema_version"`
	DefinitionTOML   []byte `json:"definition_toml"`
	DefinitionDigest string `json:"definition_digest"`
	// MCPConfigDigest pins the enabled MCP authority without storing server
	// commands, URLs, headers, environment names, or values.
	MCPConfigDigest string                          `json:"mcp_config_digest,omitempty"`
	Inputs          map[string]string               `json:"inputs,omitempty"`
	Agents          map[string]AgentSnapshot        `json:"agents,omitempty"`
	PanelBindings   map[string]PanelBindingSnapshot `json:"panel_bindings,omitempty"`
	Schemas         map[string]RefSnapshot          `json:"schemas,omitempty"`
	Templates       map[string]RefSnapshot          `json:"templates,omitempty"`
	Skills          map[string]RefSnapshot          `json:"skills,omitempty"`
	Verifiers       map[string]RefSnapshot          `json:"verifiers,omitempty"`
	// VerifierPinsVersion marks snapshots admitted by a binary that pins
	// verifier definitions (verifier-def: keys in Verifiers). 0 means the run
	// predates definition pinning and resumes without definition
	// verification; >= 1 means a referenced definition whose key is absent
	// was stripped, not merely never written, and resume fails closed.
	VerifierPinsVersion int               `json:"verifier_pins_version,omitempty"`
	Delivery            *DeliverySnapshot `json:"delivery,omitempty"`
}

Snapshot is the immutable admission record of one workflow run. It freezes the raw workflow definition file bytes (the canonical artifact), the compiler digest of the compiled definition, the validated inputs, and the resolved agent/schema/template/verifier references. Resume never re-reads a changed TOML file: everything needed is in this snapshot.

func UnmarshalSnapshot

func UnmarshalSnapshot(data []byte) (Snapshot, error)

UnmarshalSnapshot decodes a canonical snapshot JSON blob.

func (Snapshot) Validate

func (s Snapshot) Validate() error

Validate checks the snapshot for admission invariants: schema version supported, non-empty definition bytes and digest, and digest/bytes consistency for every populated schema, template, skill, and verifier ref.

type StartRequest

type StartRequest struct {
	// Workflow is the discovered workflow name (required for a new run).
	Workflow string
	// Inputs are validated name→value pairs from the tool call.
	Inputs map[string]any
	// InvocationKey identifies one caller request across retries. When set for
	// a new run, the engine derives a stable run ID and admits it once.
	InvocationKey string
	// AllowPublish is the explicit publication gate for the workflow_deliver
	// tool and the CLI --allow-publish flag. The session harness does NOT
	// consult it for auto-delivery: a workflow whose [delivery] policy is
	// active is published automatically (the policy is the publication
	// grant), so a delivery-capable run is never stranded by a missing flag.
	AllowPublish bool
	// Resume, when true, resumes RunID from the durable ledger snapshot.
	Resume bool
	// RunID is required when Resume is true.
	RunID string
	// Force clears a stale claim before resume (operator-confirmed).
	Force bool
}

StartRequest admits a new workflow run or resumes an interrupted one.

type StartResult

type StartResult struct {
	RunID    string `json:"run_id"`
	Status   string `json:"status"`
	Workflow string `json:"workflow,omitempty"`
	Resumed  bool   `json:"resumed,omitempty"`
}

StartResult is the immediate response from workflow_run (non-blocking).

type StatusView

type StatusView struct {
	RunID      string         `json:"run_id"`
	Workflow   string         `json:"workflow"`
	Status     string         `json:"status"`
	ActiveStep string         `json:"active_step"`
	Version    uint64         `json:"version"`
	StartedAt  string         `json:"started_at,omitempty"`
	DeadlineAt string         `json:"deadline_at,omitempty"`
	FinishedAt string         `json:"finished_at,omitempty"`
	BaseRef    string         `json:"base_ref,omitempty"`
	BaseCommit string         `json:"base_commit,omitempty"`
	Worktree   string         `json:"worktree,omitempty"`
	Attempts   []AttemptView  `json:"attempts"`
	Loops      []LoopView     `json:"loops,omitempty"`
	Delivery   []DeliveryView `json:"delivery,omitempty"`
	Approvals  []ApprovalView `json:"approvals,omitempty"`
	// DeliveryClaimHeld/DeliveryClaimAt mirror RunListItem's fields of the
	// same name - see that doc comment. Only populated for a
	// DELIVERY_PENDING run.
	DeliveryClaimHeld bool   `json:"delivery_claim_held,omitempty"`
	DeliveryClaimAt   string `json:"delivery_claim_at,omitempty"`
}

StatusView is the Level-1 observability payload for workflow_status.

type StepAttempt

type StepAttempt struct {
	AttemptID        string          `json:"attempt_id"`
	RunID            string          `json:"run_id"`
	StepID           string          `json:"step_id"`
	AttemptNo        int             `json:"attempt_no"`
	Status           AttemptStatus   `json:"status"`
	CoordinatorRunID string          `json:"coordinator_run_id,omitempty"`
	TaskID           string          `json:"task_id,omitempty"`
	Executions       []StepExecution `json:"executions,omitempty"`
	OutputRef        string          `json:"output_ref,omitempty"`
	OutputDigest     string          `json:"output_digest,omitempty"`
	ErrorRef         string          `json:"error_ref,omitempty"`
	ToStepID         string          `json:"to_step_id,omitempty"`
	TransitionIndex  int             `json:"transition_index,omitempty"`
	MatchDigest      string          `json:"match_digest,omitempty"`
	PromptRef        string          `json:"prompt_ref,omitempty"`
	DecisionJSON     []byte          `json:"decision_json,omitempty"`
	EvidenceJSON     []byte          `json:"evidence_json,omitempty"`
	PanelExecution   *PanelExecution `json:"panel_execution,omitempty"`
	LastHeartbeatAt  time.Time       `json:"last_heartbeat_at,omitempty"`
	StartedAt        time.Time       `json:"started_at"`
	FinishedAt       *time.Time      `json:"finished_at,omitempty"`
	Version          uint64          `json:"version"`
}

func (StepAttempt) Clone

func (s StepAttempt) Clone() StepAttempt

Clone returns a deep copy.

type StepExecution

type StepExecution struct {
	ExecutionNo      int       `json:"execution_no"`
	CoordinatorRunID string    `json:"coordinator_run_id"`
	TaskID           string    `json:"task_id"`
	StartedAt        time.Time `json:"started_at"`
}

StepExecution identifies one coordinator child that runs a step attempt. Executions remain ordered by ExecutionNo. The parent attempt mirrors the newest execution in CoordinatorRunID and TaskID for compatibility.

type StorageRepository

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

StorageRepository is the durable Repository implementation, event-sourced over a shared storage.Store (the same instance the coordinator uses — same SQLite file, same content-addressed content table, same run_claims table). It is a NON-OWNING user of the store: Close() releases only the claims this instance holds and never closes the borrowed store.

func NewMemoryRepository

func NewMemoryRepository() *StorageRepository

NewMemoryRepository returns a repository over a fresh in-memory store.

func NewStorageRepository

func NewStorageRepository(store storage.Store) *StorageRepository

NewStorageRepository wraps a shared storage.Store (non-owning).

func (*StorageRepository) ClaimRun

func (s *StorageRepository) ClaimRun(ctx context.Context, runID, holder string) error

ClaimRun acquires the exclusive execution claim on a run. Returns ErrClaimHeld if another holder owns it. Same-holder refresh succeeds.

func (*StorageRepository) ClearRunClaim

func (s *StorageRepository) ClearRunClaim(ctx context.Context, runID string) error

ClearRunClaim removes a run claim for an operator force release.

func (*StorageRepository) Close

func (s *StorageRepository) Close() error

Close releases claims held by this instance and marks the repository closed.

func (*StorageRepository) CompareAndSetPanelPhase

func (s *StorageRepository) CompareAndSetPanelPhase(ctx context.Context, runID string, attemptID string, expectedVersion uint64, from PanelPhase, to PanelPhase, synthesis *PanelSynthesisExecution) error

CompareAndSetPanelPhase stores one panel phase intent under the workflow claim.

func (*StorageRepository) CompareAndSetRunStatus

func (s *StorageRepository) CompareAndSetRunStatus(ctx context.Context, runID string, expectedVersion uint64, status RunStatus, finishedAt *time.Time) error

CompareAndSetRunStatus atomically transitions the run status, bumping the run version. Returns ErrConflict on version mismatch, ErrInvalidTransition on an illegal edge. finishedAt is persisted when the new status is terminal.

func (*StorageRepository) CompleteStepAttempt

func (s *StorageRepository) CompleteStepAttempt(ctx context.Context, runID, attemptID string, expectedVersion uint64, outcome AttemptOutcome) error

CompleteStepAttempt atomically records an attempt's terminal outcome (status + optional route/output evidence in ONE event) under CAS on the attempt version. Returns ErrConflict on version mismatch, ErrInvalidTransition for a non-terminal outcome status or an illegal status edge.

func (*StorageRepository) CreateApproval

func (s *StorageRepository) CreateApproval(ctx context.Context, a ApprovalRecord) error

CreateApproval records a pending human-gate request (provisional).

func (*StorageRepository) CreateRun

func (s *StorageRepository) CreateRun(ctx context.Context, snap RunSnapshot, snapshotJSON []byte) error

CreateRun admits a run: persists the run snapshot (typed fields + the canonical snapshot JSON) and records the wf_run_created event. Returns ErrDuplicate if the run already exists, ErrInvalidTransition if the snapshot status is not pending.

func (*StorageRepository) CreateStepAttempt

func (s *StorageRepository) CreateStepAttempt(ctx context.Context, attempt StepAttempt) error

CreateStepAttempt records a fresh numbered attempt for a step. The (runID, stepID, attemptNo) triple is unique: a second create for the same triple never appends a second event (ErrDuplicate in-process, or ErrConflict when a concurrent writer took the deterministic event ID).

func (*StorageRepository) DeleteRun

func (s *StorageRepository) DeleteRun(ctx context.Context, runID string) error

DeleteRun removes a settled run and its derived in-memory data.

func (*StorageRepository) GetDeliveryByIdempotencyKey

func (s *StorageRepository) GetDeliveryByIdempotencyKey(ctx context.Context, key string) (DeliveryRecord, error)

GetDeliveryByIdempotencyKey returns the delivery record for a key. Returns ErrNotFound if absent.

func (*StorageRepository) GetLoopCounters

func (s *StorageRepository) GetLoopCounters(ctx context.Context, runID string) ([]LoopCounter, error)

GetLoopCounters returns the run's derived loop counters.

func (*StorageRepository) GetRun

func (s *StorageRepository) GetRun(ctx context.Context, runID string) (RunSnapshot, error)

GetRun returns the current run snapshot with the DERIVED active step (see Projection.ActiveStepID). Returns ErrNotFound if absent.

func (*StorageRepository) GetRunClaim

func (s *StorageRepository) GetRunClaim(ctx context.Context, runID string) (holder string, acquiredAt time.Time, ok bool, err error)

GetRunClaim reads the run's current execution claim as a pure liveness probe.

func (*StorageRepository) GetRunSnapshot

func (s *StorageRepository) GetRunSnapshot(ctx context.Context, runID string) ([]byte, error)

GetRunSnapshot returns the canonical snapshot JSON stored at admission. Returns ErrNotFound if absent.

func (*StorageRepository) GetStepAttempt

func (s *StorageRepository) GetStepAttempt(ctx context.Context, runID, attemptID string) (StepAttempt, error)

GetStepAttempt returns one attempt. Returns ErrNotFound if absent.

func (*StorageRepository) IncrementLoopCounter

func (s *StorageRepository) IncrementLoopCounter(ctx context.Context, runID, loopName string) (int, error)

IncrementLoopCounter mints the next iteration number for a named loop under the run claim, after catch-up. Counters are derived state: the returned number is persisted via a wf_loop_incremented event and rebuilt on reopen. Returns ErrNotFound if the run is absent.

func (*StorageRepository) IsRunHeld

func (s *StorageRepository) IsRunHeld(ctx context.Context, runID string) (bool, error)

IsRunHeld reports whether runID currently has an active claim.

func (*StorageRepository) IsRunTokenFenced

func (s *StorageRepository) IsRunTokenFenced(ctx context.Context, runID, token string) (bool, error)

IsRunTokenFenced reports whether token has been fenced out of runID.

func (*StorageRepository) ListApprovals

func (s *StorageRepository) ListApprovals(ctx context.Context, runID string) ([]ApprovalRecord, error)

ListApprovals returns the run's approval records.

func (*StorageRepository) ListDeliveries

func (s *StorageRepository) ListDeliveries(ctx context.Context, runID string) ([]DeliveryRecord, error)

ListDeliveries returns the run's delivery records.

func (*StorageRepository) ListEvents

func (s *StorageRepository) ListEvents(ctx context.Context, runID string, limit, offset int) ([]EventRecord, error)

ListEvents returns the run's audit trail, ordered by event sequence. The listing is paged over the DECODABLE stream: unknown/undecodable events are filtered out first, then limit/offset slice the decodable events, so each page holds up to `limit` decodable events and never comes back short while decodable events remain. limit <= 0 means DefaultEventPageSize, offset skips that many decodable events. A limit/offset large enough to overflow the page bounds is clamped to the trail (an offset past the trail is an empty page), never a slice-bounds panic. Events whose kind or payload is not a known wf_* shape are skipped (matching the projection's tolerance), so a listing never fails on a foreign or undecodable event. Returns ErrNotFound when the run is absent.

func (*StorageRepository) ListRuns

func (s *StorageRepository) ListRuns(ctx context.Context, status ...RunStatus) ([]RunSnapshot, error)

ListRuns returns bounded snapshots, optionally filtered by status.

func (*StorageRepository) ListStepAttempts

func (s *StorageRepository) ListStepAttempts(ctx context.Context, runID string) ([]StepAttempt, error)

ListStepAttempts returns the run's attempts ordered by event sequence.

func (*StorageRepository) ListTransitions

func (s *StorageRepository) ListTransitions(ctx context.Context, runID string) ([]TransitionRecord, error)

ListTransitions returns the route decisions derived from completed attempts, ordered by event sequence.

func (*StorageRepository) LoadContent

func (s *StorageRepository) LoadContent(ctx context.Context, ref string) ([]byte, error)

LoadContent retrieves stored bytes. It returns ErrContentNotFound if absent.

func (*StorageRepository) RecordRunResumed

func (s *StorageRepository) RecordRunResumed(ctx context.Context, runID string) error

RecordRunResumed appends the wf_run_resumed audit event for a run that a controller is resuming (crash recovery, operator resume, or controller re-entry). It mutates no run state: the event is purely observational, so the projection ignores it. Returns ErrNotFound when the run is absent. The deterministic event ID is (runID, kind) and the payload carries only the run id, so a retried resume under the real clock appends at most one event (the second write is the idempotent retry path of appendEvent).

func (*StorageRepository) RecordStepAttemptOutcome

func (s *StorageRepository) RecordStepAttemptOutcome(ctx context.Context, attempt StepAttempt, outcome AttemptOutcome) error

RecordStepAttemptOutcome records a fresh numbered attempt and its TERMINAL outcome in ONE wf_attempt_completed event: the attempt is never observable in a non-terminal state. Uniqueness mirrors CreateStepAttempt (the (runID, stepID, attemptNo) triple and the AttemptID are both unique — ErrDuplicate), and the outcome rules mirror CompleteStepAttempt (ErrInvalidTransition for a non-terminal outcome or an illegal edge, including the no-route-on-interrupted/canceled/timed_out rule and the MaxEvidenceBytes cap). The recorded attempt carries Version 1 with StartedAt == FinishedAt == now. The completed payload carries the fresh attempt's StepID/AttemptNo/StartedAt identity, which the replay restores so LatestFailureText and the repair budget keep working after a rebuild.

func (*StorageRepository) Recover

func (s *StorageRepository) Recover(ctx context.Context) ([]RecoveredRun, error)

Recover brings the projection up to date, classifies every run, and clears stale claims on terminal runs only. It mutates no run status.

Carve-out: a delivery_pending run parked at a reserved terminal step ("success"/"failure") keeps its claim. The delivery phase runs after the success terminal, outside the step graph, and the live publisher holds the claim for the whole publish; unconditionally clearing it here would let a second host claim the run and double-deliver. Stale delivery claims are reclaimed by the delivery path's lease takeover (TakeoverExpiredRunClaim with DefaultClaimLease), never by Recover.

func (*StorageRepository) RefreshRunClaim

func (s *StorageRepository) RefreshRunClaim(ctx context.Context, runID, holder string) error

RefreshRunClaim refreshes the claim's acquired_at ONLY when this repository already holds the claim row.

func (*StorageRepository) ReleaseRun

func (s *StorageRepository) ReleaseRun(ctx context.Context, runID, holder string) error

ReleaseRun releases the claim. Only the current holder may release it.

func (*StorageRepository) ResolveApproval

func (s *StorageRepository) ResolveApproval(ctx context.Context, runID, approvalID, actor, status, reason string) error

ResolveApproval resolves a pending approval to approved or rejected.

func (*StorageRepository) SetStepAttemptExecution

func (s *StorageRepository) SetStepAttemptExecution(ctx context.Context, runID, attemptID, coordinatorRunID, taskID, reason string) error

SetStepAttemptExecution records the active child identity before dispatch, together with the reason for the re-dispatch when one exists (a transient retry records the provider error text that triggered it; an initial dispatch records none). This closes the crash window where a transient retry has a new child in memory but the ledger still points at the old child.

func (*StorageRepository) SetStepAttemptHeartbeat

func (s *StorageRepository) SetStepAttemptHeartbeat(ctx context.Context, runID, attemptID string, heartbeatAt time.Time) error

SetStepAttemptHeartbeat durably records one liveness observation for a RUNNING attempt. Each call appends ONE wf_attempt_heartbeat event whose deterministic event ID embeds the heartbeat timestamp, so successive ticks (distinct HeartbeatAt) append DISTINCT events and a later tick can never collide with an earlier one (replay-safe: no ErrConflict on later ticks). The payload is byte-identical for a given HeartbeatAt (CreatedAt mirrors it), so a retried append of the SAME heartbeat dedupes on the event ID and returns nil (idempotent), never ErrConflict. The attempt's status/version are never changed and no step candidate is contributed, exactly like the replay. Returns ErrNotFound if the run or attempt is absent.

func (*StorageRepository) SetStepAttemptPrompt

func (s *StorageRepository) SetStepAttemptPrompt(ctx context.Context, runID, attemptID, promptRef string) error

SetStepAttemptPrompt records the content-addressed prompt reference for one attempt (the prompt body lives in content-addressed storage; only the ref is persisted). Unlike CompleteStepAttempt it does NOT require a terminal status: the prompt is written at dispatch time, while the attempt is Running, and the attempt's status/version are never changed. Setting the same promptRef twice is an idempotent no-op; setting a DIFFERENT promptRef on an attempt that already has one returns ErrConflict (attempts are immutable after dispatch). Returns ErrNotFound if the run or attempt is absent.

func (*StorageRepository) SetTimeSource

func (s *StorageRepository) SetTimeSource(now func() time.Time)

SetTimeSource replaces the clock for deterministic tests.

func (*StorageRepository) StoreContent

func (s *StorageRepository) StoreContent(ctx context.Context, ref string, data []byte) error

StoreContent persists bytes under a content-addressed reference.

func (*StorageRepository) TakeoverExpiredRunClaim

func (s *StorageRepository) TakeoverExpiredRunClaim(ctx context.Context, runID, holder string, maxAge time.Duration) error

TakeoverExpiredRunClaim replaces a claim only when its age exceeds maxAge.

func (*StorageRepository) TakeoverRunClaim

func (s *StorageRepository) TakeoverRunClaim(ctx context.Context, runID, holder string) error

TakeoverRunClaim atomically replaces any existing execution claim.

func (*StorageRepository) UpsertDelivery

func (s *StorageRepository) UpsertDelivery(ctx context.Context, d DeliveryRecord) error

UpsertDelivery records a delivery attempt keyed by idempotency key.

type Store

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

Store is the durable, concurrency-safe plan and task ledger. It is event-sourced over a shared storage.Store (the same primitive the workflow ledger builds on): every mutation appends one durable event, and the in-memory projection is rebuilt from the event log on catch-up, so state survives restarts and is atomic per mutation. The package is a NON-OWNING user of the store: it never closes it.

func NewMemoryStore

func NewMemoryStore() *Store

NewMemoryStore returns a store over a fresh in-memory backend.

func NewStore

func NewStore(store storage.Store) *Store

NewStore wraps a shared storage.Store (non-owning).

func (*Store) BindPlanToScope

func (s *Store) BindPlanToScope(planID string, scope Scope) error

BindPlanToScope re-binds an existing plan to a scope. Binding the scope it already carries is an idempotent no-op.

func (*Store) CreateTask

func (s *Store) CreateTask(task Task) error

CreateTask durably records a task under an existing plan. Re-creating an identical record is an idempotent no-op; the same (plan, task) with different content returns ErrTaskDuplicate.

func (*Store) GetTask

func (s *Store) GetTask(planRef, taskID string) (Task, error)

GetTask returns a defensive copy of one task.

func (*Store) ListTasksByScope

func (s *Store) ListTasksByScope(scope Scope) ([]Task, error)

ListTasksByScope returns defensive copies of every task bound to scope. A scope with an empty ID matches every ID of that type (for example Scope{Type: ScopeRun, ID: ""} returns all run-bound tasks). Order is deterministic (plan ref, then task ID).

func (*Store) ListTransitions

func (s *Store) ListTransitions(planRef string) ([]Transition, error)

ListTransitions returns the plan's append-only journal in call order.

func (*Store) ReadBackPlan

func (s *Store) ReadBackPlan(planRef string) (Plan, error)

ReadBackPlan returns a defensive copy of a stored plan by ref.

func (*Store) SetTimeSource

func (s *Store) SetTimeSource(now func() time.Time)

SetTimeSource replaces the clock for deterministic tests.

func (*Store) StorePlan

func (s *Store) StorePlan(plan Plan) (string, error)

StorePlan durably stores a plan and returns its ref (the plan ID). Re-storing an identical record is an idempotent no-op (recovery re-entry); the same ref with different content returns ErrTaskDuplicate.

func (*Store) TransitionTask

func (s *Store) TransitionTask(planRef, taskID, newStatus string) error

TransitionTask atomically changes a task status: ONE durable event carries the change and its journal timestamp, so the transition and the append-only journal entry are indivisible and ordered by call sequence. The status is opaque; only non-empty is validated.

func (*Store) TransitionTaskCAS

func (s *Store) TransitionTaskCAS(planRef, taskID string, fromStatuses []string, newStatus string) (bool, error)

TransitionTaskCAS atomically changes a task status only when its current status is one of fromStatuses (compare-and-swap). ok=false with a nil error means the precondition did not hold: some other caller already moved the task, and this caller cleanly lost the race rather than double-admitting it. Store.mu already serializes every call (see mu's doc comment), so the check and the write are indivisible with respect to any other Store method.

func (*Store) TransitionTaskCASDecide

func (s *Store) TransitionTaskCASDecide(planRef, taskID string, fromStatuses []string, reopenStatus string, decide func(attempts int) (newStatus string, apply bool)) (applied bool, newStatus string, attempts int, err error)

TransitionTaskCASDecide atomically reads the task's current status and its reopened-attempt count (the count of prior transitions into reopenStatus), then applies decide's verdict, all inside one critical section. This closes the read-then-decide-then-write race a separate attempt-count read plus a later TransitionTaskCAS call would still have: two concurrent failure handlers for the same task cannot both observe the same attempt count and both decide to reopen, because the second handler's decide call runs after the first's write has already landed and sees the incremented count.

fromStatuses gates eligibility exactly like TransitionTaskCAS: decide is only invoked when the task's current status is one of fromStatuses; otherwise this returns (false, "", 0, nil), the same clean-loss shape.

type Task

type Task struct {
	ID        string   `json:"id"`
	PlanRef   string   `json:"plan_ref"`
	Scope     Scope    `json:"scope"`
	Status    string   `json:"status"`
	RunRef    string   `json:"run_ref,omitempty"`
	PRNumber  string   `json:"pr_number,omitempty"`
	Deps      []string `json:"deps,omitempty"`
	Attempts  int      `json:"attempts,omitempty"`
	LastError string   `json:"last_error,omitempty"`
}

Task is a durable work item under one plan. Status is an opaque string; only non-empty is validated. Transitions are appended to the plan's journal (see ListTransitions).

func (Task) Clone

func (t Task) Clone() Task

Clone returns a defensive copy.

type Tool

type Tool interface {
	Name() string
	Description() string
	Parameters() map[string]any
	Execute(ctx context.Context, args json.RawMessage) (string, error)
	ResultBudgetBytes() int
	// Class is "read" or "write" for capability scheduling.
	Class() string
}

Tool is the agent-facing surface for one workflow operation. Implementations live here so package tools can wrap them without cycles beyond the Service dependency.

func Tools

func Tools(svc *Service) []Tool

Tools returns the eight workflow tools bound to svc.

type Transition

type Transition struct {
	PlanRef    string    `json:"plan_ref"`
	TaskID     string    `json:"task_id"`
	FromStatus string    `json:"from_status"`
	ToStatus   string    `json:"to_status"`
	At         time.Time `json:"at"`
}

Transition is one durable journal entry: an atomic status change with its timestamp. The journal is append-only; order matches call order.

func (Transition) Clone

func (t Transition) Clone() Transition

Clone returns a defensive copy.

type TransitionRecord

type TransitionRecord struct {
	RunID           string    `json:"run_id"`
	FromAttemptID   string    `json:"from_attempt_id"`
	ToStepID        string    `json:"to_step_id"`
	TransitionIndex int       `json:"transition_index"`
	MatchDigest     string    `json:"match_digest"`
	DecisionJSON    []byte    `json:"decision_json"`
	CreatedAt       time.Time `json:"created_at"`
}

func (TransitionRecord) Clone

Clone returns a deep copy.

type TransitionView

type TransitionView struct {
	Index       int            `json:"index"`
	ToStep      string         `json:"to_step,omitempty"`
	MatchDigest string         `json:"match_digest,omitempty"`
	Selected    map[string]any `json:"selected,omitempty"`
}

TransitionView is the durable route decision for one attempt.

Jump to

Keyboard shortcuts

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