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
- Variables
- func AllToolNames() []string
- func ContextWithClaimHolder(ctx context.Context, holder string) context.Context
- func ContextWithPanelChildPrincipal(ctx context.Context, workflowRunID string) context.Context
- func ContextWithRunID(ctx context.Context, runID string) context.Context
- func DigestHex(data []byte) string
- func EventID(runID, kind string, parts ...string) string
- func FinalizePanelTaskSpec(spec *PanelTaskSpec)
- func HasWorkflows(root string) bool
- func InputDigest(inputs map[string]string) string
- func InvocationRunID(key string) string
- func IsDeletableRunStatus(s RunStatus) bool
- func IsResumableRunStatus(s RunStatus) bool
- func IsTerminalAttemptStatus(s AttemptStatus) bool
- func IsTerminalRunStatus(s RunStatus) bool
- func IsTerminalStepID(stepID string) bool
- func MarshalSnapshot(s Snapshot) ([]byte, error)
- func PanelChildIDs(workflowRunID, attemptID, childID string) (runID, taskID string)
- func PanelChildPrincipal(workflowRunID string) runtime.Caller
- func RunIDFromContext(ctx context.Context) (string, bool)
- func SnapshotDigest(data []byte) string
- func ValidAttemptTransition(from, to AttemptStatus) bool
- func ValidRunTransition(from, to RunStatus) bool
- func ValidScopeType(t string) bool
- type AgentSnapshot
- type ApprovalRecord
- type ApprovalView
- type AttemptOutcome
- type AttemptStatus
- type AttemptView
- type CancelResult
- type DeleteResult
- type DeliverResult
- type DeliveryRecord
- type DeliverySnapshot
- type DeliveryView
- type Engine
- type EventRecord
- type EventView
- type EventsPage
- type InspectView
- type ListRunsView
- type LoopCounter
- type LoopView
- type PanelBindingSnapshot
- type PanelCoordinator
- func (p PanelCoordinator) CancelMember(ctx context.Context, attemptID, memberID string, handle *coordinator.RunHandle) error
- func (p PanelCoordinator) CancelOrTombstoneMember(ctx context.Context, attemptID, memberID string) (bool, error)
- func (p PanelCoordinator) CancelOrTombstoneSynthesis(ctx context.Context, attemptID string) (bool, error)
- func (p PanelCoordinator) CancelSynthesis(ctx context.Context, attemptID string, handle *coordinator.RunHandle) error
- func (p PanelCoordinator) EnsureMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)
- func (p PanelCoordinator) EnsureRemoteMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)
- func (p PanelCoordinator) EnsureSynthesis(ctx context.Context, attemptID string) (*coordinator.RunHandle, error)
- func (p PanelCoordinator) EnsureTerminalMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)
- func (p PanelCoordinator) EnsureTerminalSynthesis(ctx context.Context, attemptID string) (*coordinator.RunHandle, error)
- func (p PanelCoordinator) JoinMember(ctx context.Context, attemptID, memberID string, handle *coordinator.RunHandle) (*coordinator.RunResult, error)
- func (p PanelCoordinator) JoinSynthesis(ctx context.Context, attemptID string, handle *coordinator.RunHandle) (*coordinator.RunResult, error)
- func (p PanelCoordinator) MemberNeedsActorPermit(ctx context.Context, attemptID, memberID string) (bool, error)
- func (p PanelCoordinator) ResumeMember(ctx context.Context, attemptID, memberID string) (*coordinator.RunHandle, error)
- func (p PanelCoordinator) ResumeSynthesis(ctx context.Context, attemptID string) (*coordinator.RunHandle, error)
- type PanelExecution
- type PanelMemberExecution
- type PanelPhase
- type PanelSynthesisExecution
- type PanelTaskSpec
- type PanelWorkLimits
- type Plan
- type Projection
- type RecoveredRun
- type RecoveryPlan
- type RefSnapshot
- type RepoFactory
- type Repository
- type RunListItem
- type RunSnapshot
- type RunStatus
- type Scope
- type Service
- func (s *Service) Cancel(ctx context.Context, runID string) (CancelResult, error)
- func (s *Service) Delete(ctx context.Context, runID string, force bool) (DeleteResult, error)
- func (s *Service) Deliver(ctx context.Context, runID string, allowPublish bool) (DeliverResult, error)
- func (s *Service) Engine() Engine
- func (s *Service) Events(ctx context.Context, runID string, limit, offset int) (EventsPage, error)
- func (s *Service) Inspect(ctx context.Context, runID, step string, attemptNo, offset, limit int) (InspectView, error)
- func (s *Service) ListRuns(ctx context.Context, statusFilter string, limit, offset int) (ListRunsView, error)
- func (s *Service) Run(ctx context.Context, req StartRequest) (StartResult, error)
- func (s *Service) SetEngine(engine Engine)
- func (s *Service) Status(ctx context.Context, runID string) (StatusView, error)
- type ServiceOptions
- type Snapshot
- type StartRequest
- type StartResult
- type StatusView
- type StepAttempt
- type StepExecution
- type StorageRepository
- func (s *StorageRepository) ClaimRun(ctx context.Context, runID, holder string) error
- func (s *StorageRepository) ClearRunClaim(ctx context.Context, runID string) error
- func (s *StorageRepository) Close() error
- func (s *StorageRepository) CompareAndSetPanelPhase(ctx context.Context, runID string, attemptID string, expectedVersion uint64, ...) error
- func (s *StorageRepository) CompareAndSetRunStatus(ctx context.Context, runID string, expectedVersion uint64, status RunStatus, ...) error
- func (s *StorageRepository) CompleteStepAttempt(ctx context.Context, runID, attemptID string, expectedVersion uint64, ...) error
- func (s *StorageRepository) CreateApproval(ctx context.Context, a ApprovalRecord) error
- func (s *StorageRepository) CreateRun(ctx context.Context, snap RunSnapshot, snapshotJSON []byte) error
- func (s *StorageRepository) CreateStepAttempt(ctx context.Context, attempt StepAttempt) error
- func (s *StorageRepository) DeleteRun(ctx context.Context, runID string) error
- func (s *StorageRepository) GetDeliveryByIdempotencyKey(ctx context.Context, key string) (DeliveryRecord, error)
- func (s *StorageRepository) GetLoopCounters(ctx context.Context, runID string) ([]LoopCounter, error)
- func (s *StorageRepository) GetRun(ctx context.Context, runID string) (RunSnapshot, error)
- func (s *StorageRepository) GetRunClaim(ctx context.Context, runID string) (holder string, acquiredAt time.Time, ok bool, err error)
- func (s *StorageRepository) GetRunSnapshot(ctx context.Context, runID string) ([]byte, error)
- func (s *StorageRepository) GetStepAttempt(ctx context.Context, runID, attemptID string) (StepAttempt, error)
- func (s *StorageRepository) IncrementLoopCounter(ctx context.Context, runID, loopName string) (int, error)
- func (s *StorageRepository) IsRunHeld(ctx context.Context, runID string) (bool, error)
- func (s *StorageRepository) IsRunTokenFenced(ctx context.Context, runID, token string) (bool, error)
- func (s *StorageRepository) ListApprovals(ctx context.Context, runID string) ([]ApprovalRecord, error)
- func (s *StorageRepository) ListDeliveries(ctx context.Context, runID string) ([]DeliveryRecord, error)
- func (s *StorageRepository) ListEvents(ctx context.Context, runID string, limit, offset int) ([]EventRecord, error)
- func (s *StorageRepository) ListRuns(ctx context.Context, status ...RunStatus) ([]RunSnapshot, error)
- func (s *StorageRepository) ListStepAttempts(ctx context.Context, runID string) ([]StepAttempt, error)
- func (s *StorageRepository) ListTransitions(ctx context.Context, runID string) ([]TransitionRecord, error)
- func (s *StorageRepository) LoadContent(ctx context.Context, ref string) ([]byte, error)
- func (s *StorageRepository) RecordRunResumed(ctx context.Context, runID string) error
- func (s *StorageRepository) RecordStepAttemptOutcome(ctx context.Context, attempt StepAttempt, outcome AttemptOutcome) error
- func (s *StorageRepository) Recover(ctx context.Context) ([]RecoveredRun, error)
- func (s *StorageRepository) RefreshRunClaim(ctx context.Context, runID, holder string) error
- func (s *StorageRepository) ReleaseRun(ctx context.Context, runID, holder string) error
- func (s *StorageRepository) ResolveApproval(ctx context.Context, runID, approvalID, actor, status, reason string) error
- func (s *StorageRepository) SetStepAttemptExecution(ctx context.Context, runID, attemptID, coordinatorRunID, taskID, reason string) error
- func (s *StorageRepository) SetStepAttemptHeartbeat(ctx context.Context, runID, attemptID string, heartbeatAt time.Time) error
- func (s *StorageRepository) SetStepAttemptPrompt(ctx context.Context, runID, attemptID, promptRef string) error
- func (s *StorageRepository) SetTimeSource(now func() time.Time)
- func (s *StorageRepository) StoreContent(ctx context.Context, ref string, data []byte) error
- func (s *StorageRepository) TakeoverExpiredRunClaim(ctx context.Context, runID, holder string, maxAge time.Duration) error
- func (s *StorageRepository) TakeoverRunClaim(ctx context.Context, runID, holder string) error
- func (s *StorageRepository) UpsertDelivery(ctx context.Context, d DeliveryRecord) error
- type Store
- func (s *Store) BindPlanToScope(planID string, scope Scope) error
- func (s *Store) CreateTask(task Task) error
- func (s *Store) GetTask(planRef, taskID string) (Task, error)
- func (s *Store) ListTasksByScope(scope Scope) ([]Task, error)
- func (s *Store) ListTransitions(planRef string) ([]Transition, error)
- func (s *Store) ReadBackPlan(planRef string) (Plan, error)
- func (s *Store) SetTimeSource(now func() time.Time)
- func (s *Store) StorePlan(plan Plan) (string, error)
- func (s *Store) TransitionTask(planRef, taskID, newStatus string) error
- func (s *Store) TransitionTaskCAS(planRef, taskID string, fromStatuses []string, newStatus string) (bool, error)
- func (s *Store) TransitionTaskCASDecide(planRef, taskID string, fromStatuses []string, reopenStatus string, ...) (applied bool, newStatus string, attempts int, err error)
- type Task
- type Tool
- type Transition
- type TransitionRecord
- type TransitionView
Constants ¶
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).
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.
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.
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.
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.
const MaxEventSummaryBytes = 512
MaxEventSummaryBytes bounds one event summary for operator display.
const MaxEvidenceBytes = 16 << 10
MaxEvidenceBytes bounds persisted evidence-selection metadata.
const SnapshotSchemaVersion = 1
SnapshotSchemaVersion is the version of the immutable run snapshot wire format.
const WorkflowsDir = workspace.Namespace + "/workflows"
WorkflowsDir is the workspace-relative path that holds workflow TOML files.
Variables ¶
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.
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.
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 ¶
ContextWithClaimHolder binds one controller's immutable claim holder to its writes.
func ContextWithPanelChildPrincipal ¶
ContextWithPanelChildPrincipal replaces the caller for every panel child coordinator operation. It never inherits a host caller's authority scope.
func ContextWithRunID ¶
ContextWithRunID binds a workflow mutation to its run.
func EventID ¶
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 ¶
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 ¶
InputDigest returns the digest of the canonical input JSON object.
func InvocationRunID ¶
InvocationRunID returns the stable workflow run ID for a caller key.
func IsDeletableRunStatus ¶
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 ¶
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 ¶
IsTerminalRunStatus reports whether the status is terminal (no outgoing transitions).
func IsTerminalStepID ¶
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 ¶
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 ¶
PanelChildIDs returns deterministic coordinator identifiers for one child. Each identifier uses the coordinator's canonical run ID encoding.
func PanelChildPrincipal ¶
PanelChildPrincipal derives the only principal panel child operations use.
func RunIDFromContext ¶
RunIDFromContext returns the workflow run bound to a mutation context.
func SnapshotDigest ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 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.
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"`
}
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 ¶
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) Delete ¶
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 ¶
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) 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.
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 ¶
UnmarshalSnapshot decodes a canonical snapshot JSON blob.
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"`
}
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 ¶
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 ¶
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 ¶
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 ¶
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 (*Store) BindPlanToScope ¶
BindPlanToScope re-binds an existing plan to a scope. Binding the scope it already carries is an idempotent no-op.
func (*Store) CreateTask ¶
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) ListTasksByScope ¶
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 ¶
ReadBackPlan returns a defensive copy of a stored plan by ref.
func (*Store) SetTimeSource ¶
SetTimeSource replaces the clock for deterministic tests.
func (*Store) StorePlan ¶
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 ¶
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).
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.
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.
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 ¶
func (t TransitionRecord) Clone() TransitionRecord
Clone returns a deep copy.
Source Files
¶
- agenttools_types.go
- claim_context.go
- discovery.go
- events.go
- events_list.go
- events_summary.go
- ledger.go
- panel_coordinator.go
- panel_types.go
- panel_validation.go
- projection.go
- recovery.go
- repository.go
- run_context.go
- service.go
- snapshot.go
- status.go
- storage.go
- storage_approvals.go
- storage_attempts.go
- storage_attempts_heartbeat.go
- storage_attempts_meta.go
- storage_claims.go
- storage_delete.go
- storage_deliveries.go
- storage_loops.go
- storage_panels.go
- storage_recovery.go
- storage_runs.go
- store.go
- tasks_events.go
- tasks_projection.go
- tasks_types.go
- tools.go
- types.go