/** * Flow execution contract — shared between the runner, any host (Skaile * platform, concept-forge, Pichi) and the frontend. * * The flow execution model is turn-based: a run is a sequence of discrete * agent turns stitched together by state transitions. Between turns the * runner is idle; state lives in the FlowAdapter store and is mirrored * durably by the host. * * This file defines the persistence/wire shape — `FlowExecution` snapshots * and their `NodeExecution` entries. The flow-engine state machine * (`@skaile/workspaces/factory-assets/connectors/flow/engine`) operates on the same types via re-exports. * * Universal reference: `docs/flow-execution.md` */ /** * Lifecycle states for a flow node. * * | State | Meaning | * |--------------------|-----------------------------------------------------------| * | not_started | prerequisites not yet met | * | available | prerequisites met, ready to run | * | running | currently executing | * | awaiting_approval | skill output exists, waiting on human sign-off | * | awaiting_input | agent needs information before/during execution | * | complete | output finalized; dependents may advance | * | skipped | optional node bypassed | * | failed | unrecoverable; always blocks (optional -> skipped) | * | blocked | engine-computed; an upstream required node failed/blocked | */ /** * Lifecycle states for a flow node. * * Represents the current status of a node within a flow execution. * * @see {@link NodeExecution} for the durable state of a node within a run * @category Flow * @since 2.0.0 * @docLink packages/types/flow#node-status */ export type NodeStatus = "not_started" | "available" | "running" | "awaiting_approval" | "awaiting_input" | "complete" | "skipped" | "failed" | "blocked"; /** * A single approval request/decision on a node. * * The `approvalHistory` on `NodeExecution` carries every request in order; * `approval` always points at the most recent entry. * * @property requestedAt - ISO timestamp when approval was requested * @property summary - Short human-readable summary * @property decidedAt - When approval was decided (if decided) * @property decidedBy - User ID of decider (informational) * @property decision - "approved" or "rejected" * @property feedback - Rejection reason or approval notes * * @see {@link NodeExecution.approvalHistory} * @category Flow * @since 2.0.0 */ /** * A single approval request/decision on a node. The `approvalHistory` on * `NodeExecution` carries every request in order; `approval` always points * at the most recent entry. * * @docLink packages/types/flow#approval */ export interface Approval { requestedAt: string; /** Short human-readable summary of what is being approved. */ summary: string; decidedAt?: string; /** User ID of the decider (informational only — framework enforces no policy). */ decidedBy?: string; decision?: "approved" | "rejected"; /** Rejection reason or approval notes. */ feedback?: string; } /** * Typed input schemas the agent can request from a user. * * Four kinds cover the common surfaces: plain text, discrete choice, * structured form, or file upload. Frontends render an appropriate * widget per `kind`. * * @see {@link InputRequest} for pending or resolved input requests * @see {@link InputChoice} for choice options * @see {@link FormField} for form field definitions * @category Flow * @since 2.0.0 */ /** * Typed input schemas the agent can request from a user. Four kinds cover * the common surfaces: plain text, discrete choice, structured form, or * file upload. Frontends render an appropriate widget per `kind`. * * @docLink packages/types/flow#input-schema */ export type InputSchema = { kind: "text"; multiline?: boolean; } | { kind: "choice"; options: InputChoice[]; multiple?: boolean; } | { kind: "form"; fields: FormField[]; } | { kind: "file"; accept?: string[]; maxSize?: number; multiple?: boolean; }; /** * A single option in an `InputSchema` of kind `'choice'`. * * @docLink packages/types/flow#input-schema */ export interface InputChoice { id: string; label: string; description?: string; } /** * A field definition within an `InputSchema` of kind `'form'`. * * @docLink packages/types/flow#input-schema */ export interface FormField { id: string; label: string; type: "text" | "textarea" | "number" | "choice" | "file" | "date" | "boolean"; required?: boolean; options?: InputChoice[]; accept?: string[]; } /** * A pending or resolved input request on a node. Like `approval`, the * `inputHistory` carries every request in order. * * @docLink packages/types/flow#input-request */ export interface InputRequest { requestedAt: string; /** Prompt shown to the user alongside the input widget. */ prompt: string; schema: InputSchema; providedAt?: string; providedBy?: string; /** Shape matches `schema.kind`; enforced by the frontend widget. */ response?: unknown; } /** * Connector-qualified reference to an artifact produced by a flow node. * Artifacts piggyback on connectors — the default destination is the * workspace connector, and flow nodes may override per output. * * @docLink packages/types/flow#artifact-ref */ export interface ArtifactRef { /** Connector-qualified URI, e.g. `workspace://concept/brief.md`. */ uri: string; /** ID of the connector backing the artifact (e.g. `workspace`). */ connectorId: string; kind: "document" | "code" | "data" | "record" | "other"; /** Retention metadata only in MVP — no framework-level GC. */ lifetime: "temporary" | "session" | "permanent"; /** nodeId of the producing node. */ producedBy: string; } /** * Structured output produced by a flow node when it calls `complete_node` * or `request_approval`. * * @docLink packages/types/flow#node-output */ export interface NodeOutput { /** 2-3 sentence human-readable summary of what was accomplished. */ summary: string; /** Named machine-readable values captured for downstream consumers. */ fields?: Record; /** Artifacts produced by this node (connector-qualified refs). */ artifacts: ArtifactRef[]; /** Concerns the agent flagged for review. */ concerns?: string[]; } /** * A failure on a node. Recoverable errors transition the node back to * `available` so the agent can retry; non-recoverable errors transition * to `failed`, which blocks dependents whether or not the node is * optional. An optional node's dependents proceed because it never * reaches `failed` — its exhausted failure resolves to `skipped` * instead. A router is the one kind that stays `failed` when optional. * * @docLink packages/types/flow#node-error */ export interface NodeError { message: string; recoverable: boolean; /** ISO timestamp of the failure. */ at: string; } /** A deterministic authored condition that currently prevents node admission. */ export interface NodePreconditionFailure { /** Authored bounded Flow expression. */ expression: string; /** Authored explanation, or the stable runtime fallback. */ message: string; /** Whether the expression evaluated false or could not resolve safely. */ reason: "false" | "error"; /** Bounded evaluator detail when `reason` is `error`. */ detail?: string; } /** Durable evidence that an agent has spent its one output-correction allowance. */ export interface NodeOutputCorrection { /** ISO timestamp when the first non-conforming output was refused. */ requestedAt: string; /** Stable schema failures the agent was asked to correct. */ issues: string[]; } /** * Runtime or agent provenance retained for one value used by a node binding. * Only a `node-output` origin can ever be `"agent"` — every other source is * always `"runtime"`. This is the flow engine's sole provenance mechanism; * nothing else determines whether a value counts as agent- or * runtime-obtained. * * Taint is **transitive**. A `node-output` origin is `"agent"` when the * producing node is an agent or subprompt node, and also when any * agent-obtained value contributed to that producer's own bindings (see * {@link NodeExecution.inputOrigins}) — a deterministic node cannot launder an * agent's value by carrying it forward. A producer whose `inputOrigins` were * never persisted (a snapshot predating that field) reads as `"agent"`: * fail-closed, never as clean. * * @docLink packages/types/flow#binding-origin */ export type BindingOrigin = { source: "literal"; path: string; obtainedBy: "runtime"; } | { source: "flow-input"; path: string; obtainedBy: "runtime"; } | { source: "default"; path: string; obtainedBy: "runtime"; } | { source: "node-status"; nodeId: string; path: string; obtainedBy: "runtime"; } | { source: "node-output"; nodeId: string; path: string; obtainedBy: "agent" | "runtime"; }; /** Whether a check's compared values trace back only to runtime-obtained origins. */ export type CheckProvenance = "verified" | "asserted"; /** Whether a check's command could have observed anything beyond the flow's own authored state. */ export type CheckInputCurrency = "live" | "flow-local"; /** One compared value with the origins that produced it. */ export interface CheckComparedValue { name: string; value: unknown; origins: BindingOrigin[]; /** "agent" when ANY contributing origin was agent-produced. */ obtainedBy: "agent" | "runtime"; } /** * Derived, non-authorable evidence for one `check` node attempt's verdict. * * `provenance` is `"verified"` only when every compared value traces back to * runtime-obtained origins *transitively* — no agent-produced value * contributed anywhere in its ancestry. A `function` node that reads an * agent's output and re-emits it as its own output stays agent-obtained, so * provenance is derived and cannot be asserted by flow configuration. A * compared value whose producer has no persisted * {@link NodeExecution.inputOrigins} — a run that straddled the upgrade that * introduced the field — reads as agent-obtained, so such a check reports * `"asserted"` rather than a false `"verified"`. `provenance` is absent (not * `"verified"`, not `"asserted"`) when there are no compared values, since * neither label is honest about an empty set. `inputs: "live"` is the floor * for any check that runs a process — `"flow-local"` requires at least one * compared value whose origins are all `literal`/`default`/`flow-input`. * * @see {@link NodeExecutionAttempt.check} * @docLink packages/types/flow#check-evidence */ export interface CheckEvidence { values: CheckComparedValue[]; provenance?: CheckProvenance; inputs: CheckInputCurrency; observedAt: string; runtime: "shell" | "node" | "python"; command: string; cwd?: string; successExit: number[]; exitStatus: number; passed: boolean; } /** * One terminal outcome of a node's execution — pushed to `NodeExecution.outputHistory` * exactly once per actual start, in the order attempts occurred. Retries (automatic or * manual) never overwrite a prior entry; a node-output correction stays inside the same * attempt rather than opening a new one. * * `result` reflects what actually happened, independent of the compatibility node * `status` a caller may apply on top of it — an optional node whose execution failed * still records `'failed'` here even though its `NodeExecution.status` becomes * `'skipped'` for backward-compatible downstream availability. * * @see {@link NodeExecution.outputHistory} * @category Flow * @since 3.9.0 * @docLink packages/types/flow#node-execution-attempt */ export interface NodeExecutionAttempt { /** 1-based sequence number within this node's execution. Numbered from 1. */ attempt: number; /** True outcome of this attempt, independent of any compatibility status degrade. */ result: "complete" | "failed" | "skipped"; /** ISO timestamp this attempt actually started (the node's `startedAt` at the time). */ startedAt: string; /** ISO timestamp this attempt reached its terminal outcome. */ completedAt: string; /** Output captured for this attempt, if any — retained even for a `'failed'` result. */ output?: NodeOutput; /** Error captured for this attempt, when `result` is `'failed'`. */ error?: NodeError; /** * True for the agent's own `fail_node(recoverable: true)` self-retry — a * genuine start-to-failure attempt, but excluded from the automatic * `control.retries` budget (which only counts automatic-failure entries). * Absent (falsy) for every other entry, including the manual `retryNode` * operator override's subsequent attempt. */ selfRetry?: boolean; /** * Derived evidence for a `check` node's verdict on this attempt — present * only when a `check` actually produced a verdict (never for an * infrastructure failure such as a spawn error or timeout). Lives here, * not on `Approval`, so a future approval surface can join to it without * `Approval` growing check-specific fields. */ check?: CheckEvidence; } /** * Lightweight snapshot passed to pure engine helpers. Callers hand the * engine whatever state they have in a uniform shape so the engine can * compute `available`, `blocked`, and `done` without reaching into the * full NodeExecution history. * * @docLink packages/types/flow#node-execution-snapshot */ export interface NodeExecutionSnapshot { status: NodeStatus; approval?: Approval; input?: InputRequest; output?: NodeOutput; error?: NodeError; /** Authored conditions that currently prevent this node from becoming available. */ failedPreconditions?: NodePreconditionFailure[]; /** @see {@link NodeExecution.inputOrigins} — carried so provenance stays derivable here. */ inputOrigins?: Record; } /** * Durable state for a single flow node within a run. Holds the current * interaction pointers (`approval`, `input`, `error`) plus their history * arrays — retries and re-requests append to history. * * @see {@link NodeExecutionSnapshot} for the lightweight engine-helper variant * @see {@link FlowExecution} for the top-level run container * @category Flow * @since 2.0.0 * @docLink packages/types/flow#node-execution */ export interface NodeExecution { id: string; status: NodeStatus; startedAt?: string; completedAt?: string; /** Latest-pointer compatibility field — mirrors the most recent attempt's output. */ output?: NodeOutput; /** Current or most recent approval — pointer into `approvalHistory`. */ approval?: Approval; approvalHistory: Approval[]; /** Current or most recent input request — pointer into `inputHistory`. */ input?: InputRequest; inputHistory: InputRequest[]; /** Current error for `failed` nodes, or last error before a retry. */ error?: NodeError; errorHistory: NodeError[]; /** Authored conditions that currently prevent this node from becoming available. */ failedPreconditions?: NodePreconditionFailure[]; /** First output mismatch retained across rehydration and terminal completion. */ outputCorrection?: NodeOutputCorrection; /** * Binding origins resolved for the attempt that produced `output` — a * latest-pointer that moves with it. Makes this node's output classifiable * without walking its ancestors: {@link BindingOrigin} taint is transitive, * and each producer's record was itself computed under that rule. Absent on * a node that has not executed and on snapshots persisted before this field * existed — absent is read as agent-tainted, never as clean. `{}` means the * node genuinely bound nothing and is read as runtime-obtained. */ inputOrigins?: Record; /** * Append-only record of every terminal attempt on this node, oldest first. * Absent or empty before this node's first terminal outcome; old persisted * snapshots predating this field are treated the same way. `output` / * `error` / `startedAt` / `completedAt` remain latest-pointers into the * final entry for compatibility — read this for full attempt history. */ outputHistory?: NodeExecutionAttempt[]; } /** * Top-level run state. One `FlowExecution` per active run; hosts persist * the latest snapshot durably and mirror it back to the runner on * container rehydration via the `flow` connector's `hydrate` op. * * ## Canonical flow-connector host sequences (Protocol >= 3.5) * * A `FlowExecution` is both the live run state and the wire payload the host * sends to restore a run. There is no envelope-level flow rehydration; every * flow lifecycle transition is a `connector_mutate` against the bare `"flow"` * connector id (the runner rewrites it to the per-run `flow:` id and * enforces one active flow per session): * * - **Fresh start** — a single `start` op: * `connector_mutate { id: "flow", op: "start", * payload: { flow: , seed: { runId, startedBy, autonomousMode? } } }` * The runner auto-creates the `flow:` connector and a fresh run. * * - **Rehydration on a cold container** — a single `hydrate` op: * `connector_mutate { id: "flow", op: "hydrate", * payload: { state: , flow: } }` * With no active flow the runner auto-creates the connector directly from * `state` (runId / startedBy / autonomousMode are read from the snapshot), * so `flow` is REQUIRED here. If a flow is already active, `hydrate` * overwrites its snapshot and `flow` may be omitted. * * Before Protocol 3.5 a cold-container `hydrate` could not bootstrap the * connector, so hosts issued `start` then `hydrate`. That two-step sequence * still works but is no longer required. See `runner/MIGRATION-flow-connector.md`. * * @see {@link NodeExecution} for per-node state within this run * @see {@link FlowExecutionStatus} for the allowed status values * @category Flow * @since 2.0.0 * @docLink packages/types/flow#flow-execution */ export interface FlowExecution { runId: string; flowId: string; /** Version pinned at startFlow — runner stores its own copy of the flow definition. */ flowVersion: string; status: FlowExecutionStatus; startedAt: string; /** User ID of the participant that started the flow. */ startedBy: string; /** Editable mid-flow. When true, agent skips approval gates. Default false. */ autonomousMode: boolean; /** Node execution state keyed by nodeId. */ nodes: Record; /** Nodes currently in flight — empty array means the runner is idle. */ focus: string[]; /** True when all required nodes are complete or skipped. */ done: boolean; } /** * Top-level status of a flow run. Carried in `FlowExecution.status`. * * | Status | Meaning | * |-----------|---------------------------------------------------------| * | running | At least one node is currently in flight | * | paused | All turns complete; waiting on external signal | * | complete | All required nodes reached `complete` or `skipped` | * | failed | One or more non-optional nodes reached `failed` | * | cancelled | Explicitly cancelled via `connector_mutate { id: "flow", op: "cancel" }` | * * @see {@link FlowExecution} * @category Flow * @since 2.0.0 * @docLink packages/types/flow#flow-execution-status */ export type FlowExecutionStatus = "running" | "paused" | "complete" | "failed" | "cancelled"; /** * A gate decision the host recorded but could not confirm was applied, carried * on the flow `hydrate` payload as `pendingDecisions[]` (Protocol 3.7) so the * runner can replay it in the same handler invocation that creates the flow * connector — removing the wake race between `hydrate` and a separate * `applyApproval` / `applyInput` command. * * Invariants and gotchas: * - Honoured **only** on the cold-container `create-hydrate` branch. A hydrate * that overwrites a live run ignores these entirely; replaying onto a live run * is the double-apply the design exists to avoid. * - Every entry is re-checked against the snapshot the host just sent, so a * decision that no longer applies is reported as a skip, not an error. * - `response` is user-supplied and unbounded. The runner validates it against * the node's own `InputSchema` and skips on mismatch; it is never logged and * never echoed back in the acknowledgement. * * @see {@link InputSchema} * @category Flow * @since 3.7.0 * @docLink packages/types/flow#pending-gate-decision */ export type PendingGateDecision = { kind: "approval"; nodeId: string; decision: "approved" | "rejected"; decidedBy: string; feedback?: string; } | { kind: "input"; nodeId: string; response: unknown; providedBy: string; }; //# sourceMappingURL=flow.d.ts.map