@noetic-tools/core 2.1.0 → 2.2.1
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 +155 -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,102 @@ 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
|
+
// A streaming codec emits per completed (newline-terminated) statement, so the
|
|
209
|
+
// final line — usually `root` — would stay buffered and never be emitted.
|
|
210
|
+
// Push a newline-terminated copy so that last statement is flushed as an event
|
|
211
|
+
// too; `finish` still reparses the original text for the returned value.
|
|
212
|
+
const textForEmit = lastText.endsWith('\n') ? lastText : `${lastText}\n`;
|
|
213
|
+
session.push(textForEmit, (eventType, data) => {
|
|
214
|
+
if (shouldEmit(emit, eventType, data)) {
|
|
215
|
+
emitFrameworkEvent({
|
|
216
|
+
broadcaster,
|
|
217
|
+
agentName,
|
|
218
|
+
eventType,
|
|
219
|
+
data,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return session.finish(lastText);
|
|
224
|
+
}
|
|
225
|
+
/** JSON-parse + Zod-validate the assistant text, raising `llm_parse_error`. */
|
|
226
|
+
function parseSchemaOutput(schema, lastText, stepId) {
|
|
227
|
+
try {
|
|
228
|
+
return schema.parse(JSON.parse(lastText));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
if (e instanceof SyntaxError || e instanceof ZodError) {
|
|
232
|
+
throw new NoeticErrorImpl({
|
|
233
|
+
kind: 'llm_parse_error',
|
|
234
|
+
stepId,
|
|
235
|
+
raw: lastText,
|
|
236
|
+
schema,
|
|
237
|
+
zodError: e instanceof ZodError
|
|
238
|
+
? e
|
|
239
|
+
: new ZodError([
|
|
240
|
+
{
|
|
241
|
+
code: 'custom',
|
|
242
|
+
message: `Invalid JSON: ${e.message}`,
|
|
243
|
+
path: [],
|
|
244
|
+
},
|
|
245
|
+
]),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
throw e;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Turn the finished assistant text into the step's typed output: a streaming
|
|
253
|
+
* codec, a Zod schema, or raw text passthrough.
|
|
254
|
+
*/
|
|
255
|
+
function finalizeStepOutput(params) {
|
|
256
|
+
if (params.outputCodec) {
|
|
257
|
+
return finalizeCodecOutput(params.outputCodec, params.lastText, params.ctx, params.emit);
|
|
258
|
+
}
|
|
259
|
+
if (params.outputSchema) {
|
|
260
|
+
return parseSchemaOutput(params.outputSchema, params.lastText, params.stepId);
|
|
261
|
+
}
|
|
262
|
+
return frameworkCast(params.lastText);
|
|
263
|
+
}
|
|
167
264
|
export async function executeLLM(step, input, ctx, layers) {
|
|
168
265
|
const baseCtx = frameworkCast(ctx);
|
|
169
266
|
const hasLayers = layers !== undefined && layers.length > 0;
|
|
@@ -178,7 +275,8 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
178
275
|
hint: "Ensure `model` (or your `(ctx) => string` getter) returns a non-empty identifier, e.g. 'anthropic/claude-sonnet-4-20250514'.",
|
|
179
276
|
});
|
|
180
277
|
}
|
|
181
|
-
const
|
|
278
|
+
const baseInstructions = await resolveLazy(step.instructions, ctx);
|
|
279
|
+
const { outputCodec, outputSchema, resolvedInstructions } = resolveOutputMode(step.output, baseInstructions);
|
|
182
280
|
const resolvedStepTools = await resolveLazy(step.tools, ctx);
|
|
183
281
|
// `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
|
|
184
282
|
// server-tool specs (web search/fetch). Partition them — client tools flow
|
|
@@ -309,7 +407,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
309
407
|
instructions: resolvedInstructions,
|
|
310
408
|
tools: resolvedTools,
|
|
311
409
|
params: step.params,
|
|
312
|
-
outputSchema:
|
|
410
|
+
outputSchema: outputSchema,
|
|
313
411
|
emit: step.emit,
|
|
314
412
|
_serverTools,
|
|
315
413
|
ctx: baseCtx,
|
|
@@ -323,7 +421,7 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
323
421
|
items: assembledItems,
|
|
324
422
|
instructions: resolvedInstructions,
|
|
325
423
|
params: step.params,
|
|
326
|
-
outputSchema:
|
|
424
|
+
outputSchema: outputSchema,
|
|
327
425
|
emit: step.emit,
|
|
328
426
|
_serverTools,
|
|
329
427
|
nodeId: step.id,
|
|
@@ -376,33 +474,14 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
376
474
|
await baseCtx.harness.storeLayers(layers, response, baseCtx);
|
|
377
475
|
}
|
|
378
476
|
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);
|
|
477
|
+
return finalizeStepOutput({
|
|
478
|
+
stepId: step.id,
|
|
479
|
+
emit: step.emit,
|
|
480
|
+
outputCodec,
|
|
481
|
+
outputSchema,
|
|
482
|
+
lastText,
|
|
483
|
+
ctx: baseCtx,
|
|
484
|
+
});
|
|
406
485
|
}
|
|
407
486
|
// Safety net: the loop above always returns or throws within the body.
|
|
408
487
|
// This throw is unreachable but protects against future refactors that
|
|
@@ -610,21 +689,32 @@ function isAsyncGenerator(value) {
|
|
|
610
689
|
async function consumeToolGenerator(params) {
|
|
611
690
|
const broadcaster = getBroadcaster(params.ctx);
|
|
612
691
|
const agentName = params.ctx.harness.config.name;
|
|
692
|
+
const events = [];
|
|
613
693
|
while (true) {
|
|
614
694
|
const next = await params.generator.next();
|
|
615
695
|
if (next.done) {
|
|
616
696
|
return next.value;
|
|
617
697
|
}
|
|
698
|
+
events.push(next.value);
|
|
618
699
|
emitFrameworkEvent({
|
|
619
700
|
broadcaster,
|
|
620
701
|
agentName,
|
|
621
702
|
eventType: 'tool_progress',
|
|
622
703
|
data: {
|
|
623
704
|
stepId: params.stepId,
|
|
624
|
-
toolName: params.
|
|
705
|
+
toolName: params.tool.name,
|
|
625
706
|
event: next.value,
|
|
626
707
|
},
|
|
627
708
|
});
|
|
709
|
+
// A direct `step.tool` has no model call id — the step id keys its region.
|
|
710
|
+
emitToolUi({
|
|
711
|
+
ctx: params.ctx,
|
|
712
|
+
tool: params.tool,
|
|
713
|
+
callId: params.stepId,
|
|
714
|
+
phase: 'progress',
|
|
715
|
+
args: params.args,
|
|
716
|
+
events,
|
|
717
|
+
});
|
|
628
718
|
}
|
|
629
719
|
}
|
|
630
720
|
export async function executeTool(step, input, ctx, harness, layers) {
|
|
@@ -648,20 +738,44 @@ export async function executeTool(step, input, ctx, harness, layers) {
|
|
|
648
738
|
});
|
|
649
739
|
}
|
|
650
740
|
}
|
|
741
|
+
emitToolUi({
|
|
742
|
+
ctx: baseCtx,
|
|
743
|
+
tool: step.tool,
|
|
744
|
+
callId: step.id,
|
|
745
|
+
phase: 'call',
|
|
746
|
+
args: parseResult.data,
|
|
747
|
+
});
|
|
651
748
|
try {
|
|
652
749
|
const toolCtx = buildToolExecutionContext(baseCtx, harness);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
generator:
|
|
750
|
+
const execResult = step.tool.execute(parseResult.data, toolCtx);
|
|
751
|
+
const result = isAsyncGenerator(execResult)
|
|
752
|
+
? await consumeToolGenerator({
|
|
753
|
+
generator: execResult,
|
|
657
754
|
stepId: step.id,
|
|
658
|
-
|
|
755
|
+
tool: step.tool,
|
|
659
756
|
ctx: baseCtx,
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
757
|
+
args: parseResult.data,
|
|
758
|
+
})
|
|
759
|
+
: await execResult;
|
|
760
|
+
emitToolUi({
|
|
761
|
+
ctx: baseCtx,
|
|
762
|
+
tool: step.tool,
|
|
763
|
+
callId: step.id,
|
|
764
|
+
phase: 'result',
|
|
765
|
+
args: parseResult.data,
|
|
766
|
+
output: result,
|
|
767
|
+
});
|
|
768
|
+
return frameworkCast(result);
|
|
663
769
|
}
|
|
664
770
|
catch (e) {
|
|
771
|
+
emitToolUi({
|
|
772
|
+
ctx: baseCtx,
|
|
773
|
+
tool: step.tool,
|
|
774
|
+
callId: step.id,
|
|
775
|
+
phase: 'error',
|
|
776
|
+
args: parseResult.data,
|
|
777
|
+
error: e,
|
|
778
|
+
});
|
|
665
779
|
if (e instanceof NoeticErrorImpl) {
|
|
666
780
|
throw e;
|
|
667
781
|
}
|
|
@@ -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.1
|
|
3
|
+
"version": "2.2.1",
|
|
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": [
|