@noetic-tools/core 5.2.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 +20 -2
- package/dist/adapters/openrouter.js +102 -6
- 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 +58 -40
- package/dist/harness/agent-harness.js +125 -51
- package/dist/harness/deps/context.d.ts +1 -1
- package/dist/harness/deps/context.js +1 -1
- package/dist/harness/deps/interpreter.d.ts +1 -0
- package/dist/harness/deps/interpreter.js +1 -0
- package/dist/harness/deps/types.d.ts +2 -2
- package/dist/harness/model-call.d.ts +1 -1
- package/dist/harness/model-call.js +45 -23
- package/dist/index.d.ts +23 -35
- package/dist/index.js +10 -26
- package/dist/interpreter/action-deps.d.ts +2 -2
- package/dist/interpreter/action-deps.js +1 -1
- package/dist/interpreter/action-types.d.ts +1 -1
- package/dist/interpreter/collect-tools.js +7 -7
- package/dist/interpreter/context-assembly.d.ts +50 -0
- package/dist/interpreter/context-assembly.js +547 -0
- package/dist/interpreter/execute-action.d.ts +5 -5
- package/dist/interpreter/execute-action.js +337 -208
- 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/observability/genai-attributes.d.ts +20 -0
- package/dist/observability/genai-attributes.js +20 -0
- 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];
|
|
@@ -23,7 +23,14 @@ export declare function itemsToInput(items: ReadonlyArray<Item>): OpenRouterInpu
|
|
|
23
23
|
* when the output array contains no message items.
|
|
24
24
|
*/
|
|
25
25
|
export declare function extractOutputItems(response: OpenRouterAgent.OpenResponsesResult): Item[];
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* @internal
|
|
28
|
+
*
|
|
29
|
+
* A missing `cachedTokens` stays `undefined` rather than collapsing to `0`:
|
|
30
|
+
* context anchoring reads it to decide whether the prompt prefix survived, and
|
|
31
|
+
* "this provider reports no cache figures" must not look like "nothing was
|
|
32
|
+
* cached".
|
|
33
|
+
*/
|
|
27
34
|
export declare function extractUsage(usage: OpenRouterAgent.Usage | null | undefined): LLMResponse['usage'];
|
|
28
35
|
/**
|
|
29
36
|
* Provider APIs (Anthropic via OpenRouter) enforce tool names match
|
|
@@ -33,6 +40,17 @@ export declare function extractUsage(usage: OpenRouterAgent.Usage | null | undef
|
|
|
33
40
|
* to names like `plan/updatePrd`) is preserved.
|
|
34
41
|
*/
|
|
35
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>;
|
|
36
54
|
/** @internal */
|
|
37
55
|
export declare function convertTools({ tools }: ConvertToolsParams): SdkTool[];
|
|
38
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';
|
|
@@ -153,7 +153,14 @@ export function extractOutputItems(response) {
|
|
|
153
153
|
...items,
|
|
154
154
|
];
|
|
155
155
|
}
|
|
156
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* @internal
|
|
158
|
+
*
|
|
159
|
+
* A missing `cachedTokens` stays `undefined` rather than collapsing to `0`:
|
|
160
|
+
* context anchoring reads it to decide whether the prompt prefix survived, and
|
|
161
|
+
* "this provider reports no cache figures" must not look like "nothing was
|
|
162
|
+
* cached".
|
|
163
|
+
*/
|
|
157
164
|
export function extractUsage(usage) {
|
|
158
165
|
if (!usage) {
|
|
159
166
|
return {
|
|
@@ -165,8 +172,23 @@ export function extractUsage(usage) {
|
|
|
165
172
|
inputTokens: usage.inputTokens,
|
|
166
173
|
outputTokens: usage.outputTokens,
|
|
167
174
|
cachedTokens: usage.inputTokensDetails?.cachedTokens,
|
|
175
|
+
cacheWriteTokens: cacheWriteTokensOf(usage.inputTokensDetails),
|
|
168
176
|
};
|
|
169
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* OpenRouter reports `cache_write_tokens` alongside `cached_tokens`, but the
|
|
180
|
+
* SDK's `InputTokensDetails` type declares only the latter. Read it off the
|
|
181
|
+
* value rather than the type, and stay `undefined` when it is genuinely absent
|
|
182
|
+
* — a write of zero and no report at all mean different things to the epoch
|
|
183
|
+
* logic (see `noteCacheOutcome`).
|
|
184
|
+
*/
|
|
185
|
+
function cacheWriteTokensOf(details) {
|
|
186
|
+
if (typeof details !== 'object' || details === null || !('cacheWriteTokens' in details)) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const value = details.cacheWriteTokens;
|
|
190
|
+
return typeof value === 'number' ? value : undefined;
|
|
191
|
+
}
|
|
170
192
|
//#endregion
|
|
171
193
|
//#region Tool Conversion
|
|
172
194
|
// We construct the SDK tool shape manually and use frameworkCast to bridge
|
|
@@ -188,6 +210,72 @@ export function extractUsage(usage) {
|
|
|
188
210
|
export function sanitizeToolNameForWire(name) {
|
|
189
211
|
return name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
190
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
|
+
}
|
|
191
279
|
/** @internal */
|
|
192
280
|
export function convertTools({ tools }) {
|
|
193
281
|
return tools.map((t) => frameworkCast({
|
|
@@ -195,7 +283,7 @@ export function convertTools({ tools }) {
|
|
|
195
283
|
function: {
|
|
196
284
|
name: sanitizeToolNameForWire(t.name),
|
|
197
285
|
description: t.description,
|
|
198
|
-
inputSchema: t
|
|
286
|
+
inputSchema: inputSchemaForWire(t),
|
|
199
287
|
},
|
|
200
288
|
}));
|
|
201
289
|
}
|
|
@@ -210,7 +298,7 @@ export async function executeToolCall(params) {
|
|
|
210
298
|
// Model sees sanitised tool names (see `sanitizeToolNameForWire`). Match
|
|
211
299
|
// against both the original and sanitised name so internal identity (e.g.
|
|
212
300
|
// `plan/updatePrd` used by steering whitelists, skill docs, and the
|
|
213
|
-
// `
|
|
301
|
+
// `plan()` layer's `beforeToolCall` hook) stays intact while the wire name is
|
|
214
302
|
// provider-compliant.
|
|
215
303
|
const matchedTool = params.tools.find((t) => t.name === params.toolName || sanitizeToolNameForWire(t.name) === params.toolName);
|
|
216
304
|
if (!matchedTool) {
|
|
@@ -234,6 +322,14 @@ export async function executeToolCall(params) {
|
|
|
234
322
|
};
|
|
235
323
|
}
|
|
236
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;
|
|
237
333
|
const toolCtx = buildToolExecutionContext(params.context, params.harness);
|
|
238
334
|
const callId = params.callId;
|
|
239
335
|
const uiBase = callId !== undefined
|
|
@@ -241,7 +337,7 @@ export async function executeToolCall(params) {
|
|
|
241
337
|
ctx: params.context,
|
|
242
338
|
tool: matchedTool,
|
|
243
339
|
callId,
|
|
244
|
-
args:
|
|
340
|
+
args: parsedArgs,
|
|
245
341
|
}
|
|
246
342
|
: undefined;
|
|
247
343
|
if (uiBase) {
|
|
@@ -251,7 +347,7 @@ export async function executeToolCall(params) {
|
|
|
251
347
|
});
|
|
252
348
|
}
|
|
253
349
|
try {
|
|
254
|
-
const executionResult = matchedTool.execute(
|
|
350
|
+
const executionResult = matchedTool.execute(parsedArgs, toolCtx);
|
|
255
351
|
// Generator tools stream progress; drive them here so tool-UI `progress`
|
|
256
352
|
// fragments emit per yield (the non-UI case just consumes to the return).
|
|
257
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';
|