@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.
- package/README.md +8 -6
- package/dist/adapters/in-memory-subprocess/step-completion.js +1 -1
- package/dist/adapters/openrouter.d.ts +12 -1
- package/dist/adapters/openrouter.js +79 -5
- package/dist/builders/control-flow-builders.d.ts +13 -13
- package/dist/builders/control-flow-builders.js +11 -11
- package/dist/{patterns → builders}/dynamic-workflow.d.ts +1 -1
- package/dist/{patterns → builders}/dynamic-workflow.js +18 -12
- package/dist/builders/every.d.ts +14 -14
- package/dist/builders/every.js +16 -16
- package/dist/builders/layer-provides-builders.d.ts +1 -1
- package/dist/builders/layer-provides-builders.js +1 -1
- package/dist/builders/provide-builder.d.ts +5 -12
- package/dist/builders/provide-builder.js +9 -10
- package/dist/builders/spawn-builder.d.ts +0 -2
- package/dist/builders/spawn-builder.js +2 -3
- package/dist/builders/step-builders.d.ts +66 -55
- package/dist/builders/step-builders.js +118 -111
- package/dist/builders/tool-builder.d.ts +18 -22
- package/dist/builders/tool-builder.js +6 -4
- package/dist/builders/workflow-hydrator.d.ts +3 -3
- package/dist/builders/workflow-hydrator.js +59 -63
- package/dist/builders/workflow-step.d.ts +13 -25
- package/dist/builders/workflow-step.js +13 -18
- package/dist/conditions/conditions.d.ts +2 -2
- package/dist/conditions/conditions.js +1 -1
- package/dist/harness/agent-harness.d.ts +54 -40
- package/dist/harness/agent-harness.js +44 -33
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/harness/model-call.d.ts +1 -1
- package/dist/harness/model-call.js +23 -18
- package/dist/index.d.ts +21 -33
- package/dist/index.js +10 -26
- package/dist/interpreter/action-types.d.ts +1 -1
- package/dist/interpreter/collect-tools.js +7 -7
- package/dist/interpreter/execute-action.d.ts +5 -5
- package/dist/interpreter/execute-action.js +284 -218
- package/dist/interpreter/execute-control.d.ts +9 -9
- package/dist/interpreter/execute-control.js +27 -27
- package/dist/interpreter/execute-sub-harness.d.ts +1 -1
- package/dist/interpreter/execute-sub-harness.js +7 -26
- package/dist/interpreter/execute.d.ts +1 -1
- package/dist/interpreter/execute.js +26 -25
- package/dist/interpreter/step-dispatcher.d.ts +1 -1
- package/dist/interpreter/step-dispatcher.js +3 -3
- package/dist/interpreter/structured-output.d.ts +14 -0
- package/dist/interpreter/structured-output.js +40 -0
- package/dist/interpreter/typeguards.d.ts +1 -1
- package/dist/interpreter/typeguards.js +1 -1
- package/dist/portable/builders.d.ts +3 -3
- package/dist/portable/builders.js +6 -5
- package/dist/portable/context-layers.d.ts +1 -1
- package/dist/portable/context-layers.js +1 -1
- package/dist/runtime/broadcaster-utils.d.ts +1 -1
- package/dist/runtime/broadcaster-utils.js +1 -1
- package/dist/runtime/context-impl.d.ts +8 -10
- package/dist/runtime/context-impl.js +8 -12
- package/dist/runtime/durable/step-ledger.d.ts +2 -2
- package/dist/runtime/durable/step-ledger.js +3 -3
- package/dist/runtime/session-runner.d.ts +12 -1
- package/dist/runtime/session-runner.js +33 -1
- package/dist/runtime/tool-context.js +0 -2
- package/dist/schemas/workflow.d.ts +3 -3
- package/dist/schemas/workflow.js +2 -2
- package/dist/types/checkpoint.d.ts +11 -2
- package/dist/types/checkpoint.js +10 -1
- package/dist/{runtime → util}/detached-handle.js +1 -1
- package/package.json +6 -4
- package/schema/noetic-workflow.schema.json +24 -11
- package/dist/builders/context-option.d.ts +0 -23
- package/dist/builders/context-option.js +0 -22
- package/dist/deprecated.d.ts +0 -30
- package/dist/deprecated.js +0 -28
- package/dist/patterns/interview.d.ts +0 -53
- package/dist/patterns/interview.js +0 -113
- package/dist/patterns/plans.d.ts +0 -44
- package/dist/patterns/plans.js +0 -125
- package/dist/patterns/ralph-wiggum.d.ts +0 -19
- package/dist/patterns/ralph-wiggum.js +0 -37
- package/dist/patterns/react.d.ts +0 -19
- package/dist/patterns/react.js +0 -41
- /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
|
|
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,
|
|
28
|
+
import { AgentHarness, callModel } from '@noetic-tools/core';
|
|
27
29
|
|
|
28
|
-
// 1. Define a single
|
|
29
|
-
const greet =
|
|
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; `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
// `
|
|
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:
|
|
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(
|
|
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,
|
|
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 `
|
|
11
|
+
* @returns A `StepInParallelRace` step.
|
|
12
12
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
13
13
|
*/
|
|
14
|
-
export declare function
|
|
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
|
-
}):
|
|
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 `
|
|
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
|
|
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
|
-
}):
|
|
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 `
|
|
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
|
|
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
|
-
}):
|
|
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 `
|
|
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
|
|
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
|
-
}):
|
|
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
|
|
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: '
|
|
8
|
-
hint: 'Pass a unique string as the id field, e.g.
|
|
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: `
|
|
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: '
|
|
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 `
|
|
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
|
|
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: '
|
|
41
|
-
hint: 'Pass a unique string as the id field, e.g.
|
|
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: '
|
|
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: '
|
|
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 `
|
|
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": "
|
|
25
|
-
- { "kind": "
|
|
26
|
-
- { "kind": "
|
|
27
|
-
- { "kind": "
|
|
28
|
-
- { "kind": "
|
|
29
|
-
- { "kind": "
|
|
30
|
-
- { "kind": "
|
|
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
|
-
|
|
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
|
|
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 =
|
|
90
|
+
const plannerStep = callModel({
|
|
85
91
|
id: `dynamic-workflow-planner-${revision}`,
|
|
86
92
|
model,
|
|
87
93
|
instructions: prompt,
|
package/dist/builders/every.d.ts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
1
|
import type { ContextData } from '@noetic-tools/context';
|
|
2
|
-
import type { Channel,
|
|
2
|
+
import type { Channel, ScheduleErrorPolicy, Step, StepSchedule } from '@noetic-tools/types';
|
|
3
3
|
/**
|
|
4
|
-
* Configuration options accepted by the `
|
|
5
|
-
* discriminant. `
|
|
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
|
|
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
|
-
|
|
16
|
+
interval: number;
|
|
17
17
|
/** Optional channel that wakes the parking interval when any value arrives. */
|
|
18
|
-
|
|
18
|
+
inbox?: Channel<unknown>;
|
|
19
19
|
/** Behavior when `step` throws. Defaults to `'continue'`. */
|
|
20
|
-
onError?:
|
|
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
|
|
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 `
|
|
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.
|
|
35
|
-
* @param opts.
|
|
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 `
|
|
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 `
|
|
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
|
|
44
|
+
export declare function schedule<TContext = ContextData, I = unknown, O = unknown>(opts: ScheduleOptions<TContext, I, O>): StepSchedule<TContext, I, O>;
|
package/dist/builders/every.js
CHANGED
|
@@ -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
|
|
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 `
|
|
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.
|
|
14
|
-
* @param opts.
|
|
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 `
|
|
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 `
|
|
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
|
|
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: '
|
|
28
|
-
hint: 'Pass a unique string as the id field, e.g.
|
|
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: '
|
|
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.
|
|
38
|
+
if (!Number.isFinite(opts.interval) || opts.interval < 0) {
|
|
39
39
|
throw new NoeticConfigError({
|
|
40
40
|
code: 'INVALID_INTERVAL_MS',
|
|
41
|
-
message: `
|
|
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: `
|
|
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: '
|
|
53
|
+
kind: 'schedule',
|
|
54
54
|
id: opts.id,
|
|
55
55
|
step: opts.step,
|
|
56
|
-
|
|
57
|
-
|
|
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,
|
|
1
|
+
export { layerData, layerFunction } from '@noetic-tools/context';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { layerData,
|
|
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,
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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>;
|