@noetic-tools/core 2.0.0 → 2.2.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/adapters/openrouter.d.ts +2 -0
- package/dist/adapters/openrouter.js +54 -15
- package/dist/builders/step-builders.d.ts +9 -4
- package/dist/builders/workflow-hydrator.d.ts +14 -1
- package/dist/builders/workflow-hydrator.js +254 -10
- package/dist/harness/model-call.js +21 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/interpreter/action-deps.d.ts +1 -1
- package/dist/interpreter/action-deps.js +1 -1
- 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 +175 -43
- package/dist/runtime/tool-ui.d.ts +36 -0
- package/dist/runtime/tool-ui.js +57 -0
- package/dist/schemas/workflow.d.ts +91 -6
- package/dist/schemas/workflow.js +66 -7
- package/package.json +2 -2
- package/schema/noetic-workflow.schema.json +103 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Action step handlers: run, llm, spawn, provide, tool.
|
|
3
3
|
*/
|
|
4
|
-
import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
|
|
4
|
+
import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, isOutputCodec, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
|
|
5
5
|
import { ZodError } from 'zod';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { emitToolUi } from '../runtime/tool-ui.js';
|
|
7
|
+
import { allocateBudgets, assembleView, buildToolExecutionContext, ContextImpl, commitLayerUsage, computeLayerUsage, contextToExecCtx, DEFAULT_PROJECTION, defaultItemSchemaRegistry, emitFrameworkEvent, getBroadcaster, resolveLayerTools, returnLayers, shouldEmit, snapshotCwdState, spawnLayers, } from './action-deps.js';
|
|
8
|
+
import { isServerToolSpec, SteeringAction } from './action-types.js';
|
|
8
9
|
import { cloneWithGuard } from './clone-guard.js';
|
|
9
10
|
import { collectAllTools, deduplicateTools } from './collect-tools.js';
|
|
10
11
|
import { trackUsage } from './message-helpers.js';
|
|
@@ -164,6 +165,97 @@ async function runInputPipeline({ ctx, layers, input, }) {
|
|
|
164
165
|
}
|
|
165
166
|
return rerenderRequests;
|
|
166
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Classify a step's `output` into the JSON-schema vs streaming-codec modes.
|
|
170
|
+
*
|
|
171
|
+
* An `OutputCodec` (e.g. OpenUI Lang) carries its own system-prompt fragment
|
|
172
|
+
* and is NOT a JSON schema — its `instructions` fold into the system prompt and
|
|
173
|
+
* it is kept off `outputSchema` (which drives JSON-schema formatting).
|
|
174
|
+
*/
|
|
175
|
+
function resolveOutputMode(output, baseInstructions) {
|
|
176
|
+
// A generic type predicate does not reliably narrow a `ZodType<O> |
|
|
177
|
+
// OutputCodec<O>` union through a free type parameter, so bridge the two
|
|
178
|
+
// branches with the sanctioned framework cast — `isOutputCodec` guarantees
|
|
179
|
+
// the discriminant at runtime.
|
|
180
|
+
const isCodec = isOutputCodec(output);
|
|
181
|
+
const outputCodec = isCodec ? frameworkCast(output) : undefined;
|
|
182
|
+
const outputSchema = isCodec ? undefined : frameworkCast(output);
|
|
183
|
+
const codecInstructions = outputCodec?.instructions;
|
|
184
|
+
const resolvedInstructions = codecInstructions !== undefined
|
|
185
|
+
? [
|
|
186
|
+
baseInstructions,
|
|
187
|
+
codecInstructions,
|
|
188
|
+
]
|
|
189
|
+
.filter((s) => s !== undefined)
|
|
190
|
+
.join('\n\n')
|
|
191
|
+
: baseInstructions;
|
|
192
|
+
return {
|
|
193
|
+
outputCodec,
|
|
194
|
+
outputSchema,
|
|
195
|
+
resolvedInstructions,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Drive an `OutputCodec` with the finished assistant text: emit its `openui.*`
|
|
200
|
+
* framework events (gated by `step.emit`) and return the codec's typed value.
|
|
201
|
+
* True per-delta streaming lives in the model caller; here the statements are
|
|
202
|
+
* delivered at turn finalization, which the transport still forwards.
|
|
203
|
+
*/
|
|
204
|
+
function finalizeCodecOutput(codec, lastText, ctx, emit) {
|
|
205
|
+
const session = codec.start();
|
|
206
|
+
const broadcaster = getBroadcaster(ctx);
|
|
207
|
+
const agentName = ctx.harness.config.name;
|
|
208
|
+
session.push(lastText, (eventType, data) => {
|
|
209
|
+
if (shouldEmit(emit, eventType, data)) {
|
|
210
|
+
emitFrameworkEvent({
|
|
211
|
+
broadcaster,
|
|
212
|
+
agentName,
|
|
213
|
+
eventType,
|
|
214
|
+
data,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return session.finish(lastText);
|
|
219
|
+
}
|
|
220
|
+
/** JSON-parse + Zod-validate the assistant text, raising `llm_parse_error`. */
|
|
221
|
+
function parseSchemaOutput(schema, lastText, stepId) {
|
|
222
|
+
try {
|
|
223
|
+
return schema.parse(JSON.parse(lastText));
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
if (e instanceof SyntaxError || e instanceof ZodError) {
|
|
227
|
+
throw new NoeticErrorImpl({
|
|
228
|
+
kind: 'llm_parse_error',
|
|
229
|
+
stepId,
|
|
230
|
+
raw: lastText,
|
|
231
|
+
schema,
|
|
232
|
+
zodError: e instanceof ZodError
|
|
233
|
+
? e
|
|
234
|
+
: new ZodError([
|
|
235
|
+
{
|
|
236
|
+
code: 'custom',
|
|
237
|
+
message: `Invalid JSON: ${e.message}`,
|
|
238
|
+
path: [],
|
|
239
|
+
},
|
|
240
|
+
]),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
throw e;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Turn the finished assistant text into the step's typed output: a streaming
|
|
248
|
+
* codec, a Zod schema, or raw text passthrough.
|
|
249
|
+
*/
|
|
250
|
+
function finalizeStepOutput(params) {
|
|
251
|
+
if (params.outputCodec) {
|
|
252
|
+
return finalizeCodecOutput(params.outputCodec, params.lastText, params.ctx, params.emit);
|
|
253
|
+
}
|
|
254
|
+
if (params.outputSchema) {
|
|
255
|
+
return parseSchemaOutput(params.outputSchema, params.lastText, params.stepId);
|
|
256
|
+
}
|
|
257
|
+
return frameworkCast(params.lastText);
|
|
258
|
+
}
|
|
167
259
|
export async function executeLLM(step, input, ctx, layers) {
|
|
168
260
|
const baseCtx = frameworkCast(ctx);
|
|
169
261
|
const hasLayers = layers !== undefined && layers.length > 0;
|
|
@@ -178,8 +270,29 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
178
270
|
hint: "Ensure `model` (or your `(ctx) => string` getter) returns a non-empty identifier, e.g. 'anthropic/claude-sonnet-4-20250514'.",
|
|
179
271
|
});
|
|
180
272
|
}
|
|
181
|
-
const
|
|
273
|
+
const baseInstructions = await resolveLazy(step.instructions, ctx);
|
|
274
|
+
const { outputCodec, outputSchema, resolvedInstructions } = resolveOutputMode(step.output, baseInstructions);
|
|
182
275
|
const resolvedStepTools = await resolveLazy(step.tools, ctx);
|
|
276
|
+
// `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
|
|
277
|
+
// server-tool specs (web search/fetch). Partition them — client tools flow
|
|
278
|
+
// through the unified-tool / allowedToolNames / steering machinery as before;
|
|
279
|
+
// server-tool specs bypass it entirely and are stamped onto the model request
|
|
280
|
+
// (`_serverTools`) for the model caller to wrap via the SDK's `serverTool()`.
|
|
281
|
+
// `undefined` (unrestricted) is preserved as-is; only a present array is
|
|
282
|
+
// partitioned so the `[] = opt-out` semantics of step.tools stay intact.
|
|
283
|
+
let clientStepTools;
|
|
284
|
+
const serverToolSpecs = [];
|
|
285
|
+
if (resolvedStepTools !== undefined) {
|
|
286
|
+
clientStepTools = [];
|
|
287
|
+
for (const entry of resolvedStepTools) {
|
|
288
|
+
if (isServerToolSpec(entry)) {
|
|
289
|
+
serverToolSpecs.push(entry);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
clientStepTools.push(entry);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
183
296
|
// Append user input — through layer pipeline if layers exist, otherwise direct.
|
|
184
297
|
// The append pipeline may request a context re-render (e.g. a layer that
|
|
185
298
|
// expanded an input reference into new content); applied after recall below.
|
|
@@ -196,7 +309,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
196
309
|
baseCtx.itemLog.append(createMessage(input, 'user'));
|
|
197
310
|
}
|
|
198
311
|
}
|
|
199
|
-
const { tools: resolvedTools, allowedToolNames } = resolveToolsAndRestrictions(
|
|
312
|
+
const { tools: resolvedTools, allowedToolNames } = resolveToolsAndRestrictions(clientStepTools, layers, baseCtx);
|
|
200
313
|
// Resolve the projection policy (step override > harness default > fallback)
|
|
201
314
|
// and split the context budget across layers via allocateBudgets.
|
|
202
315
|
const policy = step.projection ?? baseCtx.harness.config.projection ?? DEFAULT_PROJECTION;
|
|
@@ -281,6 +394,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
281
394
|
...projectedHistoryItems,
|
|
282
395
|
];
|
|
283
396
|
}
|
|
397
|
+
const _serverTools = serverToolSpecs.length > 0 ? serverToolSpecs : undefined;
|
|
284
398
|
const request = resolvedTools
|
|
285
399
|
? {
|
|
286
400
|
model: resolvedModel,
|
|
@@ -288,8 +402,9 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
288
402
|
instructions: resolvedInstructions,
|
|
289
403
|
tools: resolvedTools,
|
|
290
404
|
params: step.params,
|
|
291
|
-
outputSchema:
|
|
405
|
+
outputSchema: outputSchema,
|
|
292
406
|
emit: step.emit,
|
|
407
|
+
_serverTools,
|
|
293
408
|
ctx: baseCtx,
|
|
294
409
|
layers,
|
|
295
410
|
allowedToolNames,
|
|
@@ -301,8 +416,9 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
301
416
|
items: assembledItems,
|
|
302
417
|
instructions: resolvedInstructions,
|
|
303
418
|
params: step.params,
|
|
304
|
-
outputSchema:
|
|
419
|
+
outputSchema: outputSchema,
|
|
305
420
|
emit: step.emit,
|
|
421
|
+
_serverTools,
|
|
306
422
|
nodeId: step.id,
|
|
307
423
|
parentSpan: baseCtx.span,
|
|
308
424
|
};
|
|
@@ -353,33 +469,14 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
353
469
|
await baseCtx.harness.storeLayers(layers, response, baseCtx);
|
|
354
470
|
}
|
|
355
471
|
const lastText = extractAssistantText(response.items);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
kind: 'llm_parse_error',
|
|
365
|
-
stepId: step.id,
|
|
366
|
-
raw: lastText,
|
|
367
|
-
schema: step.output,
|
|
368
|
-
zodError: e instanceof ZodError
|
|
369
|
-
? e
|
|
370
|
-
: new ZodError([
|
|
371
|
-
{
|
|
372
|
-
code: 'custom',
|
|
373
|
-
message: `Invalid JSON: ${e.message}`,
|
|
374
|
-
path: [],
|
|
375
|
-
},
|
|
376
|
-
]),
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
throw e;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return frameworkCast(lastText);
|
|
472
|
+
return finalizeStepOutput({
|
|
473
|
+
stepId: step.id,
|
|
474
|
+
emit: step.emit,
|
|
475
|
+
outputCodec,
|
|
476
|
+
outputSchema,
|
|
477
|
+
lastText,
|
|
478
|
+
ctx: baseCtx,
|
|
479
|
+
});
|
|
383
480
|
}
|
|
384
481
|
// Safety net: the loop above always returns or throws within the body.
|
|
385
482
|
// This throw is unreachable but protects against future refactors that
|
|
@@ -587,21 +684,32 @@ function isAsyncGenerator(value) {
|
|
|
587
684
|
async function consumeToolGenerator(params) {
|
|
588
685
|
const broadcaster = getBroadcaster(params.ctx);
|
|
589
686
|
const agentName = params.ctx.harness.config.name;
|
|
687
|
+
const events = [];
|
|
590
688
|
while (true) {
|
|
591
689
|
const next = await params.generator.next();
|
|
592
690
|
if (next.done) {
|
|
593
691
|
return next.value;
|
|
594
692
|
}
|
|
693
|
+
events.push(next.value);
|
|
595
694
|
emitFrameworkEvent({
|
|
596
695
|
broadcaster,
|
|
597
696
|
agentName,
|
|
598
697
|
eventType: 'tool_progress',
|
|
599
698
|
data: {
|
|
600
699
|
stepId: params.stepId,
|
|
601
|
-
toolName: params.
|
|
700
|
+
toolName: params.tool.name,
|
|
602
701
|
event: next.value,
|
|
603
702
|
},
|
|
604
703
|
});
|
|
704
|
+
// A direct `step.tool` has no model call id — the step id keys its region.
|
|
705
|
+
emitToolUi({
|
|
706
|
+
ctx: params.ctx,
|
|
707
|
+
tool: params.tool,
|
|
708
|
+
callId: params.stepId,
|
|
709
|
+
phase: 'progress',
|
|
710
|
+
args: params.args,
|
|
711
|
+
events,
|
|
712
|
+
});
|
|
605
713
|
}
|
|
606
714
|
}
|
|
607
715
|
export async function executeTool(step, input, ctx, harness, layers) {
|
|
@@ -625,20 +733,44 @@ export async function executeTool(step, input, ctx, harness, layers) {
|
|
|
625
733
|
});
|
|
626
734
|
}
|
|
627
735
|
}
|
|
736
|
+
emitToolUi({
|
|
737
|
+
ctx: baseCtx,
|
|
738
|
+
tool: step.tool,
|
|
739
|
+
callId: step.id,
|
|
740
|
+
phase: 'call',
|
|
741
|
+
args: parseResult.data,
|
|
742
|
+
});
|
|
628
743
|
try {
|
|
629
744
|
const toolCtx = buildToolExecutionContext(baseCtx, harness);
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
generator:
|
|
745
|
+
const execResult = step.tool.execute(parseResult.data, toolCtx);
|
|
746
|
+
const result = isAsyncGenerator(execResult)
|
|
747
|
+
? await consumeToolGenerator({
|
|
748
|
+
generator: execResult,
|
|
634
749
|
stepId: step.id,
|
|
635
|
-
|
|
750
|
+
tool: step.tool,
|
|
636
751
|
ctx: baseCtx,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
752
|
+
args: parseResult.data,
|
|
753
|
+
})
|
|
754
|
+
: await execResult;
|
|
755
|
+
emitToolUi({
|
|
756
|
+
ctx: baseCtx,
|
|
757
|
+
tool: step.tool,
|
|
758
|
+
callId: step.id,
|
|
759
|
+
phase: 'result',
|
|
760
|
+
args: parseResult.data,
|
|
761
|
+
output: result,
|
|
762
|
+
});
|
|
763
|
+
return frameworkCast(result);
|
|
640
764
|
}
|
|
641
765
|
catch (e) {
|
|
766
|
+
emitToolUi({
|
|
767
|
+
ctx: baseCtx,
|
|
768
|
+
tool: step.tool,
|
|
769
|
+
callId: step.id,
|
|
770
|
+
phase: 'error',
|
|
771
|
+
args: parseResult.data,
|
|
772
|
+
error: e,
|
|
773
|
+
});
|
|
642
774
|
if (e instanceof NoeticErrorImpl) {
|
|
643
775
|
throw e;
|
|
644
776
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-authored UI fragment emission.
|
|
3
|
+
*
|
|
4
|
+
* When a tool declares a `ui` block (`ToolUiDeclaration`), the runtime runs its
|
|
5
|
+
* render functions at the call / progress / result / error lifecycle points and
|
|
6
|
+
* forwards each returned `UiFragment` as an `openui.fragment` framework event
|
|
7
|
+
* (namespaced `${agentName}:openui.fragment`). The framework never interprets
|
|
8
|
+
* the fragment source — a UI surface (memory layer + transport) composes it.
|
|
9
|
+
*
|
|
10
|
+
* The `ui` methods are declared bivariantly on `ToolUiDeclaration`, so the
|
|
11
|
+
* erased `Tool` handle the runtime holds can call them with runtime values.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context, Tool } from '@noetic-tools/types';
|
|
14
|
+
/** The tool-UI lifecycle point being rendered. */
|
|
15
|
+
export type ToolUiPhase = 'call' | 'progress' | 'result' | 'error';
|
|
16
|
+
/** Parameters for {@link emitToolUi}. */
|
|
17
|
+
export interface EmitToolUiParams {
|
|
18
|
+
ctx: Context;
|
|
19
|
+
tool: Tool;
|
|
20
|
+
callId: string;
|
|
21
|
+
phase: ToolUiPhase;
|
|
22
|
+
/** Parsed tool args (partial at `call` time). */
|
|
23
|
+
args: unknown;
|
|
24
|
+
/** Events seen so far — for `progress`. */
|
|
25
|
+
events?: unknown[];
|
|
26
|
+
/** The tool's return value — for `result`. */
|
|
27
|
+
output?: unknown;
|
|
28
|
+
/** The thrown error — for `error`. */
|
|
29
|
+
error?: unknown;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render the tool's UI fragment for one lifecycle phase and, when it produces
|
|
33
|
+
* one, emit it as an `openui.fragment` framework event. No-op when the tool
|
|
34
|
+
* declares no `ui` for that phase.
|
|
35
|
+
*/
|
|
36
|
+
export declare function emitToolUi(params: EmitToolUiParams): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-authored UI fragment emission.
|
|
3
|
+
*
|
|
4
|
+
* When a tool declares a `ui` block (`ToolUiDeclaration`), the runtime runs its
|
|
5
|
+
* render functions at the call / progress / result / error lifecycle points and
|
|
6
|
+
* forwards each returned `UiFragment` as an `openui.fragment` framework event
|
|
7
|
+
* (namespaced `${agentName}:openui.fragment`). The framework never interprets
|
|
8
|
+
* the fragment source — a UI surface (memory layer + transport) composes it.
|
|
9
|
+
*
|
|
10
|
+
* The `ui` methods are declared bivariantly on `ToolUiDeclaration`, so the
|
|
11
|
+
* erased `Tool` handle the runtime holds can call them with runtime values.
|
|
12
|
+
*/
|
|
13
|
+
import { frameworkCast } from '@noetic-tools/types';
|
|
14
|
+
import { emitFrameworkEvent, getBroadcaster } from './broadcaster-utils.js';
|
|
15
|
+
const OPENUI_FRAGMENT_EVENT = 'openui.fragment';
|
|
16
|
+
function renderFragment(params) {
|
|
17
|
+
const ui = params.tool.ui;
|
|
18
|
+
if (!ui) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
// The `ui` methods are typed against the tool's own I/O schemas, erased to
|
|
22
|
+
// `ZodTypeAny` on the `Tool` handle the runtime holds. frameworkCast bridges
|
|
23
|
+
// the runtime values (`unknown`) to those erased parameter types.
|
|
24
|
+
const args = frameworkCast(params.args);
|
|
25
|
+
const output = frameworkCast(params.output);
|
|
26
|
+
switch (params.phase) {
|
|
27
|
+
case 'call':
|
|
28
|
+
return ui.call ? ui.call(args) : null;
|
|
29
|
+
case 'progress':
|
|
30
|
+
return ui.progress ? ui.progress(params.events ?? []) : null;
|
|
31
|
+
case 'result':
|
|
32
|
+
return ui.result ? ui.result(output, args) : null;
|
|
33
|
+
case 'error':
|
|
34
|
+
return ui.error ? ui.error(params.error, args) : null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render the tool's UI fragment for one lifecycle phase and, when it produces
|
|
39
|
+
* one, emit it as an `openui.fragment` framework event. No-op when the tool
|
|
40
|
+
* declares no `ui` for that phase.
|
|
41
|
+
*/
|
|
42
|
+
export function emitToolUi(params) {
|
|
43
|
+
const fragment = renderFragment(params);
|
|
44
|
+
if (fragment === null) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
emitFrameworkEvent({
|
|
48
|
+
broadcaster: getBroadcaster(params.ctx),
|
|
49
|
+
agentName: params.ctx.harness.config.name,
|
|
50
|
+
eventType: OPENUI_FRAGMENT_EVENT,
|
|
51
|
+
data: {
|
|
52
|
+
callId: params.callId,
|
|
53
|
+
dialect: fragment.dialect,
|
|
54
|
+
source: fragment.source,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -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<{
|
|
@@ -83,18 +114,57 @@ declare const HarnessSessionPolicySchema: z.ZodObject<{
|
|
|
83
114
|
interface WorkflowNodeBase {
|
|
84
115
|
id: string;
|
|
85
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A reference to a streaming `OutputCodec` for an `llm` node's structured
|
|
119
|
+
* output. The hydrator resolves `library` from `HydrationContext.uiLibraries`
|
|
120
|
+
* to a live codec (e.g. `openUi(myLibrary)` from `@noetic-tools/openui`), the
|
|
121
|
+
* same registry-resolution pattern sub-harness nodes use for adapters. Kept a
|
|
122
|
+
* *reference* because a codec is a runtime object, not JSON-expressible.
|
|
123
|
+
*/
|
|
124
|
+
export interface OutputCodecRef {
|
|
125
|
+
/** The codec dialect. Only `'openui'` is defined today. */
|
|
126
|
+
codec: 'openui';
|
|
127
|
+
/** Registry key resolved from `HydrationContext.uiLibraries`. */
|
|
128
|
+
library: string;
|
|
129
|
+
}
|
|
86
130
|
export interface LlmWorkflowNode extends WorkflowNodeBase {
|
|
87
131
|
kind: 'llm';
|
|
88
132
|
model?: string;
|
|
89
133
|
instructions: string;
|
|
90
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Tool entries. Every entry is an object `{ type, parameters? }`. A CLIENT
|
|
136
|
+
* tool is `{ type: "<registered-tool-name>" }` (resolved from the registry);
|
|
137
|
+
* a SERVER tool is `{ type: "openrouter:web_search" | "openrouter:web_fetch",
|
|
138
|
+
* parameters?: {...} }` (executed by the provider). Client vs server is
|
|
139
|
+
* decided by the `type` value.
|
|
140
|
+
*/
|
|
141
|
+
tools?: z.infer<typeof LlmToolEntrySchema>[];
|
|
91
142
|
params?: z.infer<typeof ModelParamsSchema>;
|
|
143
|
+
/** Streaming output-codec reference, resolved from `HydrationContext.uiLibraries`. */
|
|
144
|
+
output?: OutputCodecRef;
|
|
92
145
|
}
|
|
93
146
|
export interface ToolWorkflowNode extends WorkflowNodeBase {
|
|
94
147
|
kind: 'tool';
|
|
95
148
|
toolName: string;
|
|
96
149
|
args?: Record<string, unknown>;
|
|
97
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* A node that runs a serialised code body. Unlike the programmatic `step.run`
|
|
153
|
+
* (which carries a closure), the JSON form carries `execute` as a code STRING.
|
|
154
|
+
* The code is never eval'd in-process (Cloudflare Workers forbid eval); it is
|
|
155
|
+
* dispatched through a subprocess adapter (`ctx.subprocess`, or a named adapter
|
|
156
|
+
* resolved by ref). Execution therefore requires a subprocess adapter capable
|
|
157
|
+
* of running the code and returning its stdout.
|
|
158
|
+
*/
|
|
159
|
+
export interface RunWorkflowNode extends WorkflowNodeBase {
|
|
160
|
+
kind: 'run';
|
|
161
|
+
/** Source code executed in the subprocess. Receives the step input on stdin. */
|
|
162
|
+
execute: string;
|
|
163
|
+
/** Optional retry policy applied to the step. */
|
|
164
|
+
retry?: z.infer<typeof RetryPolicySchema>;
|
|
165
|
+
/** Named subprocess adapter ref; defaults to `ctx.subprocess` when omitted. */
|
|
166
|
+
subprocess?: string;
|
|
167
|
+
}
|
|
98
168
|
export interface BranchRoute {
|
|
99
169
|
match: string;
|
|
100
170
|
target: WorkflowNode;
|
|
@@ -107,7 +177,22 @@ export interface BranchWorkflowNode extends WorkflowNodeBase {
|
|
|
107
177
|
export interface ForkWorkflowNode extends WorkflowNodeBase {
|
|
108
178
|
kind: 'fork';
|
|
109
179
|
mode: 'race' | 'all' | 'settle';
|
|
110
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Static fan-out: one child per entry. Mutually exclusive with `each` —
|
|
182
|
+
* supply exactly one.
|
|
183
|
+
*/
|
|
184
|
+
paths?: WorkflowNode[];
|
|
185
|
+
/**
|
|
186
|
+
* Dynamic fan-out: instantiate this body template once per item of a
|
|
187
|
+
* runtime-produced array (data-dependent N). Mutually exclusive with `paths`.
|
|
188
|
+
*/
|
|
189
|
+
each?: WorkflowNode;
|
|
190
|
+
/**
|
|
191
|
+
* Selector key into the fork input (parsed as JSON) locating the array to
|
|
192
|
+
* fan out over. When omitted, the input string itself is parsed as a JSON
|
|
193
|
+
* array. Only meaningful with `each`.
|
|
194
|
+
*/
|
|
195
|
+
over?: string;
|
|
111
196
|
merge?: MergeStrategy;
|
|
112
197
|
concurrency?: number;
|
|
113
198
|
}
|
|
@@ -150,7 +235,7 @@ export interface SubHarnessWorkflowNode extends WorkflowNodeBase {
|
|
|
150
235
|
session?: z.infer<typeof HarnessSessionPolicySchema>;
|
|
151
236
|
}
|
|
152
237
|
/** @public Discriminated union of all JSON-serialisable workflow node kinds. */
|
|
153
|
-
export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode | SubHarnessWorkflowNode;
|
|
238
|
+
export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | RunWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode | SubHarnessWorkflowNode;
|
|
154
239
|
/** @public Zod schema validating a single `WorkflowNode` (any JSON-safe kind). */
|
|
155
240
|
export declare const WorkflowNodeSchema: z.ZodType<WorkflowNode>;
|
|
156
241
|
/** @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(),
|
|
@@ -116,13 +151,18 @@ const WorkflowNodeRef = z.lazy(() => WorkflowNodeSchema);
|
|
|
116
151
|
const SHARED_FIELDS = {
|
|
117
152
|
id: z.string().min(1),
|
|
118
153
|
};
|
|
154
|
+
const OutputCodecRefSchema = z.object({
|
|
155
|
+
codec: z.literal('openui'),
|
|
156
|
+
library: z.string().min(1),
|
|
157
|
+
});
|
|
119
158
|
const LlmNodeSchema = z.object({
|
|
120
159
|
kind: z.literal('llm'),
|
|
121
160
|
...SHARED_FIELDS,
|
|
122
161
|
model: z.string().optional(),
|
|
123
162
|
instructions: z.string(),
|
|
124
|
-
tools: z.array(
|
|
163
|
+
tools: z.array(LlmToolEntrySchema).optional(),
|
|
125
164
|
params: ModelParamsSchema.optional(),
|
|
165
|
+
output: OutputCodecRefSchema.optional(),
|
|
126
166
|
});
|
|
127
167
|
const ToolNodeSchema = z.object({
|
|
128
168
|
kind: z.literal('tool'),
|
|
@@ -130,6 +170,13 @@ const ToolNodeSchema = z.object({
|
|
|
130
170
|
toolName: z.string().min(1),
|
|
131
171
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
132
172
|
});
|
|
173
|
+
const RunNodeSchema = z.object({
|
|
174
|
+
kind: z.literal('run'),
|
|
175
|
+
...SHARED_FIELDS,
|
|
176
|
+
execute: z.string().min(1),
|
|
177
|
+
retry: RetryPolicySchema.optional(),
|
|
178
|
+
subprocess: z.string().min(1).optional(),
|
|
179
|
+
});
|
|
133
180
|
const BranchRouteSchema = z.object({
|
|
134
181
|
match: z.string().min(1),
|
|
135
182
|
target: z.lazy(() => WorkflowNodeSchema),
|
|
@@ -140,7 +187,8 @@ const BranchNodeSchema = z.object({
|
|
|
140
187
|
routes: z.array(BranchRouteSchema).min(1),
|
|
141
188
|
default: WorkflowNodeRef.optional(),
|
|
142
189
|
});
|
|
143
|
-
const ForkNodeSchema = z
|
|
190
|
+
const ForkNodeSchema = z
|
|
191
|
+
.object({
|
|
144
192
|
kind: z.literal('fork'),
|
|
145
193
|
...SHARED_FIELDS,
|
|
146
194
|
mode: z.enum([
|
|
@@ -148,9 +196,14 @@ const ForkNodeSchema = z.object({
|
|
|
148
196
|
'all',
|
|
149
197
|
'settle',
|
|
150
198
|
]),
|
|
151
|
-
paths: z.array(WorkflowNodeRef).min(1),
|
|
199
|
+
paths: z.array(WorkflowNodeRef).min(1).optional(),
|
|
200
|
+
each: WorkflowNodeRef.optional(),
|
|
201
|
+
over: z.string().min(1).optional(),
|
|
152
202
|
merge: MergeStrategySchema.optional(),
|
|
153
203
|
concurrency: z.number().int().positive().optional(),
|
|
204
|
+
})
|
|
205
|
+
.refine((node) => (node.paths === undefined) !== (node.each === undefined), {
|
|
206
|
+
message: "fork node requires exactly one of 'paths' (static) or 'each' (dynamic).",
|
|
154
207
|
});
|
|
155
208
|
const SpawnNodeSchema = z.object({
|
|
156
209
|
kind: z.literal('spawn'),
|
|
@@ -208,6 +261,7 @@ export const WorkflowNodeSchema = z
|
|
|
208
261
|
.discriminatedUnion('kind', [
|
|
209
262
|
LlmNodeSchema,
|
|
210
263
|
ToolNodeSchema,
|
|
264
|
+
RunNodeSchema,
|
|
211
265
|
BranchNodeSchema,
|
|
212
266
|
ForkNodeSchema,
|
|
213
267
|
SpawnNodeSchema,
|
|
@@ -243,7 +297,12 @@ function childNodes(node) {
|
|
|
243
297
|
case 'sequence':
|
|
244
298
|
return node.steps;
|
|
245
299
|
case 'fork':
|
|
246
|
-
|
|
300
|
+
if (node.each) {
|
|
301
|
+
return [
|
|
302
|
+
node.each,
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
return node.paths ?? [];
|
|
247
306
|
case 'spawn':
|
|
248
307
|
case 'provide':
|
|
249
308
|
return [
|