@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
@@ -77,6 +77,7 @@ export declare function executeToolCall(params: ExecuteToolCallParams): Promise<
77
77
  * @param apiKey - OpenRouter API key.
78
78
  * @param embeddingModel - Model identifier (default: `'openai/text-embedding-3-small'`).
79
79
  * @returns An `EmbedFn` that produces embedding vectors for the given texts.
80
+ * @throws `NoeticConfigError` with code `MISSING_API_KEY` if `apiKey` is empty or omitted.
80
81
  */
81
82
  export declare function createOpenRouterEmbed(apiKey?: string, embeddingModel?: string): EmbedFn;
82
83
  export {};
@@ -407,6 +407,7 @@ export async function executeToolCall(params) {
407
407
  * @param apiKey - OpenRouter API key.
408
408
  * @param embeddingModel - Model identifier (default: `'openai/text-embedding-3-small'`).
409
409
  * @returns An `EmbedFn` that produces embedding vectors for the given texts.
410
+ * @throws `NoeticConfigError` with code `MISSING_API_KEY` if `apiKey` is empty or omitted.
410
411
  */
411
412
  export function createOpenRouterEmbed(apiKey, embeddingModel) {
412
413
  if (!apiKey) {
@@ -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.
@@ -12,9 +12,10 @@ import type { Step, StepWithContext } from '@noetic-tools/types';
12
12
  * @returns A `StepWithContext` step.
13
13
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
14
14
  * @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
15
+ * @throws `NoeticConfigError` with code `MISSING_CONTEXT_LAYERS` if `context` is not provided.
15
16
  */
16
17
  export declare function withContext<TContext = ContextData, I = unknown, O = unknown>(opts: {
17
18
  id: string;
18
19
  child: Step<TContext, I, O>;
19
20
  context: ContextConfig | ContextLayer[];
20
- }): StepWithContext<TContext, I, O>;
21
+ } & StepReplay<TContext, I>): StepWithContext<TContext, I, O>;
@@ -12,6 +12,7 @@ import { getDefaultRegistrar } from '../types/step-registrar.js';
12
12
  * @returns A `StepWithContext` step.
13
13
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
14
14
  * @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
15
+ * @throws `NoeticConfigError` with code `MISSING_CONTEXT_LAYERS` if `context` is not provided.
15
16
  */
16
17
  export function withContext(opts) {
17
18
  if (!opts.id?.trim()) {
@@ -41,6 +42,8 @@ export function withContext(opts) {
41
42
  id: opts.id,
42
43
  child: opts.child,
43
44
  context: layers,
45
+ durable: opts.durable,
46
+ replayKey: opts.replayKey,
44
47
  };
45
48
  getDefaultRegistrar().register(built);
46
49
  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[];
@@ -26,7 +26,6 @@ interface SpawnOpts<TContext, I, O> {
26
26
  * can dispatch either by id.
27
27
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
28
28
  * @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
29
- * @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered.
30
29
  */
31
30
  export declare function spawn<TContext = ContextData, I = unknown, O = unknown>(opts: SpawnOpts<TContext, I, O>): StepSpawn<TContext, I, O>;
32
31
  export {};
@@ -16,7 +16,6 @@ import { getDefaultRegistrar } from '../types/step-registrar.js';
16
16
  * can dispatch either by id.
17
17
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
18
18
  * @throws `NoeticConfigError` with code `MISSING_CHILD_STEP` if `child` is not provided.
19
- * @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered.
20
19
  */
21
20
  export function spawn(opts) {
22
21
  if (!opts.id?.trim()) {
@@ -40,6 +39,8 @@ export function spawn(opts) {
40
39
  context: opts.context,
41
40
  timeout: opts.timeout,
42
41
  subprocess: opts.subprocess,
42
+ durable: opts.durable,
43
+ replayKey: opts.replayKey,
43
44
  };
44
45
  getDefaultRegistrar().register(built);
45
46
  // 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>;
@@ -83,7 +83,6 @@ export interface StepAcpAgentOpts<TContext, O> {
83
83
  * adapter can dispatch it by id.
84
84
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
85
85
  * @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
86
- * @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered with a different body.
87
86
  */
88
87
  export declare function runCode<TContext = ContextData, I = unknown, O = unknown>(opts: RunCodeOpts<TContext, I, O>): StepRunCode<TContext, I, O>;
89
88
  /**
@@ -101,7 +100,7 @@ export declare function runCode<TContext = ContextData, I = unknown, O = unknown
101
100
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
102
101
  * @throws `NoeticConfigError` with code `MISSING_MODEL` if an eager `model` string is empty. Function-form models are validated at step execution.
103
102
  */
104
- export declare function callModel<TContext = ContextData, I = unknown, O = unknown>(opts: CallModelOpts<TContext, O>): StepCallModel<TContext, I, O>;
103
+ export declare function callModel<TContext = ContextData, I = unknown, O = unknown>(opts: CallModelOpts<TContext, O, I>): StepCallModel<TContext, I, O>;
105
104
  /**
106
105
  * Creates a tool execution step that invokes a typed tool definition.
107
106
  *
@@ -114,7 +113,7 @@ export declare function callModel<TContext = ContextData, I = unknown, O = unkno
114
113
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
115
114
  * @throws `NoeticConfigError` with code `MISSING_TOOL` if `tool` is not provided.
116
115
  */
117
- export declare function invokeTool<TContext = ContextData, I = unknown, O = unknown>(opts: InvokeToolOpts<I, O>): StepInvokeTool<TContext, I, O>;
116
+ export declare function invokeTool<TContext = ContextData, I = unknown, O = unknown>(opts: InvokeToolOpts<I, O, TContext>): StepInvokeTool<TContext, I, O>;
118
117
  /**
119
118
  * The ACP step builder namespace. Unlike the flattened base builders
120
119
  * (`runCode`, `callModel`, `invokeTool`), the coding-agent builder stays
@@ -140,5 +139,5 @@ export declare const step: {
140
139
  * @returns A `StepAcpAgent`, auto-registered in the step registry.
141
140
  * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_ACP_AGENT` / `MISSING_PROMPT`.
142
141
  */
143
- acpAgent<TContext = ContextData, I = unknown, O = unknown>(opts: StepAcpAgentOpts<TContext, O>): StepAcpAgent<TContext, I, O>;
142
+ acpAgent<TContext = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepAcpAgentOpts<TContext, O, I>): StepAcpAgent<TContext, I, O>;
144
143
  };
@@ -46,7 +46,6 @@ function validateAcpAgentOpts(opts) {
46
46
  * adapter can dispatch it by id.
47
47
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
48
48
  * @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
49
- * @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered with a different body.
50
49
  */
51
50
  export function runCode(opts) {
52
51
  if (!opts.id || opts.id.trim() === '') {
@@ -51,6 +51,8 @@ type GeneratorToolConfig<I extends StandardSchemaV1, E extends StandardSchemaV1,
51
51
  * Accepts Zod, Valibot, or any Standard Schema v1 implementation.
52
52
  *
53
53
  * @public
54
+ * @throws `NoeticConfigError` with code `EMPTY_TOOL_NAME` if `name` is empty.
55
+ * @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
54
56
  */
55
57
  export declare function tool<I extends StandardSchemaV1, O extends StandardSchemaV1>(config: ToolConfig<I, O>): Tool<I, O>;
56
58
  /**
@@ -24,6 +24,8 @@ function validateToolConfig(name, execute) {
24
24
  * Accepts Zod, Valibot, or any Standard Schema v1 implementation.
25
25
  *
26
26
  * @public
27
+ * @throws `NoeticConfigError` with code `EMPTY_TOOL_NAME` if `name` is empty.
28
+ * @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
27
29
  */
28
30
  export function tool(config) {
29
31
  validateToolConfig(config.name, config.execute);
@@ -16,6 +16,7 @@ import { loop } from './loop-builder.js';
16
16
  import { withContext } from './provide-builder.js';
17
17
  import { spawn } from './spawn-builder.js';
18
18
  import { callModel, runCode, step } from './step-builders.js';
19
+ import { workflowReplayKey } from './workflow-replay-key.js';
19
20
  //#endregion
20
21
  //#region Until Predicate Hydration
21
22
  function hydrateUntilPredicate(pred) {
@@ -99,13 +100,15 @@ function hydrateCallModelNode(node, ctx) {
99
100
  * runtime still returns the codec's real value. This is the same erasure the
100
101
  * rest of the hydrator applies via `frameworkCast`.
101
102
  */
102
- function availableLibraries(ctx) {
103
- if (!ctx.uiLibraries || ctx.uiLibraries.size === 0) {
104
- return '(none)';
103
+ /** A complete sentence naming what is registered, for a hint. */
104
+ function registeredSentence(noun, names) {
105
+ const list = [
106
+ ...names,
107
+ ];
108
+ if (list.length === 0) {
109
+ return `No ${noun} are registered.`;
105
110
  }
106
- return [
107
- ...ctx.uiLibraries.keys(),
108
- ].join(', ');
111
+ return `Registered ${noun}: ${list.join(', ')}.`;
109
112
  }
110
113
  function resolveOutputCodec(ref, nodeId, ctx) {
111
114
  if (!ref) {
@@ -118,7 +121,7 @@ function resolveOutputCodec(ref, nodeId, ctx) {
118
121
  throw new NoeticConfigError({
119
122
  code: 'UNKNOWN_UI_LIBRARY_REFERENCE',
120
123
  message: `UI library '${ref.library}' referenced in workflow node '${nodeId}' is not registered.`,
121
- hint: `Pass output codecs via HydrationContext.uiLibraries, e.g. new Map([['${ref.library}', openUi(myLibrary)]]) from @noetic-tools/openui. Available: ${availableLibraries(ctx)}`,
124
+ hint: `Pass output codecs via HydrationContext.uiLibraries, e.g. new Map([['${ref.library}', openUi(myLibrary)]]) from @noetic-tools/openui. ${registeredSentence('UI libraries', ctx.uiLibraries?.keys() ?? [])}`,
122
125
  });
123
126
  }
124
127
  function hydrateInvokeToolNode(node, ctx) {
@@ -130,9 +133,7 @@ function hydrateInvokeToolNode(node, ctx) {
130
133
  throw new NoeticConfigError({
131
134
  code: 'UNKNOWN_TOOL_REFERENCE',
132
135
  message: `Tool '${node.toolName}' referenced in workflow node '${node.id}' is not registered.`,
133
- hint: `Available tools: ${[
134
- ...ctx.tools.keys(),
135
- ].join(', ') || '(none)'}`,
136
+ hint: `Register '${node.toolName}' in HydrationContext.tools before hydrating. ${registeredSentence('tools', ctx.tools.keys())}`,
136
137
  });
137
138
  }
138
139
  return frameworkCast(runCode({
@@ -533,9 +534,7 @@ function resolveTools(toolNames, registry) {
533
534
  throw new NoeticConfigError({
534
535
  code: 'UNKNOWN_TOOL_REFERENCE',
535
536
  message: `Tool '${name}' referenced in workflow is not registered.`,
536
- hint: `Available tools: ${[
537
- ...registry.keys(),
538
- ].join(', ') || '(none)'}`,
537
+ hint: `Register '${name}' in HydrationContext.tools before hydrating. ${registeredSentence('tools', registry.keys())}`,
539
538
  });
540
539
  }
541
540
  return resolved;
@@ -767,7 +766,15 @@ export function hydrateNode(node, ctx) {
767
766
  hint: `Supported kinds: ${Object.keys(NODE_HYDRATORS).join(', ')}.`,
768
767
  });
769
768
  }
770
- return hydrator(node, ctx);
769
+ const hydrated = hydrator(node, ctx);
770
+ // Set in place rather than copied: builders register the step object itself.
771
+ if (node.durable !== undefined) {
772
+ hydrated.durable = node.durable;
773
+ }
774
+ if (node.replayKey !== undefined) {
775
+ hydrated.replayKey = workflowReplayKey(node.id, node.replayKey);
776
+ }
777
+ return hydrated;
771
778
  }
772
779
  /**
773
780
  * Hydrates a complete `WorkflowDocument` into a live `Step` tree.
@@ -0,0 +1,15 @@
1
+ import type { WorkflowReplayKey } from '@noetic-tools/types';
2
+ /**
3
+ * Build the `replayKey` function a JSON node's declarative `replayKey` describes
4
+ * (spec 26 § Replay keys). The key is `<node id>:<hash of the selected input>`: the
5
+ * node id keeps two different nodes that read the same input from fighting over one
6
+ * ledger entry, while nodes that share an id — the same worker leaf written into
7
+ * several paths — share entries by design.
8
+ *
9
+ * The interpreter turns a throwing `replayKey` into a warning and runs the step
10
+ * without replay, so the step itself never fails.
11
+ *
12
+ * @throws `NoeticConfigError` with code `INVALID_REPLAY_KEY_INPUT` from the returned
13
+ * function, when `from` meets an input that is not a JSON object.
14
+ */
15
+ export declare function workflowReplayKey(nodeId: string, spec: WorkflowReplayKey): (input: string) => string;
@@ -0,0 +1,46 @@
1
+ import { hashString, NoeticConfigError } from '@noetic-tools/types';
2
+ /**
3
+ * Build the `replayKey` function a JSON node's declarative `replayKey` describes
4
+ * (spec 26 § Replay keys). The key is `<node id>:<hash of the selected input>`: the
5
+ * node id keeps two different nodes that read the same input from fighting over one
6
+ * ledger entry, while nodes that share an id — the same worker leaf written into
7
+ * several paths — share entries by design.
8
+ *
9
+ * The interpreter turns a throwing `replayKey` into a warning and runs the step
10
+ * without replay, so the step itself never fails.
11
+ *
12
+ * @throws `NoeticConfigError` with code `INVALID_REPLAY_KEY_INPUT` from the returned
13
+ * function, when `from` meets an input that is not a JSON object.
14
+ */
15
+ export function workflowReplayKey(nodeId, spec) {
16
+ if (spec === true) {
17
+ return (input) => `${nodeId}:${hashString(asText(input))}`;
18
+ }
19
+ return (input) => {
20
+ const fields = inputObject(input, nodeId);
21
+ return `${nodeId}:${hashString(JSON.stringify(spec.from.map((key) => fields[key] ?? null)))}`;
22
+ };
23
+ }
24
+ /** JSON nodes pass strings, but a `decide` answer can arrive as an object. */
25
+ function asText(input) {
26
+ return typeof input === 'string' ? input : JSON.stringify(input);
27
+ }
28
+ function inputObject(input, nodeId) {
29
+ const parsed = typeof input === 'string' ? parseJson(input) : input;
30
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
31
+ throw new NoeticConfigError({
32
+ code: 'INVALID_REPLAY_KEY_INPUT',
33
+ message: `Node '${nodeId}' keys replay on input properties, but its input is not a JSON object.`,
34
+ hint: 'Use `"replayKey": true` to key on the whole input, or feed the node a JSON object.',
35
+ });
36
+ }
37
+ return Object.fromEntries(Object.entries(parsed));
38
+ }
39
+ function parseJson(text) {
40
+ try {
41
+ return JSON.parse(text);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }