@voltro/plugin-ai-flows 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -42,20 +42,18 @@ export declare const aiFlowRunsTable: Table<"_voltro_ai_flow_runs", FieldDefinit
42
42
  readonly currentStep: ColumnBuilder<number, "integer", true>;
43
43
  readonly totalSteps: ColumnBuilder<number, "integer", true>;
44
44
  /** The HITL answer, written by `respond`. */
45
- readonly humanResponse: ColumnBuilder<{
46
- readonly value?: string | undefined;
47
- readonly text?: string | undefined;
48
- readonly decision?: "approve" | "reject" | undefined;
49
- readonly respondedAt?: string | undefined;
50
- } | null, "json", boolean>;
45
+ readonly humanResponse: ColumnBuilder<HumanResponse | null, "json", boolean>;
51
46
  /** Staged follow-up when the chain requires confirmation. */
52
- readonly chainPending: ColumnBuilder<{
53
- readonly status: "pending" | "confirmed" | "dismissed";
54
- readonly input: {
55
- readonly [x: string]: unknown;
56
- };
57
- readonly flowRef: string;
58
- } | null, "json", boolean>;
47
+ readonly chainPending: ColumnBuilder<ChainPending | null, "json", boolean>;
48
+ /**
49
+ * Why a declared `chainTo` did NOT start — a cycle, or the depth ceiling.
50
+ *
51
+ * A column rather than a log line, because the run row is what a user looks
52
+ * at: a chain that silently does not fire is indistinguishable from a chain
53
+ * that was never declared, and "the second flow never ran" is precisely the
54
+ * report this would otherwise generate.
55
+ */
56
+ readonly chainRefusal: ColumnBuilder<string | null, "text", boolean>;
59
57
  /** Accumulated cost in micro-USD (app view projects to €/cents). */
60
58
  readonly costMicroUsd: ColumnBuilder<number, "integer", true>;
61
59
  readonly durationMs: ColumnBuilder<number | null, "integer", boolean>;
@@ -79,6 +77,15 @@ export declare const aiFlowRunsTable: Table<"_voltro_ai_flow_runs", FieldDefinit
79
77
  readonly updatedBy: ColumnDefinition<string | null, "reference", boolean>;
80
78
  }, true, never>;
81
79
 
80
+ /** Tunables an `aiFlowsPlugin({...})` call can set from `app.config.ts`. */
81
+ export declare interface AiFlowsDefaults {
82
+ /** Park bound for a `human` step, in ms. `0` = wait forever. */
83
+ readonly humanReviewTimeoutMs?: number;
84
+ /** How many flows one `chainTo` chain may traverse. See
85
+ * {@link MAX_CHAIN_DEPTH_DEFAULT}. */
86
+ readonly maxChainDepth?: number;
87
+ }
88
+
82
89
  /**
83
90
  * The AI-Flows plugin. Install it in `app.config.ts` `plugins: [...]`. It
84
91
  * contributes the canonical `ai_flows` + `ai_flow_runs` tables plus the
@@ -94,6 +101,59 @@ export declare const aiFlowsPlugin: (options?: AiFlowsPluginOptions) => VoltroPl
94
101
  export declare interface AiFlowsPluginOptions {
95
102
  /** Namespace for this plugin's routes / inspect endpoints. Default `aiFlows`. */
96
103
  readonly alias?: string;
104
+ /**
105
+ * Contribute this plugin's tables via `extendSchema.tables`. Default `true`.
106
+ *
107
+ * Set `false` when your app ALREADY declares equivalent tables and you want to
108
+ * keep them — the seam this exists for. The plugin then contributes no DDL and
109
+ * the declarative differ never proposes its tables; everything else (routes,
110
+ * inspect, interceptors) is unchanged.
111
+ *
112
+ * **What you take over, exactly:** `_voltro_ai_flows` (definitions) and
113
+ * `_voltro_ai_flow_runs` (runs). The retention sweep registered below still
114
+ * targets `_voltro_ai_flow_runs` by name, so your table inherits the same TTL
115
+ * — which is the intent, but say so to whoever owns that table.
116
+ *
117
+ * It is not offered on every plugin, and the omissions are deliberate rather
118
+ * than unfinished: a `tables: false` that quietly disables a table carrying an
119
+ * AUTHORIZATION or SAFETY decision — the SAML replay cache, SCIM provisioning
120
+ * state, billing's usage counters, cdc-out's outbox — is a security regression
121
+ * shipped as an ergonomics feature. Those plugins need a named store seam
122
+ * first, not a boolean.
123
+ */
124
+ readonly tables?: boolean;
125
+ /**
126
+ * How long a `human` step parks awaiting its review before the run fails, in
127
+ * ms. Default 7 days (`VOLTRO_AI_FLOW_HUMAN_REVIEW_TIMEOUT_HOURS` overrides at
128
+ * deploy time); `0` = wait forever. A flow's `humanTimeoutMs` and a step's own
129
+ * `timeoutMs` both outrank this.
130
+ */
131
+ readonly humanReviewTimeoutMs?: number;
132
+ /**
133
+ * How long a FINISHED flow run is kept, in ms. Default 90 days
134
+ * (`VOLTRO_AI_FLOW_RUNS_TTL_HOURS`).
135
+ *
136
+ * `_voltro_ai_flow_runs` stores every step's full output plus review payloads
137
+ * and grows with traffic, so it is registered with the framework retention
138
+ * sweep like every other run-family table. Two properties of that policy are
139
+ * deliberate:
140
+ *
141
+ * - **Only TERMINAL runs are swept.** The predicate is status-aware
142
+ * (`succeeded | failed | cancelled`), so a run parked on a human review
143
+ * survives regardless of age — a plain time-TTL would delete the pending
144
+ * approvals this plugin exists to support.
145
+ * - **Media artifacts are NOT swept with the row.** A run's steps carry hosted
146
+ * URLs whose blobs belong to the storage plugin; deleting the row orphans
147
+ * them. Keep this TTL at or above the app's own media-purge window, or purge
148
+ * by run id before the row ages out.
149
+ */
150
+ readonly runsTtlMs?: number;
151
+ /**
152
+ * Ceiling on rows ONE inspect call may return (`/flows`, `/runs`). Default
153
+ * 200. A run row carries every step's full output, so the page size is a real
154
+ * response-size knob, not a formality.
155
+ */
156
+ readonly inspectPageMax?: number;
97
157
  }
98
158
 
99
159
  /** `ai_flows` — a reusable flow DEFINITION (the data front door persists these;
@@ -115,33 +175,12 @@ export declare const aiFlowsTable: Table<"_voltro_ai_flows", FieldDefinitions<{
115
175
  /** The ordered plan (`FlowStep[]`); null ≡ empty. */
116
176
  readonly steps: ColumnBuilder<readonly FlowStep[] | null, "json", boolean>;
117
177
  /** The launch-form brief (`BriefField[]`). */
118
- readonly inputSchema: ColumnBuilder<readonly {
119
- readonly key: string;
120
- readonly type: "number" | "boolean" | "text" | "textarea";
121
- readonly required?: boolean | undefined;
122
- readonly label: string;
123
- readonly placeholder?: string | undefined;
124
- }[] | null, "json", boolean>;
125
- readonly chainTo: ColumnBuilder<{
126
- readonly flowRef: string;
127
- readonly mappings: readonly {
128
- readonly sourceKey: string;
129
- readonly targetKey: string;
130
- }[];
131
- readonly requireConfirmation?: boolean | undefined;
132
- } | null, "json", boolean>;
133
- readonly cadence: ColumnBuilder<{
134
- readonly frequency?: "weekly" | "monthly" | undefined;
135
- readonly repeats: readonly number[];
136
- readonly intervalWeeks?: number | undefined;
137
- readonly anchorDate?: string | undefined;
138
- readonly weekOfMonth?: 1 | 2 | 3 | 4 | "last" | undefined;
139
- readonly hour?: number | undefined;
140
- readonly minute?: 0 | 15 | 30 | 45 | undefined;
141
- readonly defaultInput?: {
142
- readonly [x: string]: unknown;
143
- } | undefined;
144
- } | null, "json", boolean>;
178
+ readonly inputSchema: ColumnBuilder<readonly BriefField[] | null, "json", boolean>;
179
+ readonly chainTo: ColumnBuilder<FlowChain | null, "json", boolean>;
180
+ readonly cadence: ColumnBuilder<FlowCadence | null, "json", boolean>;
181
+ /** Flow-wide park bound for `human` steps, in ms (a step's own `timeoutMs`
182
+ * wins). Null → the plugin default. `0` = wait forever. */
183
+ readonly humanTimeoutMs: ColumnBuilder<number | null, "integer", boolean>;
145
184
  /** Master on/off — gates scheduling. */
146
185
  readonly isEnabled: ColumnBuilder<boolean, "boolean", true>;
147
186
  readonly metadata: ColumnBuilder<Record<string, unknown> | null, "json", boolean>;
@@ -177,7 +216,17 @@ export declare const BriefField: Schema.Struct<{
177
216
  placeholder: Schema.optional<typeof Schema.String>;
178
217
  }>;
179
218
 
180
- export declare type BriefField = typeof BriefField.Type;
219
+ /** An INTERFACE for the same reason as {@link FlowStep} — it is reached from an
220
+ * exported table (`aiFlows.inputSchema`), so an alias to a mapped type gets
221
+ * expanded structurally into the api report, in an order that is not stable
222
+ * across build scopes. The `Equals` pin below fails to compile on any drift. */
223
+ export declare interface BriefField {
224
+ readonly key: string;
225
+ readonly label: string;
226
+ readonly type: 'text' | 'textarea' | 'number' | 'boolean';
227
+ readonly required?: boolean | undefined;
228
+ readonly placeholder?: string | undefined;
229
+ }
181
230
 
182
231
  /**
183
232
  * Build the `flow.run` executor over a live `AppContext` + injected capabilities.
@@ -221,7 +270,12 @@ export declare const ChainPending: Schema.Struct<{
221
270
  status: Schema.Literal<["pending", "confirmed", "dismissed"]>;
222
271
  }>;
223
272
 
224
- export declare type ChainPending = typeof ChainPending.Type;
273
+ /** An INTERFACE reached from `aiFlowRuns.chainPending`; see {@link BriefField}. */
274
+ export declare interface ChainPending {
275
+ readonly flowRef: string;
276
+ readonly input: Json;
277
+ readonly status: 'pending' | 'confirmed' | 'dismissed';
278
+ }
225
279
 
226
280
  export declare const clampMaxSteps: (n: number | null | undefined) => number;
227
281
 
@@ -231,6 +285,11 @@ export declare const clearRegistry: () => void;
231
285
  /** Every `{{ref}}` name referenced in `text` (order-preserving, may repeat). */
232
286
  export declare const collectRefs: (text: string) => ReadonlyArray<string>;
233
287
 
288
+ /** The comparison a `when:` performs. */
289
+ export declare const ConditionOp: Schema.Literal<["truthy", "falsy", "eq", "neq", "contains"]>;
290
+
291
+ export declare type ConditionOp = typeof ConditionOp.Type;
292
+
234
293
  /** Create a flow row. Generates an id when absent; tenant/audit columns are
235
294
  * auto-stamped by the runtime. */
236
295
  export declare const createFlow: (ctx: AppContext, fields: Record<string, unknown>) => Effect.Effect<Readonly<Record<string, unknown>>, never, never>;
@@ -257,6 +316,9 @@ export declare interface DefineFlowInput {
257
316
  };
258
317
  readonly chain?: FlowChain;
259
318
  readonly cadence?: FlowCadence;
319
+ /** Flow-wide park bound for every `human` step that sets no `timeoutMs` of its
320
+ * own, in ms. `0` = wait forever. */
321
+ readonly humanTimeoutMs?: number;
260
322
  }
261
323
 
262
324
  /** Hard-delete a flow row by id. Returns whether a row was removed. */
@@ -264,11 +326,16 @@ export declare const deleteFlow: (ctx: AppContext, args: {
264
326
  id: string;
265
327
  }) => Effect.Effect<boolean, never, never>;
266
328
 
329
+ /** A one-line rendering for the run timeline and the editor preview. Exported
330
+ * so the reason a step was skipped is the SAME sentence in both places. */
331
+ export declare const describeCondition: (condition: StepCondition) => string;
332
+
267
333
  export declare interface EngineDeps {
268
334
  /** Resolve an `agent` step's `agentRef` → system prompt + model. Absent →
269
335
  * agent steps with an `agentRef` fail with FlowCapabilityMissing. */
270
336
  readonly resolveAgent?: (agentRef: string) => Effect.Effect<AgentResolution, FlowNotFound, unknown>;
271
- /** Media generator for image/video/audio steps (task #31 plugs in here). */
337
+ /** Media generator for image/video/audio steps (`makeMediaGenerator` in
338
+ * `mediaAdapter.ts` is the @voltro/ai-backed implementation). */
272
339
  readonly generateMedia?: MediaGenerator;
273
340
  /** Fallback model per modality when a step omits `model`. */
274
341
  readonly defaultModels?: {
@@ -288,11 +355,39 @@ export declare interface EngineDeps {
288
355
  /** Long-term-memory prefix prepended to the orchestrator system prompt (wire
289
356
  * to the app's ai_user_memories). Best-effort. */
290
357
  readonly memoryPrefix?: (ownerId: string | null) => Effect.Effect<string, never, unknown>;
358
+ /** Default park bound for `human` steps in ms, for flows that set none. Sits
359
+ * between the flow's own `humanTimeoutMs` and the `aiFlowsPlugin({...})`
360
+ * option / `VOLTRO_AI_FLOW_HUMAN_REVIEW_TIMEOUT_HOURS` (see `defaults.ts`).
361
+ * `0` = wait forever. */
362
+ readonly humanReviewTimeoutMs?: number;
363
+ /** How many flows one `chainTo` chain may traverse, INCLUDING the flow that
364
+ * started it. Sits between nothing and the `aiFlowsPlugin({...})` option /
365
+ * `VOLTRO_AI_FLOW_MAX_CHAIN_DEPTH` (see `defaults.ts`). A cycle is refused
366
+ * separately and cannot be raised by this number. */
367
+ readonly maxChainDepth?: number;
291
368
  }
292
369
 
370
+ /**
371
+ * Evaluate `condition` against a run context.
372
+ *
373
+ * A condition on a ref the context does not carry evaluates as ABSENT — false
374
+ * for `truthy`/`eq`/`contains`, true for `falsy`/`neq`. That asymmetry is the
375
+ * honest reading of "the producing step did not run", and it is why
376
+ * `validateFlow` refuses a dangling ref at registration: at run time, a typo and
377
+ * a genuinely-skipped producer are indistinguishable here.
378
+ */
379
+ export declare const evaluateCondition: (condition: StepCondition, context: Readonly<Record<string, unknown>>) => boolean;
380
+
293
381
  /** The canonical name the launch action starts + the app re-export registers. */
294
382
  export declare const FLOW_RUN_WORKFLOW = "flow.run";
295
383
 
384
+ /** Deploy-time override for {@link FLOW_RUNS_TTL_HOURS_DEFAULT}, in hours. */
385
+ export declare const FLOW_RUNS_TTL_ENV = "VOLTRO_AI_FLOW_RUNS_TTL_HOURS";
386
+
387
+ /** Framework default retention for `_voltro_ai_flow_runs`: 90 days of finished
388
+ * runs. Only TERMINAL runs are swept — see `index.ts`. */
389
+ export declare const FLOW_RUNS_TTL_HOURS_DEFAULT: number;
390
+
296
391
  /** A required brief field was blank at launch. */
297
392
  declare class FlowBriefIncomplete extends FlowBriefIncomplete_base {
298
393
  }
@@ -322,7 +417,25 @@ export declare const FlowCadence: Schema.Struct<{
322
417
  defaultInput: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
323
418
  }>;
324
419
 
325
- export declare type FlowCadence = typeof FlowCadence.Type;
420
+ /**
421
+ * An INTERFACE — reached from `aiFlows.cadence`; see {@link BriefField}.
422
+ *
423
+ * This one is not hypothetical: a single-package `api:report` and a
424
+ * full-monorepo build emitted `hour`/`minute` in DIFFERENT positions inside the
425
+ * structurally-expanded alias, so the api-surface gate rejected a file nobody
426
+ * had touched, showing the same members moving. That is the exact failure
427
+ * {@link FlowStep}'s comment describes.
428
+ */
429
+ export declare interface FlowCadence {
430
+ readonly frequency?: 'weekly' | 'monthly' | undefined;
431
+ readonly repeats: ReadonlyArray<number>;
432
+ readonly intervalWeeks?: number | undefined;
433
+ readonly anchorDate?: string | undefined;
434
+ readonly weekOfMonth?: 1 | 2 | 3 | 4 | 'last' | undefined;
435
+ readonly hour?: number | undefined;
436
+ readonly minute?: 0 | 15 | 30 | 45 | undefined;
437
+ readonly defaultInput?: Json | undefined;
438
+ }
326
439
 
327
440
  /** A generation step required a capability the host did not provide (e.g. a
328
441
  * media modality with no configured generator, or storage for the artifact). */
@@ -347,7 +460,15 @@ export declare const FlowChain: Schema.Struct<{
347
460
  requireConfirmation: Schema.optional<typeof Schema.Boolean>;
348
461
  }>;
349
462
 
350
- export declare type FlowChain = typeof FlowChain.Type;
463
+ /** An INTERFACE reached from `aiFlows.chainTo`; see {@link BriefField}. */
464
+ export declare interface FlowChain {
465
+ readonly flowRef: string;
466
+ readonly mappings: ReadonlyArray<{
467
+ readonly targetKey: string;
468
+ readonly sourceKey: string;
469
+ }>;
470
+ readonly requireConfirmation?: boolean | undefined;
471
+ }
351
472
 
352
473
  /** The raw flow definition either front door supplies before normalization.
353
474
  * Optional fields tolerate explicit `undefined` (row reads produce it). */
@@ -361,6 +482,7 @@ export declare interface FlowDefinitionInput {
361
482
  readonly orchestratorInstructions?: string | null | undefined;
362
483
  readonly chain?: FlowChain | null | undefined;
363
484
  readonly cadence?: FlowCadence | null | undefined;
485
+ readonly humanTimeoutMs?: number | null | undefined;
364
486
  }
365
487
 
366
488
  /** The single representation the engine interprets, from either front door. */
@@ -376,6 +498,10 @@ export declare interface FlowIR {
376
498
  };
377
499
  readonly chain?: FlowChain;
378
500
  readonly cadence?: FlowCadence;
501
+ /** Flow-wide default for a `human` step's park bound, in ms. A step's own
502
+ * `timeoutMs` wins; absent here too → the engine/plugin default. `0` = wait
503
+ * forever. */
504
+ readonly humanTimeoutMs?: number;
379
505
  }
380
506
 
381
507
  /** A lifecycle event the host can turn into a notification (best-effort). */
@@ -436,8 +562,34 @@ export declare interface FlowRunPayload {
436
562
  readonly input: Record<string, unknown>;
437
563
  readonly source: 'manual' | 'cron';
438
564
  readonly requestId: string;
565
+ readonly chainPath?: ReadonlyArray<string> | undefined;
566
+ }
567
+
568
+ /** One unit of execution: a single step, or a set that runs concurrently. */
569
+ export declare interface FlowSegment {
570
+ /** `undefined` for an ungrouped single step. */
571
+ readonly group?: string | undefined;
572
+ /** The steps in this segment, with their ORIGINAL indices — the durable step
573
+ * name and the timeline slot both key off the index, so it travels. */
574
+ readonly steps: ReadonlyArray<{
575
+ readonly index: number;
576
+ readonly step: FlowStep;
577
+ }>;
439
578
  }
440
579
 
580
+ /**
581
+ * Split a step list into execution segments.
582
+ *
583
+ * Only CONSECUTIVE steps sharing a group name form a segment. A group that
584
+ * reappears after an interruption forms a SECOND segment — which is almost
585
+ * certainly not what the author meant, so `validateFlow` refuses it rather than
586
+ * letting this function silently produce two sequential fan-outs from what reads
587
+ * like one. This function stays permissive on purpose: it is used by the
588
+ * validator, and a validator whose input has already been sanitised cannot
589
+ * report on it.
590
+ */
591
+ export declare const flowSegments: (steps: ReadonlyArray<FlowStep>) => ReadonlyArray<FlowSegment>;
592
+
441
593
  /** Flow-definition lifecycle. */
442
594
  export declare const FlowStatus: Schema.Literal<["draft", "active", "archived"]>;
443
595
 
@@ -479,6 +631,29 @@ export declare const FlowStep: Schema.Struct<{
479
631
  value: typeof Schema.String;
480
632
  label: typeof Schema.String;
481
633
  }>>>;
634
+ /** Gate this step on a prior value. Absent → the step always runs. A step
635
+ * whose condition is false is SKIPPED, not failed: it produces no output, so
636
+ * anything referencing its `outputKey` sees an absent value. */
637
+ when: Schema.optional<Schema.Struct<{
638
+ ref: typeof Schema.String;
639
+ op: Schema.Literal<["truthy", "falsy", "eq", "neq", "contains"]>;
640
+ value: Schema.optional<typeof Schema.Unknown>;
641
+ }>>;
642
+ /**
643
+ * Fan-out group. CONSECUTIVE steps sharing a group name run CONCURRENTLY,
644
+ * each still journaled as its own durable step.
645
+ *
646
+ * Steps in one group may not reference each other's outputs — they have no
647
+ * order between them — and `validateFlow` refuses a flow that tries. A `human`
648
+ * step may not join a group either: it suspends the whole workflow, which is
649
+ * not a thing one branch of a fan-out can do.
650
+ */
651
+ group: Schema.optional<typeof Schema.String>;
652
+ /** `human` step only — how long the run parks awaiting this review before it
653
+ * fails, in milliseconds. Omitted → the flow's `humanTimeoutMs`, then the
654
+ * engine/plugin default (see `resolveHumanReviewTimeoutMs`). `0` means WAIT
655
+ * FOREVER (the park is slot-free, so an unbounded wait costs no worker). */
656
+ timeoutMs: Schema.optional<typeof Schema.Number>;
482
657
  }>;
483
658
 
484
659
  /**
@@ -519,6 +694,9 @@ export declare interface FlowStep {
519
694
  readonly schema?: Json | undefined;
520
695
  readonly reviewMode?: ReviewMode | undefined;
521
696
  readonly options?: ReadonlyArray<ReviewOption> | undefined;
697
+ readonly when?: StepCondition | undefined;
698
+ readonly group?: string | undefined;
699
+ readonly timeoutMs?: number | undefined;
522
700
  }
523
701
 
524
702
  /** Typed builders for each step kind — terser + safer than hand-writing the
@@ -531,7 +709,7 @@ export declare const flowStep: {
531
709
  prompt: string;
532
710
  model?: string;
533
711
  outputKey?: string;
534
- }) => FlowStepDraft;
712
+ } & StepControls) => FlowStepDraft;
535
713
  /** An image/video/audio generation. */
536
714
  media: (modality: Modality, o: {
537
715
  title?: string;
@@ -540,7 +718,7 @@ export declare const flowStep: {
540
718
  model?: string;
541
719
  params?: Record<string, unknown>;
542
720
  outputKey?: string;
543
- }) => FlowStepDraft;
721
+ } & StepControls) => FlowStepDraft;
544
722
  /** Delegate to an allowed sub-agent (text-only). */
545
723
  agent: (o: {
546
724
  title?: string;
@@ -549,7 +727,7 @@ export declare const flowStep: {
549
727
  prompt: string;
550
728
  model?: string;
551
729
  outputKey?: string;
552
- }) => FlowStepDraft;
730
+ } & StepControls) => FlowStepDraft;
553
731
  /** Return a JSON object validated against a JSON Schema. */
554
732
  structured: (o: {
555
733
  title?: string;
@@ -558,15 +736,16 @@ export declare const flowStep: {
558
736
  schema: Record<string, unknown>;
559
737
  model?: string;
560
738
  outputKey?: string;
561
- }) => FlowStepDraft;
739
+ } & StepControls) => FlowStepDraft;
562
740
  /** Free-text guidance — the interpolated prompt itself is the output. */
563
741
  note: (o: {
564
742
  title?: string;
565
743
  description?: string;
566
744
  prompt: string;
567
745
  outputKey?: string;
568
- }) => FlowStepDraft;
569
- /** Pause for human review. */
746
+ } & StepControls) => FlowStepDraft;
747
+ /** Pause for human review. `timeoutMs` bounds the park (default: the flow's
748
+ * `humanTimeoutMs`, then the plugin default — 7 days; `0` = wait forever). */
570
749
  human: (o: {
571
750
  title?: string;
572
751
  description?: string;
@@ -574,12 +753,16 @@ export declare const flowStep: {
574
753
  options?: ReadonlyArray<ReviewOption>;
575
754
  prompt?: string;
576
755
  outputKey?: string;
577
- }) => FlowStepDraft;
756
+ timeoutMs?: number;
757
+ } & Pick<StepControls, "when">) => FlowStepDraft;
578
758
  };
579
759
 
580
760
  /** A step without its `id` — `defineFlow` assigns stable index-based ids. */
581
761
  export declare type FlowStepDraft = Omit<FlowStep, 'id'>;
582
762
 
763
+ /** The recorded `app.config.ts` tunables. */
764
+ export declare const getAiFlowsDefaults: () => AiFlowsDefaults;
765
+
583
766
  /** One stored flow by id, or undefined (tenant-scoped by the runtime). */
584
767
  export declare const getFlow: (ctx: AppContext, args: {
585
768
  id: string;
@@ -588,8 +771,18 @@ export declare const getFlow: (ctx: AppContext, args: {
588
771
  /** Resolve a code-registered flow by name, or `undefined` for a data flow. */
589
772
  export declare const getRegisteredFlow: (name: string) => FlowIR | undefined;
590
773
 
591
- /** The signal a `human` step parks on; `respondToFlow` sends it to resume. */
592
- export declare const HUMAN_RESPONSE_SIGNAL = "flow-human-response";
774
+ /** Namespace of the signal a `human` step parks on. Never used on its own
775
+ * the signal a step actually awaits is {@link humanResponseSignalName}. */
776
+ export declare const HUMAN_RESPONSE_SIGNAL_PREFIX = "flow-human-response";
777
+
778
+ /** Deploy-time override for {@link HUMAN_REVIEW_TIMEOUT_HOURS_DEFAULT}, in
779
+ * hours. `0` (or a negative value) means WAIT FOREVER. */
780
+ export declare const HUMAN_REVIEW_TIMEOUT_ENV = "VOLTRO_AI_FLOW_HUMAN_REVIEW_TIMEOUT_HOURS";
781
+
782
+ /** Framework default park bound for a human review: 7 days. Long enough to
783
+ * survive a weekend + a holiday Monday, short enough that an abandoned review
784
+ * eventually settles instead of pinning a run forever. */
785
+ export declare const HUMAN_REVIEW_TIMEOUT_HOURS_DEFAULT: number;
593
786
 
594
787
  /** The HITL answer, written by `respond`. */
595
788
  export declare const HumanResponse: Schema.Struct<{
@@ -599,17 +792,81 @@ export declare const HumanResponse: Schema.Struct<{
599
792
  respondedAt: Schema.optional<typeof Schema.String>;
600
793
  }>;
601
794
 
602
- export declare type HumanResponse = typeof HumanResponse.Type;
795
+ /** An INTERFACE reached from `aiFlowRuns.humanResponse`; see {@link BriefField}. */
796
+ export declare interface HumanResponse {
797
+ readonly decision?: 'approve' | 'reject' | undefined;
798
+ readonly value?: string | undefined;
799
+ readonly text?: string | undefined;
800
+ readonly respondedAt?: string | undefined;
801
+ }
802
+
803
+ /**
804
+ * The signal name ONE human step parks on, derived from that step's position in
805
+ * the (journal-pinned) plan.
806
+ *
807
+ * **Why this is per-step and not one constant.** `awaitSignalSuspending` derives
808
+ * its `DurableDeferred` from workflow-name + signal-name per execution, so a
809
+ * single `'flow-human-response'` for every human step in a flow meant the SECOND
810
+ * review awaited the deferred the FIRST answer had already completed — it
811
+ * resolved instantly, with the first reviewer's payload, and the run finished
812
+ * carrying an approval nobody gave. Measured before the fix: a two-review flow,
813
+ * one answer, `status: 'succeeded'`, `output: { first: 'answer-one', second:
814
+ * 'answer-one' }`. The same collision aliased the wait's durable timeout clock
815
+ * (`await-signal-timeout/<name>`), so the second park inherited the first's
816
+ * expiry too.
817
+ *
818
+ * **Why the INDEX is the identity.** It must be identical on every replay and
819
+ * derivable by the responder without re-resolving the flow definition (which may
820
+ * have been edited mid-run). The step list is pinned in the run's journal at
821
+ * step 0, so a step's index is fixed for the life of the run; the responder
822
+ * reads it back off the run row's live timeline. A counter, a clock, or a
823
+ * `randomUUID` would all break replay; the authored step `id` would break the
824
+ * moment a definition is re-ordered under a live run.
825
+ */
826
+ export declare const humanResponseSignalName: (stepIndex: number) => string;
603
827
 
604
828
  /** Replace `{{key}}` with `context[key]` (objects/arrays JSON-stringified; an
605
829
  * absent/null value renders empty). */
606
830
  export declare const interpolate: (text: string, context: Record<string, unknown>) => string;
607
831
 
832
+ /**
833
+ * Truthiness, defined once so the engine and the editor cannot disagree.
834
+ *
835
+ * Deliberately NOT JavaScript's: `0` and `''` are falsy in JS, and a step gated
836
+ * on a count or a generated string would then be skipped for a legitimate value.
837
+ * A flow author writing `when: { ref: 'count', op: 'truthy' }` means "there is a
838
+ * count", not "the count is non-zero" — the second is `{ op: 'neq', value: 0 }`,
839
+ * which they can say if they mean it.
840
+ *
841
+ * Absent, null and the empty ARRAY are the falsy set: those are the three
842
+ * shapes a step that produced nothing actually leaves behind.
843
+ */
844
+ export declare const isTruthy: (v: unknown) => boolean;
845
+
608
846
  /** A JSON value (for `params` / `input` / a `structured` step's JSON Schema). */
609
847
  export declare const Json: Schema.Record$<typeof Schema.String, typeof Schema.Unknown>;
610
848
 
611
849
  export declare type Json = typeof Json.Type;
612
850
 
851
+ /** A JSON-Schema node as stored on a `structured` step. */
852
+ export declare type JsonSchemaNode = Record<string, unknown>;
853
+
854
+ /**
855
+ * Adapt a `structured` step's JSON Schema to the Effect `Schema` the engine
856
+ * hands `generateObject`. An absent / unusable schema falls back to the open
857
+ * record, so a step authored without one behaves exactly as before.
858
+ *
859
+ * ```ts
860
+ * jsonSchemaToEffectSchema({
861
+ * type: 'object',
862
+ * properties: { title: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } } },
863
+ * required: ['title'],
864
+ * })
865
+ * // ≡ Schema.Struct({ title: Schema.String, tags: Schema.optional(Schema.Array(Schema.String)) })
866
+ * ```
867
+ */
868
+ export declare const jsonSchemaToEffectSchema: (jsonSchema: JsonSchemaNode | undefined) => Schema.Schema<unknown, unknown>;
869
+
613
870
  /** Launch a fresh run of a flow (by code name or row id). Validates the brief,
614
871
  * creates the run row, and starts the durable `flow.run` workflow. */
615
872
  export declare const launchFlow: (ctx: AppContext, args: {
@@ -638,6 +895,20 @@ export declare const makeMediaGenerator: (persist: MediaPersist, opts?: {
638
895
  readonly resolveModel?: (model: string) => ProviderConfig;
639
896
  }) => MediaGenerator;
640
897
 
898
+ /**
899
+ * How many flows one chain may traverse, including the flow that started it.
900
+ *
901
+ * Five, because a chain is a hand-authored pipeline and nobody writes six on
902
+ * purpose — but the number is a FIELD rather than a constant precisely because
903
+ * "nobody does that" is an assumption about other people's apps. A cycle is
904
+ * refused separately and unconditionally; this bounds the acyclic case, where
905
+ * every hop is a real LLM run somebody pays for.
906
+ */
907
+ export declare const MAX_CHAIN_DEPTH_DEFAULT = 5;
908
+
909
+ /** Deploy-time override for {@link MAX_CHAIN_DEPTH_DEFAULT}. */
910
+ export declare const MAX_CHAIN_DEPTH_ENV = "VOLTRO_AI_FLOW_MAX_CHAIN_DEPTH";
911
+
641
912
  /** Legacy default was 30, clamped to [1,100]. (Distinct from `@voltro/ai`'s
642
913
  * own `maxSteps` default of 8 for a single tool loop.) */
643
914
  export declare const MAX_STEPS_DEFAULT = 30;
@@ -734,6 +1005,9 @@ export declare type Modality = typeof Modality.Type;
734
1005
  */
735
1006
  export declare const nextRuns: (cadence: FlowCadence, from: Date, count?: number) => ReadonlyArray<Date>;
736
1007
 
1008
+ /** Group names that appear in more than one segment — a non-contiguous group. */
1009
+ export declare const nonContiguousGroups: (segments: ReadonlyArray<FlowSegment>) => ReadonlyArray<string>;
1010
+
737
1011
  /**
738
1012
  * Lower a raw flow definition (from code or a row) to the normalized IR:
739
1013
  * default the mode, clamp maxSteps, sanitize outputKeys, drop empties. Pure +
@@ -745,16 +1019,52 @@ export declare const normalizeFlow: (def: FlowDefinitionInput) => FlowIR;
745
1019
  * `defineFlow` when the module is loaded. */
746
1020
  export declare const registerFlow: (ir: FlowIR) => void;
747
1021
 
1022
+ /** Test seam — forget the recorded tunables. */
1023
+ export declare const resetAiFlowsDefaults: () => void;
1024
+
1025
+ /**
1026
+ * Resolve a human step's park bound down the precedence ladder. Pure — the env
1027
+ * read is passed in so a test can drive it without touching `process.env`.
1028
+ */
1029
+ export declare const resolveHumanReviewTimeoutMs: (input: {
1030
+ readonly step?: {
1031
+ readonly timeoutMs?: number | undefined;
1032
+ };
1033
+ readonly flow?: {
1034
+ readonly humanTimeoutMs?: number | undefined;
1035
+ };
1036
+ readonly deps?: {
1037
+ readonly humanReviewTimeoutMs?: number | undefined;
1038
+ };
1039
+ readonly env?: NodeJS.ProcessEnv;
1040
+ }) => number;
1041
+
748
1042
  /** Resolve the IR: code registry by name first, else the stored `ai_flows` row. */
749
1043
  export declare const resolveIr: (ctx: AppContext, flowRef: string) => Effect.Effect<FlowIR, FlowNotFound>;
750
1044
 
1045
+ /**
1046
+ * Resolve the chain-depth ceiling down the same ladder as every other tunable:
1047
+ * engine deps → plugin option (`app.config.ts`) → env → framework default.
1048
+ *
1049
+ * A value below 1 is IGNORED rather than honoured. `0` would mean "no flow may
1050
+ * chain at all", which is expressible by not declaring a chain, and reading it
1051
+ * literally would turn a typo into a silently disabled feature — the same
1052
+ * reasoning `msFromHoursEnv` applies to a negative timeout.
1053
+ */
1054
+ export declare const resolveMaxChainDepth: (deps?: {
1055
+ readonly maxChainDepth?: number | undefined;
1056
+ readonly env?: NodeJS.ProcessEnv;
1057
+ }) => number;
1058
+
751
1059
  /** Record a human's answer to a `waiting` run + signal the parked workflow to
752
- * resume. (The park itself lands with the slot-freeing await, task #32/#35.) */
1060
+ * resume. The signal is addressed to THAT STEP's own durable deferred — one
1061
+ * shared name meant a later review resolved with an earlier answer. */
753
1062
  export declare const respondToFlow: (ctx: AppContext, args: {
754
1063
  runId: string;
755
1064
  response: HumanResponse;
756
1065
  }) => Effect.Effect<{
757
1066
  runId: string;
1067
+ stepIndex: number;
758
1068
  }, FlowRunNotFound | FlowNotAwaitingResponse, never>;
759
1069
 
760
1070
  /** Re-run a run from its first unfinished step. Reuses the SAME requestId so the
@@ -819,7 +1129,7 @@ export declare const RunStep: Schema.Struct<{
819
1129
  title: typeof Schema.String;
820
1130
  description: Schema.optional<typeof Schema.String>;
821
1131
  type: Schema.Literal<["agent", "generate", "structured", "note", "human"]>;
822
- status: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed"]>;
1132
+ status: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed", "skipped"]>;
823
1133
  startedAt: Schema.optional<typeof Schema.String>;
824
1134
  completedAt: Schema.optional<typeof Schema.String>;
825
1135
  /** Per-step cost in micro-USD (the app view projects to €/cents). */
@@ -833,6 +1143,9 @@ export declare const RunStep: Schema.Struct<{
833
1143
  text: typeof Schema.String;
834
1144
  }>>;
835
1145
  errorMessage: Schema.optional<typeof Schema.String>;
1146
+ /** Why a `skipped` step was skipped — the rendered condition, so the timeline
1147
+ * says "{{approved}} is not set" instead of leaving a silent gap. */
1148
+ skipReason: Schema.optional<typeof Schema.String>;
836
1149
  review: Schema.optional<Schema.Struct<{
837
1150
  mode: Schema.Literal<["approve", "choice", "text"]>;
838
1151
  options: Schema.optional<Schema.Array$<Schema.Struct<{
@@ -861,16 +1174,48 @@ export declare interface RunStep {
861
1174
  readonly text: string;
862
1175
  } | undefined;
863
1176
  readonly errorMessage?: string | undefined;
1177
+ readonly skipReason?: string | undefined;
864
1178
  readonly review?: RunReview | undefined;
865
1179
  }
866
1180
 
867
- export declare const RunStepStatus: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed"]>;
1181
+ export declare const RunStepStatus: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed", "skipped"]>;
868
1182
 
869
1183
  export declare type RunStepStatus = typeof RunStepStatus.Type;
870
1184
 
871
1185
  /** Sanitize an outputKey to the `{{ref}}`-safe charset. */
872
1186
  export declare const sanitizeOutputKey: (key: string) => string;
873
1187
 
1188
+ /** Record the `app.config.ts` tunables (called by the plugin factory). */
1189
+ export declare const setAiFlowsDefaults: (defaults: AiFlowsDefaults) => void;
1190
+
1191
+ declare const StepCondition: Schema.Struct<{
1192
+ /** A brief-field key or an EARLIER step's `outputKey`. */
1193
+ ref: typeof Schema.String;
1194
+ op: Schema.Literal<["truthy", "falsy", "eq", "neq", "contains"]>;
1195
+ /** The right-hand side for `eq` / `neq` / `contains`. Ignored by the unary ops. */
1196
+ value: Schema.optional<typeof Schema.Unknown>;
1197
+ }>;
1198
+
1199
+ /** An INTERFACE, for the same api-report reason as `FlowStep` — it is reached
1200
+ * from an exported table column, and an alias to a mapped type gets expanded
1201
+ * structurally in an order that is not stable across build scopes. */
1202
+ declare interface StepCondition {
1203
+ readonly ref: string;
1204
+ readonly op: ConditionOp;
1205
+ readonly value?: unknown;
1206
+ }
1207
+ export { StepCondition }
1208
+ export { StepCondition as StepConditionType }
1209
+
1210
+ /** Gating + fan-out, accepted by every builder. Kept as one type so a new
1211
+ * builder cannot quietly omit them — the six builders below spread it. */
1212
+ declare interface StepControls {
1213
+ /** Run this step only when the condition holds; otherwise SKIP it. */
1214
+ readonly when?: StepCondition;
1215
+ /** Consecutive steps sharing a group run CONCURRENTLY. */
1216
+ readonly group?: string;
1217
+ }
1218
+
874
1219
  /** The five step types. `generate` fans out to four modalities. */
875
1220
  export declare const StepType: Schema.Literal<["agent", "generate", "structured", "note", "human"]>;
876
1221
 
@@ -905,4 +1250,16 @@ export declare const Visibility: Schema.Literal<["private", "organization", "sha
905
1250
 
906
1251
  export declare type Visibility = typeof Visibility.Type;
907
1252
 
1253
+ /**
1254
+ * Which human step a `waiting` run is parked on — the index the engine used to
1255
+ * derive the step's signal name.
1256
+ *
1257
+ * Read from the LIVE TIMELINE the engine wrote when it parked (`steps[i].status
1258
+ * === 'waiting'`), with `currentStep` as the fallback for a row whose timeline
1259
+ * hasn't been written yet. Deliberately NOT re-resolved from the flow definition:
1260
+ * a definition edited mid-run would hand back a different plan than the one the
1261
+ * run's journal pinned, and the answer would be delivered to the wrong step.
1262
+ */
1263
+ export declare const waitingStepIndex: (row: Record<string, unknown>) => number;
1264
+
908
1265
  export { }