@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
|
@@ -43,6 +43,8 @@ export interface ExecuteToolCallParams {
|
|
|
43
43
|
context: Context;
|
|
44
44
|
harness: AgentHarnessContract;
|
|
45
45
|
layers?: MemoryLayer[];
|
|
46
|
+
/** The model's `function_call` id — keys this call's tool-UI region. */
|
|
47
|
+
callId?: string;
|
|
46
48
|
}
|
|
47
49
|
/** @internal Execute a single tool call with steering checks. */
|
|
48
50
|
export declare function executeToolCall(params: ExecuteToolCallParams): Promise<{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { frameworkCast, isAssistantMessage, NoeticConfigError, SteeringAction, } from '@noetic-tools/types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { buildToolExecutionContext } from '../runtime/tool-memory.js';
|
|
4
|
+
import { emitToolUi } from '../runtime/tool-ui.js';
|
|
4
5
|
const EmbeddingsResponseSchema = z.object({
|
|
5
6
|
data: z.array(z.object({
|
|
6
7
|
embedding: z.array(z.number()),
|
|
@@ -204,17 +205,6 @@ function isAsyncGenerator(value) {
|
|
|
204
205
|
}
|
|
205
206
|
return Symbol.asyncIterator in value;
|
|
206
207
|
}
|
|
207
|
-
async function consumeGenerator(generator) {
|
|
208
|
-
let result;
|
|
209
|
-
while (true) {
|
|
210
|
-
const next = await generator.next();
|
|
211
|
-
if (next.done) {
|
|
212
|
-
result = next.value;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return result;
|
|
217
|
-
}
|
|
218
208
|
/** @internal Execute a single tool call with steering checks. */
|
|
219
209
|
export async function executeToolCall(params) {
|
|
220
210
|
// Model sees sanitised tool names (see `sanitizeToolNameForWire`). Match
|
|
@@ -245,18 +235,67 @@ export async function executeToolCall(params) {
|
|
|
245
235
|
}
|
|
246
236
|
}
|
|
247
237
|
const toolCtx = buildToolExecutionContext(params.context, params.harness);
|
|
238
|
+
const callId = params.callId;
|
|
239
|
+
const uiBase = callId !== undefined
|
|
240
|
+
? {
|
|
241
|
+
ctx: params.context,
|
|
242
|
+
tool: matchedTool,
|
|
243
|
+
callId,
|
|
244
|
+
args: params.args,
|
|
245
|
+
}
|
|
246
|
+
: undefined;
|
|
247
|
+
if (uiBase) {
|
|
248
|
+
emitToolUi({
|
|
249
|
+
...uiBase,
|
|
250
|
+
phase: 'call',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
248
253
|
try {
|
|
249
254
|
const executionResult = matchedTool.execute(params.args, toolCtx);
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
// Generator tools stream progress; drive them here so tool-UI `progress`
|
|
256
|
+
// fragments emit per yield (the non-UI case just consumes to the return).
|
|
257
|
+
let result;
|
|
258
|
+
if (isAsyncGenerator(executionResult)) {
|
|
259
|
+
const events = [];
|
|
260
|
+
for (;;) {
|
|
261
|
+
const next = await executionResult.next();
|
|
262
|
+
if (next.done) {
|
|
263
|
+
result = next.value;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
events.push(next.value);
|
|
267
|
+
if (uiBase) {
|
|
268
|
+
emitToolUi({
|
|
269
|
+
...uiBase,
|
|
270
|
+
phase: 'progress',
|
|
271
|
+
events,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
result = await executionResult;
|
|
278
|
+
}
|
|
279
|
+
if (uiBase) {
|
|
280
|
+
emitToolUi({
|
|
281
|
+
...uiBase,
|
|
282
|
+
phase: 'result',
|
|
283
|
+
output: result,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
254
286
|
return {
|
|
255
287
|
output: typeof result === 'string' ? result : JSON.stringify(result),
|
|
256
288
|
result,
|
|
257
289
|
};
|
|
258
290
|
}
|
|
259
291
|
catch (e) {
|
|
292
|
+
if (uiBase) {
|
|
293
|
+
emitToolUi({
|
|
294
|
+
...uiBase,
|
|
295
|
+
phase: 'error',
|
|
296
|
+
error: e,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
260
299
|
return {
|
|
261
300
|
output: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
262
301
|
error: true,
|
|
@@ -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, OutputCodec, 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,9 +18,14 @@ 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
|
-
|
|
23
|
-
|
|
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>;
|
|
27
|
+
/** Structured output: a Zod schema or a streaming `OutputCodec` (e.g. OpenUI Lang). */
|
|
28
|
+
output?: ZodType<O> | OutputCodec<O>;
|
|
24
29
|
params?: ModelParams;
|
|
25
30
|
emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
|
|
26
31
|
}
|
|
@@ -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, OutputCodec, 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,19 @@ 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
|
+
* Output codecs keyed by the `library` ref an `llm` node's `output` codec
|
|
20
|
+
* reference names — the live codec built from a component library, e.g.
|
|
21
|
+
* `new Map([['dashboard-lib', openUi(dashboardLibrary)]])` from
|
|
22
|
+
* `@noetic-tools/openui`. Resolved the same way sub-harness adapters are.
|
|
23
|
+
*/
|
|
24
|
+
uiLibraries?: ReadonlyMap<string, OutputCodec>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves a named subprocess adapter ref declared on a `run` node's
|
|
27
|
+
* `subprocess` field. When a node omits the ref, the step falls back to
|
|
28
|
+
* `ctx.subprocess` at execution time.
|
|
29
|
+
*/
|
|
30
|
+
resolveSubprocess?: (ref: string) => SubprocessAdapter | undefined;
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
20
33
|
* 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';
|
|
@@ -48,17 +49,73 @@ function hydrateUntilPredicate(pred) {
|
|
|
48
49
|
}
|
|
49
50
|
//#endregion
|
|
50
51
|
//#region Node Hydrators
|
|
52
|
+
/**
|
|
53
|
+
* Resolve an llm node's `tools` entries into a combined client+server list.
|
|
54
|
+
*
|
|
55
|
+
* Every entry is `{ type, parameters? }`. A reserved `openrouter:*` literal is
|
|
56
|
+
* a SERVER tool (flows through unchanged); any other `type` is a CLIENT tool
|
|
57
|
+
* whose `type` is the registered tool NAME (resolved from the registry). Both
|
|
58
|
+
* kinds combine into one `tools` array; the interpreter partitions them again
|
|
59
|
+
* at execution.
|
|
60
|
+
*/
|
|
61
|
+
function resolveLlmTools(entries, registry) {
|
|
62
|
+
const toolNames = [];
|
|
63
|
+
const serverSpecs = [];
|
|
64
|
+
for (const entry of entries ?? []) {
|
|
65
|
+
if (isServerToolSpec(entry)) {
|
|
66
|
+
serverSpecs.push(entry);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
toolNames.push(entry.type);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
...resolveTools(toolNames, registry),
|
|
74
|
+
...serverSpecs,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
51
77
|
function hydrateLlmNode(node, ctx) {
|
|
52
78
|
if (node.kind !== 'llm') {
|
|
53
79
|
return frameworkCast(undefined);
|
|
54
80
|
}
|
|
55
|
-
const
|
|
81
|
+
const combined = resolveLlmTools(node.tools, ctx.tools);
|
|
56
82
|
return step.llm({
|
|
57
83
|
id: node.id,
|
|
58
84
|
model: node.model ?? 'openai/gpt-4o',
|
|
59
85
|
instructions: node.instructions,
|
|
60
|
-
tools:
|
|
86
|
+
tools: combined.length > 0 ? combined : undefined,
|
|
61
87
|
params: node.params,
|
|
88
|
+
output: resolveOutputCodec(node.output, node.id, ctx),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve an `llm` node's `output` codec reference to a live `OutputCodec`.
|
|
93
|
+
*
|
|
94
|
+
* Hydrated steps are typed `<string, string>` (the JSON boundary erases output
|
|
95
|
+
* types), so the resolved codec is cast to the step's erased output type — the
|
|
96
|
+
* runtime still returns the codec's real value. This is the same erasure the
|
|
97
|
+
* rest of the hydrator applies via `frameworkCast`.
|
|
98
|
+
*/
|
|
99
|
+
function availableLibraries(ctx) {
|
|
100
|
+
if (!ctx.uiLibraries || ctx.uiLibraries.size === 0) {
|
|
101
|
+
return '(none)';
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
...ctx.uiLibraries.keys(),
|
|
105
|
+
].join(', ');
|
|
106
|
+
}
|
|
107
|
+
function resolveOutputCodec(ref, nodeId, ctx) {
|
|
108
|
+
if (!ref) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const codec = ctx.uiLibraries?.get(ref.library);
|
|
112
|
+
if (codec) {
|
|
113
|
+
return frameworkCast(codec);
|
|
114
|
+
}
|
|
115
|
+
throw new NoeticConfigError({
|
|
116
|
+
code: 'UNKNOWN_UI_LIBRARY_REFERENCE',
|
|
117
|
+
message: `UI library '${ref.library}' referenced in workflow node '${nodeId}' is not registered.`,
|
|
118
|
+
hint: `Pass output codecs via HydrationContext.uiLibraries, e.g. new Map([['${ref.library}', openUi(myLibrary)]]) from @noetic-tools/openui. Available: ${availableLibraries(ctx)}`,
|
|
62
119
|
});
|
|
63
120
|
}
|
|
64
121
|
function hydrateToolNode(node, ctx) {
|
|
@@ -106,6 +163,31 @@ function hydrateToolNode(node, ctx) {
|
|
|
106
163
|
},
|
|
107
164
|
}));
|
|
108
165
|
}
|
|
166
|
+
function hydrateRunNode(node, ctx) {
|
|
167
|
+
if (node.kind !== 'run') {
|
|
168
|
+
return frameworkCast(undefined);
|
|
169
|
+
}
|
|
170
|
+
const code = node.execute;
|
|
171
|
+
const subprocessRef = node.subprocess;
|
|
172
|
+
return frameworkCast(step.run({
|
|
173
|
+
id: node.id,
|
|
174
|
+
retry: node.retry,
|
|
175
|
+
execute: async (input, execCtx) => {
|
|
176
|
+
const adapter = resolveSubprocessAdapter({
|
|
177
|
+
ref: subprocessRef,
|
|
178
|
+
hydrationCtx: ctx,
|
|
179
|
+
execCtx,
|
|
180
|
+
nodeId: node.id,
|
|
181
|
+
});
|
|
182
|
+
return runCodeViaSubprocess({
|
|
183
|
+
adapter,
|
|
184
|
+
nodeId: node.id,
|
|
185
|
+
code,
|
|
186
|
+
input: stringifyResult(input),
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
109
191
|
function hydrateBranchNode(node, ctx) {
|
|
110
192
|
if (node.kind !== 'branch') {
|
|
111
193
|
return frameworkCast(undefined);
|
|
@@ -137,14 +219,33 @@ function hydrateForkNode(node, ctx) {
|
|
|
137
219
|
if (node.kind !== 'fork') {
|
|
138
220
|
return frameworkCast(undefined);
|
|
139
221
|
}
|
|
140
|
-
|
|
222
|
+
// Static fork: paths are known at hydration time and feed the optimizer.
|
|
223
|
+
// Dynamic fork (`each`): paths are produced per fork-input at runtime, one
|
|
224
|
+
// child per array item, so they cannot be pre-computed or optimized.
|
|
225
|
+
const dynamic = node.each !== undefined;
|
|
226
|
+
const eachTemplate = node.each;
|
|
227
|
+
const staticPaths = dynamic ? [] : (node.paths ?? []).map((p) => hydrateNode(p, ctx));
|
|
228
|
+
const pathsFactory = (input) => {
|
|
229
|
+
if (!dynamic || !eachTemplate) {
|
|
230
|
+
return staticPaths;
|
|
231
|
+
}
|
|
232
|
+
const items = selectArray(input, node.over, node.id);
|
|
233
|
+
return items.map((item, i) => buildPerItemStep({
|
|
234
|
+
forkId: node.id,
|
|
235
|
+
eachTemplate,
|
|
236
|
+
item,
|
|
237
|
+
index: i,
|
|
238
|
+
ctx,
|
|
239
|
+
}));
|
|
240
|
+
};
|
|
241
|
+
const optimizable = dynamic ? undefined : frameworkCast(staticPaths);
|
|
141
242
|
if (node.mode === 'race') {
|
|
142
243
|
return fork({
|
|
143
244
|
id: node.id,
|
|
144
245
|
mode: 'race',
|
|
145
|
-
paths:
|
|
246
|
+
paths: pathsFactory,
|
|
146
247
|
concurrency: node.concurrency,
|
|
147
|
-
_optimizable:
|
|
248
|
+
_optimizable: optimizable,
|
|
148
249
|
});
|
|
149
250
|
}
|
|
150
251
|
const mergeFn = buildMerge(node.merge ?? 'last');
|
|
@@ -152,22 +253,22 @@ function hydrateForkNode(node, ctx) {
|
|
|
152
253
|
return fork({
|
|
153
254
|
id: node.id,
|
|
154
255
|
mode: 'settle',
|
|
155
|
-
paths:
|
|
256
|
+
paths: pathsFactory,
|
|
156
257
|
merge: (results) => {
|
|
157
258
|
const values = results.filter((r) => r.status === 'fulfilled').map((r) => r.value ?? '');
|
|
158
259
|
return mergeFn(values);
|
|
159
260
|
},
|
|
160
261
|
concurrency: node.concurrency,
|
|
161
|
-
_optimizable:
|
|
262
|
+
_optimizable: optimizable,
|
|
162
263
|
});
|
|
163
264
|
}
|
|
164
265
|
return fork({
|
|
165
266
|
id: node.id,
|
|
166
267
|
mode: 'all',
|
|
167
|
-
paths:
|
|
268
|
+
paths: pathsFactory,
|
|
168
269
|
merge: mergeFn,
|
|
169
270
|
concurrency: node.concurrency,
|
|
170
|
-
_optimizable:
|
|
271
|
+
_optimizable: optimizable,
|
|
171
272
|
});
|
|
172
273
|
}
|
|
173
274
|
function hydrateSpawnNode(node, ctx) {
|
|
@@ -281,6 +382,7 @@ function hydrateSubHarnessNode(node, ctx) {
|
|
|
281
382
|
const NODE_HYDRATORS = {
|
|
282
383
|
llm: hydrateLlmNode,
|
|
283
384
|
tool: hydrateToolNode,
|
|
385
|
+
run: hydrateRunNode,
|
|
284
386
|
branch: hydrateBranchNode,
|
|
285
387
|
fork: hydrateForkNode,
|
|
286
388
|
spawn: hydrateSpawnNode,
|
|
@@ -332,6 +434,148 @@ function stringifyResult(value) {
|
|
|
332
434
|
return JSON.stringify(value);
|
|
333
435
|
}
|
|
334
436
|
//#endregion
|
|
437
|
+
//#region Dynamic Fork Helpers
|
|
438
|
+
/**
|
|
439
|
+
* Parses the fork input (a JSON string) and locates the array to fan out over.
|
|
440
|
+
* When `over` is set, reads that property off the parsed object; otherwise the
|
|
441
|
+
* parsed value itself must be an array.
|
|
442
|
+
*/
|
|
443
|
+
function selectArray(input, over, nodeId) {
|
|
444
|
+
let parsed;
|
|
445
|
+
try {
|
|
446
|
+
parsed = JSON.parse(input);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
throw new NoeticConfigError({
|
|
450
|
+
code: 'INVALID_FORK_INPUT',
|
|
451
|
+
message: `Dynamic fork '${nodeId}' could not parse its input as JSON.`,
|
|
452
|
+
hint: 'A dynamic fork (with `each`) expects its input to be a JSON array (or a JSON object when `over` is set).',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const candidate = over === undefined ? parsed : frameworkCast(parsed)?.[over];
|
|
456
|
+
if (!Array.isArray(candidate)) {
|
|
457
|
+
throw new NoeticConfigError({
|
|
458
|
+
code: 'INVALID_FORK_INPUT',
|
|
459
|
+
message: `Dynamic fork '${nodeId}' did not resolve an array${over ? ` at key '${over}'` : ''}.`,
|
|
460
|
+
hint: over
|
|
461
|
+
? `Ensure the input JSON object has an array at '${over}'.`
|
|
462
|
+
: 'Ensure the input is a JSON array, or set `over` to select an array property.',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return candidate;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Builds one fork path for a single dynamic-fork item. The item is injected as
|
|
469
|
+
* the body's input (forks pass the same fork-input to every path), and the
|
|
470
|
+
* template's node ids are suffixed with `-${i}` so each instantiation has
|
|
471
|
+
* unique ids for tracing and step-registry uniqueness.
|
|
472
|
+
*/
|
|
473
|
+
function buildPerItemStep(opts) {
|
|
474
|
+
const { forkId, eachTemplate, item, index, ctx } = opts;
|
|
475
|
+
const hydratedEach = hydrateNode(suffixNodeIds(eachTemplate, `-${index}`), ctx);
|
|
476
|
+
return frameworkCast(step.run({
|
|
477
|
+
id: `${forkId}-item-${index}`,
|
|
478
|
+
execute: async (_input, execCtx) => {
|
|
479
|
+
const itemInput = JSON.stringify(item);
|
|
480
|
+
return stringifyResult(await ctx.executeStep(hydratedEach, itemInput, execCtx));
|
|
481
|
+
},
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
/** Keys whose values are opaque data bags, never child workflow nodes. */
|
|
485
|
+
const NON_NODE_KEYS = new Set([
|
|
486
|
+
'args',
|
|
487
|
+
'params',
|
|
488
|
+
'parameters',
|
|
489
|
+
]);
|
|
490
|
+
/**
|
|
491
|
+
* Deep-clones a workflow node template, appending `suffix` to the `id` of every
|
|
492
|
+
* nested node so a per-item instantiation has globally-unique step ids.
|
|
493
|
+
*/
|
|
494
|
+
function suffixNodeIds(node, suffix) {
|
|
495
|
+
const clone = structuredClone(node);
|
|
496
|
+
const walk = (value) => {
|
|
497
|
+
if (!value || typeof value !== 'object') {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (Array.isArray(value)) {
|
|
501
|
+
for (const entry of value) {
|
|
502
|
+
walk(entry);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const record = frameworkCast(value);
|
|
507
|
+
if (typeof record.kind === 'string' && typeof record.id === 'string') {
|
|
508
|
+
record.id = `${record.id}${suffix}`;
|
|
509
|
+
}
|
|
510
|
+
for (const [key, child] of Object.entries(record)) {
|
|
511
|
+
if (NON_NODE_KEYS.has(key)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
walk(child);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
walk(clone);
|
|
518
|
+
return clone;
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region Run Node Helpers
|
|
522
|
+
/**
|
|
523
|
+
* Resolves the subprocess adapter for a `run` node. When the node names an
|
|
524
|
+
* adapter ref, the host must supply a `resolveSubprocess` resolver; otherwise
|
|
525
|
+
* the step falls back to the harness adapter on the execution context.
|
|
526
|
+
*/
|
|
527
|
+
function resolveSubprocessAdapter(opts) {
|
|
528
|
+
const { ref, hydrationCtx, execCtx, nodeId } = opts;
|
|
529
|
+
if (ref === undefined) {
|
|
530
|
+
return execCtx.subprocess;
|
|
531
|
+
}
|
|
532
|
+
const resolved = hydrationCtx.resolveSubprocess?.(ref);
|
|
533
|
+
if (!resolved) {
|
|
534
|
+
throw new NoeticConfigError({
|
|
535
|
+
code: 'UNKNOWN_SUBPROCESS_REFERENCE',
|
|
536
|
+
message: `Subprocess adapter '${ref}' referenced in run node '${nodeId}' could not be resolved.`,
|
|
537
|
+
hint: 'Pass a `resolveSubprocess(ref)` mapping in the HydrationContext, or omit `subprocess` to use the harness default (ctx.subprocess).',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return resolved;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Dispatches a `run` node's code string through a subprocess adapter: ships the
|
|
544
|
+
* code plus the JSON-stringified input as a process request (input on stdin),
|
|
545
|
+
* waits for the handle to settle, and returns the captured stdout as the step
|
|
546
|
+
* output. A non-zero exit / failed handle surfaces as a thrown error.
|
|
547
|
+
*
|
|
548
|
+
* The code is never eval'd in-process (Cloudflare Workers forbid eval). The
|
|
549
|
+
* adapter owns running the code and capturing stdout into `handle.metadata.result`.
|
|
550
|
+
*/
|
|
551
|
+
async function runCodeViaSubprocess(opts) {
|
|
552
|
+
const { adapter, nodeId, code, input } = opts;
|
|
553
|
+
const request = {
|
|
554
|
+
kind: 'process',
|
|
555
|
+
command: 'node',
|
|
556
|
+
args: [
|
|
557
|
+
'-e',
|
|
558
|
+
code,
|
|
559
|
+
],
|
|
560
|
+
stdin: input,
|
|
561
|
+
metadata: {
|
|
562
|
+
noeticRun: true,
|
|
563
|
+
stepId: nodeId,
|
|
564
|
+
code,
|
|
565
|
+
input,
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
const spawnPromise = adapter.spawn(request);
|
|
569
|
+
const handle = new DetachedHandleImpl({
|
|
570
|
+
id: `run-${nodeId}`,
|
|
571
|
+
stepId: nodeId,
|
|
572
|
+
adapter,
|
|
573
|
+
spawnPromise,
|
|
574
|
+
});
|
|
575
|
+
const result = await handle.await();
|
|
576
|
+
return stringifyResult(result);
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
335
579
|
//#region Public API
|
|
336
580
|
/**
|
|
337
581
|
* Hydrates a single `WorkflowNode` into a live `Step`.
|
|
@@ -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;
|
|
@@ -670,6 +685,7 @@ export class AgentHarnessModelCaller {
|
|
|
670
685
|
context: request.ctx,
|
|
671
686
|
harness: this.opts.harness,
|
|
672
687
|
layers: request.layers,
|
|
688
|
+
callId: fc.callId,
|
|
673
689
|
});
|
|
674
690
|
// Owner-scoped result validation: a tool's `toolResults` schemas apply
|
|
675
691
|
// only to that tool's own result items. Harness-level `opts.itemSchemas`
|
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';
|
|
@@ -2,7 +2,7 @@ export type { LayerStateStore } from '@noetic-tools/memory';
|
|
|
2
2
|
export { allocateBudgets, assembleView, commitLayerUsage, computeLayerUsage, contextToExecCtx, DEFAULT_PROJECTION, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/memory';
|
|
3
3
|
export type { ItemSchemaRegistry } from '@noetic-tools/types';
|
|
4
4
|
export { defaultItemSchemaRegistry } from '@noetic-tools/types';
|
|
5
|
-
export { emitFrameworkEvent, getBroadcaster } from '../runtime/broadcaster-utils.js';
|
|
5
|
+
export { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
6
6
|
export { ContextImpl } from '../runtime/context-impl.js';
|
|
7
7
|
export { snapshotCwdState } from '../runtime/cwd-helpers.js';
|
|
8
8
|
export { buildToolExecutionContext } from '../runtime/tool-memory.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { allocateBudgets, assembleView, commitLayerUsage, computeLayerUsage, contextToExecCtx, DEFAULT_PROJECTION, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/memory';
|
|
2
2
|
export { defaultItemSchemaRegistry } from '@noetic-tools/types';
|
|
3
|
-
export { emitFrameworkEvent, getBroadcaster } from '../runtime/broadcaster-utils.js';
|
|
3
|
+
export { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
4
4
|
export { ContextImpl } from '../runtime/context-impl.js';
|
|
5
5
|
export { snapshotCwdState } from '../runtime/cwd-helpers.js';
|
|
6
6
|
export { buildToolExecutionContext } from '../runtime/tool-memory.js';
|
|
@@ -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':
|