@noetic-tools/core 5.7.0 → 5.8.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/builders/channel-builder.d.ts +18 -0
- package/dist/builders/control-flow-builders.d.ts +17 -30
- package/dist/builders/decide-builders.d.ts +5 -5
- package/dist/builders/decide-builders.js +4 -0
- package/dist/builders/every.d.ts +2 -2
- package/dist/builders/every.js +2 -0
- package/dist/builders/loop-builder.d.ts +1 -1
- package/dist/builders/loop-builder.js +1 -1
- package/dist/builders/provide-builder.d.ts +2 -2
- package/dist/builders/provide-builder.js +2 -0
- package/dist/builders/spawn-builder.d.ts +2 -2
- package/dist/builders/spawn-builder.js +2 -0
- package/dist/builders/step-builders.d.ts +8 -8
- package/dist/builders/workflow-hydrator.js +6 -1
- package/dist/harness/agent-harness.d.ts +18 -5
- package/dist/harness/agent-harness.js +49 -14
- package/dist/harness/deps/runtime.d.ts +3 -2
- package/dist/harness/deps/runtime.js +2 -2
- package/dist/harness/model-call.js +10 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +3 -3
- package/dist/interpreter/child-span.d.ts +17 -0
- package/dist/interpreter/child-span.js +51 -0
- package/dist/interpreter/execute-action.js +21 -16
- package/dist/interpreter/execute-control.js +64 -8
- package/dist/interpreter/execute.d.ts +10 -0
- package/dist/interpreter/execute.js +41 -12
- package/dist/observability/genai-attributes.d.ts +45 -29
- package/dist/observability/genai-attributes.js +19 -1
- package/dist/runtime/channel-store.d.ts +73 -1
- package/dist/runtime/channel-store.js +179 -63
- package/dist/runtime/context-impl.d.ts +7 -1
- package/dist/runtime/context-impl.js +15 -0
- package/dist/runtime/durable/harness-checkpoints.d.ts +5 -1
- package/dist/runtime/durable/harness-checkpoints.js +8 -2
- package/dist/runtime/durable/step-ledger.d.ts +14 -4
- package/dist/runtime/durable/step-ledger.js +22 -5
- package/dist/types/checkpoint.d.ts +31 -0
- package/dist/types/checkpoint.js +29 -0
- package/dist/until/combinators.d.ts +10 -0
- package/dist/until/combinators.js +37 -13
- package/dist/until/predicates.js +4 -2
- package/package.json +3 -3
- package/schema/noetic-workflow.schema.json +57 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spans for child contexts (`spawn` children and `inParallel` paths). A child
|
|
3
|
+
* context carries its own span, so events its steps add to `ctx.span` (e.g.
|
|
4
|
+
* `loop.verdict`) and the model/tool spans they open nest under the child
|
|
5
|
+
* rather than being lost to a no-op span.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context, Span } from '@noetic-tools/types';
|
|
8
|
+
/** Opens a span named `name` under `parent.span`, stamped with the owning step id. */
|
|
9
|
+
export declare function openChildSpan(parent: Context, name: string, stepId: string): Span;
|
|
10
|
+
/**
|
|
11
|
+
* Runs `run` inside `span`, then ends the span and exports it, whether `run`
|
|
12
|
+
* returns or throws. A failure stamps `noetic.error.kind`; any failure but
|
|
13
|
+
* cancellation also stamps `error: true`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function traceChild<T>(parent: Context, span: Span, run: () => Promise<T>): Promise<T>;
|
|
16
|
+
/** Ends a span whose child never ran: it was cancelled before it started. */
|
|
17
|
+
export declare function closeUnstartedSpan(parent: Context, span: Span): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spans for child contexts (`spawn` children and `inParallel` paths). A child
|
|
3
|
+
* context carries its own span, so events its steps add to `ctx.span` (e.g.
|
|
4
|
+
* `loop.verdict`) and the model/tool spans they open nest under the child
|
|
5
|
+
* rather than being lost to a no-op span.
|
|
6
|
+
*/
|
|
7
|
+
import { isNoeticError } from '@noetic-tools/types';
|
|
8
|
+
import { NoeticAttr } from '../observability/genai-attributes.js';
|
|
9
|
+
/** Opens a span named `name` under `parent.span`, stamped with the owning step id. */
|
|
10
|
+
export function openChildSpan(parent, name, stepId) {
|
|
11
|
+
const span = parent.harness.createSpan(name, parent.span);
|
|
12
|
+
span.setAttribute(NoeticAttr.NODE_ID, stepId);
|
|
13
|
+
return span;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs `run` inside `span`, then ends the span and exports it, whether `run`
|
|
17
|
+
* returns or throws. A failure stamps `noetic.error.kind`; any failure but
|
|
18
|
+
* cancellation also stamps `error: true`.
|
|
19
|
+
*/
|
|
20
|
+
export async function traceChild(parent, span, run) {
|
|
21
|
+
try {
|
|
22
|
+
return await run();
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
markFailed(span, e);
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await closeChildSpan(parent, span);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Ends a span whose child never ran: it was cancelled before it started. */
|
|
33
|
+
export async function closeUnstartedSpan(parent, span) {
|
|
34
|
+
span.setAttribute(NoeticAttr.ERROR_KIND, 'cancelled');
|
|
35
|
+
await closeChildSpan(parent, span);
|
|
36
|
+
}
|
|
37
|
+
function markFailed(span, error) {
|
|
38
|
+
const kind = isNoeticError(error) ? error.noeticError.kind : undefined;
|
|
39
|
+
if (kind !== undefined) {
|
|
40
|
+
span.setAttribute(NoeticAttr.ERROR_KIND, kind);
|
|
41
|
+
}
|
|
42
|
+
if (kind !== 'cancelled') {
|
|
43
|
+
span.setAttribute('error', true);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function closeChildSpan(parent, span) {
|
|
47
|
+
span.end();
|
|
48
|
+
await parent.harness.traceExporter.export([
|
|
49
|
+
span,
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
@@ -5,6 +5,7 @@ import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isN
|
|
|
5
5
|
import { emitToolUi } from '../runtime/tool-ui.js';
|
|
6
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';
|
|
7
7
|
import { isServerToolSpec, SteeringAction } from './action-types.js';
|
|
8
|
+
import { openChildSpan, traceChild } from './child-span.js';
|
|
8
9
|
import { cloneWithGuard } from './clone-guard.js';
|
|
9
10
|
import { collectAllTools, deduplicateTools } from './collect-tools.js';
|
|
10
11
|
import { prepareBandedView, stampAnchoringAttributes } from './context-assembly.js';
|
|
@@ -433,6 +434,7 @@ function recordTurnOutcome(params) {
|
|
|
433
434
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
434
435
|
usage: response.usage,
|
|
435
436
|
cost: response.cost,
|
|
437
|
+
serviceTier: response.serviceTier,
|
|
436
438
|
responseItems: response.items,
|
|
437
439
|
};
|
|
438
440
|
if (isMutableContext(ctx)) {
|
|
@@ -705,9 +707,11 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
|
|
|
705
707
|
// `id: childId` keeps the ContextImpl's id in sync with the executionId
|
|
706
708
|
// used by spawnLayers/returnLayers in the layer-state store, so writes via
|
|
707
709
|
// `ctx.context[layerId]` and spawn-boundary hooks read from the same key.
|
|
710
|
+
const span = openChildSpan(baseCtx, 'spawn', step.id);
|
|
708
711
|
const childCtx = new ContextImpl({
|
|
709
712
|
harness: baseCtx.harness,
|
|
710
713
|
parent: baseCtx,
|
|
714
|
+
span,
|
|
711
715
|
items: childItems,
|
|
712
716
|
state: cloneWithGuard(baseCtx.state, `Spawn '${step.id}'`),
|
|
713
717
|
threadId: baseCtx.threadId,
|
|
@@ -723,23 +727,24 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
|
|
|
723
727
|
ledger: baseCtx instanceof ContextImpl ? baseCtx.ledger : undefined,
|
|
724
728
|
});
|
|
725
729
|
try {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
730
|
+
return await traceChild(baseCtx, span, async () => {
|
|
731
|
+
// Execute the child step. Its token/cost usage needs no merge here: every
|
|
732
|
+
// charge recorded on `childCtx` is also charged up its `usageParent`
|
|
733
|
+
// chain as it is spent, so a child that throws still bills the parent.
|
|
734
|
+
const childOutput = await executeStep(step.child, input, frameworkCast(childCtx));
|
|
735
|
+
// Pipeline result through layer onReturn hooks
|
|
736
|
+
if (!hasLayers || !parentExecutionCtx) {
|
|
737
|
+
return childOutput;
|
|
738
|
+
}
|
|
739
|
+
return returnLayers({
|
|
740
|
+
layers,
|
|
741
|
+
parentCtx: parentExecutionCtx,
|
|
742
|
+
childCtx: childExecutionCtx,
|
|
743
|
+
childLog: childCtx.itemLog,
|
|
744
|
+
result: childOutput,
|
|
745
|
+
store: layerStore,
|
|
746
|
+
});
|
|
741
747
|
});
|
|
742
|
-
return pipelinedResult;
|
|
743
748
|
}
|
|
744
749
|
finally {
|
|
745
750
|
// Clean up child execution state to prevent memory leaks — runs on success and error
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Control-flow step handlers: conditional, inParallel, loop, schedule.
|
|
3
3
|
*/
|
|
4
4
|
import { createMessage, frameworkCast, isNoeticError, NoeticErrorImpl } from '@noetic-tools/types';
|
|
5
|
+
import { NoeticAttr, NoeticEvent } from '../observability/genai-attributes.js';
|
|
5
6
|
import { ContextImpl } from '../runtime/context-impl.js';
|
|
6
7
|
import { snapshotCwdState } from '../runtime/cwd-helpers.js';
|
|
7
8
|
import { contextToExecCtx, returnLayers, spawnLayers } from './action-deps.js';
|
|
9
|
+
import { closeUnstartedSpan, openChildSpan, traceChild } from './child-span.js';
|
|
8
10
|
import { cloneWithGuard } from './clone-guard.js';
|
|
9
11
|
import { getContextChannelStore, isContextImpl, isMutableContext } from './typeguards.js';
|
|
10
12
|
//#region conditional
|
|
@@ -19,17 +21,20 @@ export async function executeConditional(step, input, ctx, executeStep) {
|
|
|
19
21
|
}
|
|
20
22
|
//#endregion
|
|
21
23
|
//#region inParallel
|
|
22
|
-
function createChildContexts(ctx,
|
|
24
|
+
function createChildContexts(ctx, paths, stepId) {
|
|
23
25
|
const threadId = isContextImpl(ctx) ? ctx.threadId : crypto.randomUUID();
|
|
24
26
|
const resourceId = isContextImpl(ctx) ? ctx.resourceId : undefined;
|
|
25
27
|
const channelStore = getContextChannelStore(ctx);
|
|
26
28
|
const parentPath = isContextImpl(ctx) ? ctx.currentPath() : '';
|
|
27
29
|
const parentLedger = isContextImpl(ctx) ? ctx.ledger : undefined;
|
|
28
|
-
return
|
|
29
|
-
length: count,
|
|
30
|
-
}, (_unused, index) => new ContextImpl({
|
|
30
|
+
return paths.map((path, index) => new ContextImpl({
|
|
31
31
|
harness: ctx.harness,
|
|
32
32
|
parent: ctx,
|
|
33
|
+
span: openPathSpan(ctx, {
|
|
34
|
+
stepId,
|
|
35
|
+
pathId: path.id,
|
|
36
|
+
index,
|
|
37
|
+
}),
|
|
33
38
|
/* Ledger paths must stay unique across sibling paths: a child ContextImpl
|
|
34
39
|
* starts with an empty frontier, so without a prefix every path would
|
|
35
40
|
* restart at the root and collide with its siblings. */
|
|
@@ -52,6 +57,35 @@ function createChildContexts(ctx, count, stepId) {
|
|
|
52
57
|
unifiedTools: ctx.unifiedTools,
|
|
53
58
|
}));
|
|
54
59
|
}
|
|
60
|
+
function openPathSpan(ctx, { stepId, pathId, index, }) {
|
|
61
|
+
const span = openChildSpan(ctx, 'inParallel.path', pathId);
|
|
62
|
+
span.setAttribute(NoeticAttr.PARALLEL_ID, stepId);
|
|
63
|
+
span.setAttribute(NoeticAttr.PARALLEL_INDEX, index);
|
|
64
|
+
return span;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ends each path's span when the path settles. A path that never starts (queued
|
|
68
|
+
* behind a fail-fast abort, or unstarted when a race settles) has no run to
|
|
69
|
+
* wrap, so `closeUnstarted` ends its span as cancelled.
|
|
70
|
+
*/
|
|
71
|
+
function tracePaths(ctx, executeStep, childContexts) {
|
|
72
|
+
const unstarted = new Set(childContexts);
|
|
73
|
+
return {
|
|
74
|
+
executeStep: (step, input, childCtx) => {
|
|
75
|
+
const child = frameworkCast(childCtx);
|
|
76
|
+
if (!unstarted.delete(child)) {
|
|
77
|
+
return executeStep(step, input, childCtx);
|
|
78
|
+
}
|
|
79
|
+
return traceChild(ctx, child.span, () => executeStep(step, input, childCtx));
|
|
80
|
+
},
|
|
81
|
+
async closeUnstarted() {
|
|
82
|
+
for (const child of unstarted) {
|
|
83
|
+
await closeUnstartedSpan(ctx, child.span);
|
|
84
|
+
}
|
|
85
|
+
unstarted.clear();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
55
89
|
function createForkLayerBridge({ ctx, childContexts, opts, }) {
|
|
56
90
|
const layers = ctx.layers;
|
|
57
91
|
const layerStore = opts?.layerStore;
|
|
@@ -136,13 +170,14 @@ export async function executeInParallel(step, input, ctx, rawExecuteStep, opts)
|
|
|
136
170
|
failed: [],
|
|
137
171
|
});
|
|
138
172
|
}
|
|
139
|
-
const childContexts = createChildContexts(baseCtx, paths
|
|
173
|
+
const childContexts = createChildContexts(baseCtx, paths, step.id);
|
|
140
174
|
const concurrency = step.concurrency ?? paths.length;
|
|
141
|
-
const
|
|
175
|
+
const traced = tracePaths(baseCtx, withForkLayers(rawExecuteStep, childContexts, createForkLayerBridge({
|
|
142
176
|
ctx: baseCtx,
|
|
143
177
|
childContexts,
|
|
144
178
|
opts,
|
|
145
|
-
}));
|
|
179
|
+
})), childContexts);
|
|
180
|
+
const executeStep = traced.executeStep;
|
|
146
181
|
try {
|
|
147
182
|
switch (step.mode) {
|
|
148
183
|
case 'all':
|
|
@@ -168,6 +203,7 @@ export async function executeInParallel(step, input, ctx, rawExecuteStep, opts)
|
|
|
168
203
|
for (const child of childContexts) {
|
|
169
204
|
child.detachFromParent();
|
|
170
205
|
}
|
|
206
|
+
await traced.closeUnstarted();
|
|
171
207
|
}
|
|
172
208
|
}
|
|
173
209
|
async function runWithConcurrency(tasks, concurrency) {
|
|
@@ -368,6 +404,10 @@ async function executeSettle(step, paths, input, ctx, childContexts, executeStep
|
|
|
368
404
|
});
|
|
369
405
|
return step.merge(results, ctx);
|
|
370
406
|
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region loop
|
|
409
|
+
/** History entries a loop keeps when `maxHistorySize` is unset; oldest drop first. */
|
|
410
|
+
const DEFAULT_MAX_HISTORY = 100;
|
|
371
411
|
function hasTextField(value) {
|
|
372
412
|
return typeof value === 'object' && value !== null && 'text' in value;
|
|
373
413
|
}
|
|
@@ -409,7 +449,7 @@ export async function executeLoop(step, input, ctx, executeStep) {
|
|
|
409
449
|
const startTime = Date.now();
|
|
410
450
|
let stepCount = 0;
|
|
411
451
|
const maxIterations = step.maxIterations ?? 1e3;
|
|
412
|
-
const maxHistory = step.maxHistorySize ??
|
|
452
|
+
const maxHistory = step.maxHistorySize ?? DEFAULT_MAX_HISTORY;
|
|
413
453
|
let totalIterations = 0;
|
|
414
454
|
// Validate maxIterations
|
|
415
455
|
if (!Number.isFinite(maxIterations) || maxIterations < 1) {
|
|
@@ -515,6 +555,7 @@ export async function executeLoop(step, input, ctx, executeStep) {
|
|
|
515
555
|
reason: `Predicate error: ${predicateError instanceof Error ? predicateError.message : String(predicateError)}`,
|
|
516
556
|
};
|
|
517
557
|
}
|
|
558
|
+
recordVerdict(ctx, step.id, totalIterations, verdict);
|
|
518
559
|
if (verdict.stop) {
|
|
519
560
|
if (lastOutput === undefined) {
|
|
520
561
|
throw new NoeticErrorImpl({
|
|
@@ -614,6 +655,21 @@ function park(parkCtx) {
|
|
|
614
655
|
});
|
|
615
656
|
});
|
|
616
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Adds a `loop.verdict` event to the enclosing span; loops open no span of
|
|
660
|
+
* their own. Leaves out `feedback`, which can be large or sensitive.
|
|
661
|
+
*/
|
|
662
|
+
function recordVerdict(ctx, loop, iteration, verdict) {
|
|
663
|
+
const attributes = {
|
|
664
|
+
loop,
|
|
665
|
+
iteration,
|
|
666
|
+
stop: verdict.stop,
|
|
667
|
+
};
|
|
668
|
+
if (verdict.reason !== undefined) {
|
|
669
|
+
attributes.reason = verdict.reason;
|
|
670
|
+
}
|
|
671
|
+
ctx.span.addEvent(NoeticEvent.LOOP_VERDICT, attributes);
|
|
672
|
+
}
|
|
617
673
|
function recordIterationError(ctx, error) {
|
|
618
674
|
const message = error instanceof Error ? error.message : String(error);
|
|
619
675
|
const stack = error instanceof Error && error.stack ? error.stack : '';
|
|
@@ -13,6 +13,16 @@ import type { Context, Step } from '@noetic-tools/types';
|
|
|
13
13
|
* continue to work.
|
|
14
14
|
*/
|
|
15
15
|
export declare function executeNoAdapter<TContext, I, O>(step: Step<TContext, I, O>, input: I, ctx: Context<TContext>): Promise<O>;
|
|
16
|
+
/**
|
|
17
|
+
* @internal
|
|
18
|
+
* The ledger key this dispatch records and replays under, or undefined when the
|
|
19
|
+
* step bypasses the ledger (spec 23a § Identity). A `durable: false` step and a
|
|
20
|
+
* `schedule` (which never completes) are neither replayed nor recorded; the opt-out
|
|
21
|
+
* covers this step only, so its children still resolve their own keys. A
|
|
22
|
+
* `replayKey` replaces the positional path with `key:<key>`. A throwing `replayKey`
|
|
23
|
+
* costs this dispatch its durability, not its run: the step executes unrecorded.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveLedgerKey<TContext, I, O>(step: Step<TContext, I, O>, input: I, ctx: Context<TContext>): string | undefined;
|
|
16
26
|
/**
|
|
17
27
|
* Executes a step within the interpreter, dispatching to the appropriate handler by step kind.
|
|
18
28
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { frameworkCast, NoeticErrorImpl } from '@noetic-tools/types';
|
|
2
2
|
import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
3
3
|
import { ContextImpl } from '../runtime/context-impl.js';
|
|
4
|
+
import { REPLAY_KEY_PREFIX } from '../runtime/durable/step-ledger.js';
|
|
4
5
|
import { DetachedHandleImpl } from '../util/detached-handle.js';
|
|
5
6
|
import { executeAcpAgent } from './execute-acp-agent.js';
|
|
6
7
|
import { executeCallModel, executeInvokeTool, executeRunCode, executeSpawn, executeWithContext, } from './execute-action.js';
|
|
@@ -112,7 +113,35 @@ export async function executeNoAdapter(step, input, ctx) {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
|
-
*
|
|
116
|
+
* @internal
|
|
117
|
+
* The ledger key this dispatch records and replays under, or undefined when the
|
|
118
|
+
* step bypasses the ledger (spec 23a § Identity). A `durable: false` step and a
|
|
119
|
+
* `schedule` (which never completes) are neither replayed nor recorded; the opt-out
|
|
120
|
+
* covers this step only, so its children still resolve their own keys. A
|
|
121
|
+
* `replayKey` replaces the positional path with `key:<key>`. A throwing `replayKey`
|
|
122
|
+
* costs this dispatch its durability, not its run: the step executes unrecorded.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveLedgerKey(step, input, ctx) {
|
|
125
|
+
const impl = asContextImpl(frameworkCast(ctx));
|
|
126
|
+
if (!impl?.ledger || step.id.length === 0) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
if (step.durable === false || step.kind === 'schedule') {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
if (!step.replayKey) {
|
|
133
|
+
return impl.currentPath();
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return `${REPLAY_KEY_PREFIX}${step.replayKey(input, ctx)}`;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.warn(`execute: replayKey for step "${step.id}" threw; running it without replay or recording:`, error instanceof Error ? error.message : String(error));
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resume path: when a previous run recorded this exact step at `ledgerKey`,
|
|
116
145
|
* replay its output instead of re-running. Held as a helper (rather than inline
|
|
117
146
|
* in `execute`) so the resume branch stays flat: it pops the frontier itself —
|
|
118
147
|
* the caller returns before the normal `leaveStep` runs — and emits
|
|
@@ -120,13 +149,13 @@ export async function executeNoAdapter(step, input, ctx) {
|
|
|
120
149
|
* the caller dispatches the step normally.
|
|
121
150
|
*/
|
|
122
151
|
function replayFromLedger(opts) {
|
|
123
|
-
const { ledger,
|
|
124
|
-
if (!ledger ||
|
|
152
|
+
const { ledger, ledgerKey, impl, step, broadcaster, agentName } = opts;
|
|
153
|
+
if (!ledger || ledgerKey === undefined || ledger.isEmpty) {
|
|
125
154
|
return {
|
|
126
155
|
hit: false,
|
|
127
156
|
};
|
|
128
157
|
}
|
|
129
|
-
const replayed = ledger.take(
|
|
158
|
+
const replayed = ledger.take(ledgerKey, {
|
|
130
159
|
id: step.id,
|
|
131
160
|
kind: step.kind,
|
|
132
161
|
});
|
|
@@ -154,16 +183,16 @@ function replayFromLedger(opts) {
|
|
|
154
183
|
}
|
|
155
184
|
/**
|
|
156
185
|
* Record a completed step's output so a resumed run replays it rather than
|
|
157
|
-
* re-executing. A no-op without a ledger or ledger
|
|
186
|
+
* re-executing. A no-op without a ledger or ledger key. Only successes reach
|
|
158
187
|
* here — a step that threw skips this call in `execute` and runs again on resume.
|
|
159
188
|
*/
|
|
160
189
|
async function recordToLedger(opts) {
|
|
161
|
-
const { ledger,
|
|
162
|
-
if (!ledger ||
|
|
190
|
+
const { ledger, ledgerKey, step, output } = opts;
|
|
191
|
+
if (!ledger || ledgerKey === undefined) {
|
|
163
192
|
return;
|
|
164
193
|
}
|
|
165
194
|
await ledger.record({
|
|
166
|
-
path:
|
|
195
|
+
path: ledgerKey,
|
|
167
196
|
stepId: step.id,
|
|
168
197
|
kind: step.kind,
|
|
169
198
|
output,
|
|
@@ -231,14 +260,14 @@ export async function execute(step, input, ctx) {
|
|
|
231
260
|
});
|
|
232
261
|
}
|
|
233
262
|
/* Resume: a previous run that completed this exact step replays its recorded
|
|
234
|
-
* output instead of re-running it. `
|
|
263
|
+
* output instead of re-running it. `ledgerKey`/`ledger` are captured before
|
|
235
264
|
* dispatch and reused by the record below, because `leaveStep` pops the path
|
|
236
265
|
* on the way out. */
|
|
237
|
-
const
|
|
266
|
+
const ledgerKey = resolveLedgerKey(step, input, ctx);
|
|
238
267
|
const ledger = impl?.ledger;
|
|
239
268
|
const replay = replayFromLedger({
|
|
240
269
|
ledger,
|
|
241
|
-
|
|
270
|
+
ledgerKey,
|
|
242
271
|
impl,
|
|
243
272
|
step,
|
|
244
273
|
broadcaster,
|
|
@@ -323,7 +352,7 @@ export async function execute(step, input, ctx) {
|
|
|
323
352
|
* again. */
|
|
324
353
|
await recordToLedger({
|
|
325
354
|
ledger,
|
|
326
|
-
|
|
355
|
+
ledgerKey,
|
|
327
356
|
step,
|
|
328
357
|
output: result,
|
|
329
358
|
});
|
|
@@ -1,59 +1,75 @@
|
|
|
1
1
|
export declare const GenAI: {
|
|
2
|
-
readonly SYSTEM:
|
|
3
|
-
readonly REQUEST_MODEL:
|
|
4
|
-
readonly USAGE_INPUT_TOKENS:
|
|
5
|
-
readonly USAGE_OUTPUT_TOKENS:
|
|
2
|
+
readonly SYSTEM: "gen_ai.system";
|
|
3
|
+
readonly REQUEST_MODEL: "gen_ai.request.model";
|
|
4
|
+
readonly USAGE_INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
5
|
+
readonly USAGE_OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
6
6
|
/** Prompt tokens served from the provider's cache. Absent if unreported. */
|
|
7
|
-
readonly USAGE_CACHED_INPUT_TOKENS:
|
|
7
|
+
readonly USAGE_CACHED_INPUT_TOKENS: "gen_ai.usage.cached_input_tokens";
|
|
8
8
|
/** Prompt tokens written into the provider's cache. Absent if unreported. */
|
|
9
|
-
readonly USAGE_CACHE_WRITE_TOKENS:
|
|
10
|
-
readonly COST:
|
|
9
|
+
readonly USAGE_CACHE_WRITE_TOKENS: "gen_ai.usage.cache_write_tokens";
|
|
10
|
+
readonly COST: "gen_ai.cost";
|
|
11
11
|
};
|
|
12
12
|
export declare const ToolAttr: {
|
|
13
|
-
readonly NAME:
|
|
14
|
-
readonly NEEDS_APPROVAL:
|
|
13
|
+
readonly NAME: "tool.name";
|
|
14
|
+
readonly NEEDS_APPROVAL: "tool.needs_approval";
|
|
15
15
|
};
|
|
16
16
|
export declare const NoeticAttr: {
|
|
17
17
|
/** Full JSON-serialised `WorkflowDocument` (the DAG, lossless). */
|
|
18
|
-
readonly WORKFLOW_DOCUMENT:
|
|
18
|
+
readonly WORKFLOW_DOCUMENT: "noetic.workflow.document";
|
|
19
19
|
/** Workflow document schema version. */
|
|
20
|
-
readonly WORKFLOW_VERSION:
|
|
20
|
+
readonly WORKFLOW_VERSION: "noetic.workflow.version";
|
|
21
21
|
/** Count of declared nodes in the workflow tree. */
|
|
22
|
-
readonly WORKFLOW_NODE_COUNT:
|
|
22
|
+
readonly WORKFLOW_NODE_COUNT: "noetic.workflow.node_count";
|
|
23
23
|
/** JSON array of `{ id, kind }` for every declared node (flattened graph). */
|
|
24
|
-
readonly WORKFLOW_NODES:
|
|
24
|
+
readonly WORKFLOW_NODES: "noetic.workflow.nodes";
|
|
25
25
|
/** JSON array of `{ from, to }` parent→child edges between declared nodes. */
|
|
26
|
-
readonly WORKFLOW_EDGES:
|
|
27
|
-
/**
|
|
28
|
-
|
|
26
|
+
readonly WORKFLOW_EDGES: "noetic.workflow.edges";
|
|
27
|
+
/**
|
|
28
|
+
* Id of the step a span belongs to: the declared workflow node of an
|
|
29
|
+
* `llm.call`/`tool.call`, the `decide` step of a `decide.call`, the `spawn`
|
|
30
|
+
* step of a `spawn` span, the path step of an `inParallel.path` span.
|
|
31
|
+
*/
|
|
32
|
+
readonly NODE_ID: "noetic.node.id";
|
|
33
|
+
/** Id of the `inParallel` step an `inParallel.path` span belongs to. */
|
|
34
|
+
readonly PARALLEL_ID: "noetic.parallel.id";
|
|
35
|
+
/** Zero-based position of the path among its `inParallel` siblings. */
|
|
36
|
+
readonly PARALLEL_INDEX: "noetic.parallel.index";
|
|
37
|
+
/** `NoeticError` kind a `spawn`/`inParallel.path` span failed with. */
|
|
38
|
+
readonly ERROR_KIND: "noetic.error.kind";
|
|
29
39
|
/**
|
|
30
40
|
* Conversation/session this run belongs to (the run's `ctx.threadId`). Stamped
|
|
31
41
|
* on the root `workflow.run` span so every turn of a multi-turn session shares
|
|
32
42
|
* one id, letting a consumer group per-run traces back into their session.
|
|
33
43
|
*/
|
|
34
|
-
readonly SESSION_ID:
|
|
44
|
+
readonly SESSION_ID: "noetic.session.id";
|
|
35
45
|
/** Resource the session is scoped to (the run's `ctx.resourceId`), when set. */
|
|
36
|
-
readonly RESOURCE_ID:
|
|
46
|
+
readonly RESOURCE_ID: "noetic.resource.id";
|
|
37
47
|
/** Id of the anchoring epoch the assembled view belongs to. */
|
|
38
|
-
readonly CONTEXT_EPOCH_ID:
|
|
48
|
+
readonly CONTEXT_EPOCH_ID: "noetic.context.epoch.id";
|
|
39
49
|
/** Assemblies served by the current epoch, including this one. */
|
|
40
|
-
readonly CONTEXT_EPOCH_AGE:
|
|
50
|
+
readonly CONTEXT_EPOCH_AGE: "noetic.context.epoch.age";
|
|
41
51
|
/** Why the epoch re-anchored on this assembly, when it did. */
|
|
42
|
-
readonly CONTEXT_REANCHOR_REASON:
|
|
52
|
+
readonly CONTEXT_REANCHOR_REASON: "noetic.context.reanchor_reason";
|
|
43
53
|
/** Tokens in the anchor band (the cache-stable prefix). */
|
|
44
|
-
readonly CONTEXT_ANCHOR_TOKENS:
|
|
54
|
+
readonly CONTEXT_ANCHOR_TOKENS: "noetic.context.anchor_tokens";
|
|
45
55
|
/** Tokens in the live band (rendered after history). */
|
|
46
|
-
readonly CONTEXT_LIVE_TOKENS:
|
|
56
|
+
readonly CONTEXT_LIVE_TOKENS: "noetic.context.live_tokens";
|
|
47
57
|
/** Tokens spent superseding stale anchors. */
|
|
48
|
-
readonly CONTEXT_DELTA_TOKENS:
|
|
58
|
+
readonly CONTEXT_DELTA_TOKENS: "noetic.context.delta_tokens";
|
|
49
59
|
/** JSON array of `{ id, placement, served }` per contributing layer. */
|
|
50
|
-
readonly CONTEXT_LAYER_PLACEMENTS:
|
|
60
|
+
readonly CONTEXT_LAYER_PLACEMENTS: "noetic.context.layer_placements";
|
|
51
61
|
/** JSON array of `{ id, rate, rebillTokens }` per contributing layer. */
|
|
52
|
-
readonly CONTEXT_LAYER_CHURN:
|
|
62
|
+
readonly CONTEXT_LAYER_CHURN: "noetic.context.layer_churn";
|
|
53
63
|
/** `native` or `estimated` — where a decision's probabilities came from. */
|
|
54
|
-
readonly DECISION_SOURCE:
|
|
64
|
+
readonly DECISION_SOURCE: "noetic.decision.source";
|
|
55
65
|
/** Comma-separated ids of questions the model refused. */
|
|
56
|
-
readonly DECISION_REFUSED:
|
|
66
|
+
readonly DECISION_REFUSED: "noetic.decision.refused";
|
|
57
67
|
/** Prefix for per-question attributes: `noetic.decision.<id>.value` etc. */
|
|
58
|
-
readonly DECISION_PREFIX:
|
|
68
|
+
readonly DECISION_PREFIX: "noetic.decision.";
|
|
69
|
+
};
|
|
70
|
+
export declare const NoeticEvent: {
|
|
71
|
+
/** One per loop iteration: `{ loop, iteration, stop, reason? }`. */
|
|
72
|
+
readonly LOOP_VERDICT: "loop.verdict";
|
|
73
|
+
/** A channel dropped data or is waiting with no timeout: `{ kind, channelName, message, capacity?, operation? }`. */
|
|
74
|
+
readonly CHANNEL_WARNING: "channel.warning";
|
|
59
75
|
};
|
|
@@ -27,8 +27,18 @@ export const NoeticAttr = {
|
|
|
27
27
|
WORKFLOW_NODES: 'noetic.workflow.nodes',
|
|
28
28
|
/** JSON array of `{ from, to }` parent→child edges between declared nodes. */
|
|
29
29
|
WORKFLOW_EDGES: 'noetic.workflow.edges',
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Id of the step a span belongs to: the declared workflow node of an
|
|
32
|
+
* `llm.call`/`tool.call`, the `decide` step of a `decide.call`, the `spawn`
|
|
33
|
+
* step of a `spawn` span, the path step of an `inParallel.path` span.
|
|
34
|
+
*/
|
|
31
35
|
NODE_ID: 'noetic.node.id',
|
|
36
|
+
/** Id of the `inParallel` step an `inParallel.path` span belongs to. */
|
|
37
|
+
PARALLEL_ID: 'noetic.parallel.id',
|
|
38
|
+
/** Zero-based position of the path among its `inParallel` siblings. */
|
|
39
|
+
PARALLEL_INDEX: 'noetic.parallel.index',
|
|
40
|
+
/** `NoeticError` kind a `spawn`/`inParallel.path` span failed with. */
|
|
41
|
+
ERROR_KIND: 'noetic.error.kind',
|
|
32
42
|
/**
|
|
33
43
|
* Conversation/session this run belongs to (the run's `ctx.threadId`). Stamped
|
|
34
44
|
* on the root `workflow.run` span so every turn of a multi-turn session shares
|
|
@@ -60,3 +70,11 @@ export const NoeticAttr = {
|
|
|
60
70
|
/** Prefix for per-question attributes: `noetic.decision.<id>.value` etc. */
|
|
61
71
|
DECISION_PREFIX: 'noetic.decision.',
|
|
62
72
|
};
|
|
73
|
+
// Span event names. Steps that open no span of their own (e.g. `loop`) add
|
|
74
|
+
// these to the enclosing `ctx.span`.
|
|
75
|
+
export const NoeticEvent = {
|
|
76
|
+
/** One per loop iteration: `{ loop, iteration, stop, reason? }`. */
|
|
77
|
+
LOOP_VERDICT: 'loop.verdict',
|
|
78
|
+
/** A channel dropped data or is waiting with no timeout: `{ kind, channelName, message, capacity?, operation? }`. */
|
|
79
|
+
CHANNEL_WARNING: 'channel.warning',
|
|
80
|
+
};
|
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
import type { Channel, ChannelHandle, ExternalChannel } from '@noetic-tools/types';
|
|
2
|
+
import type { ChannelSnapshot } from '../types/checkpoint.js';
|
|
3
|
+
/**
|
|
4
|
+
* A channel condition worth surfacing but not worth failing on. `AgentHarness`
|
|
5
|
+
* records these as `channel.warning` span events (spec 10); a bare store
|
|
6
|
+
* prints them with `console.warn`.
|
|
7
|
+
*/
|
|
8
|
+
export type ChannelWarning = {
|
|
9
|
+
/** An external sender hit a full queue and the oldest item was dropped. */
|
|
10
|
+
kind: 'overflow_drop';
|
|
11
|
+
channelName: string;
|
|
12
|
+
capacity: number;
|
|
13
|
+
message: string;
|
|
14
|
+
} | {
|
|
15
|
+
/** An external subscriber's private buffer filled and its oldest value was dropped. */
|
|
16
|
+
kind: 'subscriber_drop';
|
|
17
|
+
channelName: string;
|
|
18
|
+
capacity: number;
|
|
19
|
+
message: string;
|
|
20
|
+
} | {
|
|
21
|
+
/** A `recv` or parked `send` is waiting with `timeout: 0`, so nothing will time it out. */
|
|
22
|
+
kind: 'unbounded_wait';
|
|
23
|
+
channelName: string;
|
|
24
|
+
operation: 'recv' | 'send';
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
/** Options accepted by the `ChannelStore` constructor. */
|
|
28
|
+
export interface ChannelStoreOpts {
|
|
29
|
+
/** Receives every channel warning. Defaults to `warnToConsole`. */
|
|
30
|
+
onWarning?: (warning: ChannelWarning) => void;
|
|
31
|
+
}
|
|
32
|
+
/** Print a channel warning with the `[noetic]` prefix. */
|
|
33
|
+
export declare function warnToConsole(warning: ChannelWarning): void;
|
|
2
34
|
/** Options accepted by internal (back-pressured) `send`. */
|
|
3
35
|
export interface ChannelSendOpts {
|
|
4
36
|
/**
|
|
@@ -21,7 +53,34 @@ export declare class ChannelStore {
|
|
|
21
53
|
*/
|
|
22
54
|
private handleObserved;
|
|
23
55
|
private subscribersByExecution;
|
|
56
|
+
/**
|
|
57
|
+
* Channel name → ids of the root executions that used it, which decides
|
|
58
|
+
* the channels an execution's checkpoint carries. Ids stay after their
|
|
59
|
+
* execution closes: a host may checkpoint a finished run, and channel
|
|
60
|
+
* state itself lives for the harness lifetime (spec 06, Lifecycle).
|
|
61
|
+
*/
|
|
62
|
+
private users;
|
|
63
|
+
private readonly warn;
|
|
64
|
+
constructor(opts?: ChannelStoreOpts);
|
|
24
65
|
private getOrCreate;
|
|
66
|
+
/**
|
|
67
|
+
* Record that `executionId` (a root execution) uses `channel`, so the
|
|
68
|
+
* channel rides along in its checkpoints.
|
|
69
|
+
*/
|
|
70
|
+
claim<T>(channel: Channel<T>, executionId: string): void;
|
|
71
|
+
private addUser;
|
|
72
|
+
/**
|
|
73
|
+
* Stored state (values, queued items) of every channel `executionId` used.
|
|
74
|
+
* Parked receivers, parked senders, and subscribers are live promises and
|
|
75
|
+
* stay out; topic channels store nothing and are skipped.
|
|
76
|
+
*/
|
|
77
|
+
snapshot(executionId: string): ChannelSnapshot[];
|
|
78
|
+
/**
|
|
79
|
+
* Refill channel state from a checkpoint and mark `executionId` as a user.
|
|
80
|
+
* A channel that already has live state keeps it — live state is never
|
|
81
|
+
* older than a snapshot.
|
|
82
|
+
*/
|
|
83
|
+
restore(executionId: string, snapshots: readonly ChannelSnapshot[]): void;
|
|
25
84
|
/**
|
|
26
85
|
* Subscribe to wake notifications on a channel without consuming any value.
|
|
27
86
|
* Fired once on the next `send()` for any mode, then auto-removed. Returns
|
|
@@ -42,8 +101,12 @@ export declare class ChannelStore {
|
|
|
42
101
|
* - **topic**: deliver to current subscribers, resolve immediately.
|
|
43
102
|
*
|
|
44
103
|
* External callers use `ChannelHandle.send` (sync, drop-oldest) instead.
|
|
104
|
+
* `opts.timeout: 0` parks with no timeout and warns `unbounded_wait`.
|
|
105
|
+
*
|
|
106
|
+
* @throws `NoeticConfigError` with code `INVALID_CHANNEL_TIMEOUT` (as a
|
|
107
|
+
* rejection) when `opts.timeout` is negative.
|
|
45
108
|
*/
|
|
46
|
-
send<T>(channel: Channel<T>, value: T
|
|
109
|
+
send<T>(channel: Channel<T>, value: NoInfer<T>, opts?: ChannelSendOpts): Promise<void>;
|
|
47
110
|
/** Park an internal sender on a full queue until a slot frees (FIFO). */
|
|
48
111
|
private parkSender;
|
|
49
112
|
/**
|
|
@@ -53,6 +116,15 @@ export declare class ChannelStore {
|
|
|
53
116
|
private promotePendingSender;
|
|
54
117
|
/** Fire-and-reset the one-shot wake subscribers for a channel state. */
|
|
55
118
|
private fireWakes;
|
|
119
|
+
/**
|
|
120
|
+
* Receive by mode: value returns the current value, queue dequeues the
|
|
121
|
+
* head, topic waits for the next send. A receiver that has to wait gets
|
|
122
|
+
* `channel_timeout` after `timeout` ms; `0` waits with no timeout in every
|
|
123
|
+
* mode and warns `unbounded_wait`.
|
|
124
|
+
*
|
|
125
|
+
* @throws `NoeticConfigError` with code `INVALID_CHANNEL_TIMEOUT` (as a
|
|
126
|
+
* rejection) when `timeout` is negative.
|
|
127
|
+
*/
|
|
56
128
|
recv<T>(channel: Channel<T>, timeout?: number, signal?: AbortSignal): Promise<T>;
|
|
57
129
|
tryRecv<T>(channel: Channel<T>): T | null;
|
|
58
130
|
private waitWithTimeout;
|