@noetic-tools/core 1.3.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builders/step-builders.d.ts +7 -3
- package/dist/builders/workflow-hydrator.d.ts +7 -1
- package/dist/builders/workflow-hydrator.js +220 -10
- package/dist/harness/agent-harness.d.ts +23 -0
- package/dist/harness/agent-harness.js +49 -12
- package/dist/harness/model-call.js +21 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/interpreter/action-types.d.ts +2 -2
- package/dist/interpreter/action-types.js +1 -1
- package/dist/interpreter/collect-tools.js +8 -3
- package/dist/interpreter/execute-action.js +25 -2
- package/dist/schemas/workflow.d.ts +76 -6
- package/dist/schemas/workflow.js +61 -7
- package/package.json +2 -2
- package/schema/noetic-workflow.schema.json +85 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextMemory } from '@noetic-tools/memory';
|
|
2
|
-
import type { Context, Lazy, ModelParams, RetryPolicy, StepLLM, StepRun, StepSubHarness, StepTool, SubHarness, SubHarnessSessionPolicy, SubHarnessSettings, SubprocessAdapter, Tool } from '@noetic-tools/types';
|
|
2
|
+
import type { Context, Lazy, ModelParams, RetryPolicy, ServerToolSpec, StepLLM, StepRun, StepSubHarness, StepTool, SubHarness, SubHarnessSessionPolicy, SubHarnessSettings, SubprocessAdapter, Tool } from '@noetic-tools/types';
|
|
3
3
|
import type { ZodType } from 'zod';
|
|
4
4
|
interface StepRunOpts<TMemory, I, O> {
|
|
5
5
|
id: string;
|
|
@@ -18,8 +18,12 @@ interface StepLLMOpts<TMemory, O> {
|
|
|
18
18
|
model: Lazy<string, TMemory>;
|
|
19
19
|
/** Optional instructions; eager string or `(ctx) => string | undefined` getter. */
|
|
20
20
|
instructions?: Lazy<string | undefined, TMemory>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Optional tools; eager array or `(ctx) => (...)[] | undefined` getter. Each
|
|
23
|
+
* entry is either a client `Tool` or an inline `ServerToolSpec` (an OpenRouter
|
|
24
|
+
* server tool the provider executes, e.g. web search/fetch).
|
|
25
|
+
*/
|
|
26
|
+
tools?: Lazy<(Tool | ServerToolSpec)[] | undefined, TMemory>;
|
|
23
27
|
output?: ZodType<O>;
|
|
24
28
|
params?: ModelParams;
|
|
25
29
|
emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ones — they register in the step registry, carry retry policies, etc.
|
|
7
7
|
*/
|
|
8
8
|
import type { ContextMemory, MemoryLayer } from '@noetic-tools/memory';
|
|
9
|
-
import type { ExecuteStepFn, Step, SubHarness, SubHarnessKind, Tool } from '@noetic-tools/types';
|
|
9
|
+
import type { ExecuteStepFn, Step, SubHarness, SubHarnessKind, SubprocessAdapter, Tool } from '@noetic-tools/types';
|
|
10
10
|
import type { WorkflowDocument, WorkflowNode } from '../schemas/workflow.js';
|
|
11
11
|
/** @public Context required to hydrate a JSON workflow into live steps. */
|
|
12
12
|
export interface HydrationContext {
|
|
@@ -15,6 +15,12 @@ export interface HydrationContext {
|
|
|
15
15
|
layers?: ReadonlyMap<string, MemoryLayer>;
|
|
16
16
|
/** SubHarness adapters keyed by harness id, resolving `claude-code`/`codex`/… nodes. */
|
|
17
17
|
subHarnesses?: ReadonlyMap<SubHarnessKind, SubHarness>;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves a named subprocess adapter ref declared on a `run` node's
|
|
20
|
+
* `subprocess` field. When a node omits the ref, the step falls back to
|
|
21
|
+
* `ctx.subprocess` at execution time.
|
|
22
|
+
*/
|
|
23
|
+
resolveSubprocess?: (ref: string) => SubprocessAdapter | undefined;
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
26
|
* Hydrates a single `WorkflowNode` into a live `Step`.
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* `loop`, etc.) so hydrated steps are indistinguishable from programmatic
|
|
6
6
|
* ones — they register in the step registry, carry retry policies, etc.
|
|
7
7
|
*/
|
|
8
|
-
import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
|
|
8
|
+
import { frameworkCast, isServerToolSpec, NoeticConfigError } from '@noetic-tools/types';
|
|
9
|
+
import { DetachedHandleImpl } from '../runtime/detached-handle.js';
|
|
9
10
|
import { all, any } from '../until/combinators.js';
|
|
10
11
|
import { until } from '../until/predicates.js';
|
|
11
12
|
import { branch, fork } from './control-flow-builders.js';
|
|
@@ -52,12 +53,34 @@ function hydrateLlmNode(node, ctx) {
|
|
|
52
53
|
if (node.kind !== 'llm') {
|
|
53
54
|
return frameworkCast(undefined);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
// Every `tools` entry is an object `{ type, parameters? }`. Partition by the
|
|
57
|
+
// `type` value: a reserved `openrouter:*` server-tool literal is a SERVER
|
|
58
|
+
// tool (flows through unchanged); any other `type` is a CLIENT tool whose
|
|
59
|
+
// `type` is the registered tool NAME (resolved from the registry; a client
|
|
60
|
+
// entry's `parameters` is ignored). Both kinds are combined back into one
|
|
61
|
+
// `tools` array; the interpreter partitions them again at execution (server
|
|
62
|
+
// specs bypass the client-tool machinery and reach the model call's
|
|
63
|
+
// server-tool channel).
|
|
64
|
+
const toolNames = [];
|
|
65
|
+
const serverSpecs = [];
|
|
66
|
+
for (const entry of node.tools ?? []) {
|
|
67
|
+
if (isServerToolSpec(entry)) {
|
|
68
|
+
serverSpecs.push(entry);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
toolNames.push(entry.type);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const resolvedTools = resolveTools(toolNames, ctx.tools);
|
|
75
|
+
const combined = [
|
|
76
|
+
...resolvedTools,
|
|
77
|
+
...serverSpecs,
|
|
78
|
+
];
|
|
56
79
|
return step.llm({
|
|
57
80
|
id: node.id,
|
|
58
81
|
model: node.model ?? 'openai/gpt-4o',
|
|
59
82
|
instructions: node.instructions,
|
|
60
|
-
tools:
|
|
83
|
+
tools: combined.length > 0 ? combined : undefined,
|
|
61
84
|
params: node.params,
|
|
62
85
|
});
|
|
63
86
|
}
|
|
@@ -106,6 +129,31 @@ function hydrateToolNode(node, ctx) {
|
|
|
106
129
|
},
|
|
107
130
|
}));
|
|
108
131
|
}
|
|
132
|
+
function hydrateRunNode(node, ctx) {
|
|
133
|
+
if (node.kind !== 'run') {
|
|
134
|
+
return frameworkCast(undefined);
|
|
135
|
+
}
|
|
136
|
+
const code = node.execute;
|
|
137
|
+
const subprocessRef = node.subprocess;
|
|
138
|
+
return frameworkCast(step.run({
|
|
139
|
+
id: node.id,
|
|
140
|
+
retry: node.retry,
|
|
141
|
+
execute: async (input, execCtx) => {
|
|
142
|
+
const adapter = resolveSubprocessAdapter({
|
|
143
|
+
ref: subprocessRef,
|
|
144
|
+
hydrationCtx: ctx,
|
|
145
|
+
execCtx,
|
|
146
|
+
nodeId: node.id,
|
|
147
|
+
});
|
|
148
|
+
return runCodeViaSubprocess({
|
|
149
|
+
adapter,
|
|
150
|
+
nodeId: node.id,
|
|
151
|
+
code,
|
|
152
|
+
input: stringifyResult(input),
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
109
157
|
function hydrateBranchNode(node, ctx) {
|
|
110
158
|
if (node.kind !== 'branch') {
|
|
111
159
|
return frameworkCast(undefined);
|
|
@@ -137,14 +185,33 @@ function hydrateForkNode(node, ctx) {
|
|
|
137
185
|
if (node.kind !== 'fork') {
|
|
138
186
|
return frameworkCast(undefined);
|
|
139
187
|
}
|
|
140
|
-
|
|
188
|
+
// Static fork: paths are known at hydration time and feed the optimizer.
|
|
189
|
+
// Dynamic fork (`each`): paths are produced per fork-input at runtime, one
|
|
190
|
+
// child per array item, so they cannot be pre-computed or optimized.
|
|
191
|
+
const dynamic = node.each !== undefined;
|
|
192
|
+
const eachTemplate = node.each;
|
|
193
|
+
const staticPaths = dynamic ? [] : (node.paths ?? []).map((p) => hydrateNode(p, ctx));
|
|
194
|
+
const pathsFactory = (input) => {
|
|
195
|
+
if (!dynamic || !eachTemplate) {
|
|
196
|
+
return staticPaths;
|
|
197
|
+
}
|
|
198
|
+
const items = selectArray(input, node.over, node.id);
|
|
199
|
+
return items.map((item, i) => buildPerItemStep({
|
|
200
|
+
forkId: node.id,
|
|
201
|
+
eachTemplate,
|
|
202
|
+
item,
|
|
203
|
+
index: i,
|
|
204
|
+
ctx,
|
|
205
|
+
}));
|
|
206
|
+
};
|
|
207
|
+
const optimizable = dynamic ? undefined : frameworkCast(staticPaths);
|
|
141
208
|
if (node.mode === 'race') {
|
|
142
209
|
return fork({
|
|
143
210
|
id: node.id,
|
|
144
211
|
mode: 'race',
|
|
145
|
-
paths:
|
|
212
|
+
paths: pathsFactory,
|
|
146
213
|
concurrency: node.concurrency,
|
|
147
|
-
_optimizable:
|
|
214
|
+
_optimizable: optimizable,
|
|
148
215
|
});
|
|
149
216
|
}
|
|
150
217
|
const mergeFn = buildMerge(node.merge ?? 'last');
|
|
@@ -152,22 +219,22 @@ function hydrateForkNode(node, ctx) {
|
|
|
152
219
|
return fork({
|
|
153
220
|
id: node.id,
|
|
154
221
|
mode: 'settle',
|
|
155
|
-
paths:
|
|
222
|
+
paths: pathsFactory,
|
|
156
223
|
merge: (results) => {
|
|
157
224
|
const values = results.filter((r) => r.status === 'fulfilled').map((r) => r.value ?? '');
|
|
158
225
|
return mergeFn(values);
|
|
159
226
|
},
|
|
160
227
|
concurrency: node.concurrency,
|
|
161
|
-
_optimizable:
|
|
228
|
+
_optimizable: optimizable,
|
|
162
229
|
});
|
|
163
230
|
}
|
|
164
231
|
return fork({
|
|
165
232
|
id: node.id,
|
|
166
233
|
mode: 'all',
|
|
167
|
-
paths:
|
|
234
|
+
paths: pathsFactory,
|
|
168
235
|
merge: mergeFn,
|
|
169
236
|
concurrency: node.concurrency,
|
|
170
|
-
_optimizable:
|
|
237
|
+
_optimizable: optimizable,
|
|
171
238
|
});
|
|
172
239
|
}
|
|
173
240
|
function hydrateSpawnNode(node, ctx) {
|
|
@@ -281,6 +348,7 @@ function hydrateSubHarnessNode(node, ctx) {
|
|
|
281
348
|
const NODE_HYDRATORS = {
|
|
282
349
|
llm: hydrateLlmNode,
|
|
283
350
|
tool: hydrateToolNode,
|
|
351
|
+
run: hydrateRunNode,
|
|
284
352
|
branch: hydrateBranchNode,
|
|
285
353
|
fork: hydrateForkNode,
|
|
286
354
|
spawn: hydrateSpawnNode,
|
|
@@ -332,6 +400,148 @@ function stringifyResult(value) {
|
|
|
332
400
|
return JSON.stringify(value);
|
|
333
401
|
}
|
|
334
402
|
//#endregion
|
|
403
|
+
//#region Dynamic Fork Helpers
|
|
404
|
+
/**
|
|
405
|
+
* Parses the fork input (a JSON string) and locates the array to fan out over.
|
|
406
|
+
* When `over` is set, reads that property off the parsed object; otherwise the
|
|
407
|
+
* parsed value itself must be an array.
|
|
408
|
+
*/
|
|
409
|
+
function selectArray(input, over, nodeId) {
|
|
410
|
+
let parsed;
|
|
411
|
+
try {
|
|
412
|
+
parsed = JSON.parse(input);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
throw new NoeticConfigError({
|
|
416
|
+
code: 'INVALID_FORK_INPUT',
|
|
417
|
+
message: `Dynamic fork '${nodeId}' could not parse its input as JSON.`,
|
|
418
|
+
hint: 'A dynamic fork (with `each`) expects its input to be a JSON array (or a JSON object when `over` is set).',
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const candidate = over === undefined ? parsed : frameworkCast(parsed)?.[over];
|
|
422
|
+
if (!Array.isArray(candidate)) {
|
|
423
|
+
throw new NoeticConfigError({
|
|
424
|
+
code: 'INVALID_FORK_INPUT',
|
|
425
|
+
message: `Dynamic fork '${nodeId}' did not resolve an array${over ? ` at key '${over}'` : ''}.`,
|
|
426
|
+
hint: over
|
|
427
|
+
? `Ensure the input JSON object has an array at '${over}'.`
|
|
428
|
+
: 'Ensure the input is a JSON array, or set `over` to select an array property.',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return candidate;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Builds one fork path for a single dynamic-fork item. The item is injected as
|
|
435
|
+
* the body's input (forks pass the same fork-input to every path), and the
|
|
436
|
+
* template's node ids are suffixed with `-${i}` so each instantiation has
|
|
437
|
+
* unique ids for tracing and step-registry uniqueness.
|
|
438
|
+
*/
|
|
439
|
+
function buildPerItemStep(opts) {
|
|
440
|
+
const { forkId, eachTemplate, item, index, ctx } = opts;
|
|
441
|
+
const hydratedEach = hydrateNode(suffixNodeIds(eachTemplate, `-${index}`), ctx);
|
|
442
|
+
return frameworkCast(step.run({
|
|
443
|
+
id: `${forkId}-item-${index}`,
|
|
444
|
+
execute: async (_input, execCtx) => {
|
|
445
|
+
const itemInput = JSON.stringify(item);
|
|
446
|
+
return stringifyResult(await ctx.executeStep(hydratedEach, itemInput, execCtx));
|
|
447
|
+
},
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
/** Keys whose values are opaque data bags, never child workflow nodes. */
|
|
451
|
+
const NON_NODE_KEYS = new Set([
|
|
452
|
+
'args',
|
|
453
|
+
'params',
|
|
454
|
+
'parameters',
|
|
455
|
+
]);
|
|
456
|
+
/**
|
|
457
|
+
* Deep-clones a workflow node template, appending `suffix` to the `id` of every
|
|
458
|
+
* nested node so a per-item instantiation has globally-unique step ids.
|
|
459
|
+
*/
|
|
460
|
+
function suffixNodeIds(node, suffix) {
|
|
461
|
+
const clone = structuredClone(node);
|
|
462
|
+
const walk = (value) => {
|
|
463
|
+
if (!value || typeof value !== 'object') {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (Array.isArray(value)) {
|
|
467
|
+
for (const entry of value) {
|
|
468
|
+
walk(entry);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const record = frameworkCast(value);
|
|
473
|
+
if (typeof record.kind === 'string' && typeof record.id === 'string') {
|
|
474
|
+
record.id = `${record.id}${suffix}`;
|
|
475
|
+
}
|
|
476
|
+
for (const [key, child] of Object.entries(record)) {
|
|
477
|
+
if (NON_NODE_KEYS.has(key)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
walk(child);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
walk(clone);
|
|
484
|
+
return clone;
|
|
485
|
+
}
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region Run Node Helpers
|
|
488
|
+
/**
|
|
489
|
+
* Resolves the subprocess adapter for a `run` node. When the node names an
|
|
490
|
+
* adapter ref, the host must supply a `resolveSubprocess` resolver; otherwise
|
|
491
|
+
* the step falls back to the harness adapter on the execution context.
|
|
492
|
+
*/
|
|
493
|
+
function resolveSubprocessAdapter(opts) {
|
|
494
|
+
const { ref, hydrationCtx, execCtx, nodeId } = opts;
|
|
495
|
+
if (ref === undefined) {
|
|
496
|
+
return execCtx.subprocess;
|
|
497
|
+
}
|
|
498
|
+
const resolved = hydrationCtx.resolveSubprocess?.(ref);
|
|
499
|
+
if (!resolved) {
|
|
500
|
+
throw new NoeticConfigError({
|
|
501
|
+
code: 'UNKNOWN_SUBPROCESS_REFERENCE',
|
|
502
|
+
message: `Subprocess adapter '${ref}' referenced in run node '${nodeId}' could not be resolved.`,
|
|
503
|
+
hint: 'Pass a `resolveSubprocess(ref)` mapping in the HydrationContext, or omit `subprocess` to use the harness default (ctx.subprocess).',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return resolved;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Dispatches a `run` node's code string through a subprocess adapter: ships the
|
|
510
|
+
* code plus the JSON-stringified input as a process request (input on stdin),
|
|
511
|
+
* waits for the handle to settle, and returns the captured stdout as the step
|
|
512
|
+
* output. A non-zero exit / failed handle surfaces as a thrown error.
|
|
513
|
+
*
|
|
514
|
+
* The code is never eval'd in-process (Cloudflare Workers forbid eval). The
|
|
515
|
+
* adapter owns running the code and capturing stdout into `handle.metadata.result`.
|
|
516
|
+
*/
|
|
517
|
+
async function runCodeViaSubprocess(opts) {
|
|
518
|
+
const { adapter, nodeId, code, input } = opts;
|
|
519
|
+
const request = {
|
|
520
|
+
kind: 'process',
|
|
521
|
+
command: 'node',
|
|
522
|
+
args: [
|
|
523
|
+
'-e',
|
|
524
|
+
code,
|
|
525
|
+
],
|
|
526
|
+
stdin: input,
|
|
527
|
+
metadata: {
|
|
528
|
+
noeticRun: true,
|
|
529
|
+
stepId: nodeId,
|
|
530
|
+
code,
|
|
531
|
+
input,
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
const spawnPromise = adapter.spawn(request);
|
|
535
|
+
const handle = new DetachedHandleImpl({
|
|
536
|
+
id: `run-${nodeId}`,
|
|
537
|
+
stepId: nodeId,
|
|
538
|
+
adapter,
|
|
539
|
+
spawnPromise,
|
|
540
|
+
});
|
|
541
|
+
const result = await handle.await();
|
|
542
|
+
return stringifyResult(result);
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
335
545
|
//#region Public API
|
|
336
546
|
/**
|
|
337
547
|
* Hydrates a single `WorkflowNode` into a live `Step`.
|
|
@@ -61,6 +61,29 @@ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<stri
|
|
|
61
61
|
/** @internal Test-only escape hatch to inject a mock callModel implementation. */
|
|
62
62
|
_testCallModel?: (request: CallModelRequest) => Promise<LLMResponse>;
|
|
63
63
|
}
|
|
64
|
+
/** Env vars consulted when resolving the LLM client (read defensively so the
|
|
65
|
+
* harness also works in runtimes without a `process` global, e.g. the browser). */
|
|
66
|
+
interface LlmEnv {
|
|
67
|
+
noeticApiKey?: string;
|
|
68
|
+
noeticBaseUrl?: string;
|
|
69
|
+
openrouterApiKey?: string;
|
|
70
|
+
}
|
|
71
|
+
/** @public Effective client options resolved from config + environment. */
|
|
72
|
+
export interface ResolvedLlmClient {
|
|
73
|
+
apiKey: string;
|
|
74
|
+
/** Undefined means "use the SDK's default OpenRouter base URL". */
|
|
75
|
+
serverURL?: string;
|
|
76
|
+
cache: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the effective LLM client options from the provider config + environment.
|
|
80
|
+
* Defaults to the Noetic platform (`provider: 'noetic'`): the `'noetic'` provider
|
|
81
|
+
* authenticates with `NOETIC_API_KEY` and targets the Noetic platform base URL,
|
|
82
|
+
* while `'openrouter'` authenticates with `OPENROUTER_API_KEY` and uses the SDK's
|
|
83
|
+
* default OpenRouter endpoint. Returns undefined when no API key is available.
|
|
84
|
+
* Exported for testing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveLlmClient(config: LlmProviderConfig | undefined, env: LlmEnv): ResolvedLlmClient | undefined;
|
|
64
87
|
/**
|
|
65
88
|
* Default agent harness for executing agent steps with built-in channel, memory, and trace support.
|
|
66
89
|
* Provides channel store, memory layer lifecycle, and trace export with no external dependencies.
|
|
@@ -36,23 +36,62 @@ function resolveContextCwdState(rootCwdState, parent, cwdInit) {
|
|
|
36
36
|
}
|
|
37
37
|
return rootCwdState;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
/** The Noetic platform's OpenAI-Responses-compatible inference base URL. */
|
|
40
|
+
const NOETIC_DEFAULT_BASE_URL = 'https://platform.noetic.tools/v1';
|
|
41
|
+
function readLlmEnv() {
|
|
40
42
|
const processValue = 'process' in globalThis ? globalThis.process : undefined;
|
|
41
|
-
const
|
|
43
|
+
const env = typeof processValue === 'object' && processValue !== null && 'env' in processValue
|
|
42
44
|
? processValue.env
|
|
43
45
|
: undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
46
|
+
if (typeof env !== 'object' || env === null) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const str = (value) => typeof value === 'string' ? value : undefined;
|
|
50
|
+
return {
|
|
51
|
+
noeticApiKey: 'NOETIC_API_KEY' in env ? str(env.NOETIC_API_KEY) : undefined,
|
|
52
|
+
noeticBaseUrl: 'NOETIC_BASE_URL' in env ? str(env.NOETIC_BASE_URL) : undefined,
|
|
53
|
+
openrouterApiKey: 'OPENROUTER_API_KEY' in env ? str(env.OPENROUTER_API_KEY) : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the effective LLM client options from the provider config + environment.
|
|
58
|
+
* Defaults to the Noetic platform (`provider: 'noetic'`): the `'noetic'` provider
|
|
59
|
+
* authenticates with `NOETIC_API_KEY` and targets the Noetic platform base URL,
|
|
60
|
+
* while `'openrouter'` authenticates with `OPENROUTER_API_KEY` and uses the SDK's
|
|
61
|
+
* default OpenRouter endpoint. Returns undefined when no API key is available.
|
|
62
|
+
* Exported for testing.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveLlmClient(config, env) {
|
|
65
|
+
const provider = config?.provider ?? 'noetic';
|
|
66
|
+
const apiKey = config?.apiKey ?? (provider === 'noetic' ? env.noeticApiKey : env.openrouterApiKey);
|
|
48
67
|
if (!apiKey) {
|
|
49
68
|
return undefined;
|
|
50
69
|
}
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
const serverURL = provider === 'noetic'
|
|
71
|
+
? (config?.baseUrl ?? env.noeticBaseUrl ?? NOETIC_DEFAULT_BASE_URL)
|
|
72
|
+
: config?.baseUrl;
|
|
73
|
+
return {
|
|
74
|
+
apiKey,
|
|
75
|
+
serverURL,
|
|
76
|
+
cache: config?.cache ?? false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createClient(config) {
|
|
80
|
+
const resolved = resolveLlmClient(config, readLlmEnv());
|
|
81
|
+
if (!resolved) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const options = {
|
|
85
|
+
apiKey: resolved.apiKey,
|
|
86
|
+
};
|
|
87
|
+
if (resolved.serverURL) {
|
|
88
|
+
options.serverURL = resolved.serverURL;
|
|
89
|
+
}
|
|
90
|
+
if (resolved.cache) {
|
|
91
|
+
// Inject `X-OpenRouter-Cache: true` on every request so the upstream serves
|
|
53
92
|
// identical model calls from cache without re-billing (deterministic re-runs).
|
|
54
93
|
return new OpenRouter({
|
|
55
|
-
|
|
94
|
+
...options,
|
|
56
95
|
hooks: {
|
|
57
96
|
beforeRequest: (_ctx, request) => {
|
|
58
97
|
request.headers.set('X-OpenRouter-Cache', 'true');
|
|
@@ -61,9 +100,7 @@ function createClient(config) {
|
|
|
61
100
|
},
|
|
62
101
|
});
|
|
63
102
|
}
|
|
64
|
-
return new OpenRouter(
|
|
65
|
-
apiKey,
|
|
66
|
-
});
|
|
103
|
+
return new OpenRouter(options);
|
|
67
104
|
}
|
|
68
105
|
//#region AgentHarness
|
|
69
106
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
|
|
2
|
+
import { serverTool } from '@openrouter/agent';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { convertTools, executeToolCall, extractOutputItems, extractSystemInstruction, extractUsage, itemsToInput, sanitizeToolNameForWire, } from '../adapters/openrouter.js';
|
|
4
5
|
import { isFunctionCall } from '../interpreter/typeguards.js';
|
|
@@ -188,15 +189,29 @@ function prepareModelRequest(request, agentName) {
|
|
|
188
189
|
.join('\n\n') || undefined;
|
|
189
190
|
const broadcaster = getBroadcaster(request.ctx);
|
|
190
191
|
const filteredTools = filterAllowedTools(request);
|
|
192
|
+
// Client (function) tools convert to SDK tool definitions; OpenRouter server
|
|
193
|
+
// tools (web_search/web_fetch) are wrapped via the SDK's `serverTool()` so
|
|
194
|
+
// its convertToolsToAPIFormat accepts them — a raw `{type}` entry crashes it.
|
|
195
|
+
// `parameters` keys stay camelCase: the SDK's outbound zod schema strips
|
|
196
|
+
// unknown keys, so snake_case would be silently dropped.
|
|
197
|
+
const clientSdkTools = filteredTools && filteredTools.length > 0
|
|
198
|
+
? convertTools({
|
|
199
|
+
tools: filteredTools,
|
|
200
|
+
})
|
|
201
|
+
: [];
|
|
202
|
+
const serverSdkTools = (request._serverTools ?? []).map((s) => serverTool(frameworkCast({
|
|
203
|
+
type: s.type,
|
|
204
|
+
parameters: s.parameters,
|
|
205
|
+
})));
|
|
206
|
+
const sdkTools = [
|
|
207
|
+
...clientSdkTools,
|
|
208
|
+
...frameworkCast(serverSdkTools),
|
|
209
|
+
];
|
|
191
210
|
return {
|
|
192
211
|
instructions,
|
|
193
212
|
remaining,
|
|
194
213
|
broadcaster,
|
|
195
|
-
sdkTools:
|
|
196
|
-
? convertTools({
|
|
197
|
-
tools: filteredTools,
|
|
198
|
-
})
|
|
199
|
-
: undefined,
|
|
214
|
+
sdkTools: sdkTools.length > 0 ? sdkTools : undefined,
|
|
200
215
|
emitIfAllowed(eventType, data) {
|
|
201
216
|
if (!shouldEmit(request.emit, eventType, data)) {
|
|
202
217
|
return;
|
|
@@ -317,7 +332,7 @@ export class AgentHarnessModelCaller {
|
|
|
317
332
|
throw new NoeticConfigError({
|
|
318
333
|
code: 'NO_LLM_PROVIDER',
|
|
319
334
|
message: 'No LLM provider configured on this harness.',
|
|
320
|
-
hint: 'Pass `llm: { provider: "openrouter", apiKey: "..." }`
|
|
335
|
+
hint: 'Pass `llm: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `llm: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
|
|
321
336
|
});
|
|
322
337
|
}
|
|
323
338
|
async callModel(request) {
|
package/dist/index.d.ts
CHANGED
|
@@ -115,7 +115,9 @@ export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDo
|
|
|
115
115
|
export type { Channel, ChannelHandle, ExternalChannel } from '@noetic-tools/types';
|
|
116
116
|
/** @public */
|
|
117
117
|
/** @public */
|
|
118
|
-
export type { LLMResponse, LlmProviderConfig, ModelParams, RetryPolicy, StepMeta, TokenUsage, Tool, ToolMemoryDeclaration, } from '@noetic-tools/types';
|
|
118
|
+
export type { LLMResponse, LlmProviderConfig, ModelParams, RetryPolicy, ServerToolSpec, StepMeta, TokenUsage, Tool, ToolMemoryDeclaration, } from '@noetic-tools/types';
|
|
119
|
+
/** @public */
|
|
120
|
+
export { isServerToolSpec } from '@noetic-tools/types';
|
|
119
121
|
/** @public */
|
|
120
122
|
/** @public */
|
|
121
123
|
/** @public */
|
package/dist/index.js
CHANGED
|
@@ -98,6 +98,8 @@ export { defaultItemSchemaRegistry, ItemSchema, ItemSchemaRegistry } from '@noet
|
|
|
98
98
|
/** @public */
|
|
99
99
|
export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, } from './schemas/workflow.js';
|
|
100
100
|
/** @public */
|
|
101
|
+
export { isServerToolSpec } from '@noetic-tools/types';
|
|
102
|
+
/** @public */
|
|
101
103
|
export { getToolCwd, setToolCwd, snapshotCwdState } from './runtime/cwd-helpers.js';
|
|
102
104
|
/** @public */
|
|
103
105
|
export { DetachedStatus } from '@noetic-tools/types';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { ContextMemory, ExecutionContext, MemoryConfig, MemoryLayer, } from '@noetic-tools/memory';
|
|
2
|
-
export type { AgentHarnessContract, Context, ContextRerenderRequest, ExecuteStepFn, FunctionCallItem, Item, ProjectionPolicy, RecallLayerOutput, RetryPolicy, StepLLM, StepMeta, StepProvide, StepRun, StepSpawn, StepTool, Tool, } from '@noetic-tools/types';
|
|
3
|
-
export { SteeringAction } from '@noetic-tools/types';
|
|
2
|
+
export type { AgentHarnessContract, Context, ContextRerenderRequest, ExecuteStepFn, FunctionCallItem, Item, ProjectionPolicy, RecallLayerOutput, RetryPolicy, ServerToolSpec, StepLLM, StepMeta, StepProvide, StepRun, StepSpawn, StepTool, Tool, } from '@noetic-tools/types';
|
|
3
|
+
export { isServerToolSpec, SteeringAction } from '@noetic-tools/types';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { SteeringAction } from '@noetic-tools/types';
|
|
1
|
+
export { isServerToolSpec, SteeringAction } from '@noetic-tools/types';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { frameworkCast } from '@noetic-tools/types';
|
|
1
|
+
import { frameworkCast, isServerToolSpec } from '@noetic-tools/types';
|
|
2
2
|
/**
|
|
3
3
|
* Recursively walks a step tree and collects all Tool instances
|
|
4
4
|
* declared on LLM steps. Used to build the unified tool set
|
|
@@ -31,9 +31,14 @@ function walkStep(step, out) {
|
|
|
31
31
|
// Skip function-form tools: they're resolved per execution from `ctx`
|
|
32
32
|
// and cannot contribute to the pre-computed unified set. Consumers that
|
|
33
33
|
// need those tools in the pool should register them via
|
|
34
|
-
// `AgentHarness.tools` instead.
|
|
34
|
+
// `AgentHarness.tools` instead. Inline server-tool specs (web search/
|
|
35
|
+
// fetch) are not client `Tool`s — they never enter the unified pool.
|
|
35
36
|
if (Array.isArray(step.tools)) {
|
|
36
|
-
|
|
37
|
+
for (const entry of step.tools) {
|
|
38
|
+
if (!isServerToolSpec(entry)) {
|
|
39
|
+
out.push(entry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
37
42
|
}
|
|
38
43
|
return;
|
|
39
44
|
case 'run':
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
|
|
5
5
|
import { ZodError } from 'zod';
|
|
6
6
|
import { allocateBudgets, assembleView, buildToolExecutionContext, ContextImpl, commitLayerUsage, computeLayerUsage, contextToExecCtx, DEFAULT_PROJECTION, defaultItemSchemaRegistry, emitFrameworkEvent, getBroadcaster, resolveLayerTools, returnLayers, snapshotCwdState, spawnLayers, } from './action-deps.js';
|
|
7
|
-
import { SteeringAction } from './action-types.js';
|
|
7
|
+
import { isServerToolSpec, SteeringAction } from './action-types.js';
|
|
8
8
|
import { cloneWithGuard } from './clone-guard.js';
|
|
9
9
|
import { collectAllTools, deduplicateTools } from './collect-tools.js';
|
|
10
10
|
import { trackUsage } from './message-helpers.js';
|
|
@@ -180,6 +180,26 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
180
180
|
}
|
|
181
181
|
const resolvedInstructions = await resolveLazy(step.instructions, ctx);
|
|
182
182
|
const resolvedStepTools = await resolveLazy(step.tools, ctx);
|
|
183
|
+
// `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
|
|
184
|
+
// server-tool specs (web search/fetch). Partition them — client tools flow
|
|
185
|
+
// through the unified-tool / allowedToolNames / steering machinery as before;
|
|
186
|
+
// server-tool specs bypass it entirely and are stamped onto the model request
|
|
187
|
+
// (`_serverTools`) for the model caller to wrap via the SDK's `serverTool()`.
|
|
188
|
+
// `undefined` (unrestricted) is preserved as-is; only a present array is
|
|
189
|
+
// partitioned so the `[] = opt-out` semantics of step.tools stay intact.
|
|
190
|
+
let clientStepTools;
|
|
191
|
+
const serverToolSpecs = [];
|
|
192
|
+
if (resolvedStepTools !== undefined) {
|
|
193
|
+
clientStepTools = [];
|
|
194
|
+
for (const entry of resolvedStepTools) {
|
|
195
|
+
if (isServerToolSpec(entry)) {
|
|
196
|
+
serverToolSpecs.push(entry);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
clientStepTools.push(entry);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
183
203
|
// Append user input — through layer pipeline if layers exist, otherwise direct.
|
|
184
204
|
// The append pipeline may request a context re-render (e.g. a layer that
|
|
185
205
|
// expanded an input reference into new content); applied after recall below.
|
|
@@ -196,7 +216,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
196
216
|
baseCtx.itemLog.append(createMessage(input, 'user'));
|
|
197
217
|
}
|
|
198
218
|
}
|
|
199
|
-
const { tools: resolvedTools, allowedToolNames } = resolveToolsAndRestrictions(
|
|
219
|
+
const { tools: resolvedTools, allowedToolNames } = resolveToolsAndRestrictions(clientStepTools, layers, baseCtx);
|
|
200
220
|
// Resolve the projection policy (step override > harness default > fallback)
|
|
201
221
|
// and split the context budget across layers via allocateBudgets.
|
|
202
222
|
const policy = step.projection ?? baseCtx.harness.config.projection ?? DEFAULT_PROJECTION;
|
|
@@ -281,6 +301,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
281
301
|
...projectedHistoryItems,
|
|
282
302
|
];
|
|
283
303
|
}
|
|
304
|
+
const _serverTools = serverToolSpecs.length > 0 ? serverToolSpecs : undefined;
|
|
284
305
|
const request = resolvedTools
|
|
285
306
|
? {
|
|
286
307
|
model: resolvedModel,
|
|
@@ -290,6 +311,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
290
311
|
params: step.params,
|
|
291
312
|
outputSchema: step.output,
|
|
292
313
|
emit: step.emit,
|
|
314
|
+
_serverTools,
|
|
293
315
|
ctx: baseCtx,
|
|
294
316
|
layers,
|
|
295
317
|
allowedToolNames,
|
|
@@ -303,6 +325,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
303
325
|
params: step.params,
|
|
304
326
|
outputSchema: step.output,
|
|
305
327
|
emit: step.emit,
|
|
328
|
+
_serverTools,
|
|
306
329
|
nodeId: step.id,
|
|
307
330
|
parentSpan: baseCtx.span,
|
|
308
331
|
};
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Zod schemas for JSON-serialisable workflow definitions.
|
|
3
3
|
*
|
|
4
4
|
* A `WorkflowDocument` is a portable, JSON-safe representation of a noetic
|
|
5
|
-
* step tree. It covers every step kind
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* step tree. It covers every step kind. The `run` node carries its body as a
|
|
6
|
+
* code STRING (not an in-process closure) dispatched through a subprocess
|
|
7
|
+
* adapter, keeping the document JSON-safe. The hydrator in
|
|
8
|
+
* `builders/workflow-hydrator.ts` converts a validated document back into live
|
|
9
|
+
* `Step` objects via the existing builders.
|
|
8
10
|
*/
|
|
9
11
|
import type { SubHarnessKind } from '@noetic-tools/types';
|
|
10
12
|
import { z } from 'zod';
|
|
@@ -60,6 +62,35 @@ declare const ModelParamsSchema: z.ZodObject<{
|
|
|
60
62
|
maxTokens: z.ZodOptional<z.ZodNumber>;
|
|
61
63
|
stopSequences: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
64
|
}, z.core.$strip>;
|
|
65
|
+
/**
|
|
66
|
+
* A uniform tool entry on an `llm` node. Every entry is an object keyed by
|
|
67
|
+
* `type`:
|
|
68
|
+
* - A CLIENT tool is `{ type: "<registered-tool-name>" }`. The hydrator
|
|
69
|
+
* resolves `type` against the tool registry; `parameters` (if present) is
|
|
70
|
+
* ignored — client tools receive their call args from the model at runtime.
|
|
71
|
+
* - A SERVER tool is `{ type: "openrouter:web_search" | "openrouter:web_fetch",
|
|
72
|
+
* parameters?: {...} }`. The provider executes it; `parameters` keys are
|
|
73
|
+
* camelCase (e.g. `maxResults`, `searchContextSize`) — the SDK re-serialises
|
|
74
|
+
* them and silently drops unknown keys.
|
|
75
|
+
*
|
|
76
|
+
* Server vs client is decided by the `type` value (a reserved `openrouter:*`
|
|
77
|
+
* server-tool literal vs an arbitrary tool name), not by the entry's shape.
|
|
78
|
+
*/
|
|
79
|
+
declare const LlmToolEntrySchema: z.ZodObject<{
|
|
80
|
+
type: z.ZodString;
|
|
81
|
+
parameters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
/** Retry policy for a `run` node, mirroring `RetryPolicy` in `@noetic-tools/types`. */
|
|
84
|
+
declare const RetryPolicySchema: z.ZodObject<{
|
|
85
|
+
maxAttempts: z.ZodNumber;
|
|
86
|
+
backoff: z.ZodEnum<{
|
|
87
|
+
fixed: "fixed";
|
|
88
|
+
linear: "linear";
|
|
89
|
+
exponential: "exponential";
|
|
90
|
+
}>;
|
|
91
|
+
initialDelay: z.ZodNumber;
|
|
92
|
+
maxDelay: z.ZodOptional<z.ZodNumber>;
|
|
93
|
+
}, z.core.$strip>;
|
|
63
94
|
declare const HarnessSettingsSchema: z.ZodObject<{
|
|
64
95
|
model: z.ZodOptional<z.ZodString>;
|
|
65
96
|
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
@@ -87,7 +118,14 @@ export interface LlmWorkflowNode extends WorkflowNodeBase {
|
|
|
87
118
|
kind: 'llm';
|
|
88
119
|
model?: string;
|
|
89
120
|
instructions: string;
|
|
90
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Tool entries. Every entry is an object `{ type, parameters? }`. A CLIENT
|
|
123
|
+
* tool is `{ type: "<registered-tool-name>" }` (resolved from the registry);
|
|
124
|
+
* a SERVER tool is `{ type: "openrouter:web_search" | "openrouter:web_fetch",
|
|
125
|
+
* parameters?: {...} }` (executed by the provider). Client vs server is
|
|
126
|
+
* decided by the `type` value.
|
|
127
|
+
*/
|
|
128
|
+
tools?: z.infer<typeof LlmToolEntrySchema>[];
|
|
91
129
|
params?: z.infer<typeof ModelParamsSchema>;
|
|
92
130
|
}
|
|
93
131
|
export interface ToolWorkflowNode extends WorkflowNodeBase {
|
|
@@ -95,6 +133,23 @@ export interface ToolWorkflowNode extends WorkflowNodeBase {
|
|
|
95
133
|
toolName: string;
|
|
96
134
|
args?: Record<string, unknown>;
|
|
97
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* A node that runs a serialised code body. Unlike the programmatic `step.run`
|
|
138
|
+
* (which carries a closure), the JSON form carries `execute` as a code STRING.
|
|
139
|
+
* The code is never eval'd in-process (Cloudflare Workers forbid eval); it is
|
|
140
|
+
* dispatched through a subprocess adapter (`ctx.subprocess`, or a named adapter
|
|
141
|
+
* resolved by ref). Execution therefore requires a subprocess adapter capable
|
|
142
|
+
* of running the code and returning its stdout.
|
|
143
|
+
*/
|
|
144
|
+
export interface RunWorkflowNode extends WorkflowNodeBase {
|
|
145
|
+
kind: 'run';
|
|
146
|
+
/** Source code executed in the subprocess. Receives the step input on stdin. */
|
|
147
|
+
execute: string;
|
|
148
|
+
/** Optional retry policy applied to the step. */
|
|
149
|
+
retry?: z.infer<typeof RetryPolicySchema>;
|
|
150
|
+
/** Named subprocess adapter ref; defaults to `ctx.subprocess` when omitted. */
|
|
151
|
+
subprocess?: string;
|
|
152
|
+
}
|
|
98
153
|
export interface BranchRoute {
|
|
99
154
|
match: string;
|
|
100
155
|
target: WorkflowNode;
|
|
@@ -107,7 +162,22 @@ export interface BranchWorkflowNode extends WorkflowNodeBase {
|
|
|
107
162
|
export interface ForkWorkflowNode extends WorkflowNodeBase {
|
|
108
163
|
kind: 'fork';
|
|
109
164
|
mode: 'race' | 'all' | 'settle';
|
|
110
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Static fan-out: one child per entry. Mutually exclusive with `each` —
|
|
167
|
+
* supply exactly one.
|
|
168
|
+
*/
|
|
169
|
+
paths?: WorkflowNode[];
|
|
170
|
+
/**
|
|
171
|
+
* Dynamic fan-out: instantiate this body template once per item of a
|
|
172
|
+
* runtime-produced array (data-dependent N). Mutually exclusive with `paths`.
|
|
173
|
+
*/
|
|
174
|
+
each?: WorkflowNode;
|
|
175
|
+
/**
|
|
176
|
+
* Selector key into the fork input (parsed as JSON) locating the array to
|
|
177
|
+
* fan out over. When omitted, the input string itself is parsed as a JSON
|
|
178
|
+
* array. Only meaningful with `each`.
|
|
179
|
+
*/
|
|
180
|
+
over?: string;
|
|
111
181
|
merge?: MergeStrategy;
|
|
112
182
|
concurrency?: number;
|
|
113
183
|
}
|
|
@@ -150,7 +220,7 @@ export interface SubHarnessWorkflowNode extends WorkflowNodeBase {
|
|
|
150
220
|
session?: z.infer<typeof HarnessSessionPolicySchema>;
|
|
151
221
|
}
|
|
152
222
|
/** @public Discriminated union of all JSON-serialisable workflow node kinds. */
|
|
153
|
-
export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode | SubHarnessWorkflowNode;
|
|
223
|
+
export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | RunWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode | SubHarnessWorkflowNode;
|
|
154
224
|
/** @public Zod schema validating a single `WorkflowNode` (any JSON-safe kind). */
|
|
155
225
|
export declare const WorkflowNodeSchema: z.ZodType<WorkflowNode>;
|
|
156
226
|
/** @public Top-level envelope for a JSON workflow definition. */
|
package/dist/schemas/workflow.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Zod schemas for JSON-serialisable workflow definitions.
|
|
3
3
|
*
|
|
4
4
|
* A `WorkflowDocument` is a portable, JSON-safe representation of a noetic
|
|
5
|
-
* step tree. It covers every step kind
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* step tree. It covers every step kind. The `run` node carries its body as a
|
|
6
|
+
* code STRING (not an in-process closure) dispatched through a subprocess
|
|
7
|
+
* adapter, keeping the document JSON-safe. The hydrator in
|
|
8
|
+
* `builders/workflow-hydrator.ts` converts a validated document back into live
|
|
9
|
+
* `Step` objects via the existing builders.
|
|
8
10
|
*/
|
|
9
11
|
import { z } from 'zod';
|
|
10
12
|
//#endregion
|
|
@@ -85,6 +87,39 @@ const ModelParamsSchema = z.object({
|
|
|
85
87
|
stopSequences: z.array(z.string()).optional(),
|
|
86
88
|
});
|
|
87
89
|
//#endregion
|
|
90
|
+
//#region Tool Entries
|
|
91
|
+
/**
|
|
92
|
+
* A uniform tool entry on an `llm` node. Every entry is an object keyed by
|
|
93
|
+
* `type`:
|
|
94
|
+
* - A CLIENT tool is `{ type: "<registered-tool-name>" }`. The hydrator
|
|
95
|
+
* resolves `type` against the tool registry; `parameters` (if present) is
|
|
96
|
+
* ignored — client tools receive their call args from the model at runtime.
|
|
97
|
+
* - A SERVER tool is `{ type: "openrouter:web_search" | "openrouter:web_fetch",
|
|
98
|
+
* parameters?: {...} }`. The provider executes it; `parameters` keys are
|
|
99
|
+
* camelCase (e.g. `maxResults`, `searchContextSize`) — the SDK re-serialises
|
|
100
|
+
* them and silently drops unknown keys.
|
|
101
|
+
*
|
|
102
|
+
* Server vs client is decided by the `type` value (a reserved `openrouter:*`
|
|
103
|
+
* server-tool literal vs an arbitrary tool name), not by the entry's shape.
|
|
104
|
+
*/
|
|
105
|
+
const LlmToolEntrySchema = z.object({
|
|
106
|
+
type: z.string().min(1),
|
|
107
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
108
|
+
});
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region Retry Policy
|
|
111
|
+
/** Retry policy for a `run` node, mirroring `RetryPolicy` in `@noetic-tools/types`. */
|
|
112
|
+
const RetryPolicySchema = z.object({
|
|
113
|
+
maxAttempts: z.number().int().positive(),
|
|
114
|
+
backoff: z.enum([
|
|
115
|
+
'fixed',
|
|
116
|
+
'linear',
|
|
117
|
+
'exponential',
|
|
118
|
+
]),
|
|
119
|
+
initialDelay: z.number().nonnegative(),
|
|
120
|
+
maxDelay: z.number().nonnegative().optional(),
|
|
121
|
+
});
|
|
122
|
+
//#endregion
|
|
88
123
|
//#region SubHarness Settings
|
|
89
124
|
const HarnessSettingsSchema = z.object({
|
|
90
125
|
model: z.string().optional(),
|
|
@@ -121,7 +156,7 @@ const LlmNodeSchema = z.object({
|
|
|
121
156
|
...SHARED_FIELDS,
|
|
122
157
|
model: z.string().optional(),
|
|
123
158
|
instructions: z.string(),
|
|
124
|
-
tools: z.array(
|
|
159
|
+
tools: z.array(LlmToolEntrySchema).optional(),
|
|
125
160
|
params: ModelParamsSchema.optional(),
|
|
126
161
|
});
|
|
127
162
|
const ToolNodeSchema = z.object({
|
|
@@ -130,6 +165,13 @@ const ToolNodeSchema = z.object({
|
|
|
130
165
|
toolName: z.string().min(1),
|
|
131
166
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
132
167
|
});
|
|
168
|
+
const RunNodeSchema = z.object({
|
|
169
|
+
kind: z.literal('run'),
|
|
170
|
+
...SHARED_FIELDS,
|
|
171
|
+
execute: z.string().min(1),
|
|
172
|
+
retry: RetryPolicySchema.optional(),
|
|
173
|
+
subprocess: z.string().min(1).optional(),
|
|
174
|
+
});
|
|
133
175
|
const BranchRouteSchema = z.object({
|
|
134
176
|
match: z.string().min(1),
|
|
135
177
|
target: z.lazy(() => WorkflowNodeSchema),
|
|
@@ -140,7 +182,8 @@ const BranchNodeSchema = z.object({
|
|
|
140
182
|
routes: z.array(BranchRouteSchema).min(1),
|
|
141
183
|
default: WorkflowNodeRef.optional(),
|
|
142
184
|
});
|
|
143
|
-
const ForkNodeSchema = z
|
|
185
|
+
const ForkNodeSchema = z
|
|
186
|
+
.object({
|
|
144
187
|
kind: z.literal('fork'),
|
|
145
188
|
...SHARED_FIELDS,
|
|
146
189
|
mode: z.enum([
|
|
@@ -148,9 +191,14 @@ const ForkNodeSchema = z.object({
|
|
|
148
191
|
'all',
|
|
149
192
|
'settle',
|
|
150
193
|
]),
|
|
151
|
-
paths: z.array(WorkflowNodeRef).min(1),
|
|
194
|
+
paths: z.array(WorkflowNodeRef).min(1).optional(),
|
|
195
|
+
each: WorkflowNodeRef.optional(),
|
|
196
|
+
over: z.string().min(1).optional(),
|
|
152
197
|
merge: MergeStrategySchema.optional(),
|
|
153
198
|
concurrency: z.number().int().positive().optional(),
|
|
199
|
+
})
|
|
200
|
+
.refine((node) => (node.paths === undefined) !== (node.each === undefined), {
|
|
201
|
+
message: "fork node requires exactly one of 'paths' (static) or 'each' (dynamic).",
|
|
154
202
|
});
|
|
155
203
|
const SpawnNodeSchema = z.object({
|
|
156
204
|
kind: z.literal('spawn'),
|
|
@@ -208,6 +256,7 @@ export const WorkflowNodeSchema = z
|
|
|
208
256
|
.discriminatedUnion('kind', [
|
|
209
257
|
LlmNodeSchema,
|
|
210
258
|
ToolNodeSchema,
|
|
259
|
+
RunNodeSchema,
|
|
211
260
|
BranchNodeSchema,
|
|
212
261
|
ForkNodeSchema,
|
|
213
262
|
SpawnNodeSchema,
|
|
@@ -243,7 +292,12 @@ function childNodes(node) {
|
|
|
243
292
|
case 'sequence':
|
|
244
293
|
return node.steps;
|
|
245
294
|
case 'fork':
|
|
246
|
-
|
|
295
|
+
if (node.each) {
|
|
296
|
+
return [
|
|
297
|
+
node.each,
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
return node.paths ?? [];
|
|
247
301
|
case 'spawn':
|
|
248
302
|
case 'provide':
|
|
249
303
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Step primitives, interpreter, runtime, and memory layer contract for Noetic agents.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@noetic-tools/memory": "^1.0.1",
|
|
70
|
-
"@noetic-tools/types": "^1.0
|
|
70
|
+
"@noetic-tools/types": "^1.2.0",
|
|
71
71
|
"@openrouter/agent": "^0.6.0",
|
|
72
72
|
"zod": "^4.0.0"
|
|
73
73
|
},
|
|
@@ -41,7 +41,24 @@
|
|
|
41
41
|
"tools": {
|
|
42
42
|
"type": "array",
|
|
43
43
|
"items": {
|
|
44
|
-
"type": "
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"type": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1
|
|
49
|
+
},
|
|
50
|
+
"parameters": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"propertyNames": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"additionalProperties": {}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"required": [
|
|
59
|
+
"type"
|
|
60
|
+
],
|
|
61
|
+
"additionalProperties": false
|
|
45
62
|
}
|
|
46
63
|
},
|
|
47
64
|
"params": {
|
|
@@ -108,6 +125,65 @@
|
|
|
108
125
|
],
|
|
109
126
|
"additionalProperties": false
|
|
110
127
|
},
|
|
128
|
+
{
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"kind": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"const": "run"
|
|
134
|
+
},
|
|
135
|
+
"id": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"minLength": 1
|
|
138
|
+
},
|
|
139
|
+
"execute": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"minLength": 1
|
|
142
|
+
},
|
|
143
|
+
"retry": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"maxAttempts": {
|
|
147
|
+
"type": "integer",
|
|
148
|
+
"exclusiveMinimum": 0,
|
|
149
|
+
"maximum": 9007199254740991
|
|
150
|
+
},
|
|
151
|
+
"backoff": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"enum": [
|
|
154
|
+
"fixed",
|
|
155
|
+
"linear",
|
|
156
|
+
"exponential"
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
"initialDelay": {
|
|
160
|
+
"type": "number",
|
|
161
|
+
"minimum": 0
|
|
162
|
+
},
|
|
163
|
+
"maxDelay": {
|
|
164
|
+
"type": "number",
|
|
165
|
+
"minimum": 0
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"required": [
|
|
169
|
+
"maxAttempts",
|
|
170
|
+
"backoff",
|
|
171
|
+
"initialDelay"
|
|
172
|
+
],
|
|
173
|
+
"additionalProperties": false
|
|
174
|
+
},
|
|
175
|
+
"subprocess": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"minLength": 1
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"required": [
|
|
181
|
+
"kind",
|
|
182
|
+
"id",
|
|
183
|
+
"execute"
|
|
184
|
+
],
|
|
185
|
+
"additionalProperties": false
|
|
186
|
+
},
|
|
111
187
|
{
|
|
112
188
|
"type": "object",
|
|
113
189
|
"properties": {
|
|
@@ -177,6 +253,13 @@
|
|
|
177
253
|
"$ref": "#/$defs/WorkflowNode"
|
|
178
254
|
}
|
|
179
255
|
},
|
|
256
|
+
"each": {
|
|
257
|
+
"$ref": "#/$defs/WorkflowNode"
|
|
258
|
+
},
|
|
259
|
+
"over": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"minLength": 1
|
|
262
|
+
},
|
|
180
263
|
"merge": {
|
|
181
264
|
"type": "string",
|
|
182
265
|
"enum": [
|
|
@@ -194,8 +277,7 @@
|
|
|
194
277
|
"required": [
|
|
195
278
|
"kind",
|
|
196
279
|
"id",
|
|
197
|
-
"mode"
|
|
198
|
-
"paths"
|
|
280
|
+
"mode"
|
|
199
281
|
],
|
|
200
282
|
"additionalProperties": false
|
|
201
283
|
},
|