@noetic-tools/core 0.0.1 → 0.1.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.
Files changed (202) hide show
  1. package/dist/adapters/in-memory-fs-adapter.js +1 -1
  2. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +8 -0
  3. package/dist/adapters/in-memory-subprocess/manifest-persistence.js +56 -0
  4. package/dist/adapters/in-memory-subprocess/metadata.d.ts +12 -0
  5. package/dist/adapters/in-memory-subprocess/metadata.js +58 -0
  6. package/dist/adapters/in-memory-subprocess/step-completion.d.ts +3 -0
  7. package/dist/adapters/in-memory-subprocess/step-completion.js +86 -0
  8. package/dist/adapters/in-memory-subprocess/types.d.ts +88 -0
  9. package/dist/adapters/in-memory-subprocess/types.js +2 -0
  10. package/dist/adapters/in-memory-subprocess-adapter.d.ts +12 -4
  11. package/dist/adapters/in-memory-subprocess-adapter.js +164 -55
  12. package/dist/adapters/openrouter.d.ts +11 -2
  13. package/dist/adapters/openrouter.js +66 -5
  14. package/dist/builders/control-flow-builders.js +8 -3
  15. package/dist/builders/every.js +4 -1
  16. package/dist/builders/layer-provides-builders.d.ts +1 -32
  17. package/dist/builders/layer-provides-builders.js +1 -33
  18. package/dist/builders/loop-builder.js +4 -1
  19. package/dist/builders/memory-builder.js +1 -1
  20. package/dist/builders/provide-builder.js +4 -1
  21. package/dist/builders/spawn-builder.d.ts +20 -7
  22. package/dist/builders/spawn-builder.js +16 -2
  23. package/dist/builders/step-builders.d.ts +49 -28
  24. package/dist/builders/step-builders.js +30 -11
  25. package/dist/builders/tool-builder.d.ts +5 -1
  26. package/dist/builders/tool-builder.js +2 -0
  27. package/dist/builders/workflow-hydrator.d.ts +38 -0
  28. package/dist/builders/workflow-hydrator.js +336 -0
  29. package/dist/conditions/conditions.js +1 -1
  30. package/dist/errors/noetic-error.js +2 -0
  31. package/dist/{runtime → harness}/agent-harness.d.ts +65 -45
  32. package/dist/harness/agent-harness.js +576 -0
  33. package/dist/harness/deps/adapters.d.ts +4 -0
  34. package/dist/harness/deps/adapters.js +4 -0
  35. package/dist/harness/deps/interpreter.d.ts +5 -0
  36. package/dist/harness/deps/interpreter.js +4 -0
  37. package/dist/harness/deps/memory.d.ts +5 -0
  38. package/dist/harness/deps/memory.js +4 -0
  39. package/dist/harness/deps/runtime.d.ts +10 -0
  40. package/dist/harness/deps/runtime.js +7 -0
  41. package/dist/harness/deps/types.d.ts +17 -0
  42. package/dist/harness/deps/types.js +1 -0
  43. package/dist/harness/model-call.d.ts +50 -0
  44. package/dist/harness/model-call.js +694 -0
  45. package/dist/harness/model-schema.d.ts +19 -0
  46. package/dist/harness/model-schema.js +46 -0
  47. package/dist/harness/step-bootstrap.d.ts +25 -0
  48. package/dist/harness/step-bootstrap.js +170 -0
  49. package/dist/index.d.ts +48 -12
  50. package/dist/index.js +29 -7
  51. package/dist/interpreter/action-deps.d.ts +12 -0
  52. package/dist/interpreter/action-deps.js +10 -0
  53. package/dist/interpreter/action-types.d.ts +8 -0
  54. package/dist/interpreter/action-types.js +1 -0
  55. package/dist/interpreter/collect-tools.d.ts +1 -1
  56. package/dist/interpreter/collect-tools.js +6 -2
  57. package/dist/interpreter/execute-action.d.ts +22 -0
  58. package/dist/interpreter/execute-action.js +527 -0
  59. package/dist/interpreter/{execute-every.d.ts → execute-control.d.ts} +7 -1
  60. package/dist/interpreter/execute-control.js +530 -0
  61. package/dist/interpreter/execute.d.ts +13 -0
  62. package/dist/interpreter/execute.js +151 -48
  63. package/dist/interpreter/message-helpers.d.ts +1 -9
  64. package/dist/interpreter/message-helpers.js +1 -42
  65. package/dist/interpreter/step-dispatcher.d.ts +61 -0
  66. package/dist/interpreter/step-dispatcher.js +67 -0
  67. package/dist/interpreter/typeguards.d.ts +3 -10
  68. package/dist/interpreter/typeguards.js +3 -20
  69. package/dist/{runtime → memory}/exec-context-factory.js +1 -1
  70. package/dist/memory/flow-schema.d.ts +52 -0
  71. package/dist/memory/flow-schema.js +102 -0
  72. package/dist/memory/function-call-utils.js +4 -2
  73. package/dist/memory/layer-api.d.ts +1 -1
  74. package/dist/memory/layer-api.js +2 -2
  75. package/dist/memory/layer-lifecycle.d.ts +1 -1
  76. package/dist/memory/layer-lifecycle.js +2 -2
  77. package/dist/memory/layer-provides.d.ts +32 -0
  78. package/dist/memory/layer-provides.js +33 -0
  79. package/dist/memory/layer-usage.d.ts +3 -2
  80. package/dist/memory/layer-usage.js +9 -3
  81. package/dist/memory/layers/durable-task-state.js +1 -1
  82. package/dist/memory/layers/file-reference.js +302 -246
  83. package/dist/memory/layers/history-window.js +1 -1
  84. package/dist/memory/layers/observational-memory.js +1 -2
  85. package/dist/memory/layers/plan.d.ts +1 -1
  86. package/dist/memory/layers/plan.js +13 -8
  87. package/dist/memory/layers/steering.js +1 -1
  88. package/dist/memory/layers/tool-memory-layer.d.ts +1 -1
  89. package/dist/memory/layers/working-memory.js +2 -2
  90. package/dist/patterns/dynamic-workflow.d.ts +55 -0
  91. package/dist/patterns/dynamic-workflow.js +194 -0
  92. package/dist/patterns/flow.d.ts +2 -52
  93. package/dist/patterns/flow.js +1 -103
  94. package/dist/patterns/interview.js +1 -1
  95. package/dist/patterns/plans.d.ts +3 -3
  96. package/dist/patterns/plans.js +3 -3
  97. package/dist/patterns/ralph-wiggum.d.ts +1 -1
  98. package/dist/patterns/react.d.ts +1 -1
  99. package/dist/portable/adapters.d.ts +6 -0
  100. package/dist/portable/adapters.js +6 -0
  101. package/dist/portable/ask-user.d.ts +4 -0
  102. package/dist/portable/ask-user.js +2 -0
  103. package/dist/portable/builders.d.ts +12 -0
  104. package/dist/portable/builders.js +12 -0
  105. package/dist/portable/memory-layers.d.ts +12 -0
  106. package/dist/portable/memory-layers.js +12 -0
  107. package/dist/portable/runtime.d.ts +4 -0
  108. package/dist/portable/runtime.js +4 -0
  109. package/dist/portable/until.d.ts +4 -0
  110. package/dist/portable/until.js +4 -0
  111. package/dist/portable.d.ts +8 -26
  112. package/dist/portable.js +8 -34
  113. package/dist/runtime/broadcaster-utils.d.ts +5 -1
  114. package/dist/runtime/broadcaster-utils.js +11 -6
  115. package/dist/runtime/channel-store.js +1 -1
  116. package/dist/runtime/context-deps.d.ts +4 -0
  117. package/dist/runtime/context-deps.js +2 -0
  118. package/dist/runtime/context-impl.d.ts +42 -12
  119. package/dist/runtime/context-impl.js +51 -4
  120. package/dist/runtime/context-type-deps.d.ts +14 -0
  121. package/dist/runtime/context-type-deps.js +1 -0
  122. package/dist/runtime/detached-handle.d.ts +50 -2
  123. package/dist/runtime/detached-handle.js +216 -12
  124. package/dist/runtime/durable/checkpoint-store.d.ts +38 -0
  125. package/dist/runtime/durable/checkpoint-store.js +101 -0
  126. package/dist/runtime/durable/detached-signal.d.ts +37 -0
  127. package/dist/runtime/durable/detached-signal.js +52 -0
  128. package/dist/runtime/durable/harness-checkpoints.d.ts +51 -0
  129. package/dist/runtime/durable/harness-checkpoints.js +104 -0
  130. package/dist/runtime/durable/index.d.ts +10 -0
  131. package/dist/runtime/durable/index.js +5 -0
  132. package/dist/runtime/durable/runnable-loop.d.ts +100 -0
  133. package/dist/runtime/durable/runnable-loop.js +90 -0
  134. package/dist/runtime/durable/session-seed.d.ts +28 -0
  135. package/dist/runtime/durable/session-seed.js +27 -0
  136. package/dist/runtime/durable/stall-nudge.d.ts +82 -0
  137. package/dist/runtime/durable/stall-nudge.js +87 -0
  138. package/dist/runtime/in-memory-storage.js +1 -1
  139. package/dist/runtime/item-log-impl.d.ts +1 -1
  140. package/dist/runtime/step-registry.d.ts +42 -0
  141. package/dist/runtime/step-registry.js +77 -0
  142. package/dist/schemas/workflow.d.ts +139 -0
  143. package/dist/schemas/workflow.js +232 -0
  144. package/dist/types/ask-user-types.d.ts +57 -0
  145. package/dist/types/ask-user-types.js +74 -0
  146. package/dist/types/checkpoint.d.ts +98 -0
  147. package/dist/types/checkpoint.js +81 -0
  148. package/dist/types/common.d.ts +1 -50
  149. package/dist/types/context-parts/item-log.d.ts +6 -0
  150. package/dist/types/context-parts/item-log.js +1 -0
  151. package/dist/types/context-parts/layer-usage.d.ts +18 -0
  152. package/dist/types/context-parts/layer-usage.js +1 -0
  153. package/dist/types/context.d.ts +135 -26
  154. package/dist/types/error.d.ts +15 -0
  155. package/dist/types/harness-result.d.ts +1 -1
  156. package/dist/types/items.d.ts +18 -2
  157. package/dist/types/memory-context.d.ts +39 -0
  158. package/dist/types/memory-context.js +1 -0
  159. package/dist/types/memory.d.ts +35 -65
  160. package/dist/types/runtime.d.ts +2 -1
  161. package/dist/types/steering.d.ts +1 -1
  162. package/dist/types/step-registrar.d.ts +46 -0
  163. package/dist/types/step-registrar.js +56 -0
  164. package/dist/types/step.d.ts +30 -5
  165. package/dist/types/subprocess-adapter.d.ts +119 -3
  166. package/dist/types/subprocess-adapter.js +2 -0
  167. package/dist/types/tool.d.ts +64 -0
  168. package/dist/types/tool.js +1 -0
  169. package/dist/unstable.d.ts +2 -2
  170. package/dist/unstable.js +2 -2
  171. package/dist/until/predicates.d.ts +9 -0
  172. package/dist/until/predicates.js +14 -0
  173. package/dist/util/message-helpers.d.ts +17 -0
  174. package/dist/util/message-helpers.js +64 -0
  175. package/package.json +1 -1
  176. package/dist/adapters/local-fs-adapter.d.ts +0 -3
  177. package/dist/adapters/local-fs-adapter.js +0 -43
  178. package/dist/adapters/local-shell-adapter.d.ts +0 -51
  179. package/dist/adapters/local-shell-adapter.js +0 -167
  180. package/dist/adapters/local-subprocess-adapter.d.ts +0 -16
  181. package/dist/adapters/local-subprocess-adapter.js +0 -187
  182. package/dist/interpreter/execute-branch.d.ts +0 -3
  183. package/dist/interpreter/execute-branch.js +0 -10
  184. package/dist/interpreter/execute-every.js +0 -143
  185. package/dist/interpreter/execute-fork.d.ts +0 -3
  186. package/dist/interpreter/execute-fork.js +0 -215
  187. package/dist/interpreter/execute-llm.d.ts +0 -4
  188. package/dist/interpreter/execute-llm.js +0 -217
  189. package/dist/interpreter/execute-loop.d.ts +0 -3
  190. package/dist/interpreter/execute-loop.js +0 -171
  191. package/dist/interpreter/execute-provide.d.ts +0 -10
  192. package/dist/interpreter/execute-provide.js +0 -64
  193. package/dist/interpreter/execute-run.d.ts +0 -3
  194. package/dist/interpreter/execute-run.js +0 -39
  195. package/dist/interpreter/execute-spawn.d.ts +0 -11
  196. package/dist/interpreter/execute-spawn.js +0 -113
  197. package/dist/interpreter/execute-tool.d.ts +0 -5
  198. package/dist/interpreter/execute-tool.js +0 -81
  199. package/dist/runtime/agent-harness.js +0 -1170
  200. /package/dist/{runtime → memory}/exec-context-factory.d.ts +0 -0
  201. /package/dist/{interpreter → util}/framework-cast.d.ts +0 -0
  202. /package/dist/{interpreter → util}/framework-cast.js +0 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Two-strike stall-nudge helper composable with `runnableLoop`.
3
+ *
4
+ * A runner's first turn can end without settling the outcome signal
5
+ * and without the agent asking for external input — i.e. the agent
6
+ * stopped without calling either a terminal tool or an ask-user-style
7
+ * tool. That's a soft failure: rather than wait forever, we nudge the
8
+ * agent once to remind it of its options. If the second turn also
9
+ * ends in the same stalled shape we escalate: the caller-supplied
10
+ * `onStall` callback records the escalation (task pause, metrics,
11
+ * whatever the domain demands) and we settle the signal ourselves with
12
+ * a canonical "stalled" outcome so the parent unwinds cleanly.
13
+ *
14
+ * The helper factors the state machine out of the loop so callers can
15
+ * opt in per-runner. Task-specific concerns (how "pending external
16
+ * input" is detected, how escalation is recorded, what outcome
17
+ * represents a stalled failure) stay with the caller and flow in via
18
+ * the config.
19
+ */
20
+ import type { InputMessageItem } from '../../types/items';
21
+ import type { DetachedSignal } from './detached-signal';
22
+ import type { AfterFirstTurnContext, RunnableLoopHarness } from './runnable-loop';
23
+ /**
24
+ * Default developer-role text sent when the agent finishes a turn
25
+ * without calling a terminal tool or an ask-user tool. Kept short and
26
+ * unambiguous — the agent should be reminded of its options without
27
+ * being lectured. Callers may override via `createNudgeMessage(text)`.
28
+ */
29
+ export declare const DEFAULT_NUDGE_MESSAGE_TEXT = "You finished a turn without calling a terminal tool or AskUserQuestion. If you need user input or instruction to continue, call AskUserQuestion now. Otherwise call your terminal tool to complete this phase.";
30
+ export interface CreateNudgeMessageOpts {
31
+ /** Unique id for the message so item-log dedupe can distinguish re-spawns. */
32
+ readonly id: string;
33
+ /** Override the developer-role text. Defaults to `DEFAULT_NUDGE_MESSAGE_TEXT`. */
34
+ readonly text?: string;
35
+ }
36
+ export interface StallNudgeOpts<TOutcome> {
37
+ readonly harness: RunnableLoopHarness;
38
+ readonly threadId: string;
39
+ readonly signal: DetachedSignal<TOutcome>;
40
+ /**
41
+ * The framing message sent as the nudge. Use `createNudgeMessage`
42
+ * for the canonical developer-role item, or pass your own when a
43
+ * domain needs a custom shape.
44
+ */
45
+ readonly nudgeMessage: InputMessageItem;
46
+ /**
47
+ * True when the agent intentionally awaits external input (e.g. an
48
+ * outstanding ask-user request). When true the nudge is suppressed
49
+ * — the agent is not stalled, it is waiting for us.
50
+ */
51
+ hasPendingExternal(): boolean;
52
+ /**
53
+ * Record-side-effect callback fired when the agent stalls a second
54
+ * time. Use this to mark the task paused, emit a metric, append an
55
+ * event log entry — whatever the domain needs before the signal
56
+ * settles.
57
+ */
58
+ onStall(): Promise<void>;
59
+ /** Build the outcome value the signal resolves to on stall escalation. */
60
+ buildStalledOutcome(): TOutcome;
61
+ }
62
+ /**
63
+ * Build the canonical developer-role nudge message item. Callers who
64
+ * want a custom framing build their own `InputMessageItem`; this
65
+ * helper exists so the common case stays one line.
66
+ */
67
+ export declare function createNudgeMessage(opts: CreateNudgeMessageOpts): InputMessageItem;
68
+ /**
69
+ * Build a `runnableLoop`-compatible `afterFirstTurn` hook that
70
+ * implements the two-strike nudge state machine described in the
71
+ * module doc. The hook is a no-op when:
72
+ *
73
+ * - the signal already settled (agent called a terminal tool), or
74
+ * - the caller reports pending external input (`hasPendingExternal`
75
+ * returns true — e.g. an outstanding ask-user request).
76
+ *
77
+ * Otherwise it sends the nudge message via `harness.execute`, waits
78
+ * for microtasks to flush, and re-checks the same two conditions. If
79
+ * the agent still hasn't progressed, the hook fires `onStall` and
80
+ * resolves the signal with `buildStalledOutcome()`.
81
+ */
82
+ export declare function createStallNudgeHook<TOutcome>(opts: StallNudgeOpts<TOutcome>): (ctx: AfterFirstTurnContext) => Promise<void>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Two-strike stall-nudge helper composable with `runnableLoop`.
3
+ *
4
+ * A runner's first turn can end without settling the outcome signal
5
+ * and without the agent asking for external input — i.e. the agent
6
+ * stopped without calling either a terminal tool or an ask-user-style
7
+ * tool. That's a soft failure: rather than wait forever, we nudge the
8
+ * agent once to remind it of its options. If the second turn also
9
+ * ends in the same stalled shape we escalate: the caller-supplied
10
+ * `onStall` callback records the escalation (task pause, metrics,
11
+ * whatever the domain demands) and we settle the signal ourselves with
12
+ * a canonical "stalled" outcome so the parent unwinds cleanly.
13
+ *
14
+ * The helper factors the state machine out of the loop so callers can
15
+ * opt in per-runner. Task-specific concerns (how "pending external
16
+ * input" is detected, how escalation is recorded, what outcome
17
+ * represents a stalled failure) stay with the caller and flow in via
18
+ * the config.
19
+ */
20
+ //#region Constants
21
+ /**
22
+ * Default developer-role text sent when the agent finishes a turn
23
+ * without calling a terminal tool or an ask-user tool. Kept short and
24
+ * unambiguous — the agent should be reminded of its options without
25
+ * being lectured. Callers may override via `createNudgeMessage(text)`.
26
+ */
27
+ export const DEFAULT_NUDGE_MESSAGE_TEXT = 'You finished a turn without calling a terminal tool or AskUserQuestion. If you need user input or instruction to continue, call AskUserQuestion now. Otherwise call your terminal tool to complete this phase.';
28
+ //#endregion
29
+ //#region Public API
30
+ /**
31
+ * Build the canonical developer-role nudge message item. Callers who
32
+ * want a custom framing build their own `InputMessageItem`; this
33
+ * helper exists so the common case stays one line.
34
+ */
35
+ export function createNudgeMessage(opts) {
36
+ return {
37
+ id: opts.id,
38
+ type: 'message',
39
+ role: 'developer',
40
+ status: 'completed',
41
+ content: [
42
+ {
43
+ type: 'input_text',
44
+ text: opts.text ?? DEFAULT_NUDGE_MESSAGE_TEXT,
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ /**
50
+ * Build a `runnableLoop`-compatible `afterFirstTurn` hook that
51
+ * implements the two-strike nudge state machine described in the
52
+ * module doc. The hook is a no-op when:
53
+ *
54
+ * - the signal already settled (agent called a terminal tool), or
55
+ * - the caller reports pending external input (`hasPendingExternal`
56
+ * returns true — e.g. an outstanding ask-user request).
57
+ *
58
+ * Otherwise it sends the nudge message via `harness.execute`, waits
59
+ * for microtasks to flush, and re-checks the same two conditions. If
60
+ * the agent still hasn't progressed, the hook fires `onStall` and
61
+ * resolves the signal with `buildStalledOutcome()`.
62
+ */
63
+ export function createStallNudgeHook(opts) {
64
+ return async (ctx) => {
65
+ if (ctx.signalSettled()) {
66
+ return;
67
+ }
68
+ if (opts.hasPendingExternal()) {
69
+ return;
70
+ }
71
+ // First stall — send one nudge.
72
+ await opts.harness.execute(opts.nudgeMessage, {
73
+ threadId: opts.threadId,
74
+ });
75
+ await Promise.resolve();
76
+ if (ctx.signalSettled()) {
77
+ return;
78
+ }
79
+ if (opts.hasPendingExternal()) {
80
+ return;
81
+ }
82
+ // Second stall — escalate and settle.
83
+ await opts.onStall();
84
+ opts.signal.resolve(opts.buildStalledOutcome());
85
+ };
86
+ }
87
+ //#endregion
@@ -1,4 +1,4 @@
1
- import { frameworkCast } from '../interpreter/framework-cast';
1
+ import { frameworkCast } from '../util/framework-cast';
2
2
  //#region Public API
3
3
  /**
4
4
  * @public Process-local, Map-backed StorageAdapter.
@@ -1,5 +1,5 @@
1
1
  import type { ItemSchemaRegistry } from '../schemas/item';
2
- import type { ItemLog } from '../types/context';
2
+ import type { ItemLog } from '../types/context-parts/item-log';
3
3
  import type { Item } from '../types/items';
4
4
  export declare class ItemLogImpl implements ItemLog {
5
5
  private readonly itemSchemas;
@@ -0,0 +1,42 @@
1
+ import type { ContextMemory } from '../types/memory';
2
+ import type { Step } from '../types/step';
3
+ /** Registry-wide step type — stored at the widest generic parameters so
4
+ * callers can register steps with narrower input/output types without
5
+ * fighting variance. The public `Step` default already uses `unknown` for
6
+ * the input and output parameters, matching this shape. */
7
+ type RegisteredStep = Step<ContextMemory, unknown, unknown>;
8
+ /**
9
+ * @public
10
+ * Registers a step so the subprocess adapter can look it up by id.
11
+ *
12
+ * Called automatically by the step builders when a `Step` is constructed.
13
+ * Policy: **latest registration wins**. Dispatch-and-lookup happen in the
14
+ * same tick, so the live entry is always the one the caller just built;
15
+ * cross-test pollution from bun's per-file process model (describe blocks
16
+ * rebuild the same step id) is not a safety concern here. Strict
17
+ * "reject duplicate id with a different body" detection + a test-side
18
+ * `clearRegistry` wiring is deferred — see the tracked follow-up task on
19
+ * the team's board (strict-registry-plus-test-clear).
20
+ */
21
+ export declare function registerStep<TMemory, I, O>(step: Step<TMemory, I, O>): void;
22
+ /**
23
+ * @public
24
+ * Looks up a registered step by id. Returns `null` when the id is not known
25
+ * — callers (e.g. the in-memory adapter's step dispatch path) translate the
26
+ * miss into a typed configuration error.
27
+ */
28
+ export declare function lookupStep(id: string): RegisteredStep | null;
29
+ /**
30
+ * @public
31
+ * Returns a read-only view over the registry. Primarily for tests and
32
+ * debugging; production callers should use `lookupStep`.
33
+ */
34
+ export declare function getRegistry(): ReadonlyMap<string, RegisteredStep>;
35
+ /**
36
+ * @public
37
+ * Clears the registry. Test-only helper — exported because several suites
38
+ * need a clean registry between describe blocks. Not part of the public API
39
+ * surface consumed by agent authors.
40
+ */
41
+ export declare function clearRegistry(): void;
42
+ export {};
@@ -0,0 +1,77 @@
1
+ import { NoeticConfigError } from '../errors/noetic-config-error';
2
+ import { setDefaultRegistrar } from '../types/step-registrar';
3
+ import { frameworkCast } from '../util/framework-cast';
4
+ //#endregion
5
+ //#region Module state
6
+ const registry = new Map();
7
+ //#endregion
8
+ //#region Public API
9
+ /**
10
+ * @public
11
+ * Registers a step so the subprocess adapter can look it up by id.
12
+ *
13
+ * Called automatically by the step builders when a `Step` is constructed.
14
+ * Policy: **latest registration wins**. Dispatch-and-lookup happen in the
15
+ * same tick, so the live entry is always the one the caller just built;
16
+ * cross-test pollution from bun's per-file process model (describe blocks
17
+ * rebuild the same step id) is not a safety concern here. Strict
18
+ * "reject duplicate id with a different body" detection + a test-side
19
+ * `clearRegistry` wiring is deferred — see the tracked follow-up task on
20
+ * the team's board (strict-registry-plus-test-clear).
21
+ */
22
+ export function registerStep(step) {
23
+ if (!step.id || step.id.trim() === '') {
24
+ throw new NoeticConfigError({
25
+ code: 'EMPTY_STEP_ID',
26
+ message: 'registerStep() requires a step with a non-empty id.',
27
+ hint: 'Set `id` on the step before constructing it.',
28
+ });
29
+ }
30
+ // `frameworkCast` is the approved single-point escape for generic variance:
31
+ // narrowing I/O from specific types to the registry's widened `unknown`
32
+ // shape is structurally safe at runtime because the registry only holds
33
+ // references and dispatches them through the same `execute()` pipeline.
34
+ const widened = frameworkCast(step);
35
+ registry.set(step.id, widened);
36
+ }
37
+ /**
38
+ * @public
39
+ * Looks up a registered step by id. Returns `null` when the id is not known
40
+ * — callers (e.g. the in-memory adapter's step dispatch path) translate the
41
+ * miss into a typed configuration error.
42
+ */
43
+ export function lookupStep(id) {
44
+ return registry.get(id) ?? null;
45
+ }
46
+ /**
47
+ * @public
48
+ * Returns a read-only view over the registry. Primarily for tests and
49
+ * debugging; production callers should use `lookupStep`.
50
+ */
51
+ export function getRegistry() {
52
+ return registry;
53
+ }
54
+ /**
55
+ * @public
56
+ * Clears the registry. Test-only helper — exported because several suites
57
+ * need a clean registry between describe blocks. Not part of the public API
58
+ * surface consumed by agent authors.
59
+ */
60
+ export function clearRegistry() {
61
+ registry.clear();
62
+ }
63
+ //#endregion
64
+ //#region Default-registrar installation
65
+ /**
66
+ * Install this module's `registerStep` as the runtime default registrar.
67
+ * Executes on first import — which happens transitively from
68
+ * `agent-harness.ts` and from anywhere that imports `@noetic-tools/core`'s
69
+ * top-level barrel. Builders call `getDefaultRegistrar().register(step)`
70
+ * from `../types/step-registrar` and get this concrete implementation
71
+ * without depending on `core-runtime` themselves (fixes the
72
+ * `core-primitives → core-runtime` layer_direction violations).
73
+ */
74
+ setDefaultRegistrar({
75
+ register: registerStep,
76
+ });
77
+ //#endregion
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Zod schemas for JSON-serialisable workflow definitions.
3
+ *
4
+ * A `WorkflowDocument` is a portable, JSON-safe representation of a noetic
5
+ * step tree. It covers every step kind except `run` (which carries arbitrary
6
+ * closures). The hydrator in `builders/workflow-hydrator.ts` converts a
7
+ * validated document back into live `Step` objects via the existing builders.
8
+ */
9
+ import { z } from 'zod';
10
+ interface UntilAnyPredicate {
11
+ kind: 'any';
12
+ predicates: UntilPredicate[];
13
+ }
14
+ interface UntilAllPredicate {
15
+ kind: 'all';
16
+ predicates: UntilPredicate[];
17
+ }
18
+ /** @public Discriminated union of named until predicates usable in JSON workflows. */
19
+ export type UntilPredicate = {
20
+ kind: 'maxSteps';
21
+ n: number;
22
+ } | {
23
+ kind: 'maxCost';
24
+ usd: number;
25
+ } | {
26
+ kind: 'maxDuration';
27
+ ms: number;
28
+ } | {
29
+ kind: 'noToolCalls';
30
+ } | {
31
+ kind: 'outputContains';
32
+ marker: string;
33
+ } | {
34
+ kind: 'outputEquals';
35
+ sentinel: string;
36
+ } | {
37
+ kind: 'converged';
38
+ threshold?: number;
39
+ } | UntilAnyPredicate | UntilAllPredicate;
40
+ /** @public Zod schema validating a named `until` predicate (including `any`/`all` combinators). */
41
+ export declare const UntilPredicateSchema: z.ZodType<UntilPredicate>;
42
+ /** @public ESM literal enum for merge strategies. */
43
+ export declare const MergeStrategy: {
44
+ readonly Last: "last";
45
+ readonly First: "first";
46
+ readonly Concat: "concat";
47
+ };
48
+ /** @public Named merge strategy for fork nodes. */
49
+ export type MergeStrategy = (typeof MergeStrategy)[keyof typeof MergeStrategy];
50
+ /** @public Zod schema for the named merge strategy used by `fork` nodes. */
51
+ export declare const MergeStrategySchema: z.ZodEnum<{
52
+ concat: "concat";
53
+ last: "last";
54
+ first: "first";
55
+ }>;
56
+ declare const ModelParamsSchema: z.ZodObject<{
57
+ temperature: z.ZodOptional<z.ZodNumber>;
58
+ topP: z.ZodOptional<z.ZodNumber>;
59
+ maxTokens: z.ZodOptional<z.ZodNumber>;
60
+ stopSequences: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
+ }, z.core.$strip>;
62
+ interface WorkflowNodeBase {
63
+ id: string;
64
+ }
65
+ export interface LlmWorkflowNode extends WorkflowNodeBase {
66
+ kind: 'llm';
67
+ model?: string;
68
+ instructions: string;
69
+ tools?: string[];
70
+ params?: z.infer<typeof ModelParamsSchema>;
71
+ }
72
+ export interface ToolWorkflowNode extends WorkflowNodeBase {
73
+ kind: 'tool';
74
+ toolName: string;
75
+ args?: Record<string, unknown>;
76
+ }
77
+ export interface BranchRoute {
78
+ match: string;
79
+ target: WorkflowNode;
80
+ }
81
+ export interface BranchWorkflowNode extends WorkflowNodeBase {
82
+ kind: 'branch';
83
+ routes: BranchRoute[];
84
+ default?: WorkflowNode;
85
+ }
86
+ export interface ForkWorkflowNode extends WorkflowNodeBase {
87
+ kind: 'fork';
88
+ mode: 'race' | 'all' | 'settle';
89
+ paths: WorkflowNode[];
90
+ merge?: MergeStrategy;
91
+ concurrency?: number;
92
+ }
93
+ export interface SpawnWorkflowNode extends WorkflowNodeBase {
94
+ kind: 'spawn';
95
+ child: WorkflowNode;
96
+ timeout?: number;
97
+ }
98
+ export interface ProvideWorkflowNode extends WorkflowNodeBase {
99
+ kind: 'provide';
100
+ child: WorkflowNode;
101
+ layers: string[];
102
+ }
103
+ export interface LoopWorkflowNode extends WorkflowNodeBase {
104
+ kind: 'loop';
105
+ body: WorkflowNode;
106
+ until: UntilPredicate;
107
+ maxIterations?: number;
108
+ }
109
+ export interface SequenceWorkflowNode extends WorkflowNodeBase {
110
+ kind: 'sequence';
111
+ steps: WorkflowNode[];
112
+ }
113
+ export interface EveryWorkflowNode extends WorkflowNodeBase {
114
+ kind: 'every';
115
+ step: WorkflowNode;
116
+ ms: number;
117
+ onError?: 'continue' | 'fail';
118
+ }
119
+ /** @public Discriminated union of all JSON-serialisable workflow node kinds. */
120
+ export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode;
121
+ /** @public Zod schema validating a single `WorkflowNode` (any of the 9 JSON-safe kinds). */
122
+ export declare const WorkflowNodeSchema: z.ZodType<WorkflowNode>;
123
+ /** @public Top-level envelope for a JSON workflow definition. */
124
+ export interface WorkflowDocument {
125
+ version: 1;
126
+ root: WorkflowNode;
127
+ }
128
+ /** @public Zod schema validating a complete `WorkflowDocument`. */
129
+ export declare const WorkflowDocumentSchema: z.ZodType<WorkflowDocument>;
130
+ /** Validates a candidate workflow document. Throws `ZodError` on invalid input. */
131
+ export declare function validateWorkflow(input: unknown): WorkflowDocument;
132
+ /** Walks a workflow tree depth-first, yielding each node. */
133
+ export declare function walkWorkflow(node: WorkflowNode): Iterable<WorkflowNode>;
134
+ /**
135
+ * Returns the maximum depth of a workflow tree.
136
+ * Leaf nodes (`llm`, `tool`) have depth 0. Structural nodes add +1.
137
+ */
138
+ export declare function workflowDepth(node: WorkflowNode): number;
139
+ export {};
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Zod schemas for JSON-serialisable workflow definitions.
3
+ *
4
+ * A `WorkflowDocument` is a portable, JSON-safe representation of a noetic
5
+ * step tree. It covers every step kind except `run` (which carries arbitrary
6
+ * closures). The hydrator in `builders/workflow-hydrator.ts` converts a
7
+ * validated document back into live `Step` objects via the existing builders.
8
+ */
9
+ import { z } from 'zod';
10
+ //#endregion
11
+ //#region Until Predicate Schemas
12
+ const MaxStepsPredicateSchema = z.object({
13
+ kind: z.literal('maxSteps'),
14
+ n: z.number().int().positive(),
15
+ });
16
+ const MaxCostPredicateSchema = z.object({
17
+ kind: z.literal('maxCost'),
18
+ usd: z.number().positive(),
19
+ });
20
+ const MaxDurationPredicateSchema = z.object({
21
+ kind: z.literal('maxDuration'),
22
+ ms: z.number().positive(),
23
+ });
24
+ const NoToolCallsPredicateSchema = z.object({
25
+ kind: z.literal('noToolCalls'),
26
+ });
27
+ const OutputContainsPredicateSchema = z.object({
28
+ kind: z.literal('outputContains'),
29
+ marker: z.string().min(1),
30
+ });
31
+ const OutputEqualsPredicateSchema = z.object({
32
+ kind: z.literal('outputEquals'),
33
+ sentinel: z.string().min(1),
34
+ });
35
+ const ConvergedPredicateSchema = z.object({
36
+ kind: z.literal('converged'),
37
+ threshold: z.number().min(0).max(1).optional(),
38
+ });
39
+ const UntilPredicateRef = z.lazy(() => UntilPredicateSchema);
40
+ const AnyPredicateSchema = z.object({
41
+ kind: z.literal('any'),
42
+ predicates: z.array(UntilPredicateRef).min(1),
43
+ });
44
+ const AllPredicateSchema = z.object({
45
+ kind: z.literal('all'),
46
+ predicates: z.array(UntilPredicateRef).min(1),
47
+ });
48
+ /** @public Zod schema validating a named `until` predicate (including `any`/`all` combinators). */
49
+ export const UntilPredicateSchema = z.union([
50
+ MaxStepsPredicateSchema,
51
+ MaxCostPredicateSchema,
52
+ MaxDurationPredicateSchema,
53
+ NoToolCallsPredicateSchema,
54
+ OutputContainsPredicateSchema,
55
+ OutputEqualsPredicateSchema,
56
+ ConvergedPredicateSchema,
57
+ AnyPredicateSchema,
58
+ AllPredicateSchema,
59
+ ]);
60
+ //#endregion
61
+ //#region Merge Strategy
62
+ /** @public ESM literal enum for merge strategies. */
63
+ export const MergeStrategy = {
64
+ Last: 'last',
65
+ First: 'first',
66
+ Concat: 'concat',
67
+ };
68
+ /** @public Zod schema for the named merge strategy used by `fork` nodes. */
69
+ export const MergeStrategySchema = z.enum([
70
+ 'last',
71
+ 'first',
72
+ 'concat',
73
+ ]);
74
+ //#endregion
75
+ //#region Model Params
76
+ const ModelParamsSchema = z.object({
77
+ temperature: z.number().min(0).optional(),
78
+ topP: z.number().min(0).max(1).optional(),
79
+ maxTokens: z.number().int().positive().optional(),
80
+ stopSequences: z.array(z.string()).optional(),
81
+ });
82
+ //#endregion
83
+ //#region Workflow Node Schema
84
+ const WorkflowNodeRef = z.lazy(() => WorkflowNodeSchema);
85
+ const SHARED_FIELDS = {
86
+ id: z.string().min(1),
87
+ };
88
+ const LlmNodeSchema = z.object({
89
+ kind: z.literal('llm'),
90
+ ...SHARED_FIELDS,
91
+ model: z.string().optional(),
92
+ instructions: z.string(),
93
+ tools: z.array(z.string()).optional(),
94
+ params: ModelParamsSchema.optional(),
95
+ });
96
+ const ToolNodeSchema = z.object({
97
+ kind: z.literal('tool'),
98
+ ...SHARED_FIELDS,
99
+ toolName: z.string().min(1),
100
+ args: z.record(z.string(), z.unknown()).optional(),
101
+ });
102
+ const BranchRouteSchema = z.object({
103
+ match: z.string().min(1),
104
+ target: z.lazy(() => WorkflowNodeSchema),
105
+ });
106
+ const BranchNodeSchema = z.object({
107
+ kind: z.literal('branch'),
108
+ ...SHARED_FIELDS,
109
+ routes: z.array(BranchRouteSchema).min(1),
110
+ default: WorkflowNodeRef.optional(),
111
+ });
112
+ const ForkNodeSchema = z.object({
113
+ kind: z.literal('fork'),
114
+ ...SHARED_FIELDS,
115
+ mode: z.enum([
116
+ 'race',
117
+ 'all',
118
+ 'settle',
119
+ ]),
120
+ paths: z.array(WorkflowNodeRef).min(1),
121
+ merge: MergeStrategySchema.optional(),
122
+ concurrency: z.number().int().positive().optional(),
123
+ });
124
+ const SpawnNodeSchema = z.object({
125
+ kind: z.literal('spawn'),
126
+ ...SHARED_FIELDS,
127
+ child: WorkflowNodeRef,
128
+ timeout: z.number().positive().optional(),
129
+ });
130
+ const ProvideNodeSchema = z.object({
131
+ kind: z.literal('provide'),
132
+ ...SHARED_FIELDS,
133
+ child: WorkflowNodeRef,
134
+ layers: z.array(z.string().min(1)).min(1),
135
+ });
136
+ const LoopNodeSchema = z.object({
137
+ kind: z.literal('loop'),
138
+ ...SHARED_FIELDS,
139
+ body: WorkflowNodeRef,
140
+ until: UntilPredicateSchema,
141
+ maxIterations: z.number().int().positive().optional(),
142
+ });
143
+ const SequenceNodeSchema = z.object({
144
+ kind: z.literal('sequence'),
145
+ ...SHARED_FIELDS,
146
+ steps: z.array(WorkflowNodeRef).min(1),
147
+ });
148
+ const EveryNodeSchema = z.object({
149
+ kind: z.literal('every'),
150
+ ...SHARED_FIELDS,
151
+ step: WorkflowNodeRef,
152
+ ms: z.number().nonnegative(),
153
+ onError: z
154
+ .enum([
155
+ 'continue',
156
+ 'fail',
157
+ ])
158
+ .optional(),
159
+ });
160
+ /** @public Zod schema validating a single `WorkflowNode` (any of the 9 JSON-safe kinds). */
161
+ export const WorkflowNodeSchema = z.discriminatedUnion('kind', [
162
+ LlmNodeSchema,
163
+ ToolNodeSchema,
164
+ BranchNodeSchema,
165
+ ForkNodeSchema,
166
+ SpawnNodeSchema,
167
+ ProvideNodeSchema,
168
+ LoopNodeSchema,
169
+ SequenceNodeSchema,
170
+ EveryNodeSchema,
171
+ ]);
172
+ /** @public Zod schema validating a complete `WorkflowDocument`. */
173
+ export const WorkflowDocumentSchema = z.object({
174
+ version: z.literal(1),
175
+ root: WorkflowNodeSchema,
176
+ });
177
+ //#endregion
178
+ //#region Helpers
179
+ /** Returns the direct child nodes of a workflow node. Leaf nodes return an empty array. */
180
+ function childNodes(node) {
181
+ switch (node.kind) {
182
+ case 'sequence':
183
+ return node.steps;
184
+ case 'fork':
185
+ return node.paths;
186
+ case 'spawn':
187
+ case 'provide':
188
+ return [
189
+ node.child,
190
+ ];
191
+ case 'loop':
192
+ return [
193
+ node.body,
194
+ ];
195
+ case 'every':
196
+ return [
197
+ node.step,
198
+ ];
199
+ case 'branch': {
200
+ const children = node.routes.map((r) => r.target);
201
+ if (node.default) {
202
+ children.push(node.default);
203
+ }
204
+ return children;
205
+ }
206
+ default:
207
+ return [];
208
+ }
209
+ }
210
+ /** Validates a candidate workflow document. Throws `ZodError` on invalid input. */
211
+ export function validateWorkflow(input) {
212
+ return WorkflowDocumentSchema.parse(input);
213
+ }
214
+ /** Walks a workflow tree depth-first, yielding each node. */
215
+ export function* walkWorkflow(node) {
216
+ yield node;
217
+ for (const child of childNodes(node)) {
218
+ yield* walkWorkflow(child);
219
+ }
220
+ }
221
+ /**
222
+ * Returns the maximum depth of a workflow tree.
223
+ * Leaf nodes (`llm`, `tool`) have depth 0. Structural nodes add +1.
224
+ */
225
+ export function workflowDepth(node) {
226
+ const children = childNodes(node);
227
+ if (children.length === 0) {
228
+ return 0;
229
+ }
230
+ return 1 + Math.max(0, ...children.map(workflowDepth));
231
+ }
232
+ //#endregion