@noetic-tools/core 5.7.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.
Files changed (63) hide show
  1. package/dist/adapters/openrouter.d.ts +1 -0
  2. package/dist/adapters/openrouter.js +1 -0
  3. package/dist/builders/channel-builder.d.ts +18 -0
  4. package/dist/builders/control-flow-builders.d.ts +17 -30
  5. package/dist/builders/decide-builders.d.ts +5 -5
  6. package/dist/builders/decide-builders.js +4 -0
  7. package/dist/builders/every.d.ts +2 -2
  8. package/dist/builders/every.js +2 -0
  9. package/dist/builders/loop-builder.d.ts +1 -1
  10. package/dist/builders/loop-builder.js +1 -1
  11. package/dist/builders/provide-builder.d.ts +3 -2
  12. package/dist/builders/provide-builder.js +3 -0
  13. package/dist/builders/spawn-builder.d.ts +2 -3
  14. package/dist/builders/spawn-builder.js +2 -1
  15. package/dist/builders/step-builders.d.ts +8 -9
  16. package/dist/builders/step-builders.js +0 -1
  17. package/dist/builders/tool-builder.d.ts +2 -0
  18. package/dist/builders/tool-builder.js +2 -0
  19. package/dist/builders/workflow-hydrator.js +21 -14
  20. package/dist/builders/workflow-replay-key.d.ts +15 -0
  21. package/dist/builders/workflow-replay-key.js +46 -0
  22. package/dist/harness/agent-harness.d.ts +43 -11
  23. package/dist/harness/agent-harness.js +121 -40
  24. package/dist/harness/deps/interpreter.d.ts +1 -0
  25. package/dist/harness/deps/interpreter.js +1 -0
  26. package/dist/harness/deps/runtime.d.ts +3 -2
  27. package/dist/harness/deps/runtime.js +2 -2
  28. package/dist/harness/model-call.d.ts +3 -0
  29. package/dist/harness/model-call.js +19 -5
  30. package/dist/index.d.ts +7 -5
  31. package/dist/index.js +3 -3
  32. package/dist/interpreter/action-deps.d.ts +1 -1
  33. package/dist/interpreter/action-deps.js +1 -1
  34. package/dist/interpreter/child-span.d.ts +17 -0
  35. package/dist/interpreter/child-span.js +51 -0
  36. package/dist/interpreter/collect-tools.js +19 -54
  37. package/dist/interpreter/execute-action.js +30 -17
  38. package/dist/interpreter/execute-control.js +67 -9
  39. package/dist/interpreter/execute-decide.js +9 -27
  40. package/dist/interpreter/execute.d.ts +10 -0
  41. package/dist/interpreter/execute.js +69 -46
  42. package/dist/interpreter/model-error.d.ts +9 -0
  43. package/dist/interpreter/model-error.js +32 -0
  44. package/dist/interpreter/step-walk.d.ts +11 -0
  45. package/dist/interpreter/step-walk.js +41 -0
  46. package/dist/observability/genai-attributes.d.ts +52 -29
  47. package/dist/observability/genai-attributes.js +27 -1
  48. package/dist/runtime/channel-store.d.ts +73 -1
  49. package/dist/runtime/channel-store.js +179 -63
  50. package/dist/runtime/context-impl.d.ts +36 -32
  51. package/dist/runtime/context-impl.js +80 -45
  52. package/dist/runtime/durable/harness-checkpoints.d.ts +33 -16
  53. package/dist/runtime/durable/harness-checkpoints.js +74 -52
  54. package/dist/runtime/durable/step-ledger.d.ts +15 -5
  55. package/dist/runtime/durable/step-ledger.js +23 -6
  56. package/dist/schemas/workflow.d.ts +1 -1
  57. package/dist/types/checkpoint.d.ts +31 -0
  58. package/dist/types/checkpoint.js +29 -0
  59. package/dist/until/combinators.d.ts +10 -0
  60. package/dist/until/combinators.js +37 -13
  61. package/dist/until/predicates.js +4 -2
  62. package/package.json +3 -3
  63. package/schema/noetic-workflow.schema.json +432 -0
@@ -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,21 +113,47 @@ export async function executeNoAdapter(step, input, ctx) {
112
113
  }
113
114
  }
114
115
  /**
115
- * Resume path: when a previous run recorded this exact step at `ledgerPath`,
116
- * replay its output instead of re-running. Held as a helper (rather than inline
117
- * in `execute`) so the resume branch stays flat: it pops the frontier itself —
118
- * the caller returns before the normal `leaveStep` runs — and emits
119
- * `step_replayed`. Returns `{ hit: false }` when there is nothing to replay so
120
- * the caller dispatches the step normally.
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`,
145
+ * replay its output instead of re-running, and emit `step_replayed`. Returns
146
+ * `{ hit: false }` when there is nothing to replay so the caller dispatches the
147
+ * step normally.
121
148
  */
122
149
  function replayFromLedger(opts) {
123
- const { ledger, ledgerPath, impl, step, broadcaster, agentName } = opts;
124
- if (!ledger || ledgerPath === undefined || ledger.isEmpty) {
150
+ const { ledger, ledgerKey, step, broadcaster, agentName } = opts;
151
+ if (!ledger || ledgerKey === undefined || ledger.isEmpty) {
125
152
  return {
126
153
  hit: false,
127
154
  };
128
155
  }
129
- const replayed = ledger.take(ledgerPath, {
156
+ const replayed = ledger.take(ledgerKey, {
130
157
  id: step.id,
131
158
  kind: step.kind,
132
159
  });
@@ -135,9 +162,6 @@ function replayFromLedger(opts) {
135
162
  hit: false,
136
163
  };
137
164
  }
138
- if (impl && step.id.length > 0) {
139
- impl.leaveStep(step.id);
140
- }
141
165
  emitFrameworkEvent({
142
166
  broadcaster,
143
167
  agentName,
@@ -154,16 +178,16 @@ function replayFromLedger(opts) {
154
178
  }
155
179
  /**
156
180
  * 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 path. Only successes reach
181
+ * re-executing. A no-op without a ledger or ledger key. Only successes reach
158
182
  * here — a step that threw skips this call in `execute` and runs again on resume.
159
183
  */
160
184
  async function recordToLedger(opts) {
161
- const { ledger, ledgerPath, step, output } = opts;
162
- if (!ledger || ledgerPath === undefined) {
185
+ const { ledger, ledgerKey, step, output } = opts;
186
+ if (!ledger || ledgerKey === undefined) {
163
187
  return;
164
188
  }
165
189
  await ledger.record({
166
- path: ledgerPath,
190
+ path: ledgerKey,
167
191
  stepId: step.id,
168
192
  kind: step.kind,
169
193
  output,
@@ -220,56 +244,57 @@ export async function execute(step, input, ctx) {
220
244
  data: startedData,
221
245
  });
222
246
  }
223
- // Push onto the durable frontier so checkpoints can record exactly which
224
- // step was in flight when the snapshot fired. Steps with empty ids are
225
- // skipped — they are not reattachable by id and would pollute the stack.
247
+ /* Register the step as in flight, so a checkpoint records it in the frontier, and
248
+ * run it on its own lane: a view of the context that carries this dispatch's path.
249
+ * Steps this step dispatches hang under that path, so concurrent dispatches on one
250
+ * context cannot interleave each other's paths. Steps with empty ids stay on the
251
+ * caller's lane — they are not reattachable by id and add no path segment. */
226
252
  const impl = asContextImpl(baseCtx);
227
- if (impl && step.id.length > 0) {
228
- impl.enterStep({
229
- stepId: step.id,
230
- input,
231
- });
232
- }
253
+ const frame = {
254
+ stepId: step.id,
255
+ input,
256
+ };
257
+ const lane = impl && step.id.length > 0 ? impl.enterStep(frame) : null;
258
+ const stepCtx = lane ? frameworkCast(lane) : ctx;
233
259
  /* Resume: a previous run that completed this exact step replays its recorded
234
- * output instead of re-running it. `ledgerPath`/`ledger` are captured before
235
- * dispatch and reused by the record below, because `leaveStep` pops the path
236
- * on the way out. */
237
- const ledgerPath = impl && step.id.length > 0 ? impl.currentPath() : undefined;
260
+ * output instead of re-running it. `ledgerKey`/`ledger` are captured before
261
+ * dispatch and reused by the record below. */
262
+ const ledgerKey = resolveLedgerKey(step, input, stepCtx);
238
263
  const ledger = impl?.ledger;
239
264
  const replay = replayFromLedger({
240
265
  ledger,
241
- ledgerPath,
242
- impl,
266
+ ledgerKey,
243
267
  step,
244
268
  broadcaster,
245
269
  agentName,
246
270
  });
247
271
  if (replay.hit) {
272
+ lane?.leaveStep(frame);
248
273
  return replay.output;
249
274
  }
250
275
  let result;
251
276
  try {
252
277
  switch (step.kind) {
253
278
  case 'runCode':
254
- result = await dispatchViaAdapter(step, input, ctx, () => executeRunCode(step, input, ctx));
279
+ result = await dispatchViaAdapter(step, input, stepCtx, () => executeRunCode(step, input, stepCtx));
255
280
  break;
256
281
  case 'callModel':
257
- result = await executeCallModel(step, input, ctx, baseCtx.layers);
282
+ result = await executeCallModel(step, input, stepCtx, baseCtx.layers);
258
283
  break;
259
284
  case 'acp-agent':
260
- result = await executeAcpAgent(step, input, ctx, baseCtx.layers);
285
+ result = await executeAcpAgent(step, input, stepCtx, baseCtx.layers);
261
286
  break;
262
287
  case 'invokeTool':
263
- result = await executeInvokeTool(step, input, ctx, baseCtx.harness);
288
+ result = await executeInvokeTool(step, input, stepCtx, baseCtx.harness);
264
289
  break;
265
290
  case 'decide':
266
- result = await executeDecide(step, input, ctx, baseCtx.layers);
291
+ result = await executeDecide(step, input, stepCtx, baseCtx.layers);
267
292
  break;
268
293
  case 'conditional':
269
- result = await executeConditional(step, input, ctx, (s, i, c) => execute(s, i, c));
294
+ result = await executeConditional(step, input, stepCtx, (s, i, c) => execute(s, i, c));
270
295
  break;
271
296
  case 'inParallel':
272
- result = await executeInParallel(step, input, ctx, (s, i, c) => execute(s, i, c), {
297
+ result = await executeInParallel(step, input, stepCtx, (s, i, c) => execute(s, i, c), {
273
298
  // Fork paths are child executions: the layer store lets their
274
299
  // `onSpawn`/`onReturn` boundary run (see `createForkLayerBridge`).
275
300
  layerStore: harnessWithLayerStore(baseCtx).layerStateStore,
@@ -277,16 +302,16 @@ export async function execute(step, input, ctx) {
277
302
  });
278
303
  break;
279
304
  case 'spawn':
280
- result = await dispatchViaAdapter(step, input, ctx, () => executeSpawn(step, input, ctx, (s, i, c) => execute(s, i, c), buildSpawnOpts(ctx)));
305
+ result = await dispatchViaAdapter(step, input, stepCtx, () => executeSpawn(step, input, stepCtx, (s, i, c) => execute(s, i, c), buildSpawnOpts(stepCtx)));
281
306
  break;
282
307
  case 'withContext':
283
- result = await executeWithContext(step, input, ctx, (s, i, c) => execute(s, i, c));
308
+ result = await executeWithContext(step, input, stepCtx, (s, i, c) => execute(s, i, c));
284
309
  break;
285
310
  case 'loop':
286
- result = await executeLoop(step, input, ctx, (s, i, c) => execute(s, i, c));
311
+ result = await executeLoop(step, input, stepCtx, (s, i, c) => execute(s, i, c));
287
312
  break;
288
313
  case 'schedule':
289
- result = await executeSchedule(step, input, ctx, (s, i, c) => execute(s, i, c));
314
+ result = await executeSchedule(step, input, stepCtx, (s, i, c) => execute(s, i, c));
290
315
  break;
291
316
  default: {
292
317
  const _exhaustive = step;
@@ -301,9 +326,7 @@ export async function execute(step, input, ctx) {
301
326
  }
302
327
  }
303
328
  finally {
304
- if (impl && step.id.length > 0) {
305
- impl.leaveStep(step.id);
306
- }
329
+ lane?.leaveStep(frame);
307
330
  }
308
331
  // Emit step_completed framework event (respects step.emit option)
309
332
  const completedData = {
@@ -323,7 +346,7 @@ export async function execute(step, input, ctx) {
323
346
  * again. */
324
347
  await recordToLedger({
325
348
  ledger,
326
- ledgerPath,
349
+ ledgerKey,
327
350
  step,
328
351
  output: result,
329
352
  });
@@ -0,0 +1,9 @@
1
+ import type { Context } from './action-types.js';
2
+ /**
3
+ * Give an error from a model provider call the step's id and a Noetic kind.
4
+ *
5
+ * Noetic errors pass through untouched. A failure on an aborted context is
6
+ * `cancelled` (the abort is what broke the call), a 429 is `model_rate_limit`,
7
+ * and anything else is `step_failed` with the provider error as its cause.
8
+ */
9
+ export declare function wrapModelError(e: unknown, stepId: string, ctx: Pick<Context, 'aborted' | 'abortReason'>): Error;
@@ -0,0 +1,32 @@
1
+ import { isNoeticError, NoeticConfigError, NoeticErrorImpl } from '@noetic-tools/types';
2
+ import { statusOf } from '../util/status-code.js';
3
+ /**
4
+ * Give an error from a model provider call the step's id and a Noetic kind.
5
+ *
6
+ * Noetic errors pass through untouched. A failure on an aborted context is
7
+ * `cancelled` (the abort is what broke the call), a 429 is `model_rate_limit`,
8
+ * and anything else is `step_failed` with the provider error as its cause.
9
+ */
10
+ export function wrapModelError(e, stepId, ctx) {
11
+ if (isNoeticError(e) || e instanceof NoeticConfigError) {
12
+ return e;
13
+ }
14
+ if (ctx.aborted) {
15
+ return new NoeticErrorImpl({
16
+ kind: 'cancelled',
17
+ reason: ctx.abortReason ?? 'context aborted',
18
+ });
19
+ }
20
+ if (statusOf(e) === 429) {
21
+ return new NoeticErrorImpl({
22
+ kind: 'model_rate_limit',
23
+ stepId,
24
+ });
25
+ }
26
+ return new NoeticErrorImpl({
27
+ kind: 'step_failed',
28
+ stepId,
29
+ cause: e instanceof Error ? e : new Error(String(e)),
30
+ retriesExhausted: false,
31
+ });
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { Step } from '@noetic-tools/types';
2
+ /**
3
+ * Whether `step` or any step nested in it satisfies `test`.
4
+ *
5
+ * Only statically declared children are visited: a `conditional` or
6
+ * `inParallel` built from functions exposes its children through
7
+ * `_optimizable` only when the builder could enumerate them.
8
+ */
9
+ export declare function someStep(step: Step, test: (step: Step) => boolean): boolean;
10
+ /** The steps nested directly inside `step`, as far as they are statically known. */
11
+ export declare function stepChildren(step: Step): ReadonlyArray<Step>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Whether `step` or any step nested in it satisfies `test`.
3
+ *
4
+ * Only statically declared children are visited: a `conditional` or
5
+ * `inParallel` built from functions exposes its children through
6
+ * `_optimizable` only when the builder could enumerate them.
7
+ */
8
+ export function someStep(step, test) {
9
+ if (test(step)) {
10
+ return true;
11
+ }
12
+ return stepChildren(step).some((child) => someStep(child, test));
13
+ }
14
+ /** The steps nested directly inside `step`, as far as they are statically known. */
15
+ export function stepChildren(step) {
16
+ switch (step.kind) {
17
+ case 'callModel':
18
+ case 'runCode':
19
+ case 'invokeTool':
20
+ case 'decide':
21
+ case 'acp-agent':
22
+ return [];
23
+ case 'conditional':
24
+ case 'inParallel':
25
+ return step._optimizable ?? [];
26
+ case 'withContext':
27
+ case 'spawn':
28
+ return [
29
+ step.child,
30
+ ];
31
+ case 'loop':
32
+ return step.steps;
33
+ case 'schedule':
34
+ return [
35
+ step.step,
36
+ ];
37
+ default:
38
+ step;
39
+ return [];
40
+ }
41
+ }
@@ -1,59 +1,82 @@
1
1
  export declare const GenAI: {
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';
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: 'gen_ai.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: 'gen_ai.usage.cache_write_tokens';
10
- readonly COST: 'gen_ai.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: 'tool.name';
14
- readonly NEEDS_APPROVAL: 'tool.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: 'noetic.workflow.document';
18
+ readonly WORKFLOW_DOCUMENT: "noetic.workflow.document";
19
19
  /** Workflow document schema version. */
20
- readonly WORKFLOW_VERSION: 'noetic.workflow.version';
20
+ readonly WORKFLOW_VERSION: "noetic.workflow.version";
21
21
  /** Count of declared nodes in the workflow tree. */
22
- readonly WORKFLOW_NODE_COUNT: 'noetic.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: 'noetic.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: 'noetic.workflow.edges';
27
- /** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
28
- readonly NODE_ID: 'noetic.node.id';
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: 'noetic.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: 'noetic.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: 'noetic.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: 'noetic.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: 'noetic.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: 'noetic.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: 'noetic.context.live_tokens';
56
+ readonly CONTEXT_LIVE_TOKENS: "noetic.context.live_tokens";
47
57
  /** Tokens spent superseding stale anchors. */
48
- readonly CONTEXT_DELTA_TOKENS: 'noetic.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: 'noetic.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: 'noetic.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: 'noetic.decision.source';
64
+ readonly DECISION_SOURCE: "noetic.decision.source";
55
65
  /** Comma-separated ids of questions the model refused. */
56
- readonly DECISION_REFUSED: 'noetic.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: 'noetic.decision.';
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";
75
+ /**
76
+ * A layer's `onReturn` hook failed, so a spawn or inParallel path returned
77
+ * its result unchanged. Added to the child's span: `{ stepId, layerId,
78
+ * message }`. Defined in `@noetic-tools/context`, whose `returnLayers`
79
+ * emits it and cannot import core.
80
+ */
81
+ readonly SPAWN_SUMMARY_FAILED: "spawn.summary_failed";
59
82
  };
@@ -1,3 +1,4 @@
1
+ import { SPAWN_SUMMARY_FAILED_EVENT } from '@noetic-tools/context';
1
2
  // GenAI Semantic Convention attributes
2
3
  export const GenAI = {
3
4
  SYSTEM: 'gen_ai.system',
@@ -27,8 +28,18 @@ export const NoeticAttr = {
27
28
  WORKFLOW_NODES: 'noetic.workflow.nodes',
28
29
  /** JSON array of `{ from, to }` parent→child edges between declared nodes. */
29
30
  WORKFLOW_EDGES: 'noetic.workflow.edges',
30
- /** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
31
+ /**
32
+ * Id of the step a span belongs to: the declared workflow node of an
33
+ * `llm.call`/`tool.call`, the `decide` step of a `decide.call`, the `spawn`
34
+ * step of a `spawn` span, the path step of an `inParallel.path` span.
35
+ */
31
36
  NODE_ID: 'noetic.node.id',
37
+ /** Id of the `inParallel` step an `inParallel.path` span belongs to. */
38
+ PARALLEL_ID: 'noetic.parallel.id',
39
+ /** Zero-based position of the path among its `inParallel` siblings. */
40
+ PARALLEL_INDEX: 'noetic.parallel.index',
41
+ /** `NoeticError` kind a `spawn`/`inParallel.path` span failed with. */
42
+ ERROR_KIND: 'noetic.error.kind',
32
43
  /**
33
44
  * Conversation/session this run belongs to (the run's `ctx.threadId`). Stamped
34
45
  * on the root `workflow.run` span so every turn of a multi-turn session shares
@@ -60,3 +71,18 @@ export const NoeticAttr = {
60
71
  /** Prefix for per-question attributes: `noetic.decision.<id>.value` etc. */
61
72
  DECISION_PREFIX: 'noetic.decision.',
62
73
  };
74
+ // Span event names. Steps that open no span of their own (e.g. `loop`) add
75
+ // these to the enclosing `ctx.span`.
76
+ export const NoeticEvent = {
77
+ /** One per loop iteration: `{ loop, iteration, stop, reason? }`. */
78
+ LOOP_VERDICT: 'loop.verdict',
79
+ /** A channel dropped data or is waiting with no timeout: `{ kind, channelName, message, capacity?, operation? }`. */
80
+ CHANNEL_WARNING: 'channel.warning',
81
+ /**
82
+ * A layer's `onReturn` hook failed, so a spawn or inParallel path returned
83
+ * its result unchanged. Added to the child's span: `{ stepId, layerId,
84
+ * message }`. Defined in `@noetic-tools/context`, whose `returnLayers`
85
+ * emits it and cannot import core.
86
+ */
87
+ SPAWN_SUMMARY_FAILED: SPAWN_SUMMARY_FAILED_EVENT,
88
+ };
@@ -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, opts?: ChannelSendOpts): Promise<void>;
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;