@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.
Files changed (44) hide show
  1. package/dist/builders/channel-builder.d.ts +18 -0
  2. package/dist/builders/control-flow-builders.d.ts +17 -30
  3. package/dist/builders/decide-builders.d.ts +5 -5
  4. package/dist/builders/decide-builders.js +4 -0
  5. package/dist/builders/every.d.ts +2 -2
  6. package/dist/builders/every.js +2 -0
  7. package/dist/builders/loop-builder.d.ts +1 -1
  8. package/dist/builders/loop-builder.js +1 -1
  9. package/dist/builders/provide-builder.d.ts +2 -2
  10. package/dist/builders/provide-builder.js +2 -0
  11. package/dist/builders/spawn-builder.d.ts +2 -2
  12. package/dist/builders/spawn-builder.js +2 -0
  13. package/dist/builders/step-builders.d.ts +8 -8
  14. package/dist/builders/workflow-hydrator.js +6 -1
  15. package/dist/harness/agent-harness.d.ts +18 -5
  16. package/dist/harness/agent-harness.js +49 -14
  17. package/dist/harness/deps/runtime.d.ts +3 -2
  18. package/dist/harness/deps/runtime.js +2 -2
  19. package/dist/harness/model-call.js +10 -0
  20. package/dist/index.d.ts +7 -5
  21. package/dist/index.js +3 -3
  22. package/dist/interpreter/child-span.d.ts +17 -0
  23. package/dist/interpreter/child-span.js +51 -0
  24. package/dist/interpreter/execute-action.js +21 -16
  25. package/dist/interpreter/execute-control.js +64 -8
  26. package/dist/interpreter/execute.d.ts +10 -0
  27. package/dist/interpreter/execute.js +41 -12
  28. package/dist/observability/genai-attributes.d.ts +45 -29
  29. package/dist/observability/genai-attributes.js +19 -1
  30. package/dist/runtime/channel-store.d.ts +73 -1
  31. package/dist/runtime/channel-store.js +179 -63
  32. package/dist/runtime/context-impl.d.ts +7 -1
  33. package/dist/runtime/context-impl.js +15 -0
  34. package/dist/runtime/durable/harness-checkpoints.d.ts +5 -1
  35. package/dist/runtime/durable/harness-checkpoints.js +8 -2
  36. package/dist/runtime/durable/step-ledger.d.ts +14 -4
  37. package/dist/runtime/durable/step-ledger.js +22 -5
  38. package/dist/types/checkpoint.d.ts +31 -0
  39. package/dist/types/checkpoint.js +29 -0
  40. package/dist/until/combinators.d.ts +10 -0
  41. package/dist/until/combinators.js +37 -13
  42. package/dist/until/predicates.js +4 -2
  43. package/package.json +3 -3
  44. package/schema/noetic-workflow.schema.json +57 -0
@@ -34,3 +34,21 @@ export declare function channel<T>(name: string, opts: {
34
34
  capacity?: number;
35
35
  external?: false;
36
36
  }): Channel<T>;
37
+ /**
38
+ * Creates a typed communication channel whose externality is decided at run
39
+ * time. Pass a literal `true`/`false` to get the narrowed return type instead.
40
+ *
41
+ * @public
42
+ * @param name - Channel name for identification and debugging.
43
+ * @param opts.schema - Zod schema used to validate every message sent through the channel.
44
+ * @param opts.mode - Delivery semantics: `'value'` (last-write-wins), `'queue'` (FIFO), `'topic'` (pub/sub).
45
+ * @param opts.capacity - Buffer size for queue mode (default: 1000). Ignored for value/topic modes.
46
+ * @param opts.external - `true` for an externally writable channel; omit or `false` otherwise.
47
+ * @returns An `ExternalChannel` when `external` is `true`, otherwise a `Channel`.
48
+ */
49
+ export declare function channel<T>(name: string, opts: {
50
+ schema: ZodType<T>;
51
+ mode: 'value' | 'queue' | 'topic';
52
+ capacity?: number;
53
+ external?: boolean;
54
+ }): Channel<T> | ExternalChannel<T>;
@@ -1,5 +1,16 @@
1
1
  import type { ContextData } from '@noetic-tools/context';
2
- import type { Context, ExecuteStepFn, SettleResult, Step, StepConditional, StepInParallelAll, StepInParallelRace, StepInParallelSettle } from '@noetic-tools/types';
2
+ import type { StepConditional, StepInParallelAll, StepInParallelRace, StepInParallelSettle } from '@noetic-tools/types';
3
+ /**
4
+ * Options for `inParallel()`, one shape per mode. `merge` is required for `all`
5
+ * and `settle` and absent for `race`.
6
+ * @public
7
+ */
8
+ export type InParallelOpts<TContext = ContextData, I = unknown, O = unknown> = InParallelRaceOpts<TContext, I, O> | InParallelAllOpts<TContext, I, O> | InParallelSettleOpts<TContext, I, O>;
9
+ type InParallelRaceOpts<TContext, I, O> = Omit<StepInParallelRace<TContext, I, O>, 'kind'>;
10
+ type InParallelAllOpts<TContext, I, O> = Omit<StepInParallelAll<TContext, I, O>, 'kind'>;
11
+ type InParallelSettleOpts<TContext, I, O> = Omit<StepInParallelSettle<TContext, I, O>, 'kind'>;
12
+ /** @public Options for `conditional()`. */
13
+ export type ConditionalOpts<TContext = ContextData, I = unknown, O = unknown> = Omit<StepConditional<TContext, I, O>, 'kind'>;
3
14
  /**
4
15
  * Creates a parallel execution step that races paths and returns the first result.
5
16
  *
@@ -11,13 +22,7 @@ import type { Context, ExecuteStepFn, SettleResult, Step, StepConditional, StepI
11
22
  * @returns A `StepInParallelRace` step.
12
23
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
13
24
  */
14
- export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: {
15
- id: string;
16
- mode: 'race';
17
- paths: (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[];
18
- concurrency?: number;
19
- _optimizable?: Step<TContext>[];
20
- }): StepInParallelRace<TContext, I, O>;
25
+ export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: InParallelRaceOpts<TContext, I, O>): StepInParallelRace<TContext, I, O>;
21
26
  /**
22
27
  * Creates a parallel execution step that waits for all paths and merges results.
23
28
  *
@@ -31,14 +36,7 @@ export declare function inParallel<TContext = ContextData, I = unknown, O = unkn
31
36
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
32
37
  * @throws `NoeticConfigError` with code `MISSING_MERGE_FUNCTION` if `merge` is not provided.
33
38
  */
34
- export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: {
35
- id: string;
36
- mode: 'all';
37
- paths: (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[];
38
- merge: (results: O[], ctx: Context<TContext>) => O;
39
- concurrency?: number;
40
- _optimizable?: Step<TContext>[];
41
- }): StepInParallelAll<TContext, I, O>;
39
+ export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: InParallelAllOpts<TContext, I, O>): StepInParallelAll<TContext, I, O>;
42
40
  /**
43
41
  * Creates a parallel execution step that waits for all paths, collecting errors as results.
44
42
  *
@@ -52,14 +50,7 @@ export declare function inParallel<TContext = ContextData, I = unknown, O = unkn
52
50
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
53
51
  * @throws `NoeticConfigError` with code `MISSING_MERGE_FUNCTION` if `merge` is not provided.
54
52
  */
55
- export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: {
56
- id: string;
57
- mode: 'settle';
58
- paths: (input: I, ctx: Context<TContext>) => Step<TContext, I, O>[];
59
- merge: (results: SettleResult<O>[], ctx: Context<TContext>) => O;
60
- concurrency?: number;
61
- _optimizable?: Step<TContext>[];
62
- }): StepInParallelSettle<TContext, I, O>;
53
+ export declare function inParallel<TContext = ContextData, I = unknown, O = unknown>(opts: InParallelSettleOpts<TContext, I, O>): StepInParallelSettle<TContext, I, O>;
63
54
  /**
64
55
  * Creates a conditional routing step that selects the next step at runtime.
65
56
  *
@@ -72,9 +63,5 @@ export declare function inParallel<TContext = ContextData, I = unknown, O = unkn
72
63
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
73
64
  * @throws `NoeticConfigError` with code `MISSING_ROUTE_FUNCTION` if `route` is not provided.
74
65
  */
75
- export declare function conditional<TContext = ContextData, I = unknown, O = unknown>(opts: {
76
- id: string;
77
- route: (input: I, ctx: Context<TContext>, run: ExecuteStepFn) => Step<TContext, I, O> | null | Promise<Step<TContext, I, O> | null>;
78
- _optimizable?: Step<TContext>[];
79
- _rebuild?: (children: Step<TContext>[]) => StepConditional<TContext, I, O>;
80
- }): StepConditional<TContext, I, O>;
66
+ export declare function conditional<TContext = ContextData, I = unknown, O = unknown>(opts: ConditionalOpts<TContext, I, O>): StepConditional<TContext, I, O>;
67
+ export {};
@@ -1,6 +1,6 @@
1
- import type { AnsweredAll, CaseKeyOf, Context, ContextData, DecideQuestions, DecideStep, DecideView, Lazy, ProjectionPolicy, Step, StepConditional } from '@noetic-tools/types';
1
+ import type { AnsweredAll, CaseKeyOf, Context, ContextData, DecideQuestions, DecideStep, DecideView, Lazy, ProjectionPolicy, Step, StepConditional, StepReplay } from '@noetic-tools/types';
2
2
  /** @public Options for `decide()`. */
3
- export interface StepDecideOpts<TContext, Q extends DecideQuestions> {
3
+ export interface StepDecideOpts<TContext, Q extends DecideQuestions, I = unknown> extends StepReplay<TContext, I> {
4
4
  id: string;
5
5
  /** Decision model id. Falls back to `harness.config.decisionModel`. */
6
6
  model?: Lazy<string, TContext>;
@@ -38,7 +38,7 @@ type OnlyKeys<Cases, Keys extends PropertyKey> = {
38
38
  * @public `routeSwitch` options that route by one question. `cases` needs exactly
39
39
  * one step per outcome; refusals and low-confidence answers take `uncertain`.
40
40
  */
41
- export interface RouteSwitchByKeyOpts<TContext, Q extends DecideQuestions, K extends keyof Q & string, Cases extends Record<CaseKeyOf<Q[K]>, CaseStep>, U extends CaseStep> {
41
+ export interface RouteSwitchByKeyOpts<TContext, Q extends DecideQuestions, K extends keyof Q & string, Cases extends Record<CaseKeyOf<Q[K]>, CaseStep>, U extends CaseStep> extends StepReplay<TContext> {
42
42
  id: string;
43
43
  decide: DecideStep<TContext, unknown, Q>;
44
44
  on: K;
@@ -49,7 +49,7 @@ export interface RouteSwitchByKeyOpts<TContext, Q extends DecideQuestions, K ext
49
49
  uncertain: U;
50
50
  }
51
51
  /** @public `routeSwitch` options that combine answers in a function; `default` catches the rest. */
52
- export interface RouteSwitchByFunctionOpts<TContext, Q extends DecideQuestions, C extends string, Cases extends Partial<Record<C, CaseStep>>, D extends CaseStep, U extends CaseStep> {
52
+ export interface RouteSwitchByFunctionOpts<TContext, Q extends DecideQuestions, C extends string, Cases extends Partial<Record<C, CaseStep>>, D extends CaseStep, U extends CaseStep> extends StepReplay<TContext> {
53
53
  id: string;
54
54
  decide: DecideStep<TContext, unknown, Q>;
55
55
  /** Receives answers with refusals already handled. */
@@ -71,7 +71,7 @@ export interface RouteSwitchByFunctionOpts<TContext, Q extends DecideQuestions,
71
71
  * @throws `NoeticConfigError` `EMPTY_STEP_ID`, `MISSING_MODEL` (empty eager model),
72
72
  * `INVALID_DECIDE_QUESTIONS`, or `INVALID_DECIDE_SAMPLES`.
73
73
  */
74
- export declare function decide<const Q extends DecideQuestions, TContext = ContextData, I = unknown>(opts: StepDecideOpts<TContext, Q>): DecideStep<TContext, I, Q>;
74
+ export declare function decide<const Q extends DecideQuestions, TContext = ContextData, I = unknown>(opts: StepDecideOpts<TContext, Q, I>): DecideStep<TContext, I, Q>;
75
75
  /**
76
76
  * Creates a conditional step that runs a decide step and routes the original input to
77
77
  * the case its answers pick. The output type is the union of every case's
@@ -59,6 +59,8 @@ export function routeSwitch(opts) {
59
59
  minConfidence: 'minConfidence' in opts ? opts.minConfidence : undefined,
60
60
  uncertain: opts.uncertain,
61
61
  default: 'default' in opts ? opts.default : undefined,
62
+ durable: opts.durable,
63
+ replayKey: opts.replayKey,
62
64
  });
63
65
  }
64
66
  /**
@@ -84,6 +86,8 @@ function buildSwitch(plan) {
84
86
  ];
85
87
  return conditional({
86
88
  id: plan.id,
89
+ durable: plan.durable,
90
+ replayKey: plan.replayKey,
87
91
  route: async (input, ctx, run) => {
88
92
  const answers = frameworkCast(await run(plan.decide, input, ctx));
89
93
  const picked = pick(answers);
@@ -1,5 +1,5 @@
1
1
  import type { ContextData } from '@noetic-tools/context';
2
- import type { Channel, ScheduleErrorPolicy, Step, StepSchedule } from '@noetic-tools/types';
2
+ import type { Channel, ScheduleErrorPolicy, Step, StepReplay, StepSchedule } from '@noetic-tools/types';
3
3
  /**
4
4
  * Configuration options accepted by the `schedule()` builder, excluding the `kind`
5
5
  * discriminant. `schedule()` schedules a body step on a fixed-interval, optionally
@@ -7,7 +7,7 @@ import type { Channel, ScheduleErrorPolicy, Step, StepSchedule } from '@noetic-t
7
7
  *
8
8
  * @public
9
9
  */
10
- export interface ScheduleOptions<TContext = ContextData, I = unknown, O = unknown> {
10
+ export interface ScheduleOptions<TContext = ContextData, I = unknown, O = unknown> extends StepReplay<TContext, I> {
11
11
  /** Unique step identifier used in traces and error messages. */
12
12
  id: string;
13
13
  /** Body step executed on each iteration. */
@@ -57,6 +57,8 @@ export function schedule(opts) {
57
57
  inbox: opts.inbox,
58
58
  onError: opts.onError ?? 'continue',
59
59
  jitter: opts.jitter ?? 0,
60
+ durable: opts.durable,
61
+ replayKey: opts.replayKey,
60
62
  };
61
63
  getDefaultRegistrar().register(built);
62
64
  return built;
@@ -10,7 +10,7 @@ export type LoopConfig<TContext = ContextData, I = unknown, O = unknown> = Omit<
10
10
  * @param opts.steps - Array of steps to execute sequentially on each iteration.
11
11
  * @param opts.until - Termination predicate `(snapshot) => Verdict | Promise<Verdict>` evaluated after each iteration.
12
12
  * @param opts.maxIterations - Hard safety cap on iterations (default: 1000).
13
- * @param opts.maxHistorySize - Maximum entries kept in the snapshot history array.
13
+ * @param opts.maxHistorySize - Maximum entries kept in the snapshot history array (default: 100); oldest drop first.
14
14
  * @param opts.inbox - Optional channel for injecting messages into the loop mid-execution.
15
15
  * @param opts.parkTimeout - Ms to wait on inbox before the loop parks itself (default: 0).
16
16
  * @param opts.prepareNext - Transforms `(output, verdict, ctx)` into the next iteration's input.
@@ -8,7 +8,7 @@ import { getDefaultRegistrar } from '../types/step-registrar.js';
8
8
  * @param opts.steps - Array of steps to execute sequentially on each iteration.
9
9
  * @param opts.until - Termination predicate `(snapshot) => Verdict | Promise<Verdict>` evaluated after each iteration.
10
10
  * @param opts.maxIterations - Hard safety cap on iterations (default: 1000).
11
- * @param opts.maxHistorySize - Maximum entries kept in the snapshot history array.
11
+ * @param opts.maxHistorySize - Maximum entries kept in the snapshot history array (default: 100); oldest drop first.
12
12
  * @param opts.inbox - Optional channel for injecting messages into the loop mid-execution.
13
13
  * @param opts.parkTimeout - Ms to wait on inbox before the loop parks itself (default: 0).
14
14
  * @param opts.prepareNext - Transforms `(output, verdict, ctx)` into the next iteration's input.
@@ -1,5 +1,5 @@
1
1
  import type { ContextConfig, ContextData, ContextLayer } from '@noetic-tools/context';
2
- import type { Step, StepWithContext } from '@noetic-tools/types';
2
+ import type { Step, StepReplay, StepWithContext } from '@noetic-tools/types';
3
3
  /**
4
4
  * Creates a provide step that attaches context layers to its child without creating an isolated context.
5
5
  * Like React's Context.Provider — layers are available to all descendant steps.
@@ -17,4 +17,4 @@ export declare function withContext<TContext = ContextData, I = unknown, O = unk
17
17
  id: string;
18
18
  child: Step<TContext, I, O>;
19
19
  context: ContextConfig | ContextLayer[];
20
- }): StepWithContext<TContext, I, O>;
20
+ } & StepReplay<TContext, I>): StepWithContext<TContext, I, O>;
@@ -41,6 +41,8 @@ export function withContext(opts) {
41
41
  id: opts.id,
42
42
  child: opts.child,
43
43
  context: layers,
44
+ durable: opts.durable,
45
+ replayKey: opts.replayKey,
44
46
  };
45
47
  getDefaultRegistrar().register(built);
46
48
  return built;
@@ -1,6 +1,6 @@
1
1
  import type { ContextConfig, ContextData, ContextLayer } from '@noetic-tools/context';
2
- import type { Step, StepSpawn, SubprocessAdapter } from '@noetic-tools/types';
3
- interface SpawnOpts<TContext, I, O> {
2
+ import type { Step, StepReplay, StepSpawn, SubprocessAdapter } from '@noetic-tools/types';
3
+ interface SpawnOpts<TContext, I, O> extends StepReplay<TContext, I> {
4
4
  id: string;
5
5
  child: Step<TContext, I, O>;
6
6
  context?: ContextConfig | ContextLayer[];
@@ -40,6 +40,8 @@ export function spawn(opts) {
40
40
  context: opts.context,
41
41
  timeout: opts.timeout,
42
42
  subprocess: opts.subprocess,
43
+ durable: opts.durable,
44
+ replayKey: opts.replayKey,
43
45
  };
44
46
  getDefaultRegistrar().register(built);
45
47
  // Ensure the child is addressable by id — builders for `runCode`/`callModel`/`invokeTool`
@@ -1,6 +1,6 @@
1
1
  import type { ContextData } from '@noetic-tools/context';
2
- import type { AcpAgent, AcpClientCapabilityConfig, AcpContentBlock, AcpMcpServer, AcpPermissionHandler, AcpPermissionPolicy, AcpSessionPolicy, Context, Lazy, ModelParams, OutputCodec, RetryPolicy, ServerToolSpec, StandardSchemaV1, StepAcpAgent, StepCallModel, StepInvokeTool, StepRunCode, SubprocessAdapter, Tool } from '@noetic-tools/types';
3
- export interface RunCodeOpts<TContext, I, O> {
2
+ import type { AcpAgent, AcpClientCapabilityConfig, AcpContentBlock, AcpMcpServer, AcpPermissionHandler, AcpPermissionPolicy, AcpSessionPolicy, Context, Lazy, ModelParams, OutputCodec, RetryPolicy, ServerToolSpec, StandardSchemaV1, StepAcpAgent, StepCallModel, StepInvokeTool, StepReplay, StepRunCode, SubprocessAdapter, Tool } from '@noetic-tools/types';
3
+ export interface RunCodeOpts<TContext, I, O> extends StepReplay<TContext, I> {
4
4
  id: string;
5
5
  execute: (input: I, ctx: Context<TContext>) => Promise<O>;
6
6
  retry?: RetryPolicy;
@@ -11,7 +11,7 @@ export interface RunCodeOpts<TContext, I, O> {
11
11
  */
12
12
  subprocess?: SubprocessAdapter;
13
13
  }
14
- export interface CallModelOpts<TContext, O> {
14
+ export interface CallModelOpts<TContext, O, I = unknown> extends StepReplay<TContext, I> {
15
15
  id: string;
16
16
  /** Model id. Eager string or `(ctx) => string` getter (resolved at step execution). */
17
17
  model: Lazy<string, TContext>;
@@ -33,12 +33,12 @@ export interface CallModelOpts<TContext, O> {
33
33
  params?: ModelParams;
34
34
  emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
35
35
  }
36
- export interface InvokeToolOpts<I, O> {
36
+ export interface InvokeToolOpts<I, O, TContext = ContextData> extends StepReplay<TContext, I> {
37
37
  id: string;
38
38
  tool: Tool<StandardSchemaV1<unknown, I>, StandardSchemaV1<unknown, O>>;
39
39
  args?: Partial<I>;
40
40
  }
41
- export interface StepAcpAgentOpts<TContext, O> {
41
+ export interface StepAcpAgentOpts<TContext, O, I = unknown> extends StepReplay<TContext, I> {
42
42
  id: string;
43
43
  /** An ACP agent adapter, e.g. `claudeCode()` from `@noetic-tools/acp`. Eager or `(ctx) => AcpAgent`. */
44
44
  agent: Lazy<AcpAgent, TContext>;
@@ -101,7 +101,7 @@ export declare function runCode<TContext = ContextData, I = unknown, O = unknown
101
101
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
102
102
  * @throws `NoeticConfigError` with code `MISSING_MODEL` if an eager `model` string is empty. Function-form models are validated at step execution.
103
103
  */
104
- export declare function callModel<TContext = ContextData, I = unknown, O = unknown>(opts: CallModelOpts<TContext, O>): StepCallModel<TContext, I, O>;
104
+ export declare function callModel<TContext = ContextData, I = unknown, O = unknown>(opts: CallModelOpts<TContext, O, I>): StepCallModel<TContext, I, O>;
105
105
  /**
106
106
  * Creates a tool execution step that invokes a typed tool definition.
107
107
  *
@@ -114,7 +114,7 @@ export declare function callModel<TContext = ContextData, I = unknown, O = unkno
114
114
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
115
115
  * @throws `NoeticConfigError` with code `MISSING_TOOL` if `tool` is not provided.
116
116
  */
117
- export declare function invokeTool<TContext = ContextData, I = unknown, O = unknown>(opts: InvokeToolOpts<I, O>): StepInvokeTool<TContext, I, O>;
117
+ export declare function invokeTool<TContext = ContextData, I = unknown, O = unknown>(opts: InvokeToolOpts<I, O, TContext>): StepInvokeTool<TContext, I, O>;
118
118
  /**
119
119
  * The ACP step builder namespace. Unlike the flattened base builders
120
120
  * (`runCode`, `callModel`, `invokeTool`), the coding-agent builder stays
@@ -140,5 +140,5 @@ export declare const step: {
140
140
  * @returns A `StepAcpAgent`, auto-registered in the step registry.
141
141
  * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_ACP_AGENT` / `MISSING_PROMPT`.
142
142
  */
143
- acpAgent<TContext = ContextData, I = unknown, O = unknown>(opts: StepAcpAgentOpts<TContext, O>): StepAcpAgent<TContext, I, O>;
143
+ acpAgent<TContext = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepAcpAgentOpts<TContext, O, I>): StepAcpAgent<TContext, I, O>;
144
144
  };
@@ -767,7 +767,12 @@ export function hydrateNode(node, ctx) {
767
767
  hint: `Supported kinds: ${Object.keys(NODE_HYDRATORS).join(', ')}.`,
768
768
  });
769
769
  }
770
- return hydrator(node, ctx);
770
+ const hydrated = hydrator(node, ctx);
771
+ // Set in place rather than copied: builders register the step object itself.
772
+ if (node.durable !== undefined) {
773
+ hydrated.durable = node.durable;
774
+ }
775
+ return hydrated;
771
776
  }
772
777
  /**
773
778
  * Hydrates a complete `WorkflowDocument` into a live `Step` tree.
@@ -3,6 +3,7 @@ import { AcpSessionStore } from '../runtime/acp-session-store.js';
3
3
  import type { LayerStateStore, RecallCache } from './deps/context.js';
4
4
  import type { DetachedSpawnOverrides } from './deps/interpreter.js';
5
5
  import type { CheckpointStore, EventBroadcaster, RestoreCheckpointOptions, StepLedgerRetention, StepLedgerStore } from './deps/runtime.js';
6
+ import { ChannelStore } from './deps/runtime.js';
6
7
  import type { AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DecideRequest, DecideResponse, DecisionRecord, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, RecallOptions, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
7
8
  export { createStreamIdleWatchdog } from './model-call.js';
8
9
  /**
@@ -163,9 +164,13 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
163
164
  private readonly decideOverride?;
164
165
  /** Model id → whether it answers natively as a decision model. */
165
166
  private readonly decisionModels;
166
- private readonly channelStore;
167
- /** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
168
- private readonly rootRunDepth;
167
+ /** @internal Channel state; read by the checkpoint functions. */
168
+ readonly channelStore: ChannelStore;
169
+ /**
170
+ * Root contexts with a `run()` in flight, keyed by id, with their
171
+ * re-entrant run depth — see `executeClosingChannels`.
172
+ */
173
+ private readonly liveRoots;
169
174
  private readonly callModelOverride?;
170
175
  private readonly defaultDeliveryMode;
171
176
  private readonly sessions;
@@ -296,11 +301,11 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
296
301
  cwdInit?: string;
297
302
  _broadcaster?: EventBroadcaster;
298
303
  }): Context;
299
- send<T>(channel: Channel<T>, value: T, ctx: Context): Promise<void>;
304
+ send<T>(channel: Channel<T>, value: NoInfer<T>, ctx: Context): Promise<void>;
300
305
  recv<T>(channel: Channel<T>, ctx: Context, opts?: {
301
306
  timeout?: number;
302
307
  }): Promise<T>;
303
- tryRecv<T>(channel: Channel<T>, _ctx: Context): T | null;
308
+ tryRecv<T>(channel: Channel<T>, ctx: Context): T | null;
304
309
  getChannelHandle<T>(channel: ExternalChannel<T>, executionId: string): ChannelHandle<T>;
305
310
  getChannelStream<T>(channel: ExternalChannel<T>, executionId: string): AsyncIterable<T>;
306
311
  /** Resolves layer-provided tools and merges with step tools into ctx.unifiedTools. */
@@ -407,6 +412,14 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
407
412
  * under cancellation (spec 09, Cancellation item 5).
408
413
  */
409
414
  private teardownCancelledContext;
415
+ /**
416
+ * Channel state is shared by every root on this harness, so a channel
417
+ * warning is recorded as a `channel.warning` event on each live root's
418
+ * span (spec 10). A root span counts only if it will reach the trace
419
+ * exporter — a harness span (`SpanImpl`) with a real exporter configured.
420
+ * With no such span the warning goes to the console, so it is never lost.
421
+ */
422
+ private recordChannelWarning;
410
423
  createSpan(name: string, parent: Span | null): Span;
411
424
  getLayerState<T>(executionId: string, layerId: string): T | undefined;
412
425
  setLayerState<T>(executionId: string, layerId: string, state: T): void;
@@ -1,4 +1,5 @@
1
1
  import { frameworkCast, ItemSchemaRegistry, NoeticConfigError } from '@noetic-tools/types';
2
+ import { NoeticEvent } from '../observability/genai-attributes.js';
2
3
  import { SpanImpl } from '../observability/span-impl.js';
3
4
  import { NoopExporter } from '../observability/trace-exporter.js';
4
5
  import { AcpSessionStore } from '../runtime/acp-session-store.js';
@@ -6,7 +7,7 @@ import { callDecide } from './decide-call.js';
6
7
  import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, OpenRouterSdk, } from './deps/adapters.js';
7
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';
8
9
  import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
9
- import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearCheckpoint, collectContextTree, createInMemoryStorage, createStepLedgerStore, filterReasoningStream, filterTextStream, resolveStepLedgerRetention, restoreFromCheckpoint, SessionRunner, StepLedger, snapshotCwdState, } from './deps/runtime.js';
10
+ import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearCheckpoint, collectContextTree, createInMemoryStorage, createStepLedgerStore, filterReasoningStream, filterTextStream, resolveStepLedgerRetention, restoreFromCheckpoint, rootIdOf, SessionRunner, StepLedger, snapshotCwdState, warnToConsole, } from './deps/runtime.js';
10
11
  import { SteeringAction } from './deps/types.js';
11
12
  import { AgentHarnessModelCaller } from './model-call.js';
12
13
  export { createStreamIdleWatchdog } from './model-call.js';
@@ -202,9 +203,13 @@ export class AgentHarness {
202
203
  decideOverride;
203
204
  /** Model id → whether it answers natively as a decision model. */
204
205
  decisionModels = new Map();
206
+ /** @internal Channel state; read by the checkpoint functions. */
205
207
  channelStore;
206
- /** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
207
- rootRunDepth = new Map();
208
+ /**
209
+ * Root contexts with a `run()` in flight, keyed by id, with their
210
+ * re-entrant run depth — see `executeClosingChannels`.
211
+ */
212
+ liveRoots = new Map();
208
213
  callModelOverride;
209
214
  defaultDeliveryMode;
210
215
  sessions = new Map();
@@ -317,7 +322,9 @@ export class AgentHarness {
317
322
  this.decisionsClient = opts._testCallModel
318
323
  ? undefined
319
324
  : createDecisionsClient(opts.callModelDefaults);
320
- this.channelStore = new ChannelStore();
325
+ this.channelStore = new ChannelStore({
326
+ onWarning: (warning) => this.recordChannelWarning(warning),
327
+ });
321
328
  this.traceExporter = opts.traceExporter ?? new NoopExporter();
322
329
  this.layerStateStore =
323
330
  storageEnv?.layerStateStore ??
@@ -589,23 +596,25 @@ export class AgentHarness {
589
596
  if (ctx.parent !== null) {
590
597
  return execute(s, input, ctx);
591
598
  }
592
- const depth = this.rootRunDepth.get(ctx.id) ?? 0;
593
- if (depth === 0) {
599
+ const live = this.liveRoots.get(ctx.id);
600
+ if (!live) {
594
601
  // A restored or sequentially re-run root context reuses its id — clear
595
602
  // any closure from a previous completion so its channels work again.
596
603
  this.channelStore.openExecution(ctx.id);
597
604
  }
598
- this.rootRunDepth.set(ctx.id, depth + 1);
605
+ const entry = live ?? {
606
+ ctx,
607
+ depth: 0,
608
+ };
609
+ entry.depth += 1;
610
+ this.liveRoots.set(ctx.id, entry);
599
611
  try {
600
612
  return await execute(s, input, ctx);
601
613
  }
602
614
  finally {
603
- const remaining = (this.rootRunDepth.get(ctx.id) ?? 1) - 1;
604
- if (remaining > 0) {
605
- this.rootRunDepth.set(ctx.id, remaining);
606
- }
607
- else {
608
- this.rootRunDepth.delete(ctx.id);
615
+ entry.depth -= 1;
616
+ if (entry.depth <= 0) {
617
+ this.liveRoots.delete(ctx.id);
609
618
  this.channelStore.closeExecution(ctx.id);
610
619
  await this.acpSessions.closeOwnedBy(ctx.id);
611
620
  }
@@ -688,6 +697,7 @@ export class AgentHarness {
688
697
  // Internal sender: back-pressured on full queue channels. The calling
689
698
  // context's abort signal rejects a parked send with 'cancelled'.
690
699
  const signal = ctx instanceof ContextImpl ? ctx.abortSignal : undefined;
700
+ this.channelStore.claim(channel, rootIdOf(ctx));
691
701
  return this.channelStore.send(channel, value, {
692
702
  signal,
693
703
  });
@@ -696,9 +706,11 @@ export class AgentHarness {
696
706
  // Wire the calling context's abort signal so ctx.abort() rejects a
697
707
  // pending recv with { kind: 'cancelled' } instead of hanging.
698
708
  const signal = ctx instanceof ContextImpl ? ctx.abortSignal : undefined;
709
+ this.channelStore.claim(channel, rootIdOf(ctx));
699
710
  return this.channelStore.recv(channel, opts?.timeout, signal);
700
711
  }
701
- tryRecv(channel, _ctx) {
712
+ tryRecv(channel, ctx) {
713
+ this.channelStore.claim(channel, rootIdOf(ctx));
702
714
  return this.channelStore.tryRecv(channel);
703
715
  }
704
716
  getChannelHandle(channel, executionId) {
@@ -1003,6 +1015,29 @@ export class AgentHarness {
1003
1015
  ...layers,
1004
1016
  ], ctx);
1005
1017
  }
1018
+ /**
1019
+ * Channel state is shared by every root on this harness, so a channel
1020
+ * warning is recorded as a `channel.warning` event on each live root's
1021
+ * span (spec 10). A root span counts only if it will reach the trace
1022
+ * exporter — a harness span (`SpanImpl`) with a real exporter configured.
1023
+ * With no such span the warning goes to the console, so it is never lost.
1024
+ */
1025
+ recordChannelWarning(warning) {
1026
+ const traced = this.traceExporter instanceof NoopExporter
1027
+ ? []
1028
+ : [
1029
+ ...this.liveRoots.values(),
1030
+ ].filter(({ ctx }) => ctx.span instanceof SpanImpl);
1031
+ if (traced.length === 0) {
1032
+ warnToConsole(warning);
1033
+ return;
1034
+ }
1035
+ for (const { ctx } of traced) {
1036
+ ctx.span.addEvent(NoeticEvent.CHANNEL_WARNING, {
1037
+ ...warning,
1038
+ });
1039
+ }
1040
+ }
1006
1041
  createSpan(name, parent) {
1007
1042
  return new SpanImpl(name, parent);
1008
1043
  }
@@ -1,5 +1,6 @@
1
- export { ChannelStore } from '../../runtime/channel-store.js';
2
- export { ContextImpl, collectContextTree } from '../../runtime/context-impl.js';
1
+ export type { ChannelWarning } from '../../runtime/channel-store.js';
2
+ export { ChannelStore, warnToConsole } from '../../runtime/channel-store.js';
3
+ export { ContextImpl, collectContextTree, rootIdOf } from '../../runtime/context-impl.js';
3
4
  export { snapshotCwdState } from '../../runtime/cwd-helpers.js';
4
5
  export type { CheckpointStore } from '../../runtime/durable/index.js';
5
6
  export type { RestoreCheckpointOptions } from '../../runtime/durable/harness-checkpoints.js';
@@ -1,5 +1,5 @@
1
- export { ChannelStore } from '../../runtime/channel-store.js';
2
- export { ContextImpl, collectContextTree } from '../../runtime/context-impl.js';
1
+ export { ChannelStore, warnToConsole } from '../../runtime/channel-store.js';
2
+ export { ContextImpl, collectContextTree, rootIdOf } from '../../runtime/context-impl.js';
3
3
  export { snapshotCwdState } from '../../runtime/cwd-helpers.js';
4
4
  export { captureCheckpoint, clearCheckpoint, restoreFromCheckpoint, } from '../../runtime/durable/harness-checkpoints.js';
5
5
  export { createStepLedgerStore, resolveStepLedgerRetention, StepLedger, } from '../../runtime/durable/step-ledger.js';
@@ -386,6 +386,7 @@ export class AgentHarnessModelCaller {
386
386
  // the assembled view was cached (see `noteCacheOutcome`).
387
387
  const rounds = [];
388
388
  let totalCost = 0;
389
+ let servedTier;
389
390
  const conversationInput = itemsToInput(prepared.remaining);
390
391
  const textFormat = request.outputSchema
391
392
  ? buildTextFormat(request.outputSchema, request.outputJsonSchema)
@@ -434,6 +435,9 @@ export class AgentHarnessModelCaller {
434
435
  temperature: request.params?.temperature,
435
436
  maxOutputTokens: request.params?.maxTokens,
436
437
  topP: request.params?.topP,
438
+ // The agent's bundled SDK types an older tier list (no `fast` or
439
+ // `ultrafast`); the field is an open enum, so newer tiers pass through.
440
+ serviceTier: frameworkCast(request.params?.serviceTier),
437
441
  ...(textFormat
438
442
  ? {
439
443
  text: textFormat,
@@ -524,6 +528,7 @@ export class AgentHarnessModelCaller {
524
528
  totalUsage.outputTokens += roundUsage.outputTokens;
525
529
  totalUsage.cachedTokens += roundUsage.cachedTokens ?? 0;
526
530
  totalCost += roundCost;
531
+ servedTier = sdkResponse.serviceTier ?? undefined;
527
532
  allItems.push(...roundItems);
528
533
  const functionCalls = roundItems.filter(isFunctionCall);
529
534
  if (functionCalls.length === 0 || !request.tools) {
@@ -563,6 +568,11 @@ export class AgentHarnessModelCaller {
563
568
  },
564
569
  rounds,
565
570
  cost: totalCost > 0 ? totalCost : undefined,
571
+ ...(servedTier
572
+ ? {
573
+ serviceTier: servedTier,
574
+ }
575
+ : {}),
566
576
  };
567
577
  }
568
578
  injectBetweenRoundMessages(ctx, conversationInput, round, emitIfAllowed) {
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export { channel } from './builders/channel-builder.js';
15
15
  /** @public */
16
16
  export { context } from './builders/context-builder.js';
17
17
  /** @public */
18
+ export type { ConditionalOpts, InParallelOpts } from './builders/control-flow-builders.js';
19
+ /** @public */
18
20
  export { conditional, inParallel } from './builders/control-flow-builders.js';
19
21
  /** @public */
20
22
  export { decide, routeSwitch } from './builders/decide-builders.js';
@@ -84,7 +86,7 @@ export { DECISION_PROJECTION, decisionsHistoryLayer, filesystem, findFunctionCal
84
86
  /** @public */
85
87
  export { createMessage, estimateTokens, recordUsage } from '@noetic-tools/types';
86
88
  /** @public */
87
- export { GenAI, NoeticAttr, ToolAttr } from './observability/genai-attributes.js';
89
+ export { GenAI, NoeticAttr, NoeticEvent, ToolAttr } from './observability/genai-attributes.js';
88
90
  /** @public */
89
91
  export { InMemoryExporter, NoopExporter } from './observability/trace-exporter.js';
90
92
  /** @public */
@@ -115,7 +117,7 @@ export type { Channel, ChannelHandle, ExternalChannel } from '@noetic-tools/type
115
117
  /** @public */
116
118
  export type { InferSchemaInput, InferSchemaOutput, InputSchemaConfig, LLMResponse, LlmProviderConfig, ModelParams, RetryPolicy, RoundUsage, SchemaValidationFailure, SchemaValidationResult, SchemaValidationSuccess, ServerToolSpec, StandardJSONSchemaV1, StandardSchemaV1, StepMeta, TokenUsage, Tool, ToolContextDeclaration, UsageCharge, } from '@noetic-tools/types';
117
119
  /** @public */
118
- export { isServerToolSpec, isStandardJsonSchema, isZodSchema, standardIssuesToZodError, validateSchema, } from '@noetic-tools/types';
120
+ export { isServerToolSpec, isStandardJsonSchema, isZodSchema, ServiceTier, standardIssuesToZodError, validateSchema, } from '@noetic-tools/types';
119
121
  /** @public */
120
122
  /** @public */
121
123
  /** @public */
@@ -129,9 +131,9 @@ export { DetachedStatus } from '@noetic-tools/types';
129
131
  /** @public */
130
132
  export type { EmbedFn } from './types/embed.js';
131
133
  /** @public */
132
- export type { CheckpointSnapshot, CwdSnapshot, FrontierFrame, ItemLogSnapshot, PendingAskUserSnapshot, } from './types/checkpoint.js';
134
+ export type { ChannelSnapshot, CheckpointSnapshot, CwdSnapshot, FrontierFrame, ItemLogSnapshot, PendingAskUserSnapshot, } from './types/checkpoint.js';
133
135
  /** @public */
134
- export { CheckpointSchemaVersion, CheckpointSnapshotSchema, CwdSnapshotSchema, FrontierFrameSchema, ItemLogSnapshotSchema, PendingAskUserSnapshotSchema, } from './types/checkpoint.js';
136
+ export { ChannelSnapshotSchema, CheckpointSchemaVersion, CheckpointSnapshotSchema, CwdSnapshotSchema, FrontierFrameSchema, ItemLogSnapshotSchema, PendingAskUserSnapshotSchema, } from './types/checkpoint.js';
135
137
  /** @public */
136
138
  export type { NoeticError } from '@noetic-tools/types';
137
139
  /** @public */
@@ -153,7 +155,7 @@ export type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams,
153
155
  /** @public */
154
156
  export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
155
157
  /** @public */
156
- export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
158
+ export type { ExecuteStepFn, MergeFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepReplay, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
157
159
  /** @public */
158
160
  export type { AnsweredAll, AnswerFor, BooleanAnswer, BooleanQuestion, CaseKeyOf, ChoiceAnswer, ChoiceQuestion, ChoiceValues, DecideAnswer, DecideAnswers, DecideQuestion, DecideQuestions, DecideRequest, DecideResponse, DecideStatePart, DecideStep, DecideView, DecisionMeta, DecisionRecord, DecisionSource, LevelLabels, OnDecisionParams, RecallOptions, ScoreAnswer, ScoreLevel, ScoreQuestion, StepDecide, } from '@noetic-tools/types';
159
161
  /** @public */
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ export { createMessage, estimateTokens, recordUsage } from '@noetic-tools/types'
70
70
  //#endregion
71
71
  //#region Observability
72
72
  /** @public */
73
- export { GenAI, NoeticAttr, ToolAttr } from './observability/genai-attributes.js';
73
+ export { GenAI, NoeticAttr, NoeticEvent, ToolAttr } from './observability/genai-attributes.js';
74
74
  /** @public */
75
75
  export { InMemoryExporter, NoopExporter } from './observability/trace-exporter.js';
76
76
  /** @public */
@@ -90,13 +90,13 @@ export { defaultItemSchemaRegistry, ItemSchema, ItemSchemaRegistry } from '@noet
90
90
  /** @public */
91
91
  export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, } from './schemas/workflow.js';
92
92
  /** @public */
93
- export { isServerToolSpec, isStandardJsonSchema, isZodSchema, standardIssuesToZodError, validateSchema, } from '@noetic-tools/types';
93
+ export { isServerToolSpec, isStandardJsonSchema, isZodSchema, ServiceTier, standardIssuesToZodError, validateSchema, } from '@noetic-tools/types';
94
94
  /** @public */
95
95
  export { getToolCwd, setToolCwd, snapshotCwdState } from './runtime/cwd-helpers.js';
96
96
  /** @public */
97
97
  export { DetachedStatus } from '@noetic-tools/types';
98
98
  /** @public */
99
- export { CheckpointSchemaVersion, CheckpointSnapshotSchema, CwdSnapshotSchema, FrontierFrameSchema, ItemLogSnapshotSchema, PendingAskUserSnapshotSchema, } from './types/checkpoint.js';
99
+ export { ChannelSnapshotSchema, CheckpointSchemaVersion, CheckpointSnapshotSchema, CwdSnapshotSchema, FrontierFrameSchema, ItemLogSnapshotSchema, PendingAskUserSnapshotSchema, } from './types/checkpoint.js';
100
100
  /** @public */
101
101
  export { Slot } from '@noetic-tools/context';
102
102
  /** @public */