@noetic-tools/core 5.2.0 → 5.4.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/README.md +8 -6
- package/dist/adapters/in-memory-subprocess/step-completion.js +1 -1
- package/dist/adapters/openrouter.d.ts +20 -2
- package/dist/adapters/openrouter.js +102 -6
- package/dist/builders/control-flow-builders.d.ts +13 -13
- package/dist/builders/control-flow-builders.js +11 -11
- package/dist/{patterns → builders}/dynamic-workflow.d.ts +1 -1
- package/dist/{patterns → builders}/dynamic-workflow.js +18 -12
- package/dist/builders/every.d.ts +14 -14
- package/dist/builders/every.js +16 -16
- package/dist/builders/layer-provides-builders.d.ts +1 -1
- package/dist/builders/layer-provides-builders.js +1 -1
- package/dist/builders/provide-builder.d.ts +5 -12
- package/dist/builders/provide-builder.js +9 -10
- package/dist/builders/spawn-builder.d.ts +0 -2
- package/dist/builders/spawn-builder.js +2 -3
- package/dist/builders/step-builders.d.ts +66 -55
- package/dist/builders/step-builders.js +118 -111
- package/dist/builders/tool-builder.d.ts +18 -22
- package/dist/builders/tool-builder.js +6 -4
- package/dist/builders/workflow-hydrator.d.ts +3 -3
- package/dist/builders/workflow-hydrator.js +59 -63
- package/dist/builders/workflow-step.d.ts +13 -25
- package/dist/builders/workflow-step.js +13 -18
- package/dist/conditions/conditions.d.ts +2 -2
- package/dist/conditions/conditions.js +1 -1
- package/dist/harness/agent-harness.d.ts +58 -40
- package/dist/harness/agent-harness.js +125 -51
- package/dist/harness/deps/context.d.ts +1 -1
- package/dist/harness/deps/context.js +1 -1
- package/dist/harness/deps/interpreter.d.ts +1 -0
- package/dist/harness/deps/interpreter.js +1 -0
- package/dist/harness/deps/types.d.ts +2 -2
- package/dist/harness/model-call.d.ts +1 -1
- package/dist/harness/model-call.js +45 -23
- package/dist/index.d.ts +23 -35
- package/dist/index.js +10 -26
- package/dist/interpreter/action-deps.d.ts +2 -2
- package/dist/interpreter/action-deps.js +1 -1
- package/dist/interpreter/action-types.d.ts +1 -1
- package/dist/interpreter/collect-tools.js +7 -7
- package/dist/interpreter/context-assembly.d.ts +50 -0
- package/dist/interpreter/context-assembly.js +547 -0
- package/dist/interpreter/execute-action.d.ts +5 -5
- package/dist/interpreter/execute-action.js +337 -208
- package/dist/interpreter/execute-control.d.ts +9 -9
- package/dist/interpreter/execute-control.js +27 -27
- package/dist/interpreter/execute-sub-harness.d.ts +1 -1
- package/dist/interpreter/execute-sub-harness.js +7 -26
- package/dist/interpreter/execute.d.ts +1 -1
- package/dist/interpreter/execute.js +26 -25
- package/dist/interpreter/step-dispatcher.d.ts +1 -1
- package/dist/interpreter/step-dispatcher.js +3 -3
- package/dist/interpreter/structured-output.d.ts +14 -0
- package/dist/interpreter/structured-output.js +40 -0
- package/dist/interpreter/typeguards.d.ts +1 -1
- package/dist/interpreter/typeguards.js +1 -1
- package/dist/observability/genai-attributes.d.ts +20 -0
- package/dist/observability/genai-attributes.js +20 -0
- package/dist/portable/builders.d.ts +3 -3
- package/dist/portable/builders.js +6 -5
- package/dist/portable/context-layers.d.ts +1 -1
- package/dist/portable/context-layers.js +1 -1
- package/dist/runtime/broadcaster-utils.d.ts +1 -1
- package/dist/runtime/broadcaster-utils.js +1 -1
- package/dist/runtime/context-impl.d.ts +8 -10
- package/dist/runtime/context-impl.js +8 -12
- package/dist/runtime/durable/step-ledger.d.ts +2 -2
- package/dist/runtime/durable/step-ledger.js +3 -3
- package/dist/runtime/session-runner.d.ts +12 -1
- package/dist/runtime/session-runner.js +33 -1
- package/dist/runtime/tool-context.js +0 -2
- package/dist/schemas/workflow.d.ts +3 -3
- package/dist/schemas/workflow.js +2 -2
- package/dist/types/checkpoint.d.ts +11 -2
- package/dist/types/checkpoint.js +10 -1
- package/dist/{runtime → util}/detached-handle.js +1 -1
- package/package.json +6 -4
- package/schema/noetic-workflow.schema.json +24 -11
- package/dist/builders/context-option.d.ts +0 -23
- package/dist/builders/context-option.js +0 -22
- package/dist/deprecated.d.ts +0 -30
- package/dist/deprecated.js +0 -28
- package/dist/patterns/interview.d.ts +0 -53
- package/dist/patterns/interview.js +0 -113
- package/dist/patterns/plans.d.ts +0 -44
- package/dist/patterns/plans.js +0 -125
- package/dist/patterns/ralph-wiggum.d.ts +0 -19
- package/dist/patterns/ralph-wiggum.js +0 -37
- package/dist/patterns/react.d.ts +0 -19
- package/dist/patterns/react.js +0 -41
- /package/dist/{runtime → util}/detached-handle.d.ts +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Action step handlers: run, llm, spawn, provide, tool.
|
|
3
3
|
*/
|
|
4
|
-
import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, isOutputCodec, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
|
|
5
|
-
import { ZodError } from 'zod';
|
|
4
|
+
import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, isOutputCodec, NoeticConfigError, NoeticErrorImpl, validateSchema, } from '@noetic-tools/types';
|
|
6
5
|
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';
|
|
6
|
+
import { allocateBudgets, assembleView, buildToolExecutionContext, ContextImpl, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DEFAULT_PROJECTION, defaultItemSchemaRegistry, emitFrameworkEvent, getBroadcaster, lineageKey, noteCacheOutcome, resolveCacheConfig, resolveLayerTools, returnLayers, shouldEmit, snapshotCwdState, spawnLayers, } from './action-deps.js';
|
|
8
7
|
import { isServerToolSpec, SteeringAction } from './action-types.js';
|
|
9
8
|
import { cloneWithGuard } from './clone-guard.js';
|
|
10
9
|
import { collectAllTools, deduplicateTools } from './collect-tools.js';
|
|
10
|
+
import { prepareBandedView, stampAnchoringAttributes } from './context-assembly.js';
|
|
11
11
|
import { trackUsage } from './message-helpers.js';
|
|
12
|
+
import { parseStructuredOutput } from './structured-output.js';
|
|
12
13
|
import { getContextChannelStore, isContextImpl, isFunctionCall, isMutableContext, } from './typeguards.js';
|
|
13
14
|
//#region run
|
|
14
|
-
export async function
|
|
15
|
+
export async function executeRunCode(step, input, ctx) {
|
|
15
16
|
const retry = step.retry;
|
|
16
17
|
const maxAttempts = retry?.maxAttempts ?? 1;
|
|
17
18
|
let lastError = new Error('No attempts executed');
|
|
@@ -173,7 +174,7 @@ async function runInputPipeline({ ctx, layers, input, }) {
|
|
|
173
174
|
* it is kept off `outputSchema` (which drives JSON-schema formatting).
|
|
174
175
|
*/
|
|
175
176
|
function resolveOutputMode(output, baseInstructions) {
|
|
176
|
-
// A generic type predicate does not reliably narrow a `
|
|
177
|
+
// A generic type predicate does not reliably narrow a `StandardSchemaV1 |
|
|
177
178
|
// OutputCodec<O>` union through a free type parameter, so bridge the two
|
|
178
179
|
// branches with the sanctioned framework cast — `isOutputCodec` guarantees
|
|
179
180
|
// the discriminant at runtime.
|
|
@@ -222,46 +223,237 @@ function finalizeCodecOutput(codec, lastText, ctx, emit) {
|
|
|
222
223
|
});
|
|
223
224
|
return session.finish(lastText);
|
|
224
225
|
}
|
|
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
226
|
/**
|
|
252
227
|
* Turn the finished assistant text into the step's typed output: a streaming
|
|
253
|
-
* codec, a
|
|
228
|
+
* codec, a Standard Schema, or raw text passthrough.
|
|
254
229
|
*/
|
|
255
|
-
function finalizeStepOutput(params) {
|
|
230
|
+
async function finalizeStepOutput(params) {
|
|
256
231
|
if (params.outputCodec) {
|
|
257
232
|
return finalizeCodecOutput(params.outputCodec, params.lastText, params.ctx, params.emit);
|
|
258
233
|
}
|
|
259
234
|
if (params.outputSchema) {
|
|
260
|
-
return
|
|
235
|
+
return parseStructuredOutput({
|
|
236
|
+
schema: params.outputSchema,
|
|
237
|
+
rawText: params.lastText,
|
|
238
|
+
stepId: params.stepId,
|
|
239
|
+
});
|
|
261
240
|
}
|
|
262
241
|
return frameworkCast(params.lastText);
|
|
263
242
|
}
|
|
264
|
-
|
|
243
|
+
/**
|
|
244
|
+
* `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
|
|
245
|
+
* server-tool specs (web search/fetch). Partition them — client tools flow
|
|
246
|
+
* through the unified-tool / allowedToolNames / steering machinery as before;
|
|
247
|
+
* server-tool specs bypass it entirely and are stamped onto the model request
|
|
248
|
+
* (`_serverTools`) for the model caller to wrap via the SDK's `serverTool()`.
|
|
249
|
+
* `undefined` (unrestricted) is preserved as-is; only a present array is
|
|
250
|
+
* partitioned so the `[] = opt-out` semantics of step.tools stay intact.
|
|
251
|
+
*/
|
|
252
|
+
function partitionStepTools(resolvedStepTools) {
|
|
253
|
+
if (resolvedStepTools === undefined) {
|
|
254
|
+
return {
|
|
255
|
+
clientStepTools: undefined,
|
|
256
|
+
serverToolSpecs: [],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const clientStepTools = [];
|
|
260
|
+
const serverToolSpecs = [];
|
|
261
|
+
for (const entry of resolvedStepTools) {
|
|
262
|
+
if (isServerToolSpec(entry)) {
|
|
263
|
+
serverToolSpecs.push(entry);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
clientStepTools.push(entry);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
clientStepTools,
|
|
270
|
+
serverToolSpecs,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Append user input — through layer pipeline if layers exist, otherwise direct.
|
|
275
|
+
* The append pipeline may request a context re-render (e.g. a layer that
|
|
276
|
+
* expanded an input reference into new content); applied after recall.
|
|
277
|
+
*/
|
|
278
|
+
async function appendUserInput(params) {
|
|
279
|
+
const { ctx, layers, input } = params;
|
|
280
|
+
if (typeof input !== 'string' || input.length === 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
if (layers !== undefined && layers.length > 0) {
|
|
284
|
+
return await runInputPipeline({
|
|
285
|
+
ctx,
|
|
286
|
+
layers,
|
|
287
|
+
input,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
ctx.itemLog.append(createMessage(input, 'user'));
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Splits the context budget across layers via `allocateBudgets`. An empty map
|
|
295
|
+
* when the step runs without layers.
|
|
296
|
+
*/
|
|
297
|
+
function computeBudgetMap(params) {
|
|
298
|
+
const { layers, policy, systemPromptTokens } = params;
|
|
299
|
+
if (layers === undefined || layers.length === 0) {
|
|
300
|
+
return new Map();
|
|
301
|
+
}
|
|
302
|
+
const { allocations } = allocateBudgets({
|
|
303
|
+
layers,
|
|
304
|
+
totalBudget: policy.tokenBudget,
|
|
305
|
+
systemPromptTokens,
|
|
306
|
+
responseReserve: policy.responseReserve,
|
|
307
|
+
});
|
|
308
|
+
return new Map(allocations.map((a) => [
|
|
309
|
+
a.layerId,
|
|
310
|
+
a.allocated,
|
|
311
|
+
]));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Recall once per LLM step: atomic layers run in the hot path; eventual layers
|
|
315
|
+
* are served from cache. Results drive both the assembled context window and
|
|
316
|
+
* the per-layer usage breakdown (ctx.lastLayerUsage). Recall fires before the
|
|
317
|
+
* steering retry loop because retries replay the same context. Re-render
|
|
318
|
+
* requests collected from the input-append pipeline are applied on top.
|
|
319
|
+
*/
|
|
320
|
+
async function gatherRecallResults(params) {
|
|
321
|
+
const { ctx, layers, recallQuery, budgetMap, slotOf, rerenderRequests } = params;
|
|
322
|
+
if (layers === undefined || layers.length === 0) {
|
|
323
|
+
return emptyRecall;
|
|
324
|
+
}
|
|
325
|
+
const atomic = await ctx.harness.recallLayersAtomic(layers, recallQuery, ctx, budgetMap);
|
|
326
|
+
const eventual = await ctx.harness.recallLayersEventual(layers, recallQuery, ctx, budgetMap);
|
|
327
|
+
let recallResults = [
|
|
328
|
+
...atomic,
|
|
329
|
+
...eventual,
|
|
330
|
+
].sort((a, b) => (slotOf.get(a.layerId) ?? 0) - (slotOf.get(b.layerId) ?? 0));
|
|
331
|
+
if (rerenderRequests.length > 0) {
|
|
332
|
+
const rerendered = await ctx.harness.executeRerender(rerenderRequests, layers, ctx, budgetMap, recallQuery);
|
|
333
|
+
recallResults = mergeRecallResults([
|
|
334
|
+
...recallResults,
|
|
335
|
+
], rerendered, slotOf);
|
|
336
|
+
}
|
|
337
|
+
return recallResults;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Builds the items the model sees for one attempt: system messages stay at the
|
|
341
|
+
* front, the banded anchor/live/delta output rides between them and history,
|
|
342
|
+
* and steering guidance from a prior retry lands at the very tail. The
|
|
343
|
+
* projector enforces the token budget (drops highest-slot layer output, then
|
|
344
|
+
* oldest history).
|
|
345
|
+
*/
|
|
346
|
+
async function assembleTurnItems(params) {
|
|
347
|
+
const { ctx, layers, banded, steeringTail, viewPolicy } = params;
|
|
348
|
+
const rawHistoryItems = ctx.itemLog.items;
|
|
349
|
+
if (layers === undefined || layers.length === 0) {
|
|
350
|
+
return rawHistoryItems;
|
|
351
|
+
}
|
|
352
|
+
const projectedHistoryItems = await ctx.harness.projectHistory(layers, rawHistoryItems, ctx);
|
|
353
|
+
const systemItems = [];
|
|
354
|
+
const nonSystemHistory = [];
|
|
355
|
+
const tailIds = new Set(steeringTail.map((i) => i.id));
|
|
356
|
+
for (const item of projectedHistoryItems) {
|
|
357
|
+
if (item.type === 'message' && item.role === 'system') {
|
|
358
|
+
systemItems.push(item);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
// Steering guidance is re-placed at the tail; skip it here so the retry
|
|
362
|
+
// does not see the same correction twice.
|
|
363
|
+
if ('id' in item && typeof item.id === 'string' && tailIds.has(item.id)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
nonSystemHistory.push(item);
|
|
367
|
+
}
|
|
368
|
+
return assembleView({
|
|
369
|
+
systemPromptItems: systemItems,
|
|
370
|
+
layerOutputItems: banded.anchorItems,
|
|
371
|
+
historyItems: nonSystemHistory,
|
|
372
|
+
liveLayerItems: banded.liveItems,
|
|
373
|
+
deltaItems: banded.deltaItems,
|
|
374
|
+
tailItems: steeringTail,
|
|
375
|
+
policy: viewPolicy,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Builds the callModel request for one attempt. Cancellation reaches INSIDE
|
|
380
|
+
* the model call, not just the step boundary: the caller's abort signal stops
|
|
381
|
+
* the stream and the tool-round loop mid flight, so `ctx.abort()` /
|
|
382
|
+
* `harness.cancel()` don't wait out a long generation before taking effect
|
|
383
|
+
* (spec 09, Cancellation item 2).
|
|
384
|
+
*/
|
|
385
|
+
function buildModelRequest(params) {
|
|
386
|
+
const { step, ctx, layers } = params;
|
|
387
|
+
const _serverTools = params.serverToolSpecs.length > 0 ? params.serverToolSpecs : undefined;
|
|
388
|
+
const signal = isContextImpl(ctx) ? ctx.abortSignal : undefined;
|
|
389
|
+
if (params.resolvedTools) {
|
|
390
|
+
return {
|
|
391
|
+
model: params.resolvedModel,
|
|
392
|
+
items: params.assembledItems,
|
|
393
|
+
instructions: params.resolvedInstructions,
|
|
394
|
+
tools: params.resolvedTools,
|
|
395
|
+
params: step.params,
|
|
396
|
+
outputSchema: params.outputSchema,
|
|
397
|
+
outputJsonSchema: params.step.outputJsonSchema,
|
|
398
|
+
emit: step.emit,
|
|
399
|
+
_serverTools,
|
|
400
|
+
ctx,
|
|
401
|
+
layers,
|
|
402
|
+
allowedToolNames: params.allowedToolNames,
|
|
403
|
+
nodeId: step.id,
|
|
404
|
+
parentSpan: ctx.span,
|
|
405
|
+
signal,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
model: params.resolvedModel,
|
|
410
|
+
items: params.assembledItems,
|
|
411
|
+
instructions: params.resolvedInstructions,
|
|
412
|
+
params: step.params,
|
|
413
|
+
outputSchema: params.outputSchema,
|
|
414
|
+
outputJsonSchema: params.step.outputJsonSchema,
|
|
415
|
+
emit: step.emit,
|
|
416
|
+
_serverTools,
|
|
417
|
+
nodeId: step.id,
|
|
418
|
+
parentSpan: ctx.span,
|
|
419
|
+
signal,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Records a finished model response on the context: appends response items to
|
|
424
|
+
* the log, publishes `lastStepMeta`, and commits the per-layer usage breakdown
|
|
425
|
+
* (what the model was actually shown, pinned replays included — raw recall
|
|
426
|
+
* output would report content it never saw).
|
|
427
|
+
*/
|
|
428
|
+
function recordTurnOutcome(params) {
|
|
429
|
+
const { ctx, response, banded } = params;
|
|
430
|
+
const toolCalls = [];
|
|
431
|
+
for (const item of response.items) {
|
|
432
|
+
ctx.itemLog.append(item);
|
|
433
|
+
if (isFunctionCall(item)) {
|
|
434
|
+
toolCalls.push(item);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const meta = {
|
|
438
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
439
|
+
usage: response.usage,
|
|
440
|
+
cost: response.cost,
|
|
441
|
+
responseItems: response.items,
|
|
442
|
+
};
|
|
443
|
+
if (isMutableContext(ctx)) {
|
|
444
|
+
ctx.lastStepMeta = meta;
|
|
445
|
+
}
|
|
446
|
+
commitLayerUsage(ctx, computeLayerUsage({
|
|
447
|
+
ctx,
|
|
448
|
+
modelId: params.resolvedModel,
|
|
449
|
+
instructions: params.resolvedInstructions,
|
|
450
|
+
tools: params.resolvedTools,
|
|
451
|
+
recallResults: banded.servedPerLayer,
|
|
452
|
+
serveInfo: banded.serveInfo,
|
|
453
|
+
epoch: banded.epoch,
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
export async function executeCallModel(step, input, ctx, layers) {
|
|
265
457
|
const baseCtx = frameworkCast(ctx);
|
|
266
458
|
const hasLayers = layers !== undefined && layers.length > 0;
|
|
267
459
|
// Resolve lazy step fields up-front. Function-form getters run once per
|
|
@@ -271,49 +463,19 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
271
463
|
if (!resolvedModel || resolvedModel.trim() === '') {
|
|
272
464
|
throw new NoeticConfigError({
|
|
273
465
|
code: 'MISSING_MODEL',
|
|
274
|
-
message: `
|
|
466
|
+
message: `callModel(${JSON.stringify(step.id)}) resolved model to an empty string.`,
|
|
275
467
|
hint: "Ensure `model` (or your `(ctx) => string` getter) returns a non-empty identifier, e.g. 'anthropic/claude-sonnet-4-20250514'.",
|
|
276
468
|
});
|
|
277
469
|
}
|
|
278
470
|
const baseInstructions = await resolveLazy(step.instructions, ctx);
|
|
279
471
|
const { outputCodec, outputSchema, resolvedInstructions } = resolveOutputMode(step.output, baseInstructions);
|
|
280
472
|
const resolvedStepTools = await resolveLazy(step.tools, ctx);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// partitioned so the `[] = opt-out` semantics of step.tools stay intact.
|
|
288
|
-
let clientStepTools;
|
|
289
|
-
const serverToolSpecs = [];
|
|
290
|
-
if (resolvedStepTools !== undefined) {
|
|
291
|
-
clientStepTools = [];
|
|
292
|
-
for (const entry of resolvedStepTools) {
|
|
293
|
-
if (isServerToolSpec(entry)) {
|
|
294
|
-
serverToolSpecs.push(entry);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
clientStepTools.push(entry);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
// Append user input — through layer pipeline if layers exist, otherwise direct.
|
|
302
|
-
// The append pipeline may request a context re-render (e.g. a layer that
|
|
303
|
-
// expanded an input reference into new content); applied after recall below.
|
|
304
|
-
let rerenderRequests = [];
|
|
305
|
-
if (typeof input === 'string' && input.length > 0) {
|
|
306
|
-
if (hasLayers) {
|
|
307
|
-
rerenderRequests = await runInputPipeline({
|
|
308
|
-
ctx: baseCtx,
|
|
309
|
-
layers,
|
|
310
|
-
input,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
baseCtx.itemLog.append(createMessage(input, 'user'));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
473
|
+
const { clientStepTools, serverToolSpecs } = partitionStepTools(resolvedStepTools);
|
|
474
|
+
const rerenderRequests = await appendUserInput({
|
|
475
|
+
ctx: baseCtx,
|
|
476
|
+
layers,
|
|
477
|
+
input,
|
|
478
|
+
});
|
|
317
479
|
const { tools: resolvedTools, allowedToolNames } = resolveToolsAndRestrictions(clientStepTools, layers, baseCtx);
|
|
318
480
|
// Resolve the projection policy (step override > harness default > fallback)
|
|
319
481
|
// and split the context budget across layers via allocateBudgets.
|
|
@@ -326,120 +488,92 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
326
488
|
...policy,
|
|
327
489
|
tokenBudget: Math.max(0, policy.tokenBudget - systemPromptTokens),
|
|
328
490
|
};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
systemPromptTokens,
|
|
335
|
-
responseReserve: policy.responseReserve,
|
|
336
|
-
});
|
|
337
|
-
budgetMap = new Map(allocations.map((a) => [
|
|
338
|
-
a.layerId,
|
|
339
|
-
a.allocated,
|
|
340
|
-
]));
|
|
341
|
-
}
|
|
342
|
-
// Recall once per LLM step: atomic layers run in the hot path; eventual layers
|
|
343
|
-
// are served from cache. Results drive both the assembled context window and
|
|
344
|
-
// the per-layer usage breakdown (ctx.lastLayerUsage). Recall fires before the
|
|
345
|
-
// steering retry loop because retries replay the same context.
|
|
491
|
+
const budgetMap = computeBudgetMap({
|
|
492
|
+
layers,
|
|
493
|
+
policy,
|
|
494
|
+
systemPromptTokens,
|
|
495
|
+
});
|
|
346
496
|
const recallQuery = typeof input === 'string' ? input : '';
|
|
347
497
|
const slotOf = new Map((layers ?? []).map((l) => [
|
|
348
498
|
l.id,
|
|
349
499
|
l.slot,
|
|
350
500
|
]));
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
501
|
+
const recallResults = await gatherRecallResults({
|
|
502
|
+
ctx: baseCtx,
|
|
503
|
+
layers,
|
|
504
|
+
recallQuery,
|
|
505
|
+
budgetMap,
|
|
506
|
+
slotOf,
|
|
507
|
+
rerenderRequests,
|
|
508
|
+
});
|
|
509
|
+
// Split recall output into the cache-stable anchor band and the live band,
|
|
510
|
+
// replaying pinned anchors and gathering any changes into one supersede
|
|
511
|
+
// message. Runs once, outside the retry loop, for the same reason recall does:
|
|
512
|
+
// a retry replays this view rather than building a new one.
|
|
513
|
+
// A harness that predates anchoring carries no store. Give it a throwaway:
|
|
514
|
+
// nothing survives the turn, so its layers render fresh every time — the
|
|
515
|
+
// behaviour it had before the bands existed.
|
|
516
|
+
const cacheStore = baseCtx.harness.contextCache ?? createContextCacheStore();
|
|
517
|
+
const banded = await prepareBandedView({
|
|
518
|
+
recallResults,
|
|
519
|
+
layers: layers ?? [],
|
|
520
|
+
execCtx: contextToExecCtx(baseCtx),
|
|
521
|
+
store: cacheStore,
|
|
522
|
+
config: baseCtx.harness.config.contextCache,
|
|
523
|
+
instructions: resolvedInstructions,
|
|
524
|
+
policy: viewPolicy,
|
|
525
|
+
systemPromptItems: [],
|
|
526
|
+
budgets: budgetMap,
|
|
527
|
+
});
|
|
528
|
+
stampAnchoringAttributes(baseCtx.span, banded);
|
|
368
529
|
let retries = 0;
|
|
530
|
+
// Guidance from a steering retry rides at the very end of the view, after the
|
|
531
|
+
// live band and the supersedes, rather than wherever history happens to put it.
|
|
532
|
+
let steeringTail = [];
|
|
533
|
+
let cacheJudged = false;
|
|
369
534
|
while (retries <= MAX_STEERING_RETRIES) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
assembledItems
|
|
388
|
-
|
|
389
|
-
layerOutputItems,
|
|
390
|
-
historyItems: nonSystemHistory,
|
|
391
|
-
policy: viewPolicy,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
assembledItems =
|
|
396
|
-
projectedHistoryItems === rawHistoryItems
|
|
397
|
-
? rawHistoryItems
|
|
398
|
-
: [
|
|
399
|
-
...projectedHistoryItems,
|
|
400
|
-
];
|
|
401
|
-
}
|
|
402
|
-
const _serverTools = serverToolSpecs.length > 0 ? serverToolSpecs : undefined;
|
|
403
|
-
// Cancellation reaches INSIDE the model call, not just the step boundary:
|
|
404
|
-
// the caller's abort signal stops the stream and the tool-round loop mid
|
|
405
|
-
// flight, so `ctx.abort()` / `harness.cancel()` don't wait out a long
|
|
406
|
-
// generation before taking effect (spec 09, Cancellation item 2).
|
|
407
|
-
const signal = isContextImpl(baseCtx) ? baseCtx.abortSignal : undefined;
|
|
408
|
-
const request = resolvedTools
|
|
409
|
-
? {
|
|
410
|
-
model: resolvedModel,
|
|
411
|
-
items: assembledItems,
|
|
412
|
-
instructions: resolvedInstructions,
|
|
413
|
-
tools: resolvedTools,
|
|
414
|
-
params: step.params,
|
|
415
|
-
outputSchema: outputSchema,
|
|
416
|
-
emit: step.emit,
|
|
417
|
-
_serverTools,
|
|
418
|
-
ctx: baseCtx,
|
|
419
|
-
layers,
|
|
420
|
-
allowedToolNames,
|
|
421
|
-
nodeId: step.id,
|
|
422
|
-
parentSpan: baseCtx.span,
|
|
423
|
-
signal,
|
|
424
|
-
}
|
|
425
|
-
: {
|
|
426
|
-
model: resolvedModel,
|
|
427
|
-
items: assembledItems,
|
|
428
|
-
instructions: resolvedInstructions,
|
|
429
|
-
params: step.params,
|
|
430
|
-
outputSchema: outputSchema,
|
|
431
|
-
emit: step.emit,
|
|
432
|
-
_serverTools,
|
|
433
|
-
nodeId: step.id,
|
|
434
|
-
parentSpan: baseCtx.span,
|
|
435
|
-
signal,
|
|
436
|
-
};
|
|
535
|
+
const assembledItems = await assembleTurnItems({
|
|
536
|
+
ctx: baseCtx,
|
|
537
|
+
layers,
|
|
538
|
+
banded,
|
|
539
|
+
steeringTail,
|
|
540
|
+
viewPolicy,
|
|
541
|
+
});
|
|
542
|
+
const request = buildModelRequest({
|
|
543
|
+
step,
|
|
544
|
+
ctx: baseCtx,
|
|
545
|
+
layers,
|
|
546
|
+
resolvedModel,
|
|
547
|
+
resolvedInstructions,
|
|
548
|
+
resolvedTools,
|
|
549
|
+
allowedToolNames,
|
|
550
|
+
outputSchema,
|
|
551
|
+
serverToolSpecs,
|
|
552
|
+
assembledItems,
|
|
553
|
+
});
|
|
437
554
|
const response = await baseCtx.harness.callModel(request);
|
|
438
555
|
// Track tokens/cost for EVERY model call, including responses steering
|
|
439
556
|
// subsequently rejects (Deny throw / Guide retry below) — the spend is
|
|
440
557
|
// real, and until.maxCost / HarnessResponse.usage must see it (spec 07:
|
|
441
558
|
// tokens accumulate across all LLM calls).
|
|
442
559
|
trackUsage(baseCtx, response);
|
|
560
|
+
// Ask the model's own token report whether the prompt prefix survived. A
|
|
561
|
+
// clear miss records a re-anchor that the next assembly picks up — pins are
|
|
562
|
+
// never touched here, so a response steering later rejects cannot leave the
|
|
563
|
+
// epoch half-rebuilt.
|
|
564
|
+
// Judged once per assembly, not once per call: a steering retry re-sends
|
|
565
|
+
// the same view, so counting its response again would reach the give-up
|
|
566
|
+
// threshold inside a single turn and blind the lineage for good.
|
|
567
|
+
if (banded.epoch && !cacheJudged) {
|
|
568
|
+
cacheJudged = true;
|
|
569
|
+
noteCacheOutcome({
|
|
570
|
+
store: cacheStore,
|
|
571
|
+
key: lineageKey(contextToExecCtx(baseCtx)),
|
|
572
|
+
response,
|
|
573
|
+
config: resolveCacheConfig(baseCtx.harness.config.contextCache),
|
|
574
|
+
expectedTokens: systemPromptTokens + banded.epoch.anchorTokens,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
443
577
|
// An abort that landed mid-call returns whatever rounds completed before
|
|
444
578
|
// the signal fired. The spend is charged above, but a truncated generation
|
|
445
579
|
// is not this step's output — surface cancellation instead.
|
|
@@ -458,34 +592,28 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
458
592
|
});
|
|
459
593
|
}
|
|
460
594
|
if (decision.action === SteeringAction.Guide && retries < MAX_STEERING_RETRIES) {
|
|
461
|
-
|
|
595
|
+
const guidance = createMessage(decision.guidance ?? 'Please adjust your response.', 'developer');
|
|
596
|
+
// Logged so it persists into the conversation, and carried as the tail
|
|
597
|
+
// so the retry's view ends on the correction rather than burying it
|
|
598
|
+
// ahead of the live band. Assembly drops it from history to avoid
|
|
599
|
+
// showing it twice.
|
|
600
|
+
baseCtx.itemLog.append(guidance);
|
|
601
|
+
steeringTail = [
|
|
602
|
+
...steeringTail,
|
|
603
|
+
guidance,
|
|
604
|
+
];
|
|
462
605
|
retries++;
|
|
463
606
|
continue;
|
|
464
607
|
}
|
|
465
608
|
}
|
|
466
|
-
|
|
467
|
-
for (const item of response.items) {
|
|
468
|
-
baseCtx.itemLog.append(item);
|
|
469
|
-
if (isFunctionCall(item)) {
|
|
470
|
-
toolCalls.push(item);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
const meta = {
|
|
474
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
475
|
-
usage: response.usage,
|
|
476
|
-
cost: response.cost,
|
|
477
|
-
responseItems: response.items,
|
|
478
|
-
};
|
|
479
|
-
if (isMutableContext(baseCtx)) {
|
|
480
|
-
baseCtx.lastStepMeta = meta;
|
|
481
|
-
}
|
|
482
|
-
commitLayerUsage(baseCtx, computeLayerUsage({
|
|
609
|
+
recordTurnOutcome({
|
|
483
610
|
ctx: baseCtx,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
611
|
+
response,
|
|
612
|
+
resolvedModel,
|
|
613
|
+
resolvedInstructions,
|
|
614
|
+
resolvedTools,
|
|
615
|
+
banded,
|
|
616
|
+
});
|
|
489
617
|
if (hasLayers) {
|
|
490
618
|
await baseCtx.harness.storeLayers(layers, response, baseCtx);
|
|
491
619
|
}
|
|
@@ -579,7 +707,7 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
|
|
|
579
707
|
// Build unified tool set for child from its step tree + layers, plus the
|
|
580
708
|
// parent's unified tools. Sub-agents that read tools dynamically (via
|
|
581
709
|
// `ctx.unifiedTools`) otherwise lose the harness tool pool at the spawn
|
|
582
|
-
// boundary — a child whose `
|
|
710
|
+
// boundary — a child whose `callModel` resolves `tools` from context would
|
|
583
711
|
// see nothing. Inheriting the parent's tools keeps the harness toolset
|
|
584
712
|
// available across spawns; the child's own step/layer tools take precedence
|
|
585
713
|
// on name collision (dedup keeps the first occurrence), and children that
|
|
@@ -674,7 +802,7 @@ function mergeLayers(existing, provided) {
|
|
|
674
802
|
* Unlike spawn, provide does not create a new itemLog or clone state.
|
|
675
803
|
* Events flow through to the parent in real-time.
|
|
676
804
|
*/
|
|
677
|
-
export async function
|
|
805
|
+
export async function executeWithContext(step, input, ctx, executeStep) {
|
|
678
806
|
const baseCtx = frameworkCast(ctx);
|
|
679
807
|
const previousLayers = baseCtx.layers;
|
|
680
808
|
const previousUnifiedTools = baseCtx.unifiedTools;
|
|
@@ -729,7 +857,7 @@ async function consumeToolGenerator(params) {
|
|
|
729
857
|
event: next.value,
|
|
730
858
|
},
|
|
731
859
|
});
|
|
732
|
-
// A direct `
|
|
860
|
+
// A direct `invokeTool` has no model call id — the step id keys its region.
|
|
733
861
|
emitToolUi({
|
|
734
862
|
ctx: params.ctx,
|
|
735
863
|
tool: params.tool,
|
|
@@ -740,20 +868,21 @@ async function consumeToolGenerator(params) {
|
|
|
740
868
|
});
|
|
741
869
|
}
|
|
742
870
|
}
|
|
743
|
-
export async function
|
|
871
|
+
export async function executeInvokeTool(step, input, ctx, harness, layers) {
|
|
744
872
|
const baseCtx = frameworkCast(ctx);
|
|
745
873
|
const args = step.args ? Object.assign({}, input, step.args) : input;
|
|
746
|
-
const parseResult = step.tool.input
|
|
874
|
+
const parseResult = await validateSchema(step.tool.input, args);
|
|
747
875
|
if (!parseResult.success) {
|
|
748
876
|
throw new NoeticErrorImpl({
|
|
749
877
|
kind: 'step_failed',
|
|
750
878
|
stepId: step.id,
|
|
751
|
-
cause: new Error(`Tool input validation failed: ${parseResult.
|
|
879
|
+
cause: new Error(`Tool input validation failed: ${parseResult.zodError.message}`),
|
|
752
880
|
retriesExhausted: false,
|
|
753
881
|
});
|
|
754
882
|
}
|
|
883
|
+
const parsedArgs = parseResult.value;
|
|
755
884
|
if (layers && layers.length > 0) {
|
|
756
|
-
const decision = await harness.beforeToolCall(layers, step.tool.name,
|
|
885
|
+
const decision = await harness.beforeToolCall(layers, step.tool.name, parsedArgs, baseCtx);
|
|
757
886
|
if (decision.action !== SteeringAction.Allow) {
|
|
758
887
|
throw new NoeticErrorImpl({
|
|
759
888
|
kind: 'steering_denied',
|
|
@@ -766,18 +895,18 @@ export async function executeTool(step, input, ctx, harness, layers) {
|
|
|
766
895
|
tool: step.tool,
|
|
767
896
|
callId: step.id,
|
|
768
897
|
phase: 'call',
|
|
769
|
-
args:
|
|
898
|
+
args: parsedArgs,
|
|
770
899
|
});
|
|
771
900
|
try {
|
|
772
901
|
const toolCtx = buildToolExecutionContext(baseCtx, harness);
|
|
773
|
-
const execResult = step.tool.execute(
|
|
902
|
+
const execResult = step.tool.execute(parsedArgs, toolCtx);
|
|
774
903
|
const result = isAsyncGenerator(execResult)
|
|
775
904
|
? await consumeToolGenerator({
|
|
776
905
|
generator: execResult,
|
|
777
906
|
stepId: step.id,
|
|
778
907
|
tool: step.tool,
|
|
779
908
|
ctx: baseCtx,
|
|
780
|
-
args:
|
|
909
|
+
args: parsedArgs,
|
|
781
910
|
})
|
|
782
911
|
: await execResult;
|
|
783
912
|
emitToolUi({
|
|
@@ -785,7 +914,7 @@ export async function executeTool(step, input, ctx, harness, layers) {
|
|
|
785
914
|
tool: step.tool,
|
|
786
915
|
callId: step.id,
|
|
787
916
|
phase: 'result',
|
|
788
|
-
args:
|
|
917
|
+
args: parsedArgs,
|
|
789
918
|
output: result,
|
|
790
919
|
});
|
|
791
920
|
return frameworkCast(result);
|
|
@@ -796,7 +925,7 @@ export async function executeTool(step, input, ctx, harness, layers) {
|
|
|
796
925
|
tool: step.tool,
|
|
797
926
|
callId: step.id,
|
|
798
927
|
phase: 'error',
|
|
799
|
-
args:
|
|
928
|
+
args: parsedArgs,
|
|
800
929
|
error: e,
|
|
801
930
|
});
|
|
802
931
|
if (e instanceof NoeticErrorImpl) {
|