@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.
@@ -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
- // Handle generator-based tools (like Bash) that return AsyncGenerator
251
- const result = isAsyncGenerator(executionResult)
252
- ? await consumeGenerator(executionResult)
253
- : await executionResult;
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
- /** Optional tools; eager array or `(ctx) => Tool[] | undefined` getter. */
22
- tools?: Lazy<Tool[] | undefined, TMemory>;
23
- output?: ZodType<O>;
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 resolvedTools = resolveTools(node.tools, ctx.tools);
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: resolvedTools.length > 0 ? resolvedTools : undefined,
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
- const hydratedPaths = node.paths.map((p) => hydrateNode(p, ctx));
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: () => hydratedPaths,
246
+ paths: pathsFactory,
146
247
  concurrency: node.concurrency,
147
- _optimizable: frameworkCast(hydratedPaths),
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: () => hydratedPaths,
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: frameworkCast(hydratedPaths),
262
+ _optimizable: optimizable,
162
263
  });
163
264
  }
164
265
  return fork({
165
266
  id: node.id,
166
267
  mode: 'all',
167
- paths: () => hydratedPaths,
268
+ paths: pathsFactory,
168
269
  merge: mergeFn,
169
270
  concurrency: node.concurrency,
170
- _optimizable: frameworkCast(hydratedPaths),
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: filteredTools && filteredTools.length > 0
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
- out.push(...step.tools);
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':