controller

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: 34 Imported by: 0

Documentation

Overview

Package blockedpath detects when task text or agent output instructs a write to a workspace path that the host write-path policy blocklists for workflow agents.

Workflow agents cannot write such paths (the write tools refuse), so any instruction that demands editing one is an admission-time or settle-time failure, never a repair-loop candidate. The detection is deliberately conservative: a path token plus a demand verb on the same line is treated as an instruction to write, except when the line places the path inside a temporary or test-only fixture workspace ("creates a temporary directory with .mivia/workflows"): that describes a throwaway fixture's layout, not a demand to edit the host's path. A read-only mention (no demand verb) is not.

Index

Constants

View Source
const (
	PanelVerdictApproved         = "approved"
	PanelVerdictChangesRequested = "changes_requested"
)

Panel verdict values. The model can supply only these two values in a member report; the host, not the model, computes the final gate verdict.

Variables

View Source
var ErrCancelBlocked = errors.New("cancel_blocked")

ErrCancelBlocked reports that a panel child's terminal state cannot be safely verified right now (D15 item 6): an ambiguous recovered claim, or a task whose persisted status looks nonterminal with no verifiable live owner. Cancellation must fail closed here instead of reporting a false "canceled" outcome, and the workflow claim stays held so a later retry can make progress.

View Source
var ErrCancelPending = errors.New("cancel_pending")

ErrCancelPending reports that cancel_pending is durably recorded but at least one intended child has not reached a terminal state yet (a slow worker, D15 item 5). The workflow stays non-terminal; a later resume or cancel retry repeats the idempotent reconciliation.

View Source
var ErrCancelReconciliationPending = errors.New("panel cancel reconciliation is not yet complete")

ErrCancelReconciliationPending reports that reconcilePanelCancelPending made no terminal progress this Advance (an ambiguous child claim, a slow child, or a claim conflict from a racing executor) and wants another Advance later. It is distinct from ErrPanelMembersComplete: without this sentinel, Run's loop cannot tell a not-yet-terminal cancel_pending reconciliation apart from the "members complete, synthesis unsupported" case in refusePanelStep, since both leave Advance returning done=false, err=nil with the active step still agent_panel. Conflating the two would make Run treat every such case as ErrPanelMembersComplete, which isNonTerminalWorkflowStop settles as a silent no-op - stranding a legitimately still-canceling run at running/cancel_pending with no automatic retry. Callers should retry Advance/Run on this error (see RunWithCancelReconciliationRetry) instead of treating it as a stop.

View Source
var ErrPanelMembersComplete = errors.New("panel members completed; synthesis is unavailable")

ErrPanelMembersComplete reports that a panel attempt completed member work while panelsEnabled is false, so refusePanelStep settles it failed instead of advancing to synthesis.

Functions

func CancelRun

func CancelRun(ctx context.Context, repo workflowledger.Repository, coord coordinator.Coordinator, runID string) error

CancelRun cancels a run that is not yet terminal. It is a thin wrapper around CancelRunWithAttempts for callers that only drive the status transition and do not need the attempts that were canceled.

func CancelRunWithAttempts

func CancelRunWithAttempts(ctx context.Context, repo workflowledger.Repository, coord coordinator.Coordinator, runID string) ([]workflowledger.StepAttempt, error)

CancelRunWithAttempts cancels a run that is not yet terminal: it CASes the run status to canceled (a pending run moves through running first, since pending->canceled is not a valid edge) and marks every non-terminal attempt canceled, returning the attempts it canceled. It mints its own claim holder and refuses delivery_pending runs (those must be delivered or cleaned up before cancel). Cancel is idempotent and re-runnable: a crash between the two pending CASes leaves a resumable running run that a re-run settles.

coord is the panel control dependency (D15): it cancels or tombstones a live panel step's exact member and synthesis children before the run is allowed to report canceled. A nil coord is only safe when no attempt can ever carry a PanelExecution (panelsEnabled is false as of Wave 6); a caller that reaches a real panel attempt without a coord fails closed with a clear error rather than silently orphaning the panel's children.

The run claim protects concurrent executors; the caller is expected to hold the workflow execution file lock and clear a stale claim before calling. Callers emit one step_completed event per returned attempt: each carries the canceled status and the operator-cancel ErrorRef.

func CancelRunWithAttemptsWithClaim

func CancelRunWithAttemptsWithClaim(ctx context.Context, repo workflowledger.Repository, coord coordinator.Coordinator, runID, holder string) ([]workflowledger.StepAttempt, error)

CancelRunWithAttemptsWithClaim settles a run with holder's existing claim. The caller must hold the claim and release it after this function returns.

A run whose active attempt carries a live PanelExecution is reconciled through ReconcilePanelCancellation first (D15): the run is never CASed to canceled until every intended member and synthesis child is confirmed terminal. If reconciliation reports ErrCancelBlocked (an ambiguous child claim) or ErrCancelPending (a slow child that has not settled yet), this function returns that error and leaves the run non-terminal and the workflow claim untouched, so a later cancel or resume can retry the same idempotent reconciliation. Non-panel attempts are unaffected: they keep the existing best-effort "mark canceled" behavior.

func CompleteExistingStepResult

func CompleteExistingStepResult(ctx context.Context, repo workflowledger.Repository, attempt workflowledger.StepAttempt, result AgentStepResult, status workflowledger.AttemptStatus, route RouteDecision) error

CompleteExistingStepResult completes an attempt that the controller already recorded before an interruption. The stable child key prevents re-dispatch.

func ComputeHostVerdict

func ComputeHostVerdict(reports []PanelMemberReport) string

ComputeHostVerdict computes the final panel gate verdict from decoded member reports (D10). The model cannot override this computation: it is changes_requested if any member reports that verdict or has one or more findings, and approved only when every member approves with no findings.

func IsBlockedPath

func IsBlockedPath(rel string, blocklist []string) bool

IsBlockedPath reports whether rel (a slash-separated workspace-relative path) falls under any blocklist entry. Matching is a cleaned prefix match: an entry ".mivia/workflows" blocks ".mivia/workflows" and ".mivia/workflows/bug-fix.toml" but not ".mivia/workflows-x/file". Entries and the checked path are normalized (dot-slash and trailing slashes stripped) before comparison. An empty blocklist blocks nothing.

Kept in sync with internal/tools.isWritePathDenied, which enforces the same rule at the write-tool boundary.

func LastStepHeartbeat

func LastStepHeartbeat(taskID string) (time.Time, bool)

LastStepHeartbeat returns the recorded heartbeat time for the task id. The boolean result is false when the task id has no recorded heartbeat.

func LineDemandsEdit

func LineDemandsEdit(line, blockedPath string) bool

LineDemandsEdit reports whether one line of text both names path and instructs editing it: a whitespace-delimited token references the path and a demand verb is present. A token references the path only when the path appears at a token or "/" boundary and is followed by the end of the token or another "/": ".git" inside a URL ("raw.githubusercontent.com") or a sibling name (".gitignore", ".mivia/workflows-x") never matches, while a quoted or backticked reference ("`.mivia/workflows/bug-fix.toml`", "foo/.git/HEAD") does. Word boundaries keep verbs inside other words ("assets", "prefix") from matching, and each matched path token itself is stripped from the line before verb matching so a verb inside a file name ("fix" in "bug-fix.toml") never counts. Noun phrases ("write access") are stripped too. A "do not edit X" instruction still matches: the text instructs a write to a blocked path either way, and the caller should refuse or route the task to a host-owned process.

func NoteStepHeartbeat

func NoteStepHeartbeat(taskID string)

NoteStepHeartbeat records the current time as the last heartbeat for the task id. An empty task id is a no-op. The workflow join watchdog uses the recorded time to distinguish a live child from a stalled one.

func PathsDemandedInText

func PathsDemandedInText(text string, blocklist []string) []string

PathsDemandedInText returns the blocklisted paths that text instructs writing, one entry per matched blocklist entry (not per file), deduplicated and sorted for deterministic error messages.

func PendingApprovalID

func PendingApprovalID(stepID string, attemptNo int) string

PendingApprovalID returns the approval id for a human_gate attempt number.

func ReconcilePanelCancellation

func ReconcilePanelCancellation(ctx context.Context, repo workflowledger.Repository, panel PanelCancelCoordinator, runID, holder, attemptID string) (workflowledger.StepAttempt, bool, error)

ReconcilePanelCancellation drives one panel attempt toward and through cancel_pending (D15): it advances the durable phase (refreshing the claim immediately before the write, D13), then cancels or tombstones every intended child. The caller must already hold the workflow claim (ctx carries the holder via workflowledger.ContextWithClaimHolder) and keep holding it until this returns.

It returns the attempt as last observed, allTerminal=true once every intended child (and the attempt phase itself, if already terminal) is terminal, or a non-nil error: ErrCancelBlocked when a child's terminal state is ambiguous, ErrCancelPending when children are known but not yet all terminal, or a wrapped durable error otherwise.

func RecordStepResult

RecordStepResult writes the child identity and bounded evidence selection to one workflow attempt. The controller calls it after attempt admission.

func ResetStepHeartbeats

func ResetStepHeartbeats()

ResetStepHeartbeats clears the heartbeat registry. It is a test helper. Tests use it to start from a clean registry.

func RunWithCancelReconciliationRetry

func RunWithCancelReconciliationRetry(ctx context.Context, run func(context.Context) (workflowledger.RunSnapshot, error)) (workflowledger.RunSnapshot, error)

RunWithCancelReconciliationRetry runs run repeatedly while it reports ErrCancelReconciliationPending, so a panel cancel_pending attempt that is not yet all-terminal (a slow-to-stop member, an ambiguous claim, or a racing executor's claim conflict) gets automatically retried instead of stranding the run at running with no driver ever calling Advance again. It gives up after cancelReconciliationRetryLimit retries or ctx cancellation, returning whatever run last reported.

func ValidateReportEvidence

func ValidateReportEvidence(reportText string, history []evidencecheck.ToolExecutionRecord) error

ValidateReportEvidence cross-checks report claims against recorded tool executions.

func ValidateSourceDispositions

func ValidateSourceDispositions(keys []CanonicalSourceKey, dispositions []PanelSourceDisposition) error

ValidateSourceDispositions checks that the synthesizer supplied exactly one legal disposition for every canonical source key the bounded member reports produced, and no dispositions for any other key (D10).

Types

type Admission

type Admission struct {
	InvocationKey    string
	BaseRef          string
	BaseCommit       string
	OriginBaseCommit string
	WorktreeName     string
	InputDigest      string
	DeadlineAt       *time.Time
	RemoteURL        string
	// WorkflowDigest is the digest RECORDED when the run was admitted. A
	// resume must pass it; a fresh admission leaves it empty.
	//
	// The digest is a hash of the marshalled definition struct, so it moves
	// whenever those types gain a field, even when the workflow text does not
	// change by one byte. Comparing a resumed run against a digest THIS binary
	// recomputed therefore asserts that this binary hashes the definition the
	// way the admitting binary did, which is a fact about the binary. Two
	// field additions moved it in one day, and every run admitted before them
	// became permanently unresumable.
	//
	// The definition text is proven by other means on the resume path: the
	// snapshot digest covers the raw snapshot bytes, StartNew compares those
	// bytes directly, and the two recorded digests are compared to each other.
	WorkflowDigest string
}

Admission contains immutable host data for one workflow run.

type AgentStepRequest

type AgentStepRequest struct {
	WorkflowRunID    string
	StepID           string
	AttemptNo        int
	TaskID           string
	CoordinatorRunID string
	AgentName        string
	AgentDigest      string
	Skill            string
	ProviderName     string
	Model            string
	Scope            string
	Permission       string
	Timeout          time.Duration
	Budget           int
	ForceResume      bool
	Template         string
	Inputs           map[string]any
	Evidence         map[string]any
	MaxBindingBytes  int
	MaxContextBytes  int
	OutputSchema     map[string]any
	// Prompt is the fully rendered step prompt (including the evidence-refs
	// block), produced by the controller. An empty value means the runner
	// must render the prompt from Template/Inputs/Evidence.
	Prompt string
	// EvidenceRefs names the artifact references bound into the prompt,
	// keyed by evidence name. Nil when the step binds no artifact
	// references.
	EvidenceRefs map[string]ArtifactRef
}

AgentStepRequest contains only the explicit, bounded step inputs.

type AgentStepResult

type AgentStepResult struct {
	CoordinatorRunID string
	TaskID           string
	Output           json.RawMessage
	ValidatedOutput  any
	EvidenceJSON     []byte
	// Status is the child task's terminal status from the coordinator result
	// ("completed", "failed", "timed_out", "canceled", "blocked"). It is
	// empty when the step runner produced no child result (for example a
	// pre-dispatch failure).
	Status string
	// ErrorRef names content-addressed failure detail for a failed attempt.
	// It is set by the controller from the step error; an empty value means
	// no error detail was persisted.
	ErrorRef string
}

AgentStepResult contains the validated output and bounded evidence metadata.

type AgentStepRunner

type AgentStepRunner interface {
	RunStep(context.Context, AgentStepRequest) (AgentStepResult, error)
}

AgentStepRunner executes one workflow agent step through the coordinator.

type ArtifactRef

type ArtifactRef struct {
	Step    string `json:"step"`
	Attempt int    `json:"attempt"`
	Ref     string `json:"ref"`
	Bytes   int    `json:"bytes"`
	Digest  string `json:"digest"`
}

ArtifactRef addresses one content-addressed artifact referenced by a workflow step's evidence.

type CanonicalSourceKey

type CanonicalSourceKey struct {
	MemberID  string
	FindingID string
}

CanonicalSourceKey is the (member_id, finding_id) pair D10 defines as the one canonical source key for every panel finding.

func AllCanonicalSourceKeys

func AllCanonicalSourceKeys(envelope PanelSynthesisEnvelope) []CanonicalSourceKey

AllCanonicalSourceKeys derives every canonical source key from an envelope's decoded member reports, in declaration order. This is what ValidateSourceDispositions checks the synthesizer's output against.

type ChunkPlanValidation

type ChunkPlanValidation struct {
	Valid   bool
	Reasons []string
}

ChunkPlanValidation is the deterministic result of validating a decompose step's chunk plan. A rejected plan carries human-readable reasons.

func ValidateChunkPlan

func ValidateChunkPlan(raw json.RawMessage, cfg *definition.StackingConfig) (ChunkPlanValidation, error)

ValidateChunkPlan deterministically validates a decompose step output against the stacking rules: stack_mode enum; chunks <= max_chunks; est_diff_lines <= hard_lines; files per chunk <= max_files; file sets disjoint; every chunk has tests (const true); depends_on is a DAG. The stack_mode=single and no_bug payloads are valid by construction. Malformed or oversized output is an error, not an invalid plan.

type CoordinatorRunner

type CoordinatorRunner struct {
	Coordinator coordinator.Coordinator
	// JoinWatchdog bounds a coordinator join from the controller side. The
	// coordinator's own Join (internal/coordinator/coordinator.go) waits on
	// the child run's done channel with no bound of its own, so a child that
	// never settles (hung pool worker, stuck referral wait, dead executor)
	// would park the controller forever. A value <= 0 uses
	// defaultJoinWatchdog; tests set it short to exercise the join-timeout
	// path.
	JoinWatchdog time.Duration
	// RegisterChildRun is an optional host hook. The runner calls it once per
	// ensured child run, right after EnsureRun returns a handle whose run ID
	// matches the request. The host uses it to register the child in the
	// orchestration handle registry, so the standard control tools
	// (inspect_agents, join_run, cancel_run) can reach the child. The hook is
	// nil-safe, and the controller stays free of any registry dependency: the
	// host owns the owner identity and calls the registry seam.
	RegisterChildRun func(ctx context.Context, runID string, handle *coordinator.RunHandle)
	// contains filtered or unexported fields
}

CoordinatorRunner is the production implementation of AgentStepRunner.

func NewCoordinatorRunner

func NewCoordinatorRunner(c coordinator.Coordinator) *CoordinatorRunner

NewCoordinatorRunner creates a workflow step adapter.

func (*CoordinatorRunner) JoinStep

JoinStep implements StepRunJoiner for the production coordinator runner. It re-dispatches the recorded CoordinatorRunID/TaskID through the same dispatch+join machinery as RunStep: coordinator.EnsureRun is idempotent on the workflow step's identity key, so an EXISTING child run is resumed and joined (a completed child yields its recorded outcome without re-executing its work) instead of creating a fresh run. The child's terminal status is reported in result.Status; joined=false when no child outcome is available (the run is unknown, an idempotency conflict from changed step inputs or a drifted deadline-derived timeout, or a join-boundary error), in which case the controller interrupts the stale attempt and re-dispatches fresh.

func (*CoordinatorRunner) RunStep

RunStep renders the bounded prompt, dispatches one child task, and validates the child's final JSON output. The child idempotency key is run-scoped.

func (*CoordinatorRunner) SetProgressEmitter

func (r *CoordinatorRunner) SetProgressEmitter(emitter func(ProgressEvent))

SetProgressEmitter wires an optional step-heartbeat emitter into the runner. The emitter receives a ProgressStepHeartbeat per watchdog tick while a join is live. Production wiring connects it to the controller's progress sink (see newWorkflowController); tests may leave it nil.

type LinearController

type LinearController struct {
	Repo           workflowledger.Repository
	Runner         AgentStepRunner
	Workflow       *definition.CompiledWorkflow
	Steps          map[string]StepRuntime
	Inputs         map[string]any
	RunID          string
	Snapshot       []byte
	Holder         string
	Verifiers      *definition.Catalogue
	WorkDir        string
	ModuleBaseline *definition.GoModuleBaseline
	SecretPolicy   secretpath.Policy

	// WritePathBlocklist is the host write-path denylist for workflow agents
	// (internal/tools enforced): paths under it can never be written by an
	// agent step. The controller uses it to recognize a succeeded step whose
	// output admits a write it cannot perform (blocked_paths, a claimed
	// files_changed entry, or a review finding demanding a blocked edit) and
	// fail the run honestly instead of looping it into review.
	WritePathBlocklist []string
	PanelLimiter       *PanelActorLimiter
	// PanelLimits bounds every agent_panel step's member and synthesis
	// children (panel_attempt.go/panel_synthesis.go). Defaults to
	// DefaultPanelLimits(); a host overrides it via SetPanelLimits
	// before Start, resolved from [workflows.panels] config.
	PanelLimits PanelLimits
	// contains filtered or unexported fields
}

LinearController advances a workflow one active step at a time. Phase 4 supports agent, agent_gate, evidence_gate, human_gate, and loops.

func NewLinearController

func NewLinearController(repo workflowledger.Repository, runner AgentStepRunner, wf *definition.CompiledWorkflow, steps map[string]StepRuntime, inputs map[string]any, runID string, snapshot []byte) (*LinearController, error)

NewLinearController creates a controller for an admitted workflow run.

func NewResolutionController

func NewResolutionController(repo workflowledger.Repository, wf *definition.CompiledWorkflow, runID string, snapshot []byte, inputs map[string]any) (*LinearController, error)

NewResolutionController builds a controller for host resolution operations (approve/reject) on an existing run. It carries the admitted workflow and inputs but no step runtimes: Approve/Reject only read the workflow's transitions and write to the ledger.

func (*LinearController) Advance

Advance executes the current step once. It returns done when the run is terminal.

func (*LinearController) Approve

func (c *LinearController) Approve(ctx context.Context, approvalID, actor string) error

Approve resolves a pending human_gate and routes the step as succeeded. It never elevates authority, tools, delivery mode, or branch policy.

func (*LinearController) EmitProgress

func (c *LinearController) EmitProgress(e ProgressEvent)

EmitProgress publishes one progress event through the attached sink. It is the exported entry point for wiring an external emitter (for example the CoordinatorRunner step-heartbeat emitter) into this controller's sink.

func (*LinearController) JoinInFlightAttempt

func (c *LinearController) JoinInFlightAttempt(ctx context.Context, attempt workflowledger.StepAttempt) error

JoinInFlightAttempt joins one recorded in-flight attempt's coordinator run per the ledger contract (a recorded attempt is JOINED, never re-dispatched). It is the CLI resume boundary's consumer of PlanResume.AttemptsInFlight: each recorded in-flight attempt is settled from its child's outcome before the controller's Run loop starts, so a completed child is never orphaned and its work is never re-executed. When the join shows nothing to join (no join capability, or the child never ran), the attempt is left in-flight for Advance to interrupt and re-dispatch under the run claim. Idempotent: an attempt that is already terminal (or no longer the latest) is a no-op.

func (*LinearController) Reject

func (c *LinearController) Reject(ctx context.Context, approvalID, actor, reason string) error

Reject resolves a pending human_gate as rejected and fails the run. It never elevates authority.

func (*LinearController) Run

Run advances until the run reaches a terminal status.

func (*LinearController) SetAdmission

func (c *LinearController) SetAdmission(admission Admission) error

SetAdmission sets immutable host admission data before Start.

func (*LinearController) SetForceResume

func (c *LinearController) SetForceResume(force bool) error

SetForceResume sets explicit claim recovery before Start.

func (*LinearController) SetGitContext

func (c *LinearController) SetGitContext(gc delivery.GitContext) error

SetGitContext wires the run's pinned git context (worktree directory and its real git directory) into the controller before Start. It is what lets the controller measure the worktree diff itself, so the post-implement diff-size gate can reroute an oversized chunk to the workflow's diff-size repair step BEFORE the panel and preflight pipeline run on it. Without it the gate is off and delivery-time enforcement (delivery.on_diff_size_failure via delivery.RepairTarget) is the only guard.

func (*LinearController) SetModuleBaseline

func (c *LinearController) SetModuleBaseline(baseline *definition.GoModuleBaseline) error

SetModuleBaseline sets immutable Go module inputs before Start.

func (*LinearController) SetPanelLimiter

func (c *LinearController) SetPanelLimiter(limiter *PanelActorLimiter) error

SetPanelLimiter installs the process-owned panel actor limiter before Start.

func (*LinearController) SetPanelLimits

func (c *LinearController) SetPanelLimits(limits PanelLimits) error

SetPanelLimits overrides the compiled PanelLimits defaults before Start. A host resolves this from [workflows.panels] config (config.WorkflowsConfig.Panels), falling back to DefaultPanelLimits() for any unset field before calling this.

func (*LinearController) SetProgressSink

func (c *LinearController) SetProgressSink(sink ProgressSink) error

SetProgressSink sets the workflow progress sink before Start.

func (*LinearController) SetSecretPolicy

func (c *LinearController) SetSecretPolicy(policy secretpath.Policy) error

SetSecretPolicy sets the secret-exclusion policy for sandboxed evidence gate commands before Start.

func (*LinearController) SetTimeSource

func (c *LinearController) SetTimeSource(now func() time.Time) error

SetTimeSource sets the immutable controller clock before Start.

func (*LinearController) SetVerifiers

func (c *LinearController) SetVerifiers(cat *definition.Catalogue) error

SetVerifiers sets the host verifier catalogue before Start.

func (*LinearController) SetWorkDir

func (c *LinearController) SetWorkDir(dir string) error

SetWorkDir sets the workspace directory for evidence_gate host checks.

func (*LinearController) SetWritePathBlocklist

func (c *LinearController) SetWritePathBlocklist(paths []string) error

SetWritePathBlocklist sets the host write-path denylist for workflow agent steps before Start. Entries must be non-empty workspace-relative paths; validation rejects absolute paths and entries that normalize to nothing.

func (*LinearController) Start

func (c *LinearController) Start(ctx context.Context) error

Start admits the run. It is idempotent for the same run ID and snapshot.

func (*LinearController) StartNew

func (c *LinearController) StartNew(ctx context.Context) (bool, error)

StartNew admits the run and reports whether this controller created it. A false result means another executor already admitted the same snapshot.

func (*LinearController) WireGitContext

func (c *LinearController) WireGitContext(mainRoot, worktree, root string) error

WireGitContext pins the run's git context (main root, worktree name, and worktree directory) for the post-implement diff-size gate. It is the engine entry point used by both fresh starts and resumes; SetGitContext is the controller-internal setter. Best-effort: an empty or unverifiable worktree keeps the gate off and delivery-time enforcement as the only guard.

func (*LinearController) WorkflowStep

func (c *LinearController) WorkflowStep(id string) (definition.Step, bool)

type PanelActorLimiter

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

PanelActorLimiter bounds local panel actors in one process.

func NewPanelActorLimiter

func NewPanelActorLimiter() *PanelActorLimiter

NewPanelActorLimiter creates the fixed process-wide four-slot limiter.

func (*PanelActorLimiter) Acquire

func (l *PanelActorLimiter) Acquire(ctx context.Context, runID string) (*panelActorLease, error)

Acquire reserves one slot for a deterministic child run ID.

type PanelCancelCoordinator

type PanelCancelCoordinator interface {
	CancelOrTombstoneMember(ctx context.Context, attemptID, memberID string) (bool, error)
	CancelOrTombstoneSynthesis(ctx context.Context, attemptID string) (bool, error)
}

PanelCancelCoordinator performs the durable per-child cancel/tombstone operations cancel_pending reconciliation needs. workflowledger.PanelCoordinator implements it.

type PanelDisposition

type PanelDisposition string

PanelDisposition is the final disposition of one canonical source key. D10 removes resolved_conflict from version 1: only these two values exist.

const (
	PanelDispositionIncluded  PanelDisposition = "included"
	PanelDispositionDuplicate PanelDisposition = "duplicate"
)

type PanelFinalReport

type PanelFinalReport struct {
	HostVerdict  string                   `json:"host_verdict"`
	Dispositions []PanelSourceDisposition `json:"dispositions"`
	Summary      string                   `json:"summary"`
}

PanelFinalReport is the host-assembled final gate result. HostVerdict is computed by ComputeHostVerdict from the bounded member reports; the synthesizer's own output can never change it.

type PanelFinding

type PanelFinding struct {
	ID          string `json:"id"`
	Title       string `json:"title"`
	Severity    string `json:"severity"`
	Description string `json:"description"`
}

PanelFinding is one finding inside a panel-review-v1.json member report. Fields match the JSON schema exactly; DecodeStrictPanelMemberReport skips unknown fields in the raw model output and stores only verdict and findings.

type PanelLimits

type PanelLimits struct {
	MemberMaxOutputPerCall    int
	MemberMaxToolCalls        int
	SynthesisMaxOutputPerCall int
	SynthesisMaxToolCalls     int
	// MemberDeadlineDefault bounds one panel member attempt's wall
	// clock when the workflow declares no run deadline
	// (max_duration_seconds = 0). The workflow's declared deadline is
	// the real contract and always wins when earlier; this default
	// only fills the gap so the durable PanelTaskSpec keeps its
	// fail-closed non-zero DeadlineAt invariant. Long multi-hour
	// agentic reviews are the intended workload (bug-fix.toml
	// documents "24h+ agentic reviews" with max_duration_seconds = 0),
	// so the default is generous; workflow authors tighten it by
	// declaring max_duration_seconds, or a host lowers the default via
	// config.
	MemberDeadlineDefault time.Duration
}

PanelLimits is the resolved, non-pointer set of tunables buildPanelAttempt/buildPanelSynthesisWork apply to every agent_panel step's member and synthesis children. It replaces what were once hardcoded package vars/consts so a host config file can override them (internal/config's WorkflowsConfig.Panels, resolved by the caller before SetPanelLimits); DefaultPanelLimits is what an unconfigured host still gets, byte-identical to the pre-config-driven values.

MaxTurns for both member and synthesis is deliberately NOT part of this struct: the turn bound is a per-step workflow knob (definition.Step.MaxTurns, default 0 = unlimited) applied at build time in buildPanelAttempt/buildPanelSynthesisWork, not a host-wide default. MaxPromptTokens/MaxOutputTokens are deliberately not part of this struct either and stay 0 (unlimited cumulative), per runtime.WorkLimits semantics: a read-only reviewer's prompt/output volume is not a work bound a host config should need to raise. A finite cumulative output cap with ceiling-charged accounting (work_limits.go charges each call its full per-call ceiling, refunded only on a steer-canceled call) previously killed deep read-only reviews mid-panel with "work limit exceeded: output tokens" (observed on live bug-fix runs: attempts 1 and 2 failed identically) - the same bogus bound class MaxTurns used to be. The member/synthesis loop stays bounded by MaxOutputPerCall, MaxToolCalls, the attempt deadline, and the panel's retry policy - exactly the fields below.

func DefaultPanelLimits

func DefaultPanelLimits() PanelLimits

DefaultPanelLimits returns the compiled defaults every panel step ran under before PanelLimits became config-driven: 8192 output tokens per call for both member and synthesis children, 64 cumulative tool calls for members, 16 for synthesis, and a 24h member deadline default.

type PanelMemberCoordinator

type PanelMemberCoordinator interface {
	EnsureMember(context.Context, string, string) (*coordinator.RunHandle, error)
	JoinMember(context.Context, string, string, *coordinator.RunHandle) (*coordinator.RunResult, error)
}

PanelMemberCoordinator performs persisted panel child operations.

type PanelMemberPermitProbe

type PanelMemberPermitProbe interface {
	MemberNeedsActorPermit(context.Context, string, string) (bool, error)
}

PanelMemberPermitProbe avoids blocking a remote wait-only join behind local actors that hold the process-wide cap.

type PanelMemberProvenance

type PanelMemberProvenance struct {
	StepID            string `json:"step_id"`
	MemberID          string `json:"member_id"`
	AgentName         string `json:"agent_name"`
	AgentDigest       string `json:"agent_digest"`
	Provider          string `json:"provider"`
	Model             string `json:"model"`
	CoordinatorRunID  string `json:"coordinator_run_id"`
	CoordinatorTaskID string `json:"coordinator_task_id"`
	TerminalStatus    string `json:"terminal_status"`
	OutputDigest      string `json:"output_digest"`
	FindingCount      int    `json:"finding_count"`
	SourceKeyDigest   string `json:"source_key_digest"`
}

PanelMemberProvenance holds the fields the host stamps for one member per D11. Every field here comes from host-known data (the admitted work spec, the coordinator result, and the bounded decoded report) and never from parsing arbitrary model-authored JSON, so the model cannot author or conflict with any of them.

type PanelMemberRemoteJoiner

type PanelMemberRemoteJoiner interface {
	EnsureRemoteMember(context.Context, string, string) (*coordinator.RunHandle, error)
}

type PanelMemberReport

type PanelMemberReport struct {
	Verdict  string         `json:"verdict"`
	Findings []PanelFinding `json:"findings"`
}

PanelMemberReport is one decoded panel-review-v1.json member report.

func DecodeStrictPanelMemberReport

func DecodeStrictPanelMemberReport(raw []byte) (PanelMemberReport, []byte, error)

DecodeStrictPanelMemberReport strictly decodes one panel-review-v1.json member report from untrusted raw model output. It rejects duplicate JSON keys, duplicate finding IDs, an invalid verdict, too many findings, and a finding ID that exceeds either the schema's character bound or the host's byte bound (D10). Unknown fields are skipped, not rejected: a model occasionally adds a junk field (e.g. "elapsed"), and one extra field must not fail an entire review panel. It returns the decoded report and its canonical (re-encoded) form, which carries exactly the bounded fields and is bounded to maxCanonicalPanelMemberReportBytes.

type PanelMemberRequest

type PanelMemberRequest struct {
	MemberID string
	RunID    string
}

PanelMemberRequest names one deterministic persisted child run.

type PanelMemberResult

type PanelMemberResult struct {
	MemberID string
	Result   *coordinator.RunResult
	Err      error
}

PanelMemberResult keeps one child outcome separate from all siblings.

type PanelMembersRequest

type PanelMembersRequest struct {
	AttemptID   string
	Members     []PanelMemberRequest
	Coordinator PanelMemberCoordinator
}

PanelMembersRequest names the exact already-admitted member work.

type PanelMembersResult

type PanelMembersResult struct {
	Members []PanelMemberResult
}

PanelMembersResult contains every outcome, including successful siblings when another required member fails.

func RunPanelMembers

func RunPanelMembers(ctx context.Context, limiter *PanelActorLimiter, req PanelMembersRequest) (PanelMembersResult, error)

RunPanelMembers starts every member concurrently and waits for each result. It never starts synthesis.

RunPanelMembers is policy-agnostic about member outcomes: it returns every member result (success and failure alike) and never fails the whole panel on a member failure. Each member's failure is preserved in PanelMemberResult.Err for the caller to act on; the caller applies the failure policy. RunPanelMembers only returns a non-nil error for request-level problems (nil limiter or coordinator, empty attempt ID, or no members), which are rejected up front.

type PanelSourceDisposition

type PanelSourceDisposition struct {
	MemberID       string           `json:"member_id"`
	FindingID      string           `json:"finding_id"`
	Disposition    PanelDisposition `json:"disposition"`
	FinalFindingID string           `json:"final_finding_id"`
}

PanelSourceDisposition is one synthesizer-authored disposition for one canonical source key. The host validates it; it never trusts it blindly.

type PanelSynthesisEnvelope

type PanelSynthesisEnvelope struct {
	StepID      string                         `json:"step_id"`
	HostVerdict string                         `json:"host_verdict"`
	Members     []PanelSynthesisMemberEnvelope `json:"members"`
	// DroppedFindings records, per member id, the finding ids the host
	// chunk-scope filter removed before synthesis. Nil when nothing was
	// dropped. Host-authored audit data: it lets an auditor reconcile the
	// member raw output digest against the filtered report in the envelope.
	DroppedFindings map[string][]string `json:"dropped_findings,omitempty"`
}

PanelSynthesisEnvelope is the one host-owned JSON document the synthesizer receives. HostVerdict is computed by ComputeHostVerdict; the synthesizer cannot change it (D10).

func BuildSynthesisEnvelope

func BuildSynthesisEnvelope(stepID string, inputs []PanelSynthesisMemberInput) (PanelSynthesisEnvelope, []byte, error)

BuildSynthesisEnvelope builds the one host-owned JSON envelope the synthesizer receives (D11) with no member-report filtering. It decodes each member's raw output with DecodeStrictPanelMemberReport, so invalid or oversized member JSON never reaches synthesis (Fan-in matrix item 2). It stamps provenance for every member from host-known data only, computes the monotonic host verdict from the bounded reports, and enforces every bound in the plan's Bounds table with overflow-safe sums. A panel with a single surviving member still synthesizes: one successful member is sufficient to build an envelope.

func BuildSynthesisEnvelopeWithFilter

func BuildSynthesisEnvelopeWithFilter(stepID string, inputs []PanelSynthesisMemberInput, filter func(memberID string, report *PanelMemberReport) []string) (PanelSynthesisEnvelope, []byte, error)

BuildSynthesisEnvelopeWithFilter is BuildSynthesisEnvelope plus a host-side member-report filter. The filter returns the ids of findings the host drops (the chunk finding-scope rule); the builder removes them, records them in the envelope's DroppedFindings, and neutralizes a changes_requested verdict whose findings list is empty after the drop - or was empty from the start. A verdict with no findings carries no actionable content, and the synthesizer's dispositions validate against the filtered reports only. With a nil filter the builder applies none of this, so non-chunk panels keep the exact legacy behavior. A panel with a single surviving member still synthesizes: one successful member is sufficient to build an envelope.

type PanelSynthesisMemberEnvelope

type PanelSynthesisMemberEnvelope struct {
	Provenance PanelMemberProvenance `json:"provenance"`
	Report     PanelMemberReport     `json:"report"`
}

PanelSynthesisMemberEnvelope is one member's entry in the host-owned synthesis envelope: host-stamped provenance next to the member's own bounded, untrusted report content. The two never merge into one text blob.

type PanelSynthesisMemberInput

type PanelSynthesisMemberInput struct {
	MemberID          string
	AgentName         string
	AgentDigest       string
	Provider          string
	Model             string
	CoordinatorRunID  string
	CoordinatorTaskID string
	TerminalStatus    string
	RawOutput         []byte
}

PanelSynthesisMemberInput names one member's raw, already-terminal coordinator output plus the host-known identity fields needed to stamp its provenance. RawOutput is untrusted model content until decoded.

type PanelSynthesisOutput

type PanelSynthesisOutput struct {
	Dispositions []PanelSourceDisposition `json:"dispositions"`
	Summary      string                   `json:"summary"`
}

PanelSynthesisOutput is the decoded review-panel-v1.json synthesizer output: a disposition for every canonical source key, plus a bounded summary. It never carries a verdict field: the host computes the verdict (ComputeHostVerdict) and the model cannot override it (D10).

func DecodeStrictPanelSynthesisOutput

func DecodeStrictPanelSynthesisOutput(raw []byte, keys []CanonicalSourceKey) (PanelSynthesisOutput, error)

DecodeStrictPanelSynthesisOutput strictly decodes the synthesizer's review-panel-v1.json output: it applies the same duplicate-key, size-bound, and unknown-field-skipping defenses as DecodeStrictPanelMemberReport, then requires every disposition to reference a real canonical source key exactly once with a legal value (ValidateSourceDispositions).

type ProgressEvent

type ProgressEvent struct {
	Kind             ProgressKind
	RunID            string
	StepID           string
	TaskID           string
	CoordinatorRunID string
	AttemptNo        int
	Detail           string
	Timestamp        time.Time
}

ProgressEvent is one workflow progress observation. The CLI writer marshals the struct directly; no custom JSON encoding is required.

func (ProgressEvent) String

func (e ProgressEvent) String() string

String renders a compact readable form of the event for logs.

type ProgressKind

type ProgressKind string

ProgressKind identifies one workflow progress event type.

const (
	// ProgressStepStarted reports a step attempt beginning.
	ProgressStepStarted ProgressKind = "step_started"
	// ProgressStepCompleted reports a step attempt reaching a terminal status.
	ProgressStepCompleted ProgressKind = "step_completed"
	// ProgressStepHeartbeat reports a step that is still running.
	ProgressStepHeartbeat ProgressKind = "step_heartbeat"
	// ProgressGateStarted reports a gate step beginning.
	ProgressGateStarted ProgressKind = "gate_started"
	// ProgressApprovalRequested reports a run waiting for operator approval.
	ProgressApprovalRequested ProgressKind = "approval_requested"
	// ProgressRunFinished reports a run reaching a terminal status.
	ProgressRunFinished ProgressKind = "run_finished"
	// ProgressRunFailed reports a run failing.
	ProgressRunFailed ProgressKind = "run_failed"
	// ProgressPanelRefused reports a panel run refused by a member.
	ProgressPanelRefused ProgressKind = "panel_refused"
	// ProgressDeliveryStage reports one numbered delivery stage observation
	// (guard, eligibility, commit, push, pr, success, failed) with the stage
	// name and its free-form detail in Detail.
	ProgressDeliveryStage ProgressKind = "delivery_stage"
	// ProgressDeliveryRefused reports a delivery refusal: no publication
	// grant (allow_publish=false or an inactive [delivery] policy), decided
	// before any attempt.
	ProgressDeliveryRefused ProgressKind = "delivery_refused"
	// ProgressChunkScopeDropped reports the chunk finding-scope filter
	// dropping review findings that demand sibling-chunk work, with the
	// dropped finding ids in Detail.
	ProgressChunkScopeDropped ProgressKind = "chunk_scope_dropped"
	// ProgressPanelMemberFailed reports a panel member failing while the
	// panel attempt continues under the allow_partial failure policy, with
	// the member id and cause in Detail.
	ProgressPanelMemberFailed ProgressKind = "panel_member_failed"
)

type ProgressSink

type ProgressSink interface {
	// Emit delivers one progress event to the consumer.
	Emit(ProgressEvent)
}

ProgressSink receives workflow progress events.

type RouteDecision

type RouteDecision struct {
	ToStepID        string
	TransitionIndex int
	MatchDigest     string
	DecisionJSON    []byte
	// Loop is set when the selected transition is a named back-edge.
	// The controller checks the loop cap before route selection returns, and
	// increments the counter only after the attempt completion is durable.
	Loop          string
	MaxIterations int
	// PartialAccept marks a loop-exhaustion escape: the run advanced to the
	// declared partial_target with verified outputs salvaged into evidence.
	PartialAccept bool
}

RouteDecision is the durable transition decision attached to one attempt.

type SalvagedAttempt

type SalvagedAttempt struct {
	StepID       string `json:"step_id"`
	AttemptNo    int    `json:"attempt_no"`
	OutputRef    string `json:"output_ref,omitempty"`
	OutputDigest string `json:"output_digest,omitempty"`
}

SalvagedAttempt names one durable step output preserved when a repair loop exhausts, so the verified work survives the terminal failure (R2 Phase 2: partial-accept foundation - the outputs are content-addressed and recoverable by ref from the failure evidence).

type SchemaValidationError

type SchemaValidationError struct {
	StepID string
	Err    error
}

SchemaValidationError marks output that fails the declared step schema.

func (*SchemaValidationError) Error

func (e *SchemaValidationError) Error() string

func (*SchemaValidationError) Unwrap

func (e *SchemaValidationError) Unwrap() error

type StepRunJoiner

type StepRunJoiner interface {
	// JoinStep joins the coordinator run named by spec.CoordinatorRunID and
	// waits for its terminal outcome. joined=true means the child ran (or is
	// being joined to completion) and result carries its terminal status
	// ("completed", "failed", "timed_out", "canceled"); the caller must
	// complete the attempt with that outcome instead of re-dispatching.
	// joined=false means there was nothing to join (the child never ran, the
	// run is unknown, or the join could not be completed) and the caller may
	// interrupt the stale attempt and re-dispatch fresh.
	JoinStep(context.Context, AgentStepRequest) (AgentStepResult, bool, error)
}

StepRunJoiner is an optional AgentStepRunner capability: join a previously dispatched step's coordinator run by its recorded identity and report its terminal outcome. The ledger contract (internal/workflows/ledger/recovery.go) requires a recorded in-flight attempt to be JOINED, never re-dispatched, so the controller tries JoinStep on resume before admitting a fresh attempt. A runner that cannot join (test seams, non-coordinator runners) simply does not implement this interface; the controller then falls back to interrupting the stale attempt and admitting a fresh one.

type StepRuntime

type StepRuntime struct {
	Agent        agents.ResolvedAgent
	Digest       string
	ProviderName string
	Model        string
	Template     string
	Schema       map[string]any
}

StepRuntime contains snapshotted data required to execute one agent step.

Jump to

Keyboard shortcuts

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