@noetic-tools/core 5.3.0 → 5.4.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 (82) hide show
  1. package/README.md +8 -6
  2. package/dist/adapters/in-memory-subprocess/step-completion.js +1 -1
  3. package/dist/adapters/openrouter.d.ts +12 -1
  4. package/dist/adapters/openrouter.js +79 -5
  5. package/dist/builders/control-flow-builders.d.ts +13 -13
  6. package/dist/builders/control-flow-builders.js +11 -11
  7. package/dist/{patterns → builders}/dynamic-workflow.d.ts +1 -1
  8. package/dist/{patterns → builders}/dynamic-workflow.js +18 -12
  9. package/dist/builders/every.d.ts +14 -14
  10. package/dist/builders/every.js +16 -16
  11. package/dist/builders/layer-provides-builders.d.ts +1 -1
  12. package/dist/builders/layer-provides-builders.js +1 -1
  13. package/dist/builders/provide-builder.d.ts +5 -12
  14. package/dist/builders/provide-builder.js +9 -10
  15. package/dist/builders/spawn-builder.d.ts +0 -2
  16. package/dist/builders/spawn-builder.js +2 -3
  17. package/dist/builders/step-builders.d.ts +66 -55
  18. package/dist/builders/step-builders.js +118 -111
  19. package/dist/builders/tool-builder.d.ts +18 -22
  20. package/dist/builders/tool-builder.js +6 -4
  21. package/dist/builders/workflow-hydrator.d.ts +3 -3
  22. package/dist/builders/workflow-hydrator.js +59 -63
  23. package/dist/builders/workflow-step.d.ts +13 -25
  24. package/dist/builders/workflow-step.js +13 -18
  25. package/dist/conditions/conditions.d.ts +2 -2
  26. package/dist/conditions/conditions.js +1 -1
  27. package/dist/harness/agent-harness.d.ts +54 -40
  28. package/dist/harness/agent-harness.js +44 -33
  29. package/dist/harness/deps/types.d.ts +1 -1
  30. package/dist/harness/model-call.d.ts +1 -1
  31. package/dist/harness/model-call.js +23 -18
  32. package/dist/index.d.ts +21 -33
  33. package/dist/index.js +10 -26
  34. package/dist/interpreter/action-types.d.ts +1 -1
  35. package/dist/interpreter/collect-tools.js +7 -7
  36. package/dist/interpreter/execute-action.d.ts +5 -5
  37. package/dist/interpreter/execute-action.js +284 -218
  38. package/dist/interpreter/execute-control.d.ts +9 -9
  39. package/dist/interpreter/execute-control.js +27 -27
  40. package/dist/interpreter/execute-sub-harness.d.ts +1 -1
  41. package/dist/interpreter/execute-sub-harness.js +7 -26
  42. package/dist/interpreter/execute.d.ts +1 -1
  43. package/dist/interpreter/execute.js +26 -25
  44. package/dist/interpreter/step-dispatcher.d.ts +1 -1
  45. package/dist/interpreter/step-dispatcher.js +3 -3
  46. package/dist/interpreter/structured-output.d.ts +14 -0
  47. package/dist/interpreter/structured-output.js +40 -0
  48. package/dist/interpreter/typeguards.d.ts +1 -1
  49. package/dist/interpreter/typeguards.js +1 -1
  50. package/dist/portable/builders.d.ts +3 -3
  51. package/dist/portable/builders.js +6 -5
  52. package/dist/portable/context-layers.d.ts +1 -1
  53. package/dist/portable/context-layers.js +1 -1
  54. package/dist/runtime/broadcaster-utils.d.ts +1 -1
  55. package/dist/runtime/broadcaster-utils.js +1 -1
  56. package/dist/runtime/context-impl.d.ts +8 -10
  57. package/dist/runtime/context-impl.js +8 -12
  58. package/dist/runtime/durable/step-ledger.d.ts +2 -2
  59. package/dist/runtime/durable/step-ledger.js +3 -3
  60. package/dist/runtime/session-runner.d.ts +12 -1
  61. package/dist/runtime/session-runner.js +33 -1
  62. package/dist/runtime/tool-context.js +0 -2
  63. package/dist/schemas/workflow.d.ts +3 -3
  64. package/dist/schemas/workflow.js +2 -2
  65. package/dist/types/checkpoint.d.ts +11 -2
  66. package/dist/types/checkpoint.js +10 -1
  67. package/dist/{runtime → util}/detached-handle.js +1 -1
  68. package/package.json +6 -4
  69. package/schema/noetic-workflow.schema.json +24 -11
  70. package/dist/builders/context-option.d.ts +0 -23
  71. package/dist/builders/context-option.js +0 -22
  72. package/dist/deprecated.d.ts +0 -30
  73. package/dist/deprecated.js +0 -28
  74. package/dist/patterns/interview.d.ts +0 -53
  75. package/dist/patterns/interview.js +0 -113
  76. package/dist/patterns/plans.d.ts +0 -44
  77. package/dist/patterns/plans.js +0 -125
  78. package/dist/patterns/ralph-wiggum.d.ts +0 -19
  79. package/dist/patterns/ralph-wiggum.js +0 -37
  80. package/dist/patterns/react.d.ts +0 -19
  81. package/dist/patterns/react.js +0 -41
  82. /package/dist/{runtime → util}/detached-handle.d.ts +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @noetic-tools/core
2
2
 
3
- Core primitives for the [Noetic](https://github.com/mattapperson/noetic) agent framework: step types, the interpreter, the runtime, the memory layer contract, and built-in patterns.
3
+ Core primitives for the [Noetic](https://github.com/mattapperson/noetic) agent framework: step types, the interpreter, the runtime, and the context layer contract.
4
4
 
5
5
  ## Install
6
6
 
@@ -10,6 +10,8 @@ npm install @noetic-tools/core
10
10
 
11
11
  `@noetic-tools/core` depends on `@openrouter/agent` (the OpenRouter LLM adapter) and `zod`; your package manager installs both automatically. Runs on Node.js 18+ and Bun.
12
12
 
13
+ Tool and structured-output schemas accept any [Standard Schema v1](https://standardschema.dev/schema) validator. Model-facing JSON Schema resolves through three tiers: Zod's native converter, the [Standard JSON Schema v1](https://standardschema.dev/json-schema) companion trait, then an explicit `inputJsonSchema` / `outputJsonSchema` override. Zod 4.2+, ArkType 2.1.28+, Zod Mini, VineJS, and Sury implement the companion trait; Valibot provides it via `toStandardJsonSchema()` from `@valibot/to-json-schema`.
14
+
13
15
  ## Set your API key
14
16
 
15
17
  Noetic talks to LLMs through [OpenRouter](https://openrouter.ai). Grab a key at [openrouter.ai/keys](https://openrouter.ai/keys) and export it — the runtime reads `OPENROUTER_API_KEY` by default:
@@ -23,20 +25,20 @@ export OPENROUTER_API_KEY=sk-or-...
23
25
  A complete, runnable program. Save it as `agent.mjs` and run `node agent.mjs`:
24
26
 
25
27
  ```ts
26
- import { AgentHarness, step } from '@noetic-tools/core';
28
+ import { AgentHarness, callModel } from '@noetic-tools/core';
27
29
 
28
- // 1. Define a single LLM step. `model` and `instructions` live here.
29
- const greet = step.llm({
30
+ // 1. Define a single model-call step. `model` and `instructions` live here.
31
+ const greet = callModel({
30
32
  id: 'greet',
31
33
  model: 'openai/gpt-4o-mini', // any OpenRouter model id (provider/model)
32
34
  instructions: 'You are a friendly assistant. Greet the user by name in one short sentence.',
33
35
  });
34
36
 
35
- // 2. Create the harness. `name` and `params` are required; `llm` selects the provider.
37
+ // 2. Create the harness. `name` and `params` are required; `callModelDefaults` selects the provider.
36
38
  const harness = new AgentHarness({
37
39
  name: 'greeter',
38
40
  params: {},
39
- llm: { provider: 'openrouter' }, // apiKey defaults to process.env.OPENROUTER_API_KEY
41
+ callModelDefaults: { provider: 'openrouter' }, // apiKey defaults to process.env.OPENROUTER_API_KEY
40
42
  });
41
43
 
42
44
  // 3. Run the step with a context and print the model's reply.
@@ -31,7 +31,7 @@ export async function completeProcessRun({ request, handle, run, handles, active
31
31
  error: serializeError(err),
32
32
  },
33
33
  });
34
- // Same rationale as the success branch above — clear the durable
34
+ // Same rationale as the success path above — clear the durable
35
35
  // manifest so `listLive()` doesn't resurrect this handle on reboot.
36
36
  await clearIfDurable(handle.id);
37
37
  }
@@ -1,5 +1,5 @@
1
1
  import type { ContextLayer } from '@noetic-tools/context';
2
- import type { AgentHarnessContract, Context, Item, LLMResponse, Tool } from '@noetic-tools/types';
2
+ import type { AgentHarnessContract, Context, Item, LLMResponse, StandardSchemaV1, Tool } from '@noetic-tools/types';
3
3
  import type * as OpenRouterAgent from '@openrouter/agent';
4
4
  import type { EmbedFn } from '../types/embed.js';
5
5
  type ProviderOutputItem = OpenRouterAgent.OpenResponsesResult['output'][number];
@@ -40,6 +40,17 @@ export declare function extractUsage(usage: OpenRouterAgent.Usage | null | undef
40
40
  * to names like `plan/updatePrd`) is preserved.
41
41
  */
42
42
  export declare function sanitizeToolNameForWire(name: string): string;
43
+ /**
44
+ * Resolve the JSON Schema sent to the model. Zod keeps its legacy-compatible
45
+ * fast path. For other validators, an explicit override wins over the Standard
46
+ * JSON Schema v1 companion trait.
47
+ * @internal
48
+ */
49
+ export declare function resolveWireJsonSchema(params: {
50
+ schema: StandardSchemaV1;
51
+ explicitJsonSchema?: Record<string, unknown>;
52
+ what: string;
53
+ }): Record<string, unknown>;
43
54
  /** @internal */
44
55
  export declare function convertTools({ tools }: ConvertToolsParams): SdkTool[];
45
56
  /** @internal */
@@ -1,4 +1,4 @@
1
- import { frameworkCast, isAssistantMessage, NoeticConfigError, SteeringAction, } from '@noetic-tools/types';
1
+ import { frameworkCast, isAssistantMessage, isStandardJsonSchema, isZodSchema, NoeticConfigError, SteeringAction, validateSchema, } from '@noetic-tools/types';
2
2
  import { z } from 'zod';
3
3
  import { buildToolExecutionContext } from '../runtime/tool-context.js';
4
4
  import { emitToolUi } from '../runtime/tool-ui.js';
@@ -210,6 +210,72 @@ function cacheWriteTokensOf(details) {
210
210
  export function sanitizeToolNameForWire(name) {
211
211
  return name.replace(/[^a-zA-Z0-9_-]/g, '_');
212
212
  }
213
+ /**
214
+ * Resolve the JSON Schema sent to the model. Zod keeps its legacy-compatible
215
+ * fast path. For other validators, an explicit override wins over the Standard
216
+ * JSON Schema v1 companion trait.
217
+ * @internal
218
+ */
219
+ export function resolveWireJsonSchema(params) {
220
+ const { schema, explicitJsonSchema, what } = params;
221
+ if (isZodSchema(schema)) {
222
+ return sanitizeJsonSchema(z.toJSONSchema(schema, {
223
+ target: 'draft-07',
224
+ }));
225
+ }
226
+ if (explicitJsonSchema) {
227
+ return sanitizeJsonSchema(explicitJsonSchema);
228
+ }
229
+ if (isStandardJsonSchema(schema)) {
230
+ try {
231
+ return sanitizeJsonSchema(schema['~standard'].jsonSchema.input({
232
+ target: 'draft-07',
233
+ }));
234
+ }
235
+ catch {
236
+ // Fall through to the actionable configuration error below.
237
+ }
238
+ }
239
+ throw new NoeticConfigError({
240
+ code: 'MISSING_JSON_SCHEMA',
241
+ message: `The ${what} cannot be converted to JSON Schema.`,
242
+ hint: 'Use a schema implementing StandardJSONSchemaV1 or pass `inputJsonSchema` on tool() / `outputJsonSchema` on step.llm().',
243
+ });
244
+ }
245
+ function sanitizeJsonSchema(value) {
246
+ return Object.fromEntries(Object.entries(value)
247
+ .filter(([key]) => !key.startsWith('~'))
248
+ .map(([key, nested]) => [
249
+ key,
250
+ sanitizeJsonSchemaValue(nested),
251
+ ]));
252
+ }
253
+ function sanitizeJsonSchemaValue(value) {
254
+ if (Array.isArray(value)) {
255
+ return value.map(sanitizeJsonSchemaValue);
256
+ }
257
+ if (typeof value === 'object' && value !== null) {
258
+ return sanitizeJsonSchema(frameworkCast(value));
259
+ }
260
+ return value;
261
+ }
262
+ /**
263
+ * The OpenRouter SDK requires a live Zod `inputSchema` and runs
264
+ * `z.toJSONSchema` over it; it never executes these tool objects (Noetic owns
265
+ * its tool loop). For non-Zod inputs, bridge the resolved JSON Schema through
266
+ * a wire-only `z.any().meta(...)` so the SDK emits it unchanged. Noetic, not
267
+ * the SDK, validates tool-call args.
268
+ */
269
+ function inputSchemaForWire(tool) {
270
+ if (isZodSchema(tool.input)) {
271
+ return tool.input;
272
+ }
273
+ return z.any().meta(resolveWireJsonSchema({
274
+ schema: tool.input,
275
+ explicitJsonSchema: tool.inputJsonSchema,
276
+ what: `input schema of tool '${tool.name}'`,
277
+ }));
278
+ }
213
279
  /** @internal */
214
280
  export function convertTools({ tools }) {
215
281
  return tools.map((t) => frameworkCast({
@@ -217,7 +283,7 @@ export function convertTools({ tools }) {
217
283
  function: {
218
284
  name: sanitizeToolNameForWire(t.name),
219
285
  description: t.description,
220
- inputSchema: t.input,
286
+ inputSchema: inputSchemaForWire(t),
221
287
  },
222
288
  }));
223
289
  }
@@ -232,7 +298,7 @@ export async function executeToolCall(params) {
232
298
  // Model sees sanitised tool names (see `sanitizeToolNameForWire`). Match
233
299
  // against both the original and sanitised name so internal identity (e.g.
234
300
  // `plan/updatePrd` used by steering whitelists, skill docs, and the
235
- // `planContext.beforeToolCall` hook) stays intact while the wire name is
301
+ // `plan()` layer's `beforeToolCall` hook) stays intact while the wire name is
236
302
  // provider-compliant.
237
303
  const matchedTool = params.tools.find((t) => t.name === params.toolName || sanitizeToolNameForWire(t.name) === params.toolName);
238
304
  if (!matchedTool) {
@@ -256,6 +322,14 @@ export async function executeToolCall(params) {
256
322
  };
257
323
  }
258
324
  }
325
+ const validated = await validateSchema(matchedTool.input, params.args);
326
+ if (!validated.success) {
327
+ return {
328
+ output: `Error: invalid arguments for tool '${params.toolName}': ${validated.zodError.message}`,
329
+ error: true,
330
+ };
331
+ }
332
+ const parsedArgs = validated.value;
259
333
  const toolCtx = buildToolExecutionContext(params.context, params.harness);
260
334
  const callId = params.callId;
261
335
  const uiBase = callId !== undefined
@@ -263,7 +337,7 @@ export async function executeToolCall(params) {
263
337
  ctx: params.context,
264
338
  tool: matchedTool,
265
339
  callId,
266
- args: params.args,
340
+ args: parsedArgs,
267
341
  }
268
342
  : undefined;
269
343
  if (uiBase) {
@@ -273,7 +347,7 @@ export async function executeToolCall(params) {
273
347
  });
274
348
  }
275
349
  try {
276
- const executionResult = matchedTool.execute(params.args, toolCtx);
350
+ const executionResult = matchedTool.execute(parsedArgs, toolCtx);
277
351
  // Generator tools stream progress; drive them here so tool-UI `progress`
278
352
  // fragments emit per yield (the non-UI case just consumes to the return).
279
353
  let result;
@@ -1,5 +1,5 @@
1
1
  import type { ContextData } from '@noetic-tools/context';
2
- import type { Context, SettleResult, Step, StepBranch, StepForkAll, StepForkRace, StepForkSettle } from '@noetic-tools/types';
2
+ import type { Context, SettleResult, Step, StepConditional, StepInParallelAll, StepInParallelRace, StepInParallelSettle } from '@noetic-tools/types';
3
3
  /**
4
4
  * Creates a parallel execution step that races paths and returns the first result.
5
5
  *
@@ -8,16 +8,16 @@ import type { Context, SettleResult, Step, StepBranch, StepForkAll, StepForkRace
8
8
  * @param opts.mode - Must be `'race'`: first path to resolve wins, others are cancelled.
9
9
  * @param opts.paths - Factory `(input, ctx) => Step[]` generating the parallel paths.
10
10
  * @param opts.concurrency - Optional maximum number of paths to run simultaneously.
11
- * @returns A `StepForkRace` step.
11
+ * @returns A `StepInParallelRace` step.
12
12
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
13
13
  */
14
- export declare function fork<TContext = ContextData, I = unknown, O = unknown>(opts: {
14
+ export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: {
15
15
  id: string;
16
16
  mode: 'race';
17
17
  paths: (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[];
18
18
  concurrency?: number;
19
19
  _optimizable?: Step<TContext>[];
20
- }): StepForkRace<TContext, I, O>;
20
+ }): StepInParallelRace<TContext, I, O>;
21
21
  /**
22
22
  * Creates a parallel execution step that waits for all paths and merges results.
23
23
  *
@@ -27,18 +27,18 @@ export declare function fork<TContext = ContextData, I = unknown, O = unknown>(o
27
27
  * @param opts.paths - Factory `(input, ctx) => Step[]` generating the parallel paths.
28
28
  * @param opts.merge - Combines all path results into a single output value.
29
29
  * @param opts.concurrency - Optional maximum number of paths to run simultaneously.
30
- * @returns A `StepForkAll` step.
30
+ * @returns A `StepInParallelAll` step.
31
31
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
32
32
  * @throws `NoeticConfigError` with code `MISSING_MERGE_FUNCTION` if `merge` is not provided.
33
33
  */
34
- export declare function fork<TContext = ContextData, I = unknown, O = unknown>(opts: {
34
+ export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: {
35
35
  id: string;
36
36
  mode: 'all';
37
37
  paths: (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[];
38
38
  merge: (results: O[], ctx: Context<TContext>) => O;
39
39
  concurrency?: number;
40
40
  _optimizable?: Step<TContext>[];
41
- }): StepForkAll<TContext, I, O>;
41
+ }): StepInParallelAll<TContext, I, O>;
42
42
  /**
43
43
  * Creates a parallel execution step that waits for all paths, collecting errors as results.
44
44
  *
@@ -48,18 +48,18 @@ export declare function fork<TContext = ContextData, I = unknown, O = unknown>(o
48
48
  * @param opts.paths - Factory `(input, ctx) => Step[]` generating the parallel paths.
49
49
  * @param opts.merge - Combines settled results (successes and failures) into a single output.
50
50
  * @param opts.concurrency - Optional maximum number of paths to run simultaneously.
51
- * @returns A `StepForkSettle` step.
51
+ * @returns A `StepInParallelSettle` step.
52
52
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
53
53
  * @throws `NoeticConfigError` with code `MISSING_MERGE_FUNCTION` if `merge` is not provided.
54
54
  */
55
- export declare function fork<TContext = ContextData, I = unknown, O = unknown>(opts: {
55
+ export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: {
56
56
  id: string;
57
57
  mode: 'settle';
58
58
  paths: (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[];
59
59
  merge: (results: SettleResult<O>[], ctx: Context<TContext>) => O;
60
60
  concurrency?: number;
61
61
  _optimizable?: Step<TContext>[];
62
- }): StepForkSettle<TContext, I, O>;
62
+ }): StepInParallelSettle<TContext, I, O>;
63
63
  /**
64
64
  * Creates a conditional routing step that selects the next step at runtime.
65
65
  *
@@ -67,12 +67,12 @@ export declare function fork<TContext = ContextData, I = unknown, O = unknown>(o
67
67
  * @param opts.id - Unique step identifier used in traces and error messages.
68
68
  * @param opts.route - Routing function `(input, ctx) => Step | null`. Returns the next step
69
69
  * to execute, or `null` to skip (pass input through as output). May be async.
70
- * @returns A `StepBranch` step.
70
+ * @returns A `StepConditional` step.
71
71
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
72
72
  * @throws `NoeticConfigError` with code `MISSING_ROUTE_FUNCTION` if `route` is not provided.
73
73
  */
74
- export declare function branch<TContext = ContextData, I = unknown, O = unknown>(opts: {
74
+ export declare function conditional<TContext = ContextData, I = unknown, O = unknown>(opts: {
75
75
  id: string;
76
76
  route: (input: I, ctx: Context<TContext>) => Step<TContext, I, O> | null | Promise<Step<TContext, I, O> | null>;
77
77
  _optimizable?: Step<TContext>[];
78
- }): StepBranch<TContext, I, O>;
78
+ }): StepConditional<TContext, I, O>;
@@ -1,22 +1,22 @@
1
1
  import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
2
2
  import { getDefaultRegistrar } from '../types/step-registrar.js';
3
- export function fork(opts) {
3
+ export function inParallel(opts) {
4
4
  if (!opts.id || opts.id.trim() === '') {
5
5
  throw new NoeticConfigError({
6
6
  code: 'EMPTY_STEP_ID',
7
- message: 'fork() requires a non-empty id.',
8
- hint: 'Pass a unique string as the id field, e.g. fork({ id: "my-fork", ... }).',
7
+ message: 'inParallel() requires a non-empty id.',
8
+ hint: 'Pass a unique string as the id field, e.g. inParallel({ id: "my-inParallel", ... }).',
9
9
  });
10
10
  }
11
11
  if ((opts.mode === 'all' || opts.mode === 'settle') && !opts.merge) {
12
12
  throw new NoeticConfigError({
13
13
  code: 'MISSING_MERGE_FUNCTION',
14
- message: `fork() mode '${opts.mode}' requires a merge function.`,
14
+ message: `inParallel() mode '${opts.mode}' requires a merge function.`,
15
15
  hint: 'Provide a merge function that combines path results into a single output.',
16
16
  });
17
17
  }
18
18
  const built = frameworkCast({
19
- kind: 'fork',
19
+ kind: 'inParallel',
20
20
  ...opts,
21
21
  });
22
22
  getDefaultRegistrar().register(built);
@@ -29,27 +29,27 @@ export function fork(opts) {
29
29
  * @param opts.id - Unique step identifier used in traces and error messages.
30
30
  * @param opts.route - Routing function `(input, ctx) => Step | null`. Returns the next step
31
31
  * to execute, or `null` to skip (pass input through as output). May be async.
32
- * @returns A `StepBranch` step.
32
+ * @returns A `StepConditional` step.
33
33
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
34
34
  * @throws `NoeticConfigError` with code `MISSING_ROUTE_FUNCTION` if `route` is not provided.
35
35
  */
36
- export function branch(opts) {
36
+ export function conditional(opts) {
37
37
  if (!opts.id || opts.id.trim() === '') {
38
38
  throw new NoeticConfigError({
39
39
  code: 'EMPTY_STEP_ID',
40
- message: 'branch() requires a non-empty id.',
41
- hint: 'Pass a unique string as the id field, e.g. branch({ id: "my-branch", ... }).',
40
+ message: 'conditional() requires a non-empty id.',
41
+ hint: 'Pass a unique string as the id field, e.g. conditional({ id: "my-conditional", ... }).',
42
42
  });
43
43
  }
44
44
  if (!opts.route) {
45
45
  throw new NoeticConfigError({
46
46
  code: 'MISSING_ROUTE_FUNCTION',
47
- message: 'branch() requires a route function.',
47
+ message: 'conditional() requires a route function.',
48
48
  hint: 'Provide a route function that selects the next step at runtime.',
49
49
  });
50
50
  }
51
51
  const built = {
52
- kind: 'branch',
52
+ kind: 'conditional',
53
53
  ...opts,
54
54
  };
55
55
  getDefaultRegistrar().register(built);
@@ -36,7 +36,7 @@ export interface ParseAndRunWorkflowOpts {
36
36
  input?: string;
37
37
  maxDepth?: number;
38
38
  /**
39
- * Context layers the document's `provide` / `spawn` nodes may reference by name.
39
+ * Context layers the document's `withContext` / `spawn` nodes may reference by name.
40
40
  * Without a registry those nodes resolve to NO layers rather than failing, so a
41
41
  * host that runs layer-bearing workflows must pass its layers here.
42
42
  */
@@ -3,10 +3,10 @@
3
3
  * then hydrates and runs it within the same harness session.
4
4
  */
5
5
  import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
6
- import { step } from '../builders/step-builders.js';
7
- import { hydrateWorkflow } from '../builders/workflow-hydrator.js';
8
6
  import { NoeticAttr } from '../observability/genai-attributes.js';
9
7
  import { WorkflowDocumentSchema, workflowDepth, workflowGraph } from '../schemas/workflow.js';
8
+ import { callModel, runCode } from './step-builders.js';
9
+ import { hydrateWorkflow } from './workflow-hydrator.js';
10
10
  //#endregion
11
11
  //#region Constants
12
12
  const DEFAULT_MAX_DEPTH = 5;
@@ -21,19 +21,25 @@ The workflow document must be valid JSON with this structure:
21
21
  }
22
22
 
23
23
  A WorkflowNode is one of:
24
- - { "kind": "llm", "id": "<unique>", "instructions": "<prompt>", "model": "<optional>", "tools": ["<tool-name>", ...] }
25
- - { "kind": "tool", "id": "<unique>", "toolName": "<name>", "args": { ... } }
26
- - { "kind": "sequence", "id": "<unique>", "steps": [<WorkflowNode>, ...] }
27
- - { "kind": "fork", "id": "<unique>", "mode": "all"|"race"|"settle", "paths": [<WorkflowNode>, ...], "merge": "last"|"first"|"concat" }
28
- - { "kind": "loop", "id": "<unique>", "body": <WorkflowNode>, "until": { "kind": "maxSteps", "n": <number> } }
29
- - { "kind": "branch", "id": "<unique>", "routes": [{ "match": "<substring>", "target": <WorkflowNode> }], "default": <WorkflowNode> }
30
- - { "kind": "spawn", "id": "<unique>", "child": <WorkflowNode> }
24
+ - { "kind": "callModel", "id": "<unique>", "instructions": "<prompt>", "model": "<optional>", "tools": [{ "type": "<tool-name>" }, ...] } (one LLM call)
25
+ - { "kind": "invokeTool", "id": "<unique>", "toolName": "<name>", "args": { ... } } (call one tool directly, no LLM)
26
+ - { "kind": "runCode", "id": "<unique>", "execute": "<source code>" } (runs the code in a subprocess with the step input on stdin; only emit if the task needs deterministic computation AND a subprocess adapter is available)
27
+ - { "kind": "sequence", "id": "<unique>", "steps": [<WorkflowNode>, ...] } (run children in order, threading each output into the next)
28
+ - { "kind": "inParallel", "id": "<unique>", "mode": "all"|"race"|"settle", "paths": [<WorkflowNode>, ...], "merge": "last"|"first"|"concat" } (fan out; supply exactly one of "paths" or "each": <WorkflowNode> — with "each", add "over": "<key>" to fan out over an array in the input JSON)
29
+ - { "kind": "loop", "id": "<unique>", "body": <WorkflowNode>, "until": <UntilPredicate>, "maxIterations": <optional number> } (repeat the body until the predicate holds)
30
+ - { "kind": "conditional", "id": "<unique>", "routes": [{ "match": "<substring>", "target": <WorkflowNode> }], "default": <WorkflowNode> } (route on a substring of the input)
31
+ - { "kind": "spawn", "id": "<unique>", "child": <WorkflowNode>, "timeout": <optional ms> } (run the child in an isolated sub-context)
32
+ - { "kind": "withContext", "id": "<unique>", "child": <WorkflowNode>, "layers": ["<layer-name>", ...] } (run the child with named context layers; only emit if the task names layers that exist, since this planner provides no layer registry and unknown names resolve to NO layers)
33
+ - { "kind": "schedule", "id": "<unique>", "step": <WorkflowNode>, "interval": <ms>, "onError": "continue"|"fail" } (re-runs the step forever on an interval and never returns — only emit for an explicitly daemon-style task)
31
34
  - { "kind": "subflow", "id": "<unique>", "document": { "version": 1, "root": <WorkflowNode> } } (an inline sub-workflow run as one step; only emit the inline form — named refs require a registry this planner does not provide)
32
35
  - { "kind": "claude-code"|"codex"|"opencode"|"pi", "id": "<unique>", "prompt": "<turn prompt>", "settings": { "model": "<optional>", "permissionMode": "<optional>" } }
33
36
 
37
+ Every entry of a callModel node's "tools" is an OBJECT, never a bare string. Use { "type": "<tool-name>" } to let the model call one of the tools listed below. Two provider-executed tools are also available without being listed: { "type": "openrouter:web_search" } and { "type": "openrouter:web_fetch" }, each accepting an optional "parameters" object.
38
+
34
39
  SubHarness nodes (claude-code, codex, opencode, pi) delegate a turn to an external coding agent; only emit one if a matching harness adapter is registered for the workflow.
35
40
 
36
- Until predicates: maxSteps, maxCost, maxDuration, noToolCalls, outputContains, outputEquals, converged, any, all.
41
+ An UntilPredicate (the "until" field of a loop) is one of:
42
+ { "kind": "maxSteps", "n": <positive int> }, { "kind": "maxCost", "usd": <positive number> }, { "kind": "maxDuration", "duration": <ms> }, { "kind": "noToolCalls" }, { "kind": "never" } (never stops — pair with "maxIterations"), { "kind": "outputContains", "marker": "<text>" }, { "kind": "outputEquals", "sentinel": "<text>" }, { "kind": "converged", "threshold": <optional 0-1> }, { "kind": "any", "predicates": [<UntilPredicate>, ...] }, { "kind": "all", "predicates": [<UntilPredicate>, ...] }.
37
43
 
38
44
  Respond with ONLY the JSON document, no markdown fences or explanation.`;
39
45
  //#endregion
@@ -56,7 +62,7 @@ export function dynamicWorkflow(opts) {
56
62
  const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
57
63
  const maxRevisions = opts.maxRevisions ?? DEFAULT_MAX_REVISIONS;
58
64
  const toolMap = buildToolMap(opts.tools);
59
- return step.run({
65
+ return runCode({
60
66
  id: 'dynamic-workflow',
61
67
  execute: async (input, ctx) => {
62
68
  const harness = ctx.harness;
@@ -81,7 +87,7 @@ export function dynamicWorkflow(opts) {
81
87
  const prompt = lastError
82
88
  ? `${basePrompt}\n\nPrevious attempt failed with error: ${lastError}\nPlease fix the workflow and try again.`
83
89
  : basePrompt;
84
- const plannerStep = step.llm({
90
+ const plannerStep = callModel({
85
91
  id: `dynamic-workflow-planner-${revision}`,
86
92
  model,
87
93
  instructions: prompt,
@@ -1,44 +1,44 @@
1
1
  import type { ContextData } from '@noetic-tools/context';
2
- import type { Channel, EveryErrorPolicy, Step, StepEvery } from '@noetic-tools/types';
2
+ import type { Channel, ScheduleErrorPolicy, Step, StepSchedule } from '@noetic-tools/types';
3
3
  /**
4
- * Configuration options accepted by the `every()` builder, excluding the `kind`
5
- * discriminant. `every()` schedules a body step on a fixed-interval, optionally
4
+ * Configuration options accepted by the `schedule()` builder, excluding the `kind`
5
+ * discriminant. `schedule()` schedules a body step on a fixed-interval, optionally
6
6
  * woken early by a channel, and runs until the executing context is cancelled.
7
7
  *
8
8
  * @public
9
9
  */
10
- export interface EveryOptions<TContext = ContextData, I = unknown, O = unknown> {
10
+ export interface ScheduleOptions<TContext = ContextData, I = unknown, O = unknown> {
11
11
  /** Unique step identifier used in traces and error messages. */
12
12
  id: string;
13
13
  /** Body step executed on each iteration. */
14
14
  step: Step<TContext, I, O>;
15
15
  /** Park duration between iterations in milliseconds. Must be >= 0. */
16
- ms: number;
16
+ interval: number;
17
17
  /** Optional channel that wakes the parking interval when any value arrives. */
18
- wakeOn?: Channel<unknown>;
18
+ inbox?: Channel<unknown>;
19
19
  /** Behavior when `step` throws. Defaults to `'continue'`. */
20
- onError?: EveryErrorPolicy;
20
+ onError?: ScheduleErrorPolicy;
21
21
  /** Random jitter applied to the park duration in milliseconds. Must be >= 0. Default 0. */
22
22
  jitter?: number;
23
23
  }
24
24
  /**
25
- * Creates an `every` step that runs a body step on a fixed-interval schedule.
25
+ * Creates a `schedule` step that runs a body step on a fixed-interval schedule.
26
26
  *
27
27
  * The operator runs forever until the executing context is cancelled. After each
28
- * iteration it parks for `ms ± jitter` milliseconds (or until `wakeOn` receives
28
+ * iteration it parks for `interval ± jitter` milliseconds (or until `inbox` receives
29
29
  * a message, whichever comes first).
30
30
  *
31
31
  * @public
32
32
  * @param opts.id - Unique step identifier used in traces and error messages.
33
33
  * @param opts.step - Body step executed on each iteration.
34
- * @param opts.ms - Park duration between iterations in milliseconds.
35
- * @param opts.wakeOn - Optional channel that wakes the parking interval when any value arrives.
34
+ * @param opts.interval - Park duration between iterations in milliseconds.
35
+ * @param opts.inbox - Optional channel that wakes the parking interval when any value arrives.
36
36
  * @param opts.onError - Error policy. `'continue'` (default) records a span event and continues; `'fail'` re-throws.
37
37
  * @param opts.jitter - Random jitter in ms applied to the park duration. Default 0.
38
- * @returns A `StepEvery` step.
38
+ * @returns A `StepSchedule` step.
39
39
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
40
40
  * @throws `NoeticConfigError` with code `MISSING_BODY_STEP` if `step` is not provided.
41
- * @throws `NoeticConfigError` with code `INVALID_INTERVAL_MS` if `ms` is negative or not finite.
41
+ * @throws `NoeticConfigError` with code `INVALID_INTERVAL_MS` if `interval` is negative or not finite.
42
42
  * @throws `NoeticConfigError` with code `INVALID_JITTER` if `jitter` is negative or not finite.
43
43
  */
44
- export declare function every<TContext = ContextData, I = unknown, O = unknown>(opts: EveryOptions<TContext, I, O>): StepEvery<TContext, I, O>;
44
+ export declare function schedule<TContext = ContextData, I = unknown, O = unknown>(opts: ScheduleOptions<TContext, I, O>): StepSchedule<TContext, I, O>;
@@ -1,60 +1,60 @@
1
1
  import { NoeticConfigError } from '@noetic-tools/types';
2
2
  import { getDefaultRegistrar } from '../types/step-registrar.js';
3
3
  /**
4
- * Creates an `every` step that runs a body step on a fixed-interval schedule.
4
+ * Creates a `schedule` step that runs a body step on a fixed-interval schedule.
5
5
  *
6
6
  * The operator runs forever until the executing context is cancelled. After each
7
- * iteration it parks for `ms ± jitter` milliseconds (or until `wakeOn` receives
7
+ * iteration it parks for `interval ± jitter` milliseconds (or until `inbox` receives
8
8
  * a message, whichever comes first).
9
9
  *
10
10
  * @public
11
11
  * @param opts.id - Unique step identifier used in traces and error messages.
12
12
  * @param opts.step - Body step executed on each iteration.
13
- * @param opts.ms - Park duration between iterations in milliseconds.
14
- * @param opts.wakeOn - Optional channel that wakes the parking interval when any value arrives.
13
+ * @param opts.interval - Park duration between iterations in milliseconds.
14
+ * @param opts.inbox - Optional channel that wakes the parking interval when any value arrives.
15
15
  * @param opts.onError - Error policy. `'continue'` (default) records a span event and continues; `'fail'` re-throws.
16
16
  * @param opts.jitter - Random jitter in ms applied to the park duration. Default 0.
17
- * @returns A `StepEvery` step.
17
+ * @returns A `StepSchedule` step.
18
18
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
19
19
  * @throws `NoeticConfigError` with code `MISSING_BODY_STEP` if `step` is not provided.
20
- * @throws `NoeticConfigError` with code `INVALID_INTERVAL_MS` if `ms` is negative or not finite.
20
+ * @throws `NoeticConfigError` with code `INVALID_INTERVAL_MS` if `interval` is negative or not finite.
21
21
  * @throws `NoeticConfigError` with code `INVALID_JITTER` if `jitter` is negative or not finite.
22
22
  */
23
- export function every(opts) {
23
+ export function schedule(opts) {
24
24
  if (!opts.id || opts.id.trim() === '') {
25
25
  throw new NoeticConfigError({
26
26
  code: 'EMPTY_STEP_ID',
27
- message: 'every() requires a non-empty id.',
28
- hint: 'Pass a unique string as the id field, e.g. every({ id: "my-every", ... }).',
27
+ message: 'schedule() requires a non-empty id.',
28
+ hint: 'Pass a unique string as the id field, e.g. schedule({ id: "my-schedule", ... }).',
29
29
  });
30
30
  }
31
31
  if (!opts.step) {
32
32
  throw new NoeticConfigError({
33
33
  code: 'MISSING_BODY_STEP',
34
- message: 'every() requires a body step.',
34
+ message: 'schedule() requires a body step.',
35
35
  hint: 'Provide the step to execute on each scheduled iteration.',
36
36
  });
37
37
  }
38
- if (!Number.isFinite(opts.ms) || opts.ms < 0) {
38
+ if (!Number.isFinite(opts.interval) || opts.interval < 0) {
39
39
  throw new NoeticConfigError({
40
40
  code: 'INVALID_INTERVAL_MS',
41
- message: `every() requires a non-negative finite ms, got ${opts.ms}.`,
41
+ message: `schedule() requires a non-negative finite interval, got ${opts.interval}.`,
42
42
  hint: 'Pass a non-negative number of milliseconds for the park interval.',
43
43
  });
44
44
  }
45
45
  if (opts.jitter !== undefined && (!Number.isFinite(opts.jitter) || opts.jitter < 0)) {
46
46
  throw new NoeticConfigError({
47
47
  code: 'INVALID_JITTER',
48
- message: `every() requires a non-negative finite jitter, got ${opts.jitter}.`,
48
+ message: `schedule() requires a non-negative finite jitter, got ${opts.jitter}.`,
49
49
  hint: 'Pass a non-negative number of milliseconds for jitter, or omit it.',
50
50
  });
51
51
  }
52
52
  const built = {
53
- kind: 'every',
53
+ kind: 'schedule',
54
54
  id: opts.id,
55
55
  step: opts.step,
56
- ms: opts.ms,
57
- wakeOn: opts.wakeOn,
56
+ interval: opts.interval,
57
+ inbox: opts.inbox,
58
58
  onError: opts.onError ?? 'continue',
59
59
  jitter: opts.jitter ?? 0,
60
60
  };
@@ -1 +1 @@
1
- export { layerData, layerFn } from '@noetic-tools/context';
1
+ export { layerData, layerFunction } from '@noetic-tools/context';
@@ -1 +1 @@
1
- export { layerData, layerFn } from '@noetic-tools/context';
1
+ export { layerData, layerFunction } from '@noetic-tools/context';
@@ -1,5 +1,5 @@
1
1
  import type { ContextConfig, ContextData, ContextLayer } from '@noetic-tools/context';
2
- import type { Step, StepProvide } from '@noetic-tools/types';
2
+ import type { Step, StepWithContext } from '@noetic-tools/types';
3
3
  /**
4
4
  * Creates a provide step that attaches context layers to its child without creating an isolated context.
5
5
  * Like React's Context.Provider — layers are available to all descendant steps.
@@ -9,19 +9,12 @@ import type { Step, StepProvide } from '@noetic-tools/types';
9
9
  * @param opts.id - Unique step identifier used in traces and error messages.
10
10
  * @param opts.child - Step to execute with the provided layers.
11
11
  * @param opts.context - Context layers to provide to descendant steps.
12
- * @returns A `StepProvide` step.
12
+ * @returns A `StepWithContext` step.
13
13
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
14
14
  * @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
15
15
  */
16
- export declare function provide<TContext = ContextData, I = unknown, O = unknown>(opts: {
16
+ export declare function withContext<TContext = ContextData, I = unknown, O = unknown>(opts: {
17
17
  id: string;
18
18
  child: Step<TContext, I, O>;
19
- /**
20
- * Optional in the type only so the deprecated `memory` spelling remains a
21
- * valid way to supply the layers. Exactly one of the two is required — the
22
- * runtime enforces it, the same way `id` and `child` are enforced.
23
- */
24
- context?: ContextConfig | ContextLayer[];
25
- /** @deprecated Renamed to `context`. */
26
- memory?: ContextConfig | ContextLayer[];
27
- }): StepProvide<TContext, I, O>;
19
+ context: ContextConfig | ContextLayer[];
20
+ }): StepWithContext<TContext, I, O>;