@voltro/plugin-ai-flows 0.34.0 → 0.36.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
@@ -45,6 +45,15 @@ export declare const aiFlowRunsTable: Table<"_voltro_ai_flow_runs", FieldDefinit
45
45
  readonly humanResponse: ColumnBuilder<HumanResponse | null, "json", boolean>;
46
46
  /** Staged follow-up when the chain requires confirmation. */
47
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>;
48
57
  /** Accumulated cost in micro-USD (app view projects to €/cents). */
49
58
  readonly costMicroUsd: ColumnBuilder<number, "integer", true>;
50
59
  readonly durationMs: ColumnBuilder<number | null, "integer", boolean>;
@@ -72,6 +81,9 @@ export declare const aiFlowRunsTable: Table<"_voltro_ai_flow_runs", FieldDefinit
72
81
  export declare interface AiFlowsDefaults {
73
82
  /** Park bound for a `human` step, in ms. `0` = wait forever. */
74
83
  readonly humanReviewTimeoutMs?: number;
84
+ /** How many flows one `chainTo` chain may traverse. See
85
+ * {@link MAX_CHAIN_DEPTH_DEFAULT}. */
86
+ readonly maxChainDepth?: number;
75
87
  }
76
88
 
77
89
  /**
@@ -273,6 +285,11 @@ export declare const clearRegistry: () => void;
273
285
  /** Every `{{ref}}` name referenced in `text` (order-preserving, may repeat). */
274
286
  export declare const collectRefs: (text: string) => ReadonlyArray<string>;
275
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
+
276
293
  /** Create a flow row. Generates an id when absent; tenant/audit columns are
277
294
  * auto-stamped by the runtime. */
278
295
  export declare const createFlow: (ctx: AppContext, fields: Record<string, unknown>) => Effect.Effect<Readonly<Record<string, unknown>>, never, never>;
@@ -309,6 +326,10 @@ export declare const deleteFlow: (ctx: AppContext, args: {
309
326
  id: string;
310
327
  }) => Effect.Effect<boolean, never, never>;
311
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
+
312
333
  export declare interface EngineDeps {
313
334
  /** Resolve an `agent` step's `agentRef` → system prompt + model. Absent →
314
335
  * agent steps with an `agentRef` fail with FlowCapabilityMissing. */
@@ -339,8 +360,24 @@ export declare interface EngineDeps {
339
360
  * option / `VOLTRO_AI_FLOW_HUMAN_REVIEW_TIMEOUT_HOURS` (see `defaults.ts`).
340
361
  * `0` = wait forever. */
341
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;
342
368
  }
343
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
+
344
381
  /** The canonical name the launch action starts + the app re-export registers. */
345
382
  export declare const FLOW_RUN_WORKFLOW = "flow.run";
346
383
 
@@ -525,8 +562,34 @@ export declare interface FlowRunPayload {
525
562
  readonly input: Record<string, unknown>;
526
563
  readonly source: 'manual' | 'cron';
527
564
  readonly requestId: string;
565
+ readonly chainPath?: ReadonlyArray<string> | undefined;
528
566
  }
529
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
+ }>;
578
+ }
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
+
530
593
  /** Flow-definition lifecycle. */
531
594
  export declare const FlowStatus: Schema.Literal<["draft", "active", "archived"]>;
532
595
 
@@ -568,6 +631,24 @@ export declare const FlowStep: Schema.Struct<{
568
631
  value: typeof Schema.String;
569
632
  label: typeof Schema.String;
570
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>;
571
652
  /** `human` step only — how long the run parks awaiting this review before it
572
653
  * fails, in milliseconds. Omitted → the flow's `humanTimeoutMs`, then the
573
654
  * engine/plugin default (see `resolveHumanReviewTimeoutMs`). `0` means WAIT
@@ -613,6 +694,8 @@ export declare interface FlowStep {
613
694
  readonly schema?: Json | undefined;
614
695
  readonly reviewMode?: ReviewMode | undefined;
615
696
  readonly options?: ReadonlyArray<ReviewOption> | undefined;
697
+ readonly when?: StepCondition | undefined;
698
+ readonly group?: string | undefined;
616
699
  readonly timeoutMs?: number | undefined;
617
700
  }
618
701
 
@@ -626,7 +709,7 @@ export declare const flowStep: {
626
709
  prompt: string;
627
710
  model?: string;
628
711
  outputKey?: string;
629
- }) => FlowStepDraft;
712
+ } & StepControls) => FlowStepDraft;
630
713
  /** An image/video/audio generation. */
631
714
  media: (modality: Modality, o: {
632
715
  title?: string;
@@ -635,7 +718,7 @@ export declare const flowStep: {
635
718
  model?: string;
636
719
  params?: Record<string, unknown>;
637
720
  outputKey?: string;
638
- }) => FlowStepDraft;
721
+ } & StepControls) => FlowStepDraft;
639
722
  /** Delegate to an allowed sub-agent (text-only). */
640
723
  agent: (o: {
641
724
  title?: string;
@@ -644,7 +727,7 @@ export declare const flowStep: {
644
727
  prompt: string;
645
728
  model?: string;
646
729
  outputKey?: string;
647
- }) => FlowStepDraft;
730
+ } & StepControls) => FlowStepDraft;
648
731
  /** Return a JSON object validated against a JSON Schema. */
649
732
  structured: (o: {
650
733
  title?: string;
@@ -653,14 +736,14 @@ export declare const flowStep: {
653
736
  schema: Record<string, unknown>;
654
737
  model?: string;
655
738
  outputKey?: string;
656
- }) => FlowStepDraft;
739
+ } & StepControls) => FlowStepDraft;
657
740
  /** Free-text guidance — the interpolated prompt itself is the output. */
658
741
  note: (o: {
659
742
  title?: string;
660
743
  description?: string;
661
744
  prompt: string;
662
745
  outputKey?: string;
663
- }) => FlowStepDraft;
746
+ } & StepControls) => FlowStepDraft;
664
747
  /** Pause for human review. `timeoutMs` bounds the park (default: the flow's
665
748
  * `humanTimeoutMs`, then the plugin default — 7 days; `0` = wait forever). */
666
749
  human: (o: {
@@ -671,7 +754,7 @@ export declare const flowStep: {
671
754
  prompt?: string;
672
755
  outputKey?: string;
673
756
  timeoutMs?: number;
674
- }) => FlowStepDraft;
757
+ } & Pick<StepControls, "when">) => FlowStepDraft;
675
758
  };
676
759
 
677
760
  /** A step without its `id` — `defineFlow` assigns stable index-based ids. */
@@ -746,6 +829,20 @@ export declare const humanResponseSignalName: (stepIndex: number) => string;
746
829
  * absent/null value renders empty). */
747
830
  export declare const interpolate: (text: string, context: Record<string, unknown>) => string;
748
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
+
749
846
  /** A JSON value (for `params` / `input` / a `structured` step's JSON Schema). */
750
847
  export declare const Json: Schema.Record$<typeof Schema.String, typeof Schema.Unknown>;
751
848
 
@@ -798,6 +895,20 @@ export declare const makeMediaGenerator: (persist: MediaPersist, opts?: {
798
895
  readonly resolveModel?: (model: string) => ProviderConfig;
799
896
  }) => MediaGenerator;
800
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
+
801
912
  /** Legacy default was 30, clamped to [1,100]. (Distinct from `@voltro/ai`'s
802
913
  * own `maxSteps` default of 8 for a single tool loop.) */
803
914
  export declare const MAX_STEPS_DEFAULT = 30;
@@ -894,6 +1005,9 @@ export declare type Modality = typeof Modality.Type;
894
1005
  */
895
1006
  export declare const nextRuns: (cadence: FlowCadence, from: Date, count?: number) => ReadonlyArray<Date>;
896
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
+
897
1011
  /**
898
1012
  * Lower a raw flow definition (from code or a row) to the normalized IR:
899
1013
  * default the mode, clamp maxSteps, sanitize outputKeys, drop empties. Pure +
@@ -928,6 +1042,20 @@ export declare const resolveHumanReviewTimeoutMs: (input: {
928
1042
  /** Resolve the IR: code registry by name first, else the stored `ai_flows` row. */
929
1043
  export declare const resolveIr: (ctx: AppContext, flowRef: string) => Effect.Effect<FlowIR, FlowNotFound>;
930
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
+
931
1059
  /** Record a human's answer to a `waiting` run + signal the parked workflow to
932
1060
  * resume. The signal is addressed to THAT STEP's own durable deferred — one
933
1061
  * shared name meant a later review resolved with an earlier answer. */
@@ -1001,7 +1129,7 @@ export declare const RunStep: Schema.Struct<{
1001
1129
  title: typeof Schema.String;
1002
1130
  description: Schema.optional<typeof Schema.String>;
1003
1131
  type: Schema.Literal<["agent", "generate", "structured", "note", "human"]>;
1004
- status: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed"]>;
1132
+ status: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed", "skipped"]>;
1005
1133
  startedAt: Schema.optional<typeof Schema.String>;
1006
1134
  completedAt: Schema.optional<typeof Schema.String>;
1007
1135
  /** Per-step cost in micro-USD (the app view projects to €/cents). */
@@ -1015,6 +1143,9 @@ export declare const RunStep: Schema.Struct<{
1015
1143
  text: typeof Schema.String;
1016
1144
  }>>;
1017
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>;
1018
1149
  review: Schema.optional<Schema.Struct<{
1019
1150
  mode: Schema.Literal<["approve", "choice", "text"]>;
1020
1151
  options: Schema.optional<Schema.Array$<Schema.Struct<{
@@ -1043,10 +1174,11 @@ export declare interface RunStep {
1043
1174
  readonly text: string;
1044
1175
  } | undefined;
1045
1176
  readonly errorMessage?: string | undefined;
1177
+ readonly skipReason?: string | undefined;
1046
1178
  readonly review?: RunReview | undefined;
1047
1179
  }
1048
1180
 
1049
- export declare const RunStepStatus: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed"]>;
1181
+ export declare const RunStepStatus: Schema.Literal<["pending", "running", "waiting", "succeeded", "failed", "skipped"]>;
1050
1182
 
1051
1183
  export declare type RunStepStatus = typeof RunStepStatus.Type;
1052
1184
 
@@ -1056,6 +1188,34 @@ export declare const sanitizeOutputKey: (key: string) => string;
1056
1188
  /** Record the `app.config.ts` tunables (called by the plugin factory). */
1057
1189
  export declare const setAiFlowsDefaults: (defaults: AiFlowsDefaults) => void;
1058
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
+
1059
1219
  /** The five step types. `generate` fans out to four modalities. */
1060
1220
  export declare const StepType: Schema.Literal<["agent", "generate", "structured", "note", "human"]>;
1061
1221