@noetic-tools/core 5.8.0 → 5.9.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 +1 -0
- package/dist/adapters/openrouter.js +1 -0
- package/dist/builders/provide-builder.d.ts +1 -0
- package/dist/builders/provide-builder.js +1 -0
- package/dist/builders/spawn-builder.d.ts +0 -1
- package/dist/builders/spawn-builder.js +0 -1
- package/dist/builders/step-builders.d.ts +0 -1
- package/dist/builders/step-builders.js +0 -1
- package/dist/builders/tool-builder.d.ts +2 -0
- package/dist/builders/tool-builder.js +2 -0
- package/dist/builders/workflow-hydrator.js +15 -13
- package/dist/builders/workflow-replay-key.d.ts +15 -0
- package/dist/builders/workflow-replay-key.js +46 -0
- package/dist/harness/agent-harness.d.ts +25 -6
- package/dist/harness/agent-harness.js +72 -26
- package/dist/harness/deps/interpreter.d.ts +1 -0
- package/dist/harness/deps/interpreter.js +1 -0
- package/dist/harness/model-call.d.ts +3 -0
- package/dist/harness/model-call.js +9 -5
- package/dist/interpreter/action-deps.d.ts +1 -1
- package/dist/interpreter/action-deps.js +1 -1
- package/dist/interpreter/collect-tools.js +19 -54
- package/dist/interpreter/execute-action.js +10 -2
- package/dist/interpreter/execute-control.js +3 -1
- package/dist/interpreter/execute-decide.js +9 -27
- package/dist/interpreter/execute.js +30 -36
- package/dist/interpreter/model-error.d.ts +9 -0
- package/dist/interpreter/model-error.js +32 -0
- package/dist/interpreter/step-walk.d.ts +11 -0
- package/dist/interpreter/step-walk.js +41 -0
- package/dist/observability/genai-attributes.d.ts +7 -0
- package/dist/observability/genai-attributes.js +8 -0
- package/dist/runtime/context-impl.d.ts +29 -31
- package/dist/runtime/context-impl.js +65 -45
- package/dist/runtime/durable/harness-checkpoints.d.ts +31 -18
- package/dist/runtime/durable/harness-checkpoints.js +70 -54
- package/dist/runtime/durable/step-ledger.d.ts +1 -1
- package/dist/runtime/durable/step-ledger.js +1 -1
- package/dist/schemas/workflow.d.ts +1 -1
- package/package.json +3 -3
- package/schema/noetic-workflow.schema.json +375 -0
|
@@ -77,6 +77,7 @@ export declare function executeToolCall(params: ExecuteToolCallParams): Promise<
|
|
|
77
77
|
* @param apiKey - OpenRouter API key.
|
|
78
78
|
* @param embeddingModel - Model identifier (default: `'openai/text-embedding-3-small'`).
|
|
79
79
|
* @returns An `EmbedFn` that produces embedding vectors for the given texts.
|
|
80
|
+
* @throws `NoeticConfigError` with code `MISSING_API_KEY` if `apiKey` is empty or omitted.
|
|
80
81
|
*/
|
|
81
82
|
export declare function createOpenRouterEmbed(apiKey?: string, embeddingModel?: string): EmbedFn;
|
|
82
83
|
export {};
|
|
@@ -407,6 +407,7 @@ export async function executeToolCall(params) {
|
|
|
407
407
|
* @param apiKey - OpenRouter API key.
|
|
408
408
|
* @param embeddingModel - Model identifier (default: `'openai/text-embedding-3-small'`).
|
|
409
409
|
* @returns An `EmbedFn` that produces embedding vectors for the given texts.
|
|
410
|
+
* @throws `NoeticConfigError` with code `MISSING_API_KEY` if `apiKey` is empty or omitted.
|
|
410
411
|
*/
|
|
411
412
|
export function createOpenRouterEmbed(apiKey, embeddingModel) {
|
|
412
413
|
if (!apiKey) {
|
|
@@ -12,6 +12,7 @@ import type { Step, StepReplay, StepWithContext } from '@noetic-tools/types';
|
|
|
12
12
|
* @returns A `StepWithContext` step.
|
|
13
13
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
14
14
|
* @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
|
|
15
|
+
* @throws `NoeticConfigError` with code `MISSING_CONTEXT_LAYERS` if `context` is not provided.
|
|
15
16
|
*/
|
|
16
17
|
export declare function withContext<TContext = ContextData, I = unknown, O = unknown>(opts: {
|
|
17
18
|
id: string;
|
|
@@ -12,6 +12,7 @@ import { getDefaultRegistrar } from '../types/step-registrar.js';
|
|
|
12
12
|
* @returns A `StepWithContext` step.
|
|
13
13
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
14
14
|
* @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
|
|
15
|
+
* @throws `NoeticConfigError` with code `MISSING_CONTEXT_LAYERS` if `context` is not provided.
|
|
15
16
|
*/
|
|
16
17
|
export function withContext(opts) {
|
|
17
18
|
if (!opts.id?.trim()) {
|
|
@@ -26,7 +26,6 @@ interface SpawnOpts<TContext, I, O> extends StepReplay<TContext, I> {
|
|
|
26
26
|
* can dispatch either by id.
|
|
27
27
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
28
28
|
* @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
|
|
29
|
-
* @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered.
|
|
30
29
|
*/
|
|
31
30
|
export declare function spawn<TContext = ContextData, I = unknown, O = unknown>(opts: SpawnOpts<TContext, I, O>): StepSpawn<TContext, I, O>;
|
|
32
31
|
export {};
|
|
@@ -16,7 +16,6 @@ import { getDefaultRegistrar } from '../types/step-registrar.js';
|
|
|
16
16
|
* can dispatch either by id.
|
|
17
17
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
18
18
|
* @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
|
|
19
|
-
* @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered.
|
|
20
19
|
*/
|
|
21
20
|
export function spawn(opts) {
|
|
22
21
|
if (!opts.id?.trim()) {
|
|
@@ -83,7 +83,6 @@ export interface StepAcpAgentOpts<TContext, O, I = unknown> extends StepReplay<T
|
|
|
83
83
|
* adapter can dispatch it by id.
|
|
84
84
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
85
85
|
* @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
|
|
86
|
-
* @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered with a different body.
|
|
87
86
|
*/
|
|
88
87
|
export declare function runCode<TContext = ContextData, I = unknown, O = unknown>(opts: RunCodeOpts<TContext, I, O>): StepRunCode<TContext, I, O>;
|
|
89
88
|
/**
|
|
@@ -46,7 +46,6 @@ function validateAcpAgentOpts(opts) {
|
|
|
46
46
|
* adapter can dispatch it by id.
|
|
47
47
|
* @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
|
|
48
48
|
* @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
|
|
49
|
-
* @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered with a different body.
|
|
50
49
|
*/
|
|
51
50
|
export function runCode(opts) {
|
|
52
51
|
if (!opts.id || opts.id.trim() === '') {
|
|
@@ -51,6 +51,8 @@ type GeneratorToolConfig<I extends StandardSchemaV1, E extends StandardSchemaV1,
|
|
|
51
51
|
* Accepts Zod, Valibot, or any Standard Schema v1 implementation.
|
|
52
52
|
*
|
|
53
53
|
* @public
|
|
54
|
+
* @throws `NoeticConfigError` with code `EMPTY_TOOL_NAME` if `name` is empty.
|
|
55
|
+
* @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
|
|
54
56
|
*/
|
|
55
57
|
export declare function tool<I extends StandardSchemaV1, O extends StandardSchemaV1>(config: ToolConfig<I, O>): Tool<I, O>;
|
|
56
58
|
/**
|
|
@@ -24,6 +24,8 @@ function validateToolConfig(name, execute) {
|
|
|
24
24
|
* Accepts Zod, Valibot, or any Standard Schema v1 implementation.
|
|
25
25
|
*
|
|
26
26
|
* @public
|
|
27
|
+
* @throws `NoeticConfigError` with code `EMPTY_TOOL_NAME` if `name` is empty.
|
|
28
|
+
* @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
|
|
27
29
|
*/
|
|
28
30
|
export function tool(config) {
|
|
29
31
|
validateToolConfig(config.name, config.execute);
|
|
@@ -16,6 +16,7 @@ import { loop } from './loop-builder.js';
|
|
|
16
16
|
import { withContext } from './provide-builder.js';
|
|
17
17
|
import { spawn } from './spawn-builder.js';
|
|
18
18
|
import { callModel, runCode, step } from './step-builders.js';
|
|
19
|
+
import { workflowReplayKey } from './workflow-replay-key.js';
|
|
19
20
|
//#endregion
|
|
20
21
|
//#region Until Predicate Hydration
|
|
21
22
|
function hydrateUntilPredicate(pred) {
|
|
@@ -99,13 +100,15 @@ function hydrateCallModelNode(node, ctx) {
|
|
|
99
100
|
* runtime still returns the codec's real value. This is the same erasure the
|
|
100
101
|
* rest of the hydrator applies via `frameworkCast`.
|
|
101
102
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
/** A complete sentence naming what is registered, for a hint. */
|
|
104
|
+
function registeredSentence(noun, names) {
|
|
105
|
+
const list = [
|
|
106
|
+
...names,
|
|
107
|
+
];
|
|
108
|
+
if (list.length === 0) {
|
|
109
|
+
return `No ${noun} are registered.`;
|
|
105
110
|
}
|
|
106
|
-
return
|
|
107
|
-
...ctx.uiLibraries.keys(),
|
|
108
|
-
].join(', ');
|
|
111
|
+
return `Registered ${noun}: ${list.join(', ')}.`;
|
|
109
112
|
}
|
|
110
113
|
function resolveOutputCodec(ref, nodeId, ctx) {
|
|
111
114
|
if (!ref) {
|
|
@@ -118,7 +121,7 @@ function resolveOutputCodec(ref, nodeId, ctx) {
|
|
|
118
121
|
throw new NoeticConfigError({
|
|
119
122
|
code: 'UNKNOWN_UI_LIBRARY_REFERENCE',
|
|
120
123
|
message: `UI library '${ref.library}' referenced in workflow node '${nodeId}' is not registered.`,
|
|
121
|
-
hint: `Pass output codecs via HydrationContext.uiLibraries, e.g. new Map([['${ref.library}', openUi(myLibrary)]]) from @noetic-tools/openui.
|
|
124
|
+
hint: `Pass output codecs via HydrationContext.uiLibraries, e.g. new Map([['${ref.library}', openUi(myLibrary)]]) from @noetic-tools/openui. ${registeredSentence('UI libraries', ctx.uiLibraries?.keys() ?? [])}`,
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
function hydrateInvokeToolNode(node, ctx) {
|
|
@@ -130,9 +133,7 @@ function hydrateInvokeToolNode(node, ctx) {
|
|
|
130
133
|
throw new NoeticConfigError({
|
|
131
134
|
code: 'UNKNOWN_TOOL_REFERENCE',
|
|
132
135
|
message: `Tool '${node.toolName}' referenced in workflow node '${node.id}' is not registered.`,
|
|
133
|
-
hint: `
|
|
134
|
-
...ctx.tools.keys(),
|
|
135
|
-
].join(', ') || '(none)'}`,
|
|
136
|
+
hint: `Register '${node.toolName}' in HydrationContext.tools before hydrating. ${registeredSentence('tools', ctx.tools.keys())}`,
|
|
136
137
|
});
|
|
137
138
|
}
|
|
138
139
|
return frameworkCast(runCode({
|
|
@@ -533,9 +534,7 @@ function resolveTools(toolNames, registry) {
|
|
|
533
534
|
throw new NoeticConfigError({
|
|
534
535
|
code: 'UNKNOWN_TOOL_REFERENCE',
|
|
535
536
|
message: `Tool '${name}' referenced in workflow is not registered.`,
|
|
536
|
-
hint: `
|
|
537
|
-
...registry.keys(),
|
|
538
|
-
].join(', ') || '(none)'}`,
|
|
537
|
+
hint: `Register '${name}' in HydrationContext.tools before hydrating. ${registeredSentence('tools', registry.keys())}`,
|
|
539
538
|
});
|
|
540
539
|
}
|
|
541
540
|
return resolved;
|
|
@@ -772,6 +771,9 @@ export function hydrateNode(node, ctx) {
|
|
|
772
771
|
if (node.durable !== undefined) {
|
|
773
772
|
hydrated.durable = node.durable;
|
|
774
773
|
}
|
|
774
|
+
if (node.replayKey !== undefined) {
|
|
775
|
+
hydrated.replayKey = workflowReplayKey(node.id, node.replayKey);
|
|
776
|
+
}
|
|
775
777
|
return hydrated;
|
|
776
778
|
}
|
|
777
779
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WorkflowReplayKey } from '@noetic-tools/types';
|
|
2
|
+
/**
|
|
3
|
+
* Build the `replayKey` function a JSON node's declarative `replayKey` describes
|
|
4
|
+
* (spec 26 § Replay keys). The key is `<node id>:<hash of the selected input>`: the
|
|
5
|
+
* node id keeps two different nodes that read the same input from fighting over one
|
|
6
|
+
* ledger entry, while nodes that share an id — the same worker leaf written into
|
|
7
|
+
* several paths — share entries by design.
|
|
8
|
+
*
|
|
9
|
+
* The interpreter turns a throwing `replayKey` into a warning and runs the step
|
|
10
|
+
* without replay, so the step itself never fails.
|
|
11
|
+
*
|
|
12
|
+
* @throws `NoeticConfigError` with code `INVALID_REPLAY_KEY_INPUT` from the returned
|
|
13
|
+
* function, when `from` meets an input that is not a JSON object.
|
|
14
|
+
*/
|
|
15
|
+
export declare function workflowReplayKey(nodeId: string, spec: WorkflowReplayKey): (input: string) => string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { hashString, NoeticConfigError } from '@noetic-tools/types';
|
|
2
|
+
/**
|
|
3
|
+
* Build the `replayKey` function a JSON node's declarative `replayKey` describes
|
|
4
|
+
* (spec 26 § Replay keys). The key is `<node id>:<hash of the selected input>`: the
|
|
5
|
+
* node id keeps two different nodes that read the same input from fighting over one
|
|
6
|
+
* ledger entry, while nodes that share an id — the same worker leaf written into
|
|
7
|
+
* several paths — share entries by design.
|
|
8
|
+
*
|
|
9
|
+
* The interpreter turns a throwing `replayKey` into a warning and runs the step
|
|
10
|
+
* without replay, so the step itself never fails.
|
|
11
|
+
*
|
|
12
|
+
* @throws `NoeticConfigError` with code `INVALID_REPLAY_KEY_INPUT` from the returned
|
|
13
|
+
* function, when `from` meets an input that is not a JSON object.
|
|
14
|
+
*/
|
|
15
|
+
export function workflowReplayKey(nodeId, spec) {
|
|
16
|
+
if (spec === true) {
|
|
17
|
+
return (input) => `${nodeId}:${hashString(asText(input))}`;
|
|
18
|
+
}
|
|
19
|
+
return (input) => {
|
|
20
|
+
const fields = inputObject(input, nodeId);
|
|
21
|
+
return `${nodeId}:${hashString(JSON.stringify(spec.from.map((key) => fields[key] ?? null)))}`;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** JSON nodes pass strings, but a `decide` answer can arrive as an object. */
|
|
25
|
+
function asText(input) {
|
|
26
|
+
return typeof input === 'string' ? input : JSON.stringify(input);
|
|
27
|
+
}
|
|
28
|
+
function inputObject(input, nodeId) {
|
|
29
|
+
const parsed = typeof input === 'string' ? parseJson(input) : input;
|
|
30
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
31
|
+
throw new NoeticConfigError({
|
|
32
|
+
code: 'INVALID_REPLAY_KEY_INPUT',
|
|
33
|
+
message: `Node '${nodeId}' keys replay on input properties, but its input is not a JSON object.`,
|
|
34
|
+
hint: 'Use `"replayKey": true` to key on the whole input, or feed the node a JSON object.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return Object.fromEntries(Object.entries(parsed));
|
|
38
|
+
}
|
|
39
|
+
function parseJson(text) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(text);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -223,6 +223,12 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
223
223
|
private readonly initializedExecutions;
|
|
224
224
|
/** Executions restored from a checkpoint: their first layer init keeps the restored state. */
|
|
225
225
|
private readonly restoredExecutions;
|
|
226
|
+
/**
|
|
227
|
+
* Layer hooks still running per context. `cancel()` waits for them before
|
|
228
|
+
* `onComplete`/`dispose`, so teardown never races a half-finished `store`
|
|
229
|
+
* or `recall`. Each hook is bounded by its own timeout.
|
|
230
|
+
*/
|
|
231
|
+
private readonly runningHooks;
|
|
226
232
|
readonly traceExporter: TraceExporter;
|
|
227
233
|
/**
|
|
228
234
|
* Long-lived shared cwd state. The same reference is seeded into every
|
|
@@ -261,6 +267,13 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
261
267
|
decide(request: DecideRequest): Promise<DecideResponse>;
|
|
262
268
|
notifyDecision(layers: ContextLayer[], record: DecisionRecord, ctx: Context): Promise<void>;
|
|
263
269
|
run<I, O>(s: Step<ContextData, I, O>, input: I, ctx: Context): Promise<O>;
|
|
270
|
+
/**
|
|
271
|
+
* Whether `step` calls a model this harness has no credentials for. Checked
|
|
272
|
+
* before any step runs so a missing provider fails fast instead of after
|
|
273
|
+
* earlier steps' side effects. Steps the walk cannot see (dynamic branches)
|
|
274
|
+
* still hit the same error lazily at their model call.
|
|
275
|
+
*/
|
|
276
|
+
private lacksModelFor;
|
|
264
277
|
/** Run all layer `init` hooks before the first step executes. Per spec 11,
|
|
265
278
|
* init MUST complete before any recall fires so layer state is populated. */
|
|
266
279
|
private initAndRun;
|
|
@@ -353,8 +366,11 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
353
366
|
*/
|
|
354
367
|
checkpoint(ctx: Context): Promise<void>;
|
|
355
368
|
/**
|
|
356
|
-
* Rebuild a `Context` from a
|
|
357
|
-
*
|
|
369
|
+
* Rebuild a `Context` from what a previous run of `executionId` recorded: its
|
|
370
|
+
* checkpoint snapshot, its step-completion ledger, or both. Without a snapshot the
|
|
371
|
+
* context starts empty (no item log or layer state) but keeps the execution id and
|
|
372
|
+
* ledger, so recorded steps replay and the rest run fresh. Returns `null` when
|
|
373
|
+
* neither is recorded — start a fresh execution. Surface a typed
|
|
358
374
|
* `NoeticConfigError(CHECKPOINT_SCHEMA_MISMATCH)` when the snapshot's
|
|
359
375
|
* schema version is unrecognised — the caller is expected to discard the
|
|
360
376
|
* checkpoint via `CheckpointStore.clear()` and start a fresh execution.
|
|
@@ -401,15 +417,16 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
401
417
|
* `cancelled`.
|
|
402
418
|
*
|
|
403
419
|
* Cancellation is cooperative: a layer hook already in flight is allowed to
|
|
404
|
-
* settle, and step code that ignores `ctx.aborted` between `await`s runs to
|
|
420
|
+
* settle (up to its timeout) before that context's teardown, and step code that ignores `ctx.aborted` between `await`s runs to
|
|
405
421
|
* its next boundary. Calling `cancel()` on an already-cancelled context is a
|
|
406
422
|
* no-op.
|
|
407
423
|
*/
|
|
408
424
|
cancel(ctx: Context, reason?: string): Promise<void>;
|
|
409
425
|
/**
|
|
410
|
-
* Run the layer lifecycle's abort path for one cancelled context:
|
|
411
|
-
*
|
|
412
|
-
*
|
|
426
|
+
* Run the layer lifecycle's abort path for one cancelled context: wait for
|
|
427
|
+
* its in-flight layer hooks to settle, then `onComplete` with
|
|
428
|
+
* `outcome: 'aborted'`, then `dispose`. Both always run under cancellation
|
|
429
|
+
* (spec 09, Cancellation item 6).
|
|
413
430
|
*/
|
|
414
431
|
private teardownCancelledContext;
|
|
415
432
|
/**
|
|
@@ -425,6 +442,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
425
442
|
setLayerState<T>(executionId: string, layerId: string, state: T): void;
|
|
426
443
|
beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context, callId?: string): Promise<SteeringDecision>;
|
|
427
444
|
afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
|
|
445
|
+
/** Record `hook` as running on `ctx` until it settles (see `runningHooks`). */
|
|
446
|
+
private trackHook;
|
|
428
447
|
projectHistory(layers: ContextLayer[], items: ReadonlyArray<Item>, ctx: Context): Promise<ReadonlyArray<Item>>;
|
|
429
448
|
runAppendPipeline(layers: ContextLayer[], items: Item[], ctx: Context): Promise<{
|
|
430
449
|
items: Item[];
|
|
@@ -6,10 +6,10 @@ import { AcpSessionStore } from '../runtime/acp-session-store.js';
|
|
|
6
6
|
import { callDecide } from './decide-call.js';
|
|
7
7
|
import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, OpenRouterSdk, } from './deps/adapters.js';
|
|
8
8
|
import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, notifyDecision, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
|
|
9
|
-
import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
|
|
9
|
+
import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, someStep, } from './deps/interpreter.js';
|
|
10
10
|
import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearCheckpoint, collectContextTree, createInMemoryStorage, createStepLedgerStore, filterReasoningStream, filterTextStream, resolveStepLedgerRetention, restoreFromCheckpoint, rootIdOf, SessionRunner, StepLedger, snapshotCwdState, warnToConsole, } from './deps/runtime.js';
|
|
11
11
|
import { SteeringAction } from './deps/types.js';
|
|
12
|
-
import { AgentHarnessModelCaller } from './model-call.js';
|
|
12
|
+
import { AgentHarnessModelCaller, noLlmProviderError } from './model-call.js';
|
|
13
13
|
export { createStreamIdleWatchdog } from './model-call.js';
|
|
14
14
|
import { buildItemSchemaRegistry } from './model-schema.js';
|
|
15
15
|
//#endregion
|
|
@@ -275,6 +275,12 @@ export class AgentHarness {
|
|
|
275
275
|
initializedExecutions = new Set();
|
|
276
276
|
/** Executions restored from a checkpoint: their first layer init keeps the restored state. */
|
|
277
277
|
restoredExecutions = new Set();
|
|
278
|
+
/**
|
|
279
|
+
* Layer hooks still running per context. `cancel()` waits for them before
|
|
280
|
+
* `onComplete`/`dispose`, so teardown never races a half-finished `store`
|
|
281
|
+
* or `recall`. Each hook is bounded by its own timeout.
|
|
282
|
+
*/
|
|
283
|
+
runningHooks = new WeakMap();
|
|
278
284
|
traceExporter;
|
|
279
285
|
/**
|
|
280
286
|
* Long-lived shared cwd state. The same reference is seeded into every
|
|
@@ -363,6 +369,9 @@ export class AgentHarness {
|
|
|
363
369
|
hint: 'Pass `agentGraph` in constructor options, or use run() directly.',
|
|
364
370
|
}));
|
|
365
371
|
}
|
|
372
|
+
if (this.lacksModelFor(this.agentGraph)) {
|
|
373
|
+
return Promise.reject(noLlmProviderError());
|
|
374
|
+
}
|
|
366
375
|
const threadId = options?.threadId ?? DEFAULT_THREAD_ID;
|
|
367
376
|
const deliveryMode = options?.deliveryMode ?? this.defaultDeliveryMode;
|
|
368
377
|
const session = this.getOrCreateSession(threadId);
|
|
@@ -552,16 +561,19 @@ export class AgentHarness {
|
|
|
552
561
|
});
|
|
553
562
|
}
|
|
554
563
|
async notifyDecision(layers, record, ctx) {
|
|
555
|
-
await notifyDecision({
|
|
564
|
+
await this.trackHook(ctx, notifyDecision({
|
|
556
565
|
layers,
|
|
557
566
|
record,
|
|
558
567
|
ctx: this.toExecCtx(ctx),
|
|
559
568
|
store: this.layerStateStore,
|
|
560
569
|
recallCache: this.recallCache,
|
|
561
|
-
});
|
|
570
|
+
}));
|
|
562
571
|
}
|
|
563
572
|
//#endregion
|
|
564
573
|
async run(s, input, ctx) {
|
|
574
|
+
if (this.lacksModelFor(s)) {
|
|
575
|
+
throw noLlmProviderError();
|
|
576
|
+
}
|
|
565
577
|
// Populate the unified tool pool when an embedder drives a step directly on
|
|
566
578
|
// a bare `createContext()` context (the turn pipeline does this per turn).
|
|
567
579
|
// Without it, steps that resolve tools dynamically from `ctx.unifiedTools`
|
|
@@ -578,6 +590,18 @@ export class AgentHarness {
|
|
|
578
590
|
await this.ensureLayersInit(ctx);
|
|
579
591
|
return this.executeClosingChannels(s, input, ctx);
|
|
580
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Whether `step` calls a model this harness has no credentials for. Checked
|
|
595
|
+
* before any step runs so a missing provider fails fast instead of after
|
|
596
|
+
* earlier steps' side effects. Steps the walk cannot see (dynamic branches)
|
|
597
|
+
* still hit the same error lazily at their model call.
|
|
598
|
+
*/
|
|
599
|
+
lacksModelFor(step) {
|
|
600
|
+
if (this.client || this.callModelOverride) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
return someStep(frameworkCast(step), (s) => s.kind === 'callModel' || (s.kind === 'decide' && !this.decideOverride));
|
|
604
|
+
}
|
|
581
605
|
/** Run all layer `init` hooks before the first step executes. Per spec 11,
|
|
582
606
|
* init MUST complete before any recall fires so layer state is populated. */
|
|
583
607
|
async initAndRun(s, input, ctx) {
|
|
@@ -746,7 +770,7 @@ export class AgentHarness {
|
|
|
746
770
|
});
|
|
747
771
|
}
|
|
748
772
|
async recallLayers(layers, input, ctx) {
|
|
749
|
-
return recallLayers({
|
|
773
|
+
return this.trackHook(ctx, recallLayers({
|
|
750
774
|
layers,
|
|
751
775
|
query: input,
|
|
752
776
|
ctx: this.toExecCtx(ctx),
|
|
@@ -754,7 +778,7 @@ export class AgentHarness {
|
|
|
754
778
|
budgets: this.layerBudgets(layers),
|
|
755
779
|
store: this.layerStateStore,
|
|
756
780
|
itemSchemas: this.itemSchemas,
|
|
757
|
-
});
|
|
781
|
+
}));
|
|
758
782
|
}
|
|
759
783
|
/** Allocate per-layer recall budgets from the harness projection policy. */
|
|
760
784
|
layerBudgets(layers) {
|
|
@@ -771,7 +795,7 @@ export class AgentHarness {
|
|
|
771
795
|
]));
|
|
772
796
|
}
|
|
773
797
|
async recallLayersAtomic(layers, input, ctx, budgets, opts) {
|
|
774
|
-
return recallLayersAtomic({
|
|
798
|
+
return this.trackHook(ctx, recallLayersAtomic({
|
|
775
799
|
layers,
|
|
776
800
|
query: input,
|
|
777
801
|
ctx: this.toExecCtx(ctx),
|
|
@@ -781,10 +805,10 @@ export class AgentHarness {
|
|
|
781
805
|
itemSchemas: this.itemSchemas,
|
|
782
806
|
forceAtomic: this.config.forceAtomicRecall,
|
|
783
807
|
peek: opts?.peek,
|
|
784
|
-
});
|
|
808
|
+
}));
|
|
785
809
|
}
|
|
786
810
|
async recallLayersEventual(layers, input, ctx, budgets, opts) {
|
|
787
|
-
return recallLayersEventual({
|
|
811
|
+
return this.trackHook(ctx, recallLayersEventual({
|
|
788
812
|
layers,
|
|
789
813
|
query: input,
|
|
790
814
|
ctx: this.toExecCtx(ctx),
|
|
@@ -795,7 +819,7 @@ export class AgentHarness {
|
|
|
795
819
|
forceAtomic: this.config.forceAtomicRecall,
|
|
796
820
|
cache: this.recallCache,
|
|
797
821
|
peek: opts?.peek,
|
|
798
|
-
});
|
|
822
|
+
}));
|
|
799
823
|
}
|
|
800
824
|
/**
|
|
801
825
|
* Compute the items array that would be sent to the model on the next turn —
|
|
@@ -870,7 +894,7 @@ export class AgentHarness {
|
|
|
870
894
|
if (!storage) {
|
|
871
895
|
return;
|
|
872
896
|
}
|
|
873
|
-
await storeLayers({
|
|
897
|
+
await this.trackHook(ctx, storeLayers({
|
|
874
898
|
layers,
|
|
875
899
|
response,
|
|
876
900
|
ctx: this.toExecCtx(ctx),
|
|
@@ -878,7 +902,7 @@ export class AgentHarness {
|
|
|
878
902
|
store: this.layerStateStore,
|
|
879
903
|
storage,
|
|
880
904
|
recallCache: this.recallCache,
|
|
881
|
-
});
|
|
905
|
+
}));
|
|
882
906
|
}
|
|
883
907
|
async disposeLayers(layers, ctx) {
|
|
884
908
|
// Drop the init guard so a deliberate dispose→run cycle re-hydrates from
|
|
@@ -914,8 +938,11 @@ export class AgentHarness {
|
|
|
914
938
|
return captureCheckpoint(this, ctx);
|
|
915
939
|
}
|
|
916
940
|
/**
|
|
917
|
-
* Rebuild a `Context` from a
|
|
918
|
-
*
|
|
941
|
+
* Rebuild a `Context` from what a previous run of `executionId` recorded: its
|
|
942
|
+
* checkpoint snapshot, its step-completion ledger, or both. Without a snapshot the
|
|
943
|
+
* context starts empty (no item log or layer state) but keeps the execution id and
|
|
944
|
+
* ledger, so recorded steps replay and the rest run fresh. Returns `null` when
|
|
945
|
+
* neither is recorded — start a fresh execution. Surface a typed
|
|
919
946
|
* `NoeticConfigError(CHECKPOINT_SCHEMA_MISMATCH)` when the snapshot's
|
|
920
947
|
* schema version is unrecognised — the caller is expected to discard the
|
|
921
948
|
* checkpoint via `CheckpointStore.clear()` and start a fresh execution.
|
|
@@ -933,11 +960,14 @@ export class AgentHarness {
|
|
|
933
960
|
* whose `id` is already the original `executionId`.
|
|
934
961
|
*/
|
|
935
962
|
async restore(executionId, opts) {
|
|
936
|
-
const
|
|
937
|
-
if (
|
|
938
|
-
|
|
963
|
+
const restored = await restoreFromCheckpoint(this, executionId, opts);
|
|
964
|
+
if (!restored) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
if (restored.fromSnapshot) {
|
|
968
|
+
this.restoredExecutions.add(executionId);
|
|
939
969
|
}
|
|
940
|
-
return ctx;
|
|
970
|
+
return restored.ctx;
|
|
941
971
|
}
|
|
942
972
|
/**
|
|
943
973
|
* Discard everything `restore(executionId)` would have used — the snapshot *and* the
|
|
@@ -971,7 +1001,7 @@ export class AgentHarness {
|
|
|
971
1001
|
* `cancelled`.
|
|
972
1002
|
*
|
|
973
1003
|
* Cancellation is cooperative: a layer hook already in flight is allowed to
|
|
974
|
-
* settle, and step code that ignores `ctx.aborted` between `await`s runs to
|
|
1004
|
+
* settle (up to its timeout) before that context's teardown, and step code that ignores `ctx.aborted` between `await`s runs to
|
|
975
1005
|
* its next boundary. Calling `cancel()` on an already-cancelled context is a
|
|
976
1006
|
* no-op.
|
|
977
1007
|
*/
|
|
@@ -993,15 +1023,17 @@ export class AgentHarness {
|
|
|
993
1023
|
}
|
|
994
1024
|
}
|
|
995
1025
|
/**
|
|
996
|
-
* Run the layer lifecycle's abort path for one cancelled context:
|
|
997
|
-
*
|
|
998
|
-
*
|
|
1026
|
+
* Run the layer lifecycle's abort path for one cancelled context: wait for
|
|
1027
|
+
* its in-flight layer hooks to settle, then `onComplete` with
|
|
1028
|
+
* `outcome: 'aborted'`, then `dispose`. Both always run under cancellation
|
|
1029
|
+
* (spec 09, Cancellation item 6).
|
|
999
1030
|
*/
|
|
1000
1031
|
async teardownCancelledContext(ctx) {
|
|
1001
1032
|
const layers = ctx.layers ?? [];
|
|
1002
1033
|
if (layers.length === 0) {
|
|
1003
1034
|
return;
|
|
1004
1035
|
}
|
|
1036
|
+
await Promise.allSettled(this.runningHooks.get(ctx) ?? []);
|
|
1005
1037
|
await completeLayers({
|
|
1006
1038
|
layers: [
|
|
1007
1039
|
...layers,
|
|
@@ -1054,14 +1086,14 @@ export class AgentHarness {
|
|
|
1054
1086
|
action: SteeringAction.Allow,
|
|
1055
1087
|
};
|
|
1056
1088
|
}
|
|
1057
|
-
return beforeToolCallLayers({
|
|
1089
|
+
return this.trackHook(ctx, beforeToolCallLayers({
|
|
1058
1090
|
layers,
|
|
1059
1091
|
toolName,
|
|
1060
1092
|
toolArgs,
|
|
1061
1093
|
callId,
|
|
1062
1094
|
ctx: this.toExecCtx(ctx),
|
|
1063
1095
|
store: this.layerStateStore,
|
|
1064
|
-
});
|
|
1096
|
+
}));
|
|
1065
1097
|
}
|
|
1066
1098
|
async afterModelCall(layers, response, ctx) {
|
|
1067
1099
|
const hasHook = layers.some((l) => l.hooks.afterModelCall);
|
|
@@ -1070,12 +1102,26 @@ export class AgentHarness {
|
|
|
1070
1102
|
action: SteeringAction.Allow,
|
|
1071
1103
|
};
|
|
1072
1104
|
}
|
|
1073
|
-
return afterModelCallLayers({
|
|
1105
|
+
return this.trackHook(ctx, afterModelCallLayers({
|
|
1074
1106
|
layers,
|
|
1075
1107
|
response,
|
|
1076
1108
|
ctx: this.toExecCtx(ctx),
|
|
1077
1109
|
store: this.layerStateStore,
|
|
1078
|
-
});
|
|
1110
|
+
}));
|
|
1111
|
+
}
|
|
1112
|
+
/** Record `hook` as running on `ctx` until it settles (see `runningHooks`). */
|
|
1113
|
+
trackHook(ctx, hook) {
|
|
1114
|
+
let running = this.runningHooks.get(ctx);
|
|
1115
|
+
if (!running) {
|
|
1116
|
+
running = new Set();
|
|
1117
|
+
this.runningHooks.set(ctx, running);
|
|
1118
|
+
}
|
|
1119
|
+
running.add(hook);
|
|
1120
|
+
const forget = () => {
|
|
1121
|
+
running.delete(hook);
|
|
1122
|
+
};
|
|
1123
|
+
hook.then(forget, forget);
|
|
1124
|
+
return hook;
|
|
1079
1125
|
}
|
|
1080
1126
|
async projectHistory(layers, items, ctx) {
|
|
1081
1127
|
const hasHook = layers.some((l) => l.hooks.projectHistory);
|
|
@@ -3,4 +3,5 @@ export { prepareBandedView } from '../../interpreter/context-assembly.js';
|
|
|
3
3
|
export { execute } from '../../interpreter/execute.js';
|
|
4
4
|
export type { DetachedSpawnOverrides } from '../../interpreter/step-dispatcher.js';
|
|
5
5
|
export { dispatchStepThroughAdapter } from '../../interpreter/step-dispatcher.js';
|
|
6
|
+
export { someStep } from '../../interpreter/step-walk.js';
|
|
6
7
|
export { isFunctionCall } from '../../interpreter/typeguards.js';
|
|
@@ -2,4 +2,5 @@ export { collectAllTools, deduplicateTools } from '../../interpreter/collect-too
|
|
|
2
2
|
export { prepareBandedView } from '../../interpreter/context-assembly.js';
|
|
3
3
|
export { execute } from '../../interpreter/execute.js';
|
|
4
4
|
export { dispatchStepThroughAdapter } from '../../interpreter/step-dispatcher.js';
|
|
5
|
+
export { someStep } from '../../interpreter/step-walk.js';
|
|
5
6
|
export { isFunctionCall } from '../../interpreter/typeguards.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentHarnessContract, CallModelRequest, ItemSchemaRegistry, LLMResponse, TraceExporter } from '@noetic-tools/types';
|
|
2
|
+
import { NoeticConfigError } from '@noetic-tools/types';
|
|
2
3
|
import type { OpenRouter } from '@openrouter/agent';
|
|
3
4
|
import type { MessageQueue } from '../runtime/message-queue.js';
|
|
4
5
|
interface StreamIdleWatchdog {
|
|
@@ -7,6 +8,8 @@ interface StreamIdleWatchdog {
|
|
|
7
8
|
/** Clear the pending timer. Safe to call multiple times. */
|
|
8
9
|
stop: () => void;
|
|
9
10
|
}
|
|
11
|
+
/** @internal The error for a model call on a harness with no LLM credentials. */
|
|
12
|
+
export declare function noLlmProviderError(): NoeticConfigError;
|
|
10
13
|
/** @internal Create a watchdog that aborts `controller` when no
|
|
11
14
|
* {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
|
|
12
15
|
* `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
|
|
@@ -133,6 +133,14 @@ async function pipeStreamEventsToBroadcaster(opts) {
|
|
|
133
133
|
throw err;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
/** @internal The error for a model call on a harness with no LLM credentials. */
|
|
137
|
+
export function noLlmProviderError() {
|
|
138
|
+
return new NoeticConfigError({
|
|
139
|
+
code: 'NO_LLM_PROVIDER',
|
|
140
|
+
message: 'No LLM provider configured on this harness.',
|
|
141
|
+
hint: 'Pass `callModelDefaults: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `callModelDefaults: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
136
144
|
/** @internal Create a watchdog that aborts `controller` when no
|
|
137
145
|
* {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
|
|
138
146
|
* `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
|
|
@@ -354,11 +362,7 @@ export class AgentHarnessModelCaller {
|
|
|
354
362
|
if (this.opts.client) {
|
|
355
363
|
return this.opts.client;
|
|
356
364
|
}
|
|
357
|
-
throw
|
|
358
|
-
code: 'NO_LLM_PROVIDER',
|
|
359
|
-
message: 'No LLM provider configured on this harness.',
|
|
360
|
-
hint: 'Pass `callModelDefaults: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `callModelDefaults: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
|
|
361
|
-
});
|
|
365
|
+
throw noLlmProviderError();
|
|
362
366
|
}
|
|
363
367
|
async callModel(request) {
|
|
364
368
|
if (this.opts.callModelOverride) {
|
|
@@ -3,6 +3,6 @@ export { allocateBudgets, assembleView, churnFor, churnRate, commitLayerUsage, c
|
|
|
3
3
|
export type { ItemSchemaRegistry } from '@noetic-tools/types';
|
|
4
4
|
export { defaultItemSchemaRegistry } from '@noetic-tools/types';
|
|
5
5
|
export { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
6
|
-
export { ContextImpl } from '../runtime/context-impl.js';
|
|
6
|
+
export { ContextImpl, laneBase } from '../runtime/context-impl.js';
|
|
7
7
|
export { snapshotCwdState } from '../runtime/cwd-helpers.js';
|
|
8
8
|
export { buildToolExecutionContext } from '../runtime/tool-context.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { allocateBudgets, assembleView, churnFor, churnRate, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DECISION_PROJECTION, DEFAULT_PROJECTION, lineageKey, noteCacheOutcome, openEpoch, pin, pinKey, reanchorReason, resolveCacheConfig, resolveLayerTools, returnLayers, spawnLayers, } from '@noetic-tools/context';
|
|
2
2
|
export { defaultItemSchemaRegistry } from '@noetic-tools/types';
|
|
3
3
|
export { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
4
|
-
export { ContextImpl } from '../runtime/context-impl.js';
|
|
4
|
+
export { ContextImpl, laneBase } from '../runtime/context-impl.js';
|
|
5
5
|
export { snapshotCwdState } from '../runtime/cwd-helpers.js';
|
|
6
6
|
export { buildToolExecutionContext } from '../runtime/tool-context.js';
|