@noetic-tools/core 2.1.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 +3 -2
- package/dist/builders/workflow-hydrator.d.ts +8 -1
- package/dist/builders/workflow-hydrator.js +50 -16
- package/dist/harness/model-call.js +1 -0
- package/dist/interpreter/action-deps.d.ts +1 -1
- package/dist/interpreter/action-deps.js +1 -1
- package/dist/interpreter/execute-action.js +150 -41
- package/dist/runtime/tool-ui.d.ts +36 -0
- package/dist/runtime/tool-ui.js +57 -0
- package/dist/schemas/workflow.d.ts +15 -0
- package/dist/schemas/workflow.js +5 -0
- package/package.json +2 -2
- package/schema/noetic-workflow.schema.json +18 -0
|
@@ -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, ServerToolSpec, 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;
|
|
@@ -24,7 +24,8 @@ interface StepLLMOpts<TMemory, O> {
|
|
|
24
24
|
* server tool the provider executes, e.g. web search/fetch).
|
|
25
25
|
*/
|
|
26
26
|
tools?: Lazy<(Tool | ServerToolSpec)[] | undefined, TMemory>;
|
|
27
|
-
output
|
|
27
|
+
/** Structured output: a Zod schema or a streaming `OutputCodec` (e.g. OpenUI Lang). */
|
|
28
|
+
output?: ZodType<O> | OutputCodec<O>;
|
|
28
29
|
params?: ModelParams;
|
|
29
30
|
emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
|
|
30
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, SubprocessAdapter, 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,13 @@ 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>;
|
|
18
25
|
/**
|
|
19
26
|
* Resolves a named subprocess adapter ref declared on a `run` node's
|
|
20
27
|
* `subprocess` field. When a node omits the ref, the step falls back to
|
|
@@ -49,21 +49,19 @@ function hydrateUntilPredicate(pred) {
|
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
51
|
//#region Node Hydrators
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// specs bypass the client-tool machinery and reach the model call's
|
|
63
|
-
// server-tool channel).
|
|
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) {
|
|
64
62
|
const toolNames = [];
|
|
65
63
|
const serverSpecs = [];
|
|
66
|
-
for (const entry of
|
|
64
|
+
for (const entry of entries ?? []) {
|
|
67
65
|
if (isServerToolSpec(entry)) {
|
|
68
66
|
serverSpecs.push(entry);
|
|
69
67
|
}
|
|
@@ -71,17 +69,53 @@ function hydrateLlmNode(node, ctx) {
|
|
|
71
69
|
toolNames.push(entry.type);
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
...resolvedTools,
|
|
72
|
+
return [
|
|
73
|
+
...resolveTools(toolNames, registry),
|
|
77
74
|
...serverSpecs,
|
|
78
75
|
];
|
|
76
|
+
}
|
|
77
|
+
function hydrateLlmNode(node, ctx) {
|
|
78
|
+
if (node.kind !== 'llm') {
|
|
79
|
+
return frameworkCast(undefined);
|
|
80
|
+
}
|
|
81
|
+
const combined = resolveLlmTools(node.tools, ctx.tools);
|
|
79
82
|
return step.llm({
|
|
80
83
|
id: node.id,
|
|
81
84
|
model: node.model ?? 'openai/gpt-4o',
|
|
82
85
|
instructions: node.instructions,
|
|
83
86
|
tools: combined.length > 0 ? combined : undefined,
|
|
84
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)}`,
|
|
85
119
|
});
|
|
86
120
|
}
|
|
87
121
|
function hydrateToolNode(node, ctx) {
|
|
@@ -685,6 +685,7 @@ export class AgentHarnessModelCaller {
|
|
|
685
685
|
context: request.ctx,
|
|
686
686
|
harness: this.opts.harness,
|
|
687
687
|
layers: request.layers,
|
|
688
|
+
callId: fc.callId,
|
|
688
689
|
});
|
|
689
690
|
// Owner-scoped result validation: a tool's `toolResults` schemas apply
|
|
690
691
|
// only to that tool's own result items. Harness-level `opts.itemSchemas`
|
|
@@ -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,9 +1,10 @@
|
|
|
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 {
|
|
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';
|
|
7
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';
|
|
@@ -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,7 +270,8 @@ 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);
|
|
183
276
|
// `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
|
|
184
277
|
// server-tool specs (web search/fetch). Partition them — client tools flow
|
|
@@ -309,7 +402,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
309
402
|
instructions: resolvedInstructions,
|
|
310
403
|
tools: resolvedTools,
|
|
311
404
|
params: step.params,
|
|
312
|
-
outputSchema:
|
|
405
|
+
outputSchema: outputSchema,
|
|
313
406
|
emit: step.emit,
|
|
314
407
|
_serverTools,
|
|
315
408
|
ctx: baseCtx,
|
|
@@ -323,7 +416,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
323
416
|
items: assembledItems,
|
|
324
417
|
instructions: resolvedInstructions,
|
|
325
418
|
params: step.params,
|
|
326
|
-
outputSchema:
|
|
419
|
+
outputSchema: outputSchema,
|
|
327
420
|
emit: step.emit,
|
|
328
421
|
_serverTools,
|
|
329
422
|
nodeId: step.id,
|
|
@@ -376,33 +469,14 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
376
469
|
await baseCtx.harness.storeLayers(layers, response, baseCtx);
|
|
377
470
|
}
|
|
378
471
|
const lastText = extractAssistantText(response.items);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
kind: 'llm_parse_error',
|
|
388
|
-
stepId: step.id,
|
|
389
|
-
raw: lastText,
|
|
390
|
-
schema: step.output,
|
|
391
|
-
zodError: e instanceof ZodError
|
|
392
|
-
? e
|
|
393
|
-
: new ZodError([
|
|
394
|
-
{
|
|
395
|
-
code: 'custom',
|
|
396
|
-
message: `Invalid JSON: ${e.message}`,
|
|
397
|
-
path: [],
|
|
398
|
-
},
|
|
399
|
-
]),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
throw e;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return frameworkCast(lastText);
|
|
472
|
+
return finalizeStepOutput({
|
|
473
|
+
stepId: step.id,
|
|
474
|
+
emit: step.emit,
|
|
475
|
+
outputCodec,
|
|
476
|
+
outputSchema,
|
|
477
|
+
lastText,
|
|
478
|
+
ctx: baseCtx,
|
|
479
|
+
});
|
|
406
480
|
}
|
|
407
481
|
// Safety net: the loop above always returns or throws within the body.
|
|
408
482
|
// This throw is unreachable but protects against future refactors that
|
|
@@ -610,21 +684,32 @@ function isAsyncGenerator(value) {
|
|
|
610
684
|
async function consumeToolGenerator(params) {
|
|
611
685
|
const broadcaster = getBroadcaster(params.ctx);
|
|
612
686
|
const agentName = params.ctx.harness.config.name;
|
|
687
|
+
const events = [];
|
|
613
688
|
while (true) {
|
|
614
689
|
const next = await params.generator.next();
|
|
615
690
|
if (next.done) {
|
|
616
691
|
return next.value;
|
|
617
692
|
}
|
|
693
|
+
events.push(next.value);
|
|
618
694
|
emitFrameworkEvent({
|
|
619
695
|
broadcaster,
|
|
620
696
|
agentName,
|
|
621
697
|
eventType: 'tool_progress',
|
|
622
698
|
data: {
|
|
623
699
|
stepId: params.stepId,
|
|
624
|
-
toolName: params.
|
|
700
|
+
toolName: params.tool.name,
|
|
625
701
|
event: next.value,
|
|
626
702
|
},
|
|
627
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
|
+
});
|
|
628
713
|
}
|
|
629
714
|
}
|
|
630
715
|
export async function executeTool(step, input, ctx, harness, layers) {
|
|
@@ -648,20 +733,44 @@ export async function executeTool(step, input, ctx, harness, layers) {
|
|
|
648
733
|
});
|
|
649
734
|
}
|
|
650
735
|
}
|
|
736
|
+
emitToolUi({
|
|
737
|
+
ctx: baseCtx,
|
|
738
|
+
tool: step.tool,
|
|
739
|
+
callId: step.id,
|
|
740
|
+
phase: 'call',
|
|
741
|
+
args: parseResult.data,
|
|
742
|
+
});
|
|
651
743
|
try {
|
|
652
744
|
const toolCtx = buildToolExecutionContext(baseCtx, harness);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
generator:
|
|
745
|
+
const execResult = step.tool.execute(parseResult.data, toolCtx);
|
|
746
|
+
const result = isAsyncGenerator(execResult)
|
|
747
|
+
? await consumeToolGenerator({
|
|
748
|
+
generator: execResult,
|
|
657
749
|
stepId: step.id,
|
|
658
|
-
|
|
750
|
+
tool: step.tool,
|
|
659
751
|
ctx: baseCtx,
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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);
|
|
663
764
|
}
|
|
664
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
|
+
});
|
|
665
774
|
if (e instanceof NoeticErrorImpl) {
|
|
666
775
|
throw e;
|
|
667
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
|
+
}
|
|
@@ -114,6 +114,19 @@ declare const HarnessSessionPolicySchema: z.ZodObject<{
|
|
|
114
114
|
interface WorkflowNodeBase {
|
|
115
115
|
id: string;
|
|
116
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
|
+
}
|
|
117
130
|
export interface LlmWorkflowNode extends WorkflowNodeBase {
|
|
118
131
|
kind: 'llm';
|
|
119
132
|
model?: string;
|
|
@@ -127,6 +140,8 @@ export interface LlmWorkflowNode extends WorkflowNodeBase {
|
|
|
127
140
|
*/
|
|
128
141
|
tools?: z.infer<typeof LlmToolEntrySchema>[];
|
|
129
142
|
params?: z.infer<typeof ModelParamsSchema>;
|
|
143
|
+
/** Streaming output-codec reference, resolved from `HydrationContext.uiLibraries`. */
|
|
144
|
+
output?: OutputCodecRef;
|
|
130
145
|
}
|
|
131
146
|
export interface ToolWorkflowNode extends WorkflowNodeBase {
|
|
132
147
|
kind: 'tool';
|
package/dist/schemas/workflow.js
CHANGED
|
@@ -151,6 +151,10 @@ const WorkflowNodeRef = z.lazy(() => WorkflowNodeSchema);
|
|
|
151
151
|
const SHARED_FIELDS = {
|
|
152
152
|
id: z.string().min(1),
|
|
153
153
|
};
|
|
154
|
+
const OutputCodecRefSchema = z.object({
|
|
155
|
+
codec: z.literal('openui'),
|
|
156
|
+
library: z.string().min(1),
|
|
157
|
+
});
|
|
154
158
|
const LlmNodeSchema = z.object({
|
|
155
159
|
kind: z.literal('llm'),
|
|
156
160
|
...SHARED_FIELDS,
|
|
@@ -158,6 +162,7 @@ const LlmNodeSchema = z.object({
|
|
|
158
162
|
instructions: z.string(),
|
|
159
163
|
tools: z.array(LlmToolEntrySchema).optional(),
|
|
160
164
|
params: ModelParamsSchema.optional(),
|
|
165
|
+
output: OutputCodecRefSchema.optional(),
|
|
161
166
|
});
|
|
162
167
|
const ToolNodeSchema = z.object({
|
|
163
168
|
kind: z.literal('tool'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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.
|
|
70
|
+
"@noetic-tools/types": "^1.3.0",
|
|
71
71
|
"@openrouter/agent": "^0.6.0",
|
|
72
72
|
"zod": "^4.0.0"
|
|
73
73
|
},
|
|
@@ -86,6 +86,24 @@
|
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"additionalProperties": false
|
|
89
|
+
},
|
|
90
|
+
"output": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"codec": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"const": "openui"
|
|
96
|
+
},
|
|
97
|
+
"library": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"minLength": 1
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"required": [
|
|
103
|
+
"codec",
|
|
104
|
+
"library"
|
|
105
|
+
],
|
|
106
|
+
"additionalProperties": false
|
|
89
107
|
}
|
|
90
108
|
},
|
|
91
109
|
"required": [
|