@kindgi/flow 0.0.0-bootstrap.0 → 0.1.1

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.
@@ -0,0 +1,792 @@
1
+ import type { EdgeId, FlowId, NodeId } from '@kindgi/types';
2
+ /**
3
+ * Path roots addressable in edge predicates.
4
+ *
5
+ * Every path in an operand starts with one of these roots. The kernel resolves
6
+ * each root against the current run state at the moment an edge is evaluated:
7
+ *
8
+ * - `runInput` → the input value passed to `runGraph()`.
9
+ * - `nodeOutputs.<id>` → the resolved output of a previously-completed node,
10
+ * keyed by its declared `NodeId`. Immutable — one
11
+ * writer per node, no fan-in contention.
12
+ * - `state` → the run's cumulative state channel. Last-writer-wins
13
+ * under concurrent writes; use `nodeOutputs.<id>` for
14
+ * conflict-free per-node results.
15
+ * - `iterationIndex` → 0-based iteration counter. Only meaningful inside a
16
+ * loop node's `exitCondition`. Resolves to `undefined`
17
+ * in any other context.
18
+ * - `iterationOutput` → the `$loop-end` output of the just-completed
19
+ * iteration. Only meaningful inside a loop node's
20
+ * `exitCondition`. Resolves to `undefined` elsewhere.
21
+ */
22
+ export type PathRoot = 'runInput' | 'state' | `nodeOutputs.${string}` | 'iterationIndex' | 'iterationOutput';
23
+ /**
24
+ * A path into the run's addressable state. Dot-separated segments, always
25
+ * starting with a recognized root. Path syntax is deliberately narrow — no
26
+ * wildcards, no array indexing, no filters. If a predicate needs more, the
27
+ * caller should compute a normalized field into `state` from a prior node
28
+ * and reference that.
29
+ *
30
+ * @example
31
+ * 'runInput.userId'
32
+ * 'nodeOutputs.classify.category'
33
+ * 'state.retryCount'
34
+ * 'iterationOutput.finishReason' // only inside a loop exitCondition
35
+ * 'iterationIndex' // only inside a loop exitCondition
36
+ */
37
+ export type Path = string;
38
+ /** Literal operand — a JSON-serializable value compared against a path. */
39
+ export interface LiteralOperand {
40
+ readonly literal: unknown;
41
+ }
42
+ /** Path operand — resolves at eval time against the current run's environment. */
43
+ export interface PathOperand {
44
+ readonly path: Path;
45
+ }
46
+ /**
47
+ * Operand = one side of a comparison. Every operand is either a static literal
48
+ * or a path reference. Keeping them symmetric means predicates naturally
49
+ * express path-vs-path comparisons (`state.attempts` vs `state.maxAttempts`)
50
+ * without a second operator flavor.
51
+ */
52
+ export type Operand = LiteralOperand | PathOperand;
53
+ /**
54
+ * A declarative object (schema-version 1.8.0+): each key maps to an
55
+ * Operand — a `{ literal }`, or a `{ path }` rooted at `runInput`,
56
+ * `state` or `nodeOutputs.<nodeId>`. Resolved against the run
57
+ * environment with `resolveMapping`; a path that doesn't resolve omits
58
+ * its key, so a consumer can tell "not provided" from any real value.
59
+ *
60
+ * Used for a leaf node's `inputMapping`, a subgraph node's
61
+ * `inputMapping` and a flow's `output`.
62
+ */
63
+ export type Mapping = Readonly<Record<string, Operand>>;
64
+ /**
65
+ * Predicate expression. Serializable JSON. Evaluated to a boolean at edge
66
+ * dispatch time; an edge fires iff its `when` evaluates true (or is absent).
67
+ *
68
+ * Design constraint: closed operator set + JSON-serializable operand shape.
69
+ * This is deliberately narrower than a JS closure so flows can round-trip
70
+ * through storage, get rendered in a visual editor, and stay reproducible
71
+ * across process restarts.
72
+ */
73
+ export type Expr = {
74
+ readonly op: 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte';
75
+ readonly left: Operand;
76
+ readonly right: Operand;
77
+ } | {
78
+ readonly op: 'in' | 'notIn';
79
+ readonly value: Operand;
80
+ readonly set: Operand;
81
+ } | {
82
+ readonly op: 'exists' | 'notExists' | 'truthy' | 'falsy';
83
+ readonly value: Operand;
84
+ } | {
85
+ readonly op: 'and' | 'or';
86
+ readonly children: readonly Expr[];
87
+ } | {
88
+ readonly op: 'not';
89
+ readonly child: Expr;
90
+ };
91
+ /** The set of operator names recognized by the DSL, exported for tooling. */
92
+ export declare const OPERATORS: readonly ["eq", "ne", "lt", "lte", "gt", "gte", "in", "notIn", "exists", "notExists", "truthy", "falsy", "and", "or", "not"];
93
+ export type OperatorName = (typeof OPERATORS)[number];
94
+ /**
95
+ * A leaf node — the classic "one handler" primitive. `kind` distinguishes what
96
+ * this node executes:
97
+ * - `tool` → an in-process or MCP tool call.
98
+ * - `agent` → an agent invocation.
99
+ *
100
+ * Invoking another flow is not a leaf kind: it is the `SubflowNode`
101
+ * variant (`kind: 'subgraph'`, schema-version 1.7.0+) with `flowRef` /
102
+ * `inputMapping` / `outputSchema` / `convergence`.
103
+ *
104
+ * The flow package holds no handler bindings; the runtime looks `ref` up
105
+ * in its `HandlerRegistry` (`@kindgi/handler`).
106
+ */
107
+ export interface LeafNode {
108
+ /** Node id (unique within the containing flow). */
109
+ readonly id: NodeId;
110
+ /**
111
+ * Which handler kind runs at this node. `'tool'` dispatches
112
+ * `ref` as a `ToolId`; `'agent'` dispatches `ref` as an `AgentId`.
113
+ */
114
+ readonly kind: 'tool' | 'agent';
115
+ /**
116
+ * Reference to the concrete implementation. Interpretation depends on
117
+ * `kind` and is opaque to the flow package.
118
+ */
119
+ readonly ref: string;
120
+ readonly config?: Readonly<Record<string, unknown>>;
121
+ /**
122
+ * What this node's handler receives (schema-version 1.8.0+): each key
123
+ * resolved when the node dispatches, so a node can combine the run
124
+ * input, state and the outputs of any earlier nodes. Absent → the
125
+ * output of the node's single upstream node, or the run input for a
126
+ * `$start` edge. The loader rejects paths that aren't rooted at
127
+ * `runInput` / `state` / `nodeOutputs.<existing node>`.
128
+ */
129
+ readonly inputMapping?: Mapping;
130
+ }
131
+ /**
132
+ * JSON Schema (draft 2020-12) describing the shape a loop's iteration
133
+ * outputs must conform to. Stored as an opaque object at the flow layer;
134
+ * the kernel validates each iteration's `$loop-end` output against this
135
+ * schema at iteration boundary. Malformed schema surfaces at flow load
136
+ * time via `LoopOutputSchemaError`.
137
+ *
138
+ * Kept as `object` here (not a specific TypeScript type) because JSON
139
+ * Schema's full expressiveness doesn't map cleanly onto a static type.
140
+ * The loader validates that the schema conforms to the draft-2020-12
141
+ * meta-schema; the kernel treats it as opaque input to Ajv.
142
+ */
143
+ export type LoopOutputSchema = Readonly<Record<string, unknown>>;
144
+ /**
145
+ * Fields shared by every loop variant. See the concrete variants
146
+ * (`WhileLoopNode`, `ForeachLoopNode`) for variant-specific fields.
147
+ *
148
+ * Every iteration produces distinct journal entries (`iteration.started`,
149
+ * `iteration.completed`); body-node journal entries carry a `loopContext`
150
+ * field attributing them to the enclosing iteration. Iterations run one
151
+ * at a time, except in a foreach loop that sets `concurrency` (see
152
+ * `ForeachLoopNode`).
153
+ *
154
+ * Node ids in the body must be globally unique across the entire flow
155
+ * (including sibling loop bodies at any nesting level). This is enforced
156
+ * by the loader.
157
+ *
158
+ * State inside a loop is always loop-internal: body-handler `stateDelta`
159
+ * writes do NOT leak to outer flow state after loop completion. The
160
+ * escape contract is `outputSchema` — required on every loop, no
161
+ * exceptions. There is no `stateScope` option.
162
+ */
163
+ interface LoopNodeCommon {
164
+ /** Node id (unique across the ENTIRE flow including nested loop bodies). */
165
+ readonly id: NodeId;
166
+ /** Discriminant — always `'loop'` for this node kind. */
167
+ readonly kind: 'loop';
168
+ /** The body sub-flow iterated per loop pass. */
169
+ readonly body: LoopBody;
170
+ /**
171
+ * Hard cap on iteration count. Required. Bounded to `1 ≤ maxIterations ≤ 10000`
172
+ * by the loader. When the cap is reached, the loop exits with
173
+ * `stopReason: 'max-iterations'`.
174
+ */
175
+ readonly maxIterations: number;
176
+ /**
177
+ * REQUIRED. JSON Schema (draft 2020-12) describing the shape every
178
+ * iteration's `$loop-end` output must satisfy. Also constrains
179
+ * `finalOutput` and (when `collectAllIterations`) each element of
180
+ * `outputs[]`. Every loop declares a typed contract — no exceptions.
181
+ */
182
+ readonly outputSchema: LoopOutputSchema;
183
+ /**
184
+ * When true, the loop node's output includes an `outputs` array with
185
+ * every iteration's `$loop-end` output. When false (default), only the
186
+ * last iteration's output is exposed via `finalOutput`; callers wanting
187
+ * history read the journal.
188
+ */
189
+ readonly collectAllIterations?: boolean;
190
+ }
191
+ /**
192
+ * A `while`-style loop node: iteration is condition-driven. Runs the
193
+ * body until `exitCondition` evaluates true (or `maxIterations` fires).
194
+ *
195
+ * `evaluationTiming` selects do-while (default) vs while semantics:
196
+ * - `'after'` → body runs first, then exit condition is checked
197
+ * against the just-completed iteration output. Loop
198
+ * always runs at least once. This is `do { body } while (!exit)`.
199
+ * - `'before'` → exit condition is checked first, against the previous
200
+ * iteration's output (or `null` on iteration 0). Body
201
+ * may run zero times. This is `while (!exit) { body }`.
202
+ *
203
+ * Exit condition path roots: `state`, `iterationIndex`, `iterationOutput`,
204
+ * and `nodeOutputs.<bodyNodeId>` (of the just-completed iteration; not
205
+ * available on `before`-timed iteration 0).
206
+ */
207
+ export interface WhileLoopNode extends LoopNodeCommon {
208
+ readonly loopKind: 'while';
209
+ readonly exitCondition: Expr;
210
+ /**
211
+ * When the exit condition is evaluated relative to the body. Default `'after'`.
212
+ */
213
+ readonly evaluationTiming?: 'before' | 'after';
214
+ }
215
+ /**
216
+ * A `foreach`-style loop node: iteration is array-driven. Runs the body
217
+ * once per element of `iterateOver` (up to `maxIterations`).
218
+ *
219
+ * `iterateOver` is resolved ONCE at loop-enter time (not re-resolved per
220
+ * iteration) — the array is pinned for the duration of the loop. If it
221
+ * resolves to a non-array, the loop fails immediately with
222
+ * `stopReason: 'iterate-over-not-array'`.
223
+ *
224
+ * Each iteration's `$loop-start` input is the corresponding array element.
225
+ * The loop exits when the array is exhausted (`stopReason: 'array-exhausted'`)
226
+ * or `maxIterations` fires. No exit condition — attempting to declare one
227
+ * is a load-time validation error.
228
+ *
229
+ * `state.$loop.currentElement` inside a foreach body handler is the
230
+ * element fed to the current iteration. `iterationIndex` is the 0-based
231
+ * position in the source array.
232
+ */
233
+ export interface ForeachLoopNode extends LoopNodeCommon {
234
+ readonly loopKind: 'foreach';
235
+ /**
236
+ * Operand that must resolve to an array at loop-enter time. Typical
237
+ * shape: `{ path: 'runInput.items' }` or `{ path: 'nodeOutputs.list.records' }`.
238
+ */
239
+ readonly iterateOver: Operand;
240
+ /**
241
+ * Parallel-worker count for iteration dispatch. Default `1` (strictly
242
+ * sequential). When set to N > 1, the
243
+ * kernel spins up N workers that pull iteration indices from a shared
244
+ * counter — iteration `i` is assigned in array order (0, 1, 2, …) but
245
+ * may settle in wall-clock order.
246
+ *
247
+ * Bounded by the loader to `1 ≤ concurrency ≤ min(maxIterations, 32)`.
248
+ * The upper cap of 32 is a fixed safety limit.
249
+ *
250
+ * Canonical guarantees under `concurrency > 1`:
251
+ * - `outputs[]` (when `collectAllIterations` is true) is sorted by
252
+ * iteration index, NOT settle order.
253
+ * - Journal `iteration.*` entries reflect wall-clock settle order.
254
+ * Consumers sort by `iteration` for canonical projection.
255
+ * - Handler failure in one iteration cancels all in-flight siblings
256
+ * cleanly (each journals `iteration.completed` with
257
+ * `stopReason: 'cancelled'`); the failed iteration journals
258
+ * `stopReason: 'body-failure'` and the loop node fails.
259
+ *
260
+ * State inside a loop body is per-iteration already (by design),
261
+ * so parallel iterations cannot race on shared body state.
262
+ */
263
+ readonly concurrency?: number;
264
+ }
265
+ /**
266
+ * A loop node — first-class iteration primitive (schema-version 1.2.0+).
267
+ * Discriminated by `loopKind` into `while` and `foreach` variants.
268
+ */
269
+ export type LoopNode = WhileLoopNode | ForeachLoopNode;
270
+ /**
271
+ * Convergence mode for a `fanout` node — declared at flow-authoring time,
272
+ * decidable at flow-load time (so downstream consumers know exactly what
273
+ * shape they'll receive).
274
+ *
275
+ * - `'all-succeed'` — fanout fails on the FIRST branch failure. In-flight
276
+ * sibling branches are cancelled through the standard kernel abort
277
+ * path. Output emitted only when EVERY branch succeeds; shape is
278
+ * `{ [branchId: string]: output }`.
279
+ * - `'any-succeed'` — fanout succeeds on the FIRST branch success. In-flight
280
+ * sibling branches are cancelled through the standard kernel abort path.
281
+ * Output shape is `{ winnerBranchId, output }`. Fails only when every
282
+ * branch fails.
283
+ * - `'settle-all'` — fanout waits for every branch to reach a terminal
284
+ * state (success or fail); no sibling cancellation on failure. Output
285
+ * shape is `{ [branchId: string]: { status: 'succeeded' | 'failed',
286
+ * output? | error? } }`. This is the default recommended mode for
287
+ * supervisor-style "invoke N in parallel and reduce" patterns.
288
+ */
289
+ export type ConvergenceMode = 'all-succeed' | 'any-succeed' | 'settle-all';
290
+ /**
291
+ * The set of convergence-mode names recognized by the runtime, exported for
292
+ * tooling that needs the closed enum without repeating literals.
293
+ */
294
+ export declare const CONVERGENCE_MODES: readonly ["all-succeed", "any-succeed", "settle-all"];
295
+ /**
296
+ * One branch of a `fanout` node. Each branch is a distinct handler-registry
297
+ * entry executed concurrently with its siblings; a branch is NOT a sub-flow
298
+ * (unlike a loop body). `branchId` is the stable label used in the fan-in
299
+ * output object and the journal; `handler` is the string key used to look
300
+ * the handler up in the kernel's `HandlerRegistry` (same lookup mechanism
301
+ * as a leaf node's `ref`). `outputSchema` is REQUIRED — the typed contract
302
+ * for this branch's output, validated at settle time.
303
+ */
304
+ export interface FanoutBranch {
305
+ readonly branchId: string;
306
+ readonly handler: string;
307
+ readonly outputSchema: LoopOutputSchema;
308
+ }
309
+ /**
310
+ * A fanout node — first-class parallel-branch primitive (schema-version
311
+ * 1.6.0+). Runs N DIFFERENT handler bodies in parallel from a single
312
+ * dispatch and converges by a declared mode. Complementary to `foreach`
313
+ * (N iterations of the SAME body): fanout invokes N distinct handlers
314
+ * concurrently.
315
+ *
316
+ * The fanout node itself is treated by the outer scheduler as an ordinary
317
+ * node with a single output — its incoming edges resolve the input passed
318
+ * to every branch, and its outgoing edges receive the fan-in output. The
319
+ * fan-in shape depends on `convergence` (see `ConvergenceMode`).
320
+ *
321
+ * Sibling cancellation for `all-succeed` / `any-succeed` propagates via a
322
+ * shared `AbortController` composed with the outer abort signal, so
323
+ * cooperative branch handlers observe cancellation through their
324
+ * `NodeContext.abortSignal` — same discipline as parallel foreach and
325
+ * the outer executor's cancelRun path.
326
+ *
327
+ * Replay semantics: the outer envelope (`step.started` / `step.completed`
328
+ * / `step.failed` for the fanout node id) is journaled as with any other
329
+ * node. Per-branch progress is journaled through the fanout-specific
330
+ * `JournalKind` values (`fanout.dispatched`, `fanout.branch-completed`,
331
+ * `fanout.branch-failed`, `fanout.converged`, `fanout.cancelled-siblings`).
332
+ * On resume, if the fanout's outer `step.completed` is present the
333
+ * converged output is already in `nodeOutputs`; otherwise the whole
334
+ * fanout re-dispatches (same rule as loops — branch handlers are
335
+ * expected to be idempotent, or to use waitForToken for durable side
336
+ * effects).
337
+ */
338
+ export interface FanoutNode {
339
+ /** Node id (unique within the containing flow). */
340
+ readonly id: NodeId;
341
+ /** Discriminant — always `'fanout'` for this node kind. */
342
+ readonly kind: 'fanout';
343
+ /**
344
+ * Parallel branches to dispatch. Each branch is one handler (see
345
+ * `FanoutBranch`), not a sub-flow. All branches receive the same
346
+ * input; convergence collects their outputs.
347
+ */
348
+ readonly branches: readonly FanoutBranch[];
349
+ /**
350
+ * Optional bounded worker-pool count. Default: `branches.length` (all
351
+ * branches dispatch concurrently). Loader enforces `1 ≤ concurrency ≤
352
+ * branches.length`. When `concurrency < branches.length`, branches are
353
+ * pulled from the queue in declaration order (deterministic +
354
+ * replay-safe).
355
+ */
356
+ readonly concurrency?: number;
357
+ /** The convergence rule. Decides the fan-in shape (see `ConvergenceMode`). */
358
+ readonly convergence: ConvergenceMode;
359
+ }
360
+ /**
361
+ * Convergence mode for a `subgraph` node (schema-version 1.7.0+). Declared
362
+ * at flow-authoring time; decidable at flow-load time so downstream
363
+ * consumers know exactly what shape they'll receive from the parent node.
364
+ *
365
+ * - `'success-only'` — child failure fails the PARENT node. The
366
+ * parent's `nodeOutputs` entry is never written; the journal records
367
+ * `subgraph.failed` and the parent step transitions to `step.failed`
368
+ * with an attribution that names the child run id + stop reason.
369
+ * - `'settle-all'` — the parent node's output is always a settled
370
+ * envelope: `{ status: 'succeeded'; output }` when the child run
371
+ * completed cleanly + its terminal output validated against the
372
+ * subgraph node's declared `outputSchema`; `{ status: 'failed';
373
+ * error }` otherwise (child failure, cancellation, or output
374
+ * schema violation). The parent step transitions to `step.completed`
375
+ * regardless of child outcome — the failure is DATA at the parent
376
+ * boundary, not a fault. This is the default recommended mode for
377
+ * supervisor-style orchestration where "sub-run failed" is a
378
+ * first-class outcome the parent flow handles explicitly.
379
+ */
380
+ export type SubflowConvergence = 'success-only' | 'settle-all';
381
+ /**
382
+ * The set of subgraph convergence-mode names recognized by the runtime,
383
+ * exported for tooling that needs the closed enum without duplicating
384
+ * literals.
385
+ */
386
+ export declare const SUBGRAPH_CONVERGENCE_MODES: readonly ["success-only", "settle-all"];
387
+ /**
388
+ * Reference to another registered flow resolvable via
389
+ * `FlowRegistryBinding.getVersion(...)`. Version is an EXACT semver — no
390
+ * ranges — so deterministic replay always resolves the same child flow.
391
+ * Cross-tenant lookup is rejected by construction: the kernel invokes the
392
+ * resolver with the PARENT run's tenantId; a flow registered under a
393
+ * different tenant will not be found.
394
+ */
395
+ export interface SubflowRef {
396
+ readonly flowId: string;
397
+ readonly version: string;
398
+ }
399
+ /**
400
+ * A first-class subgraph-invocation node (schema-version 1.7.0+).
401
+ * Dispatches a full kernel run of another registered flow as one
402
+ * outer-node step. The sub-run has its own independent journal, replay
403
+ * semantics, lease/cancel machinery — every kernel durability
404
+ * guarantee is inherited without re-invention.
405
+ *
406
+ * Complementary to the loop + fanout primitives:
407
+ * - `loop.foreach` runs the SAME body × N iterations,
408
+ * - `fanout` runs N DIFFERENT bodies concurrently,
409
+ * - `subgraph` runs a whole OTHER flow inline as a node.
410
+ *
411
+ * `inputMapping` is a declarative object whose values are `Operand`s
412
+ * (path into `runInput` / `state` / `nodeOutputs.<id>`, or a `literal`).
413
+ * The sub-run's `runInput` is the object produced by resolving every
414
+ * mapping at dispatch time.
415
+ *
416
+ * `outputSchema` is the ESCAPE CONTRACT declared on the parent — decoupled
417
+ * from the sub-flow's own internal shape. Parents state what they can
418
+ * consume; the kernel validates the sub-run's terminal output against
419
+ * this schema at settle time. A validation failure is a `settle-all`
420
+ * `{ status: 'failed', error }` OR a `success-only` parent-step failure.
421
+ *
422
+ * A kernel-enforced max depth (default 10, configurable via
423
+ * `RunOptions.maxSubflowDepth`) prevents runaway recursion. Exceeding
424
+ * the depth fails the parent node at dispatch with attribution
425
+ * `subgraph-depth-exceeded`.
426
+ */
427
+ export interface SubflowNode {
428
+ /** Node id (unique within the parent flow). */
429
+ readonly id: NodeId;
430
+ /** Discriminant — always `'subgraph'` for this node kind. */
431
+ readonly kind: 'subgraph';
432
+ /**
433
+ * Which flow to invoke. `{flowId, version}` — `loadFlow` checks
434
+ * only the shape (exact semver). The runtime resolves the sub-flow
435
+ * through the run's `FlowResolver` (`@kindgi/runtime`) when the node
436
+ * dispatches; a flow it can't find fails the node with
437
+ * `subgraph-flow-not-found`. The exact version pins the child flow,
438
+ * so a mid-flight sub-run survives parent-flow edits.
439
+ */
440
+ readonly flowRef: SubflowRef;
441
+ /**
442
+ * Declarative object mapping each top-level key of the sub-run's
443
+ * `runInput` to an Operand. Loader statically verifies every value is
444
+ * a valid Operand; runtime resolution mirrors edge-predicate operand
445
+ * resolution.
446
+ */
447
+ readonly inputMapping: Mapping;
448
+ /**
449
+ * REQUIRED. JSON Schema (draft 2020-12) describing the shape the
450
+ * sub-run's terminal output must satisfy at the parent boundary. The
451
+ * loader validates well-formedness via `InvalidSubflowNodeError`;
452
+ * the kernel validates the sub-run's output at settle time.
453
+ */
454
+ readonly outputSchema: LoopOutputSchema;
455
+ /**
456
+ * Convergence mode — closed enum, extensible additively. See
457
+ * `SubflowConvergence` for the semantics.
458
+ */
459
+ readonly convergence: SubflowConvergence;
460
+ }
461
+ /**
462
+ * A flow node — either a leaf (tool / agent), a loop (iterating a body
463
+ * sub-flow until an exit condition or array is exhausted), a fanout
464
+ * (running N different bodies in parallel with declared convergence),
465
+ * or a subgraph (invoking another registered flow as a full kernel
466
+ * sub-run). Discriminated by `kind`.
467
+ *
468
+ * Leaf nodes are the majority case. Loop nodes are used for iterative
469
+ * primitives (agent turn loops, retrieval refinement, batch ingest,
470
+ * retry-with-backoff). Fanout nodes are used for supervisor "invoke N
471
+ * sub-agents in parallel and reduce" patterns. Subgraph nodes are used
472
+ * for hierarchical orchestration — composing a large workflow out of
473
+ * smaller registered flows.
474
+ */
475
+ export type FlowNode = LeafNode | LoopNode | FanoutNode | SubflowNode;
476
+ /**
477
+ * Backoff shape for `RetryPolicy`. Selects how the base `delayMs` grows
478
+ * across successive retry attempts.
479
+ *
480
+ * - `'fixed'` → same `delayMs` every retry.
481
+ * - `'linear'` → `delayMs * attempt` (capped by `maxDelayMs`).
482
+ * - `'exponential'` → `delayMs * 2^(attempt - 1)` (capped by `maxDelayMs`).
483
+ *
484
+ * `attempt` is 1-based: the first retry (after the initial failure) uses
485
+ * `attempt = 1`, the second retry `attempt = 2`, etc.
486
+ */
487
+ export type BackoffShape = 'fixed' | 'linear' | 'exponential';
488
+ /**
489
+ * Declarative retry policy attached to a `FlowEdge` via `policy.retry`.
490
+ * When the destination node's handler fails and the destination has exactly
491
+ * one incoming edge carrying this policy, the kernel schedules successive
492
+ * attempts up to `maxAttempts` (inclusive of the initial call) before
493
+ * journaling `step.failed` terminally.
494
+ *
495
+ * Fan-in nodes (destinations with 2+ incoming edges) intentionally do NOT
496
+ * apply retry — there is no rule for which incoming edge's retry policy
497
+ * would win. A fan-in node with a retry policy on any
498
+ * incoming edge fails on first handler failure exactly as it would with no
499
+ * policy declared.
500
+ */
501
+ export interface RetryPolicy {
502
+ /**
503
+ * Total handler attempts, including the first invocation. Bounded to
504
+ * `1 ≤ maxAttempts ≤ 10` by the loader. `1` is equivalent to omitting
505
+ * the policy (no retries). Values above 10 are a design smell — retry-
506
+ * heavy control flow belongs in a supervising flow, not on an edge.
507
+ */
508
+ readonly maxAttempts: number;
509
+ /**
510
+ * Base delay between retries in milliseconds. Default `0`. Combined with
511
+ * `backoff` to produce the actual per-attempt delay. Must be non-negative.
512
+ */
513
+ readonly delayMs?: number;
514
+ /**
515
+ * Backoff shape. Default `'fixed'`. `'exponential'` REQUIRES `maxDelayMs`
516
+ * so the delay cannot grow unbounded.
517
+ */
518
+ readonly backoff?: BackoffShape;
519
+ /**
520
+ * Ceiling on the per-retry delay for `'linear'` / `'exponential'` backoff.
521
+ * Required when `backoff = 'exponential'`; ignored when `backoff = 'fixed'`.
522
+ * For `'linear'`, defaults to `60_000` ms if unset.
523
+ */
524
+ readonly maxDelayMs?: number;
525
+ }
526
+ /**
527
+ * Per-edge runtime policy. Every field is optional. Four fields are
528
+ * honored by the kernel and are the complete set: `retry`,
529
+ * `timeoutMs`, `concurrencyKey`, and `priority`. There is no `suspend`
530
+ * field — declarative suspension uses `ctx.waitForToken` at handler
531
+ * level.
532
+ * Unknown keys are rejected at load time (`additionalProperties:
533
+ * false` on the JSON schema).
534
+ */
535
+ export interface EdgePolicy {
536
+ /**
537
+ * Retry policy for the destination handler. When the sole incoming
538
+ * edge has this set, the kernel retries `step.failed` up to
539
+ * `maxAttempts` with the configured backoff. `retry` is bounded to
540
+ * `1 ≤ maxAttempts ≤ 10`.
541
+ */
542
+ readonly retry?: RetryPolicy;
543
+ /**
544
+ * Per-edge handler-invocation timeout in milliseconds. When set on the
545
+ * SOLE incoming edge of a destination node, the kernel aborts the
546
+ * handler's `ctx.abortSignal` after `timeoutMs` and journals
547
+ * `step.failed` with `payload.reason: 'timeout'`, `limitMs`, and
548
+ * `elapsedMs`. Fan-in nodes (2+ incoming) ignore the timeout — same
549
+ * constraint as `retry`.
550
+ *
551
+ * Bounded to `1 ≤ timeoutMs ≤ 3_600_000` (1 hour). Larger values are
552
+ * almost always a design mistake: long-running work belongs behind
553
+ * `ctx.waitForToken` (durable) or in a `foreach` with `concurrency`
554
+ * (parallelization).
555
+ *
556
+ * Interaction with `retry`: a timed-out attempt counts as one failed
557
+ * attempt. If `retry.maxAttempts > 1`, the node retries the timed-out
558
+ * attempt on the standard schedule.
559
+ *
560
+ * Interaction with cancel: `cancelRun` and the timeout both abort the
561
+ * same handler. Whichever aborts first wins in the journal — cancel
562
+ * fires with `reason: 'cancelled'`, timeout with `reason: 'timeout'`.
563
+ */
564
+ readonly timeoutMs?: number;
565
+ /**
566
+ * Per-key kernel semaphore. When set on the SOLE incoming edge of a
567
+ * destination node, the kernel serializes dispatch across every node
568
+ * (across every run, across every flow, within a single tenant) that
569
+ * shares the same key: at most one such node runs at a time.
570
+ *
571
+ * A node whose lease is unavailable at dispatch time is journaled as
572
+ * `step.concurrency-deferred` and re-considered on the next tick. When
573
+ * the current holder releases the lease (when that node completes,
574
+ * fails or is cancelled), a deferred waiter can acquire it and
575
+ * dispatch.
576
+ *
577
+ * Constraint: max length 256, printable ASCII only (Unicode >0x7E
578
+ * rejected — the key is a coordination handle, not a display label).
579
+ * Empty strings rejected.
580
+ *
581
+ * Fan-in nodes (2+ incoming) ignore `concurrencyKey` — same rule as
582
+ * retry + timeoutMs.
583
+ *
584
+ * Interaction with `retry`: retry-scheduled attempts RE-ACQUIRE the
585
+ * lease on each retry (they don't hold across the backoff delay).
586
+ *
587
+ * Interaction with `timeoutMs`: a timed-out handler releases its
588
+ * lease when the terminal `step.failed` lands.
589
+ *
590
+ * Interaction with cancellation: `cancelRun` releases the lease when
591
+ * the run's holding node journals its terminal event.
592
+ *
593
+ * Interaction with `waitForToken`: a suspending handler RELEASES the
594
+ * lease so peers can proceed. Resumed handler re-acquires the lease
595
+ * before continuing.
596
+ *
597
+ * Sweeper: on `resumeRun` boot, orphaned leases (holder run terminal)
598
+ * are cleaned before scheduling continues.
599
+ */
600
+ readonly concurrencyKey?: string;
601
+ /**
602
+ * Declarative scheduler priority. When the ready set has more than one
603
+ * node, higher-priority nodes dispatch first; ties are broken by
604
+ * ascending lexical node id (deterministic + replay-safe). Applied to
605
+ * the destination node when it has EXACTLY ONE incoming edge — fan-in
606
+ * nodes (2+ incoming) inherit the default (`0`) regardless of any
607
+ * declared priority. Same fan-in rule as retry / timeoutMs /
608
+ * concurrencyKey.
609
+ *
610
+ * Integer, `-100 ≤ priority ≤ 100`. Default `0` (baseline). Positive
611
+ * = more urgent than baseline; negative = background. Integer-only:
612
+ * floats introduce IEEE-754 comparison ambiguity in a sort key that
613
+ * must round-trip identically across every replay. Bounded ±100
614
+ * because the primitive expresses tiers ("urgent" / "normal" /
615
+ * "background") plus finer-grained ordering within a tier — wider
616
+ * ranges add no expressive power.
617
+ *
618
+ * `priority` orders WHICH nodes dispatch first, not HOW MANY dispatch
619
+ * at once. `maxParallelism` still caps the concurrent dispatch count
620
+ * — priority just picks the top-N of the ready set.
621
+ *
622
+ * Interaction with `concurrencyKey`: when multiple nodes contend for
623
+ * the same lease within one executor tick, the priority sort applies
624
+ * before lease acquisition — the higher-priority node attempts (and
625
+ * wins) the lease first; the lower-priority sibling journals
626
+ * `step.concurrency-deferred` and re-attempts on the next tick.
627
+ * Priority does not order nodes that are already waiting on a lease
628
+ * across runs or processes.
629
+ *
630
+ * Interaction with loop bodies: iterations of a loop body dispatch
631
+ * per-iteration; each iteration reads the priority from the body
632
+ * node's edges independently.
633
+ *
634
+ * Determinism: because priority is declarative (from the flow) and
635
+ * tiebreak is lexical (from the node id), the same flow + same
636
+ * journal + same ready set produce the same dispatch order on every
637
+ * replay. No JournalKind is emitted — priority is a scheduling hint,
638
+ * not a durable state transition.
639
+ */
640
+ readonly priority?: number;
641
+ }
642
+ /**
643
+ * Minimum + maximum values for `EdgePolicy.priority`. Kept as exported
644
+ * constants so downstream tooling (linters, UI editors, docs) can share
645
+ * the same bounds without duplicating literals.
646
+ */
647
+ export declare const EDGE_POLICY_PRIORITY_MIN: -100;
648
+ export declare const EDGE_POLICY_PRIORITY_MAX: 100;
649
+ /** The `EdgePolicy.priority` default when the field is unset. */
650
+ export declare const EDGE_POLICY_PRIORITY_DEFAULT: 0;
651
+ /**
652
+ * A directed edge from one node (or the flow's virtual start) to another
653
+ * (or the flow's virtual end).
654
+ *
655
+ * Sentinel endpoints:
656
+ * - `$start` — the flow entry point. At least one edge must originate here.
657
+ * - `$end` — the flow exit. Zero or more edges may terminate here.
658
+ */
659
+ export interface FlowEdge {
660
+ readonly id: EdgeId;
661
+ readonly from: NodeId | '$start';
662
+ readonly to: NodeId | '$end';
663
+ /**
664
+ * Optional predicate. When absent, the edge fires unconditionally as soon
665
+ * as its `from` node completes. When present, the edge fires iff the
666
+ * predicate evaluates true against the current run environment.
667
+ */
668
+ readonly when?: Expr;
669
+ /**
670
+ * Optional per-edge runtime policy. Complete set of honored fields
671
+ * (each applies only when the destination has EXACTLY ONE incoming
672
+ * edge; fan-in destinations ignore per-edge policy): `retry`,
673
+ * `timeoutMs`, `concurrencyKey`, `priority`.
674
+ */
675
+ readonly policy?: EdgePolicy;
676
+ }
677
+ /**
678
+ * A directed edge inside a loop body. Same structure as `FlowEdge` but
679
+ * uses `$loop-start` / `$loop-end` sentinels instead of `$start` / `$end`.
680
+ * Enforcing distinct sentinels per scope makes journal queries unambiguous
681
+ * ("was this `$end` for the outer flow or a loop body?").
682
+ */
683
+ export interface LoopEdge {
684
+ readonly id: EdgeId;
685
+ readonly from: NodeId | '$loop-start';
686
+ readonly to: NodeId | '$loop-end';
687
+ readonly when?: Expr;
688
+ }
689
+ /**
690
+ * The sub-flow a loop node iterates. Must be a DAG (own cycle check runs
691
+ * at load time). Node ids must not collide with any other node in the
692
+ * enclosing flow or in sibling loop bodies at any nesting level.
693
+ */
694
+ export interface LoopBody {
695
+ readonly nodes: readonly FlowNode[];
696
+ readonly edges: readonly LoopEdge[];
697
+ }
698
+ /**
699
+ * A durable, versioned task flow. Serializable JSON — the same flow
700
+ * definition can execute on any conforming runtime. Runs pin to a specific
701
+ * `version` so mid-flight executions survive flow edits.
702
+ */
703
+ export interface Flow {
704
+ /**
705
+ * Globally-unique flow identifier. Convention:
706
+ * `<pack-id>.<flow-name>` (kebab-case, dot-namespaced) — e.g.
707
+ * `acme.brief-review-flow`. The framework's built-in agent-turn
708
+ * flow is `agent.turn` (`AGENT_TURN_FLOW_ID` in `@kindgi/agents`).
709
+ */
710
+ readonly id: FlowId;
711
+ /**
712
+ * Semver — REQUIRED. Runs pin to a specific flow version at
713
+ * `runGraph` time so mid-flight executions survive later flow
714
+ * edits. `SubflowNode.flowRef.version` names one of these versions
715
+ * exactly.
716
+ */
717
+ readonly version: string;
718
+ /** Human-readable name shown in UI + provenance. Doesn't affect execution. */
719
+ readonly name?: string;
720
+ /** Prose describing what the flow does. Surfaced in the flow catalog. */
721
+ readonly description?: string;
722
+ /**
723
+ * All nodes in the flow. Each node has a `kind` (`tool`, `agent`,
724
+ * `loop`, `fanout`, `subgraph`) and kind-specific fields.
725
+ * Order in the array doesn't matter — execution follows the edge
726
+ * DAG, not array order.
727
+ */
728
+ readonly nodes: readonly FlowNode[];
729
+ /**
730
+ * Directed edges connecting node ids. Each edge carries an optional
731
+ * `policy` (`retry`, `timeoutMs`, `concurrencyKey`, `priority`). `$start` + `$end`
732
+ * sentinels are the outer flow's entry/exit.
733
+ */
734
+ readonly edges: readonly FlowEdge[];
735
+ /**
736
+ * Flow-level default for concurrent node dispatch. Kernel uses this
737
+ * when `runGraph` is called without an explicit `options.maxParallelism`.
738
+ * Absent → `DEFAULT_MAX_PARALLELISM` (8) from `@kindgi/runtime`.
739
+ * Per-invocation overrides always win.
740
+ */
741
+ readonly maxParallelism?: number;
742
+ /**
743
+ * Free-form key/value metadata that rides with the flow. Not
744
+ * interpreted by the framework — passthrough for pack-authored
745
+ * annotations (owner, ticket, tags).
746
+ */
747
+ readonly metadata?: Readonly<Record<string, unknown>>;
748
+ /**
749
+ * The run's declared output (schema-version 1.8.0+), resolved when the
750
+ * run completes. Absent → the output of the node feeding the run's
751
+ * `$end` edge (the lowest-id taken one when several fire).
752
+ */
753
+ readonly output?: FlowOutputSpec;
754
+ }
755
+ /**
756
+ * A flow's declared output: what the run returns, built from the
757
+ * finished run's environment rather than from whichever node happened to
758
+ * reach `$end` last.
759
+ */
760
+ export interface FlowOutputSpec {
761
+ readonly mapping: Mapping;
762
+ /**
763
+ * Optional JSON Schema (draft 2020-12) the resolved output must
764
+ * satisfy. The loader checks it compiles; the kernel fails the run on
765
+ * a violation.
766
+ */
767
+ readonly schema?: LoopOutputSchema;
768
+ }
769
+ /**
770
+ * Sentinel node ids used by flow edges. `$start` / `$end` are the outer
771
+ * flow's entry/exit. `$loop-start` / `$loop-end` are the body-scope
772
+ * entry/exit inside a loop node.
773
+ *
774
+ * These sentinels are never present in `flow.nodes` or `LoopBody.nodes` —
775
+ * they're virtual, and no handler executes at them.
776
+ */
777
+ export declare const START_NODE: "$start";
778
+ export declare const END_NODE: "$end";
779
+ export declare const LOOP_START_NODE: "$loop-start";
780
+ export declare const LOOP_END_NODE: "$loop-end";
781
+ /** All sentinel ids. Useful for validators that need to reject them as node ids. */
782
+ export declare const SENTINEL_IDS: readonly ["$start", "$end", "$loop-start", "$loop-end"];
783
+ /** Type guard: is this node a loop node? */
784
+ export declare function isLoopNode(node: FlowNode): node is LoopNode;
785
+ /** Type guard: is this node a fanout node? */
786
+ export declare function isFanoutNode(node: FlowNode): node is FanoutNode;
787
+ /** Type guard: is this node a subgraph-invocation node? */
788
+ export declare function isSubflowNode(node: FlowNode): node is SubflowNode;
789
+ /** Type guard: is this node a leaf? */
790
+ export declare function isLeafNode(node: FlowNode): node is LeafNode;
791
+ export {};
792
+ //# sourceMappingURL=types.d.ts.map