@noetic-tools/core 2.2.1 → 2.4.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.
@@ -66,7 +66,7 @@ export declare const step: {
66
66
  * @throws `NoeticConfigError` with code `MISSING_EXECUTE_FUNCTION` if `execute` is not provided.
67
67
  * @throws `NoeticConfigError` with code `DUPLICATE_STEP_ID` if another step with the same id is already registered with a different body.
68
68
  */
69
- run<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepRunOpts<TMemory, I, O>): StepRun<TMemory, I, O>;
69
+ run<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepRunOpts<TMemory, I, O>): StepRun<TMemory, I, O>;
70
70
  /**
71
71
  * Creates an LLM model call step with optional tools and structured output.
72
72
  *
@@ -82,7 +82,7 @@ export declare const step: {
82
82
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
83
83
  * @throws `NoeticConfigError` with code `MISSING_MODEL` if an eager `model` string is empty. Function-form models are validated at step execution.
84
84
  */
85
- llm<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepLLMOpts<TMemory, O>): StepLLM<TMemory, I, O>;
85
+ llm<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepLLMOpts<TMemory, O>): StepLLM<TMemory, I, O>;
86
86
  /**
87
87
  * Creates a tool execution step that invokes a typed tool definition.
88
88
  *
@@ -95,7 +95,7 @@ export declare const step: {
95
95
  * @throws `NoeticConfigError` with code `EMPTY_STEP_ID` if `id` is empty.
96
96
  * @throws `NoeticConfigError` with code `MISSING_TOOL` if `tool` is not provided.
97
97
  */
98
- tool<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepToolOpts<I, O>): StepTool<TMemory, I, O>;
98
+ tool<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepToolOpts<I, O>): StepTool<TMemory, I, O>;
99
99
  /**
100
100
  * Creates a step that delegates a turn to the Claude Code harness.
101
101
  *
@@ -108,27 +108,27 @@ export declare const step: {
108
108
  * @returns A `StepSubHarness` of kind `claude-code`, auto-registered in the step registry.
109
109
  * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_SUB_HARNESS` / `SUB_HARNESS_KIND_MISMATCH`.
110
110
  */
111
- claudeCode<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
111
+ claudeCode<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
112
112
  /**
113
113
  * Creates a step that delegates a turn to the Codex harness.
114
114
  * @public
115
115
  * @param opts - See {@link step.claudeCode}; `opts.harness` is a `codex(...)` adapter.
116
116
  * @returns A `StepSubHarness` of kind `codex`.
117
117
  */
118
- codex<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
118
+ codex<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
119
119
  /**
120
120
  * Creates a step that delegates a turn to the opencode harness.
121
121
  * @public
122
122
  * @param opts - See {@link step.claudeCode}; `opts.harness` is an `opencode(...)` adapter.
123
123
  * @returns A `StepSubHarness` of kind `opencode`.
124
124
  */
125
- opencode<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
125
+ opencode<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
126
126
  /**
127
127
  * Creates a step that delegates a turn to the pi harness.
128
128
  * @public
129
129
  * @param opts - See {@link step.claudeCode}; `opts.harness` is a `pi(...)` adapter.
130
130
  * @returns A `StepSubHarness` of kind `pi`.
131
131
  */
132
- pi<TMemory = Readonly<Record<string, Record<string, unknown>>>, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
132
+ pi<TMemory = ContextMemory, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TMemory, O>): StepSubHarness<TMemory, I, O>;
133
133
  };
134
134
  export {};
@@ -1,4 +1,4 @@
1
- import type { FunctionCallItem, FunctionCallOutputItem, Item, ItemSchemaExtensions, Tool, ToolExecutionContext, ToolMemoryDeclaration, ToolResultExtensionItem } from '@noetic-tools/types';
1
+ import type { FunctionCallItem, FunctionCallOutputItem, Item, ItemSchemaExtensions, Tool, ToolExecutionContext, ToolMemoryDeclaration, ToolResultExtensionItem, ToolUiDeclaration } from '@noetic-tools/types';
2
2
  import type { ZodTypeAny, z } from 'zod';
3
3
  interface ToolConfig<I extends ZodTypeAny, O extends ZodTypeAny> {
4
4
  name: string;
@@ -18,6 +18,8 @@ interface ToolConfig<I extends ZodTypeAny, O extends ZodTypeAny> {
18
18
  needsApproval?: boolean;
19
19
  /** Optional memory declaration — the runtime generates a MemoryLayer from this via toolMemoryLayer(). */
20
20
  memory?: ToolMemoryDeclaration;
21
+ /** Optional UI declaration — the runtime emits the rendered fragments at call/progress/result points. */
22
+ ui?: ToolUiDeclaration<I, O>;
21
23
  }
22
24
  interface GeneratorToolConfig<I extends ZodTypeAny, E extends ZodTypeAny, O extends ZodTypeAny> {
23
25
  name: string;
@@ -38,6 +40,8 @@ interface GeneratorToolConfig<I extends ZodTypeAny, E extends ZodTypeAny, O exte
38
40
  needsApproval?: boolean;
39
41
  /** Optional memory declaration — the runtime generates a MemoryLayer from this via toolMemoryLayer(). */
40
42
  memory?: ToolMemoryDeclaration;
43
+ /** Optional UI declaration — the runtime emits the rendered fragments at call/progress/result points. */
44
+ ui?: ToolUiDeclaration<I, O, z.infer<E>>;
41
45
  }
42
46
  /**
43
47
  * Creates a typed Tool with Zod schema inference for input and output.
@@ -36,6 +36,7 @@ export function tool(config) {
36
36
  execute: config.execute,
37
37
  needsApproval: config.needsApproval,
38
38
  memory: config.memory,
39
+ ui: config.ui,
39
40
  };
40
41
  }
41
42
  /**
@@ -56,6 +57,7 @@ export function toolWithGenerator(config) {
56
57
  execute: config.execute,
57
58
  needsApproval: config.needsApproval,
58
59
  memory: config.memory,
60
+ ui: config.ui,
59
61
  };
60
62
  }
61
63
  //#endregion
@@ -271,39 +271,63 @@ function hydrateForkNode(node, ctx) {
271
271
  _optimizable: optimizable,
272
272
  });
273
273
  }
274
+ /**
275
+ * Resolves layer names declared on a `provide` / `spawn` node against the
276
+ * registry on `HydrationContext.layers`. Returns `[]` when no registry was
277
+ * supplied — the host runs with its harness-default layers in that case.
278
+ */
279
+ function resolveNamedLayers({ names, nodeKind, nodeId, ctx, }) {
280
+ if (!ctx.layers) {
281
+ return [];
282
+ }
283
+ return names.map((name) => {
284
+ const layer = ctx.layers?.get(name);
285
+ if (!layer) {
286
+ throw new NoeticConfigError({
287
+ code: 'UNKNOWN_LAYER_REFERENCE',
288
+ message: `Memory layer '${name}' referenced in ${nodeKind} node '${nodeId}' is not registered.`,
289
+ hint: `Available layers: ${[
290
+ ...(ctx.layers?.keys() ?? []),
291
+ ].join(', ') || '(none)'}. Pass named layers via HydrationContext.layers.`,
292
+ });
293
+ }
294
+ return layer;
295
+ });
296
+ }
274
297
  function hydrateSpawnNode(node, ctx) {
275
298
  if (node.kind !== 'spawn') {
276
299
  return frameworkCast(undefined);
277
300
  }
301
+ // `memory` is left undefined when the node names no layers so the child
302
+ // inherits the parent's layers (spec 04). An explicit list replaces them.
303
+ const resolvedLayers = node.layers
304
+ ? resolveNamedLayers({
305
+ names: node.layers,
306
+ nodeKind: 'spawn',
307
+ nodeId: node.id,
308
+ ctx,
309
+ })
310
+ : undefined;
278
311
  return spawn({
279
312
  id: node.id,
280
313
  child: hydrateNode(node.child, ctx),
281
314
  timeout: node.timeout,
315
+ memory: resolvedLayers,
282
316
  });
283
317
  }
284
318
  function hydrateProvideNode(node, ctx) {
285
319
  if (node.kind !== 'provide') {
286
320
  return frameworkCast(undefined);
287
321
  }
288
- const resolvedLayers = ctx.layers
289
- ? node.layers.map((name) => {
290
- const layer = ctx.layers?.get(name);
291
- if (!layer) {
292
- throw new NoeticConfigError({
293
- code: 'UNKNOWN_LAYER_REFERENCE',
294
- message: `Memory layer '${name}' referenced in provide node '${node.id}' is not registered.`,
295
- hint: `Available layers: ${[
296
- ...(ctx.layers?.keys() ?? []),
297
- ].join(', ') || '(none)'}. Pass named layers via HydrationContext.layers.`,
298
- });
299
- }
300
- return layer;
301
- })
302
- : [];
303
322
  return provide({
304
323
  id: node.id,
305
324
  child: hydrateNode(node.child, ctx),
306
- memory: resolvedLayers,
325
+ memory: resolveNamedLayers({
326
+ names: node.layers,
327
+ nodeKind: 'provide',
328
+ nodeId: node.id,
329
+ ctx,
330
+ }),
307
331
  });
308
332
  }
309
333
  function hydrateLoopNode(node, ctx) {
@@ -2,8 +2,14 @@
2
2
  * Control-flow step handlers: branch, fork, loop, every.
3
3
  */
4
4
  import type { Context, ExecuteStepFn, StepBranch, StepEvery, StepFork, StepLoop } from '@noetic-tools/types';
5
+ import type { ItemSchemaRegistry, LayerStateStore } from './action-deps.js';
5
6
  export declare function executeBranch<TMemory, I, O>(step: StepBranch<TMemory, I, O>, input: I, ctx: Context<TMemory>, executeStep: ExecuteStepFn): Promise<O>;
6
- export declare function executeFork<TMemory, I, O>(step: StepFork<TMemory, I, O>, input: I, ctx: Context<TMemory>, executeStep: ExecuteStepFn): Promise<O>;
7
+ /** Layer-boundary wiring supplied by the interpreter (see `buildSpawnOpts`). */
8
+ export interface ExecuteForkOpts {
9
+ layerStore?: LayerStateStore;
10
+ itemSchemas?: ItemSchemaRegistry;
11
+ }
12
+ export declare function executeFork<TMemory, I, O>(step: StepFork<TMemory, I, O>, input: I, ctx: Context<TMemory>, rawExecuteStep: ExecuteStepFn, opts?: ExecuteForkOpts): Promise<O>;
7
13
  export declare function executeLoop<TMemory, I, O>(step: StepLoop<TMemory, I, O>, input: I, ctx: Context<TMemory>, executeStep: ExecuteStepFn): Promise<O>;
8
14
  /**
9
15
  * Executes an `every` step: runs the body step forever, paced by `ms ± jitter`,
@@ -4,6 +4,7 @@
4
4
  import { createMessage, frameworkCast, isNoeticError, NoeticErrorImpl } from '@noetic-tools/types';
5
5
  import { ContextImpl } from '../runtime/context-impl.js';
6
6
  import { snapshotCwdState } from '../runtime/cwd-helpers.js';
7
+ import { contextToExecCtx, returnLayers, spawnLayers } from './action-deps.js';
7
8
  import { cloneWithGuard } from './clone-guard.js';
8
9
  import { getContextChannelStore, isContextImpl, isMutableContext } from './typeguards.js';
9
10
  //#region branch
@@ -35,9 +36,83 @@ function createChildContexts(ctx, count, stepId) {
35
36
  resourceId,
36
37
  channelStore,
37
38
  cwdState: snapshotCwdState(ctx),
39
+ // Layers and the harness tool pool cross the fork boundary: without
40
+ // them an `llm` step inside a path would run with no memory
41
+ // projection and no layer tools, and a nested `spawn` would have no
42
+ // parent layers to inherit. Per-path layer STATE is still isolated —
43
+ // see `createForkLayerBridge`.
44
+ layers: ctx.layers,
45
+ unifiedTools: ctx.unifiedTools,
38
46
  }));
39
47
  }
40
- export async function executeFork(step, input, ctx, executeStep) {
48
+ function createForkLayerBridge({ ctx, childContexts, opts, }) {
49
+ const layers = ctx.layers;
50
+ const layerStore = opts?.layerStore;
51
+ if (!layers || layers.length === 0 || !layerStore) {
52
+ return null;
53
+ }
54
+ const parentExecCtx = contextToExecCtx(ctx);
55
+ const childExecCtxs = childContexts.map((child) => contextToExecCtx(child));
56
+ let tail = Promise.resolve();
57
+ function serialize(fn) {
58
+ const run = tail.then(fn, fn);
59
+ tail = run.then(() => undefined, () => undefined);
60
+ return run;
61
+ }
62
+ return {
63
+ async seed(index) {
64
+ await spawnLayers({
65
+ layers: layers ?? [],
66
+ parentCtx: parentExecCtx,
67
+ childCtx: childExecCtxs[index],
68
+ store: layerStore,
69
+ itemSchemas: opts?.itemSchemas,
70
+ });
71
+ },
72
+ settle(index, result) {
73
+ return serialize(() => returnLayers({
74
+ layers: layers ?? [],
75
+ parentCtx: parentExecCtx,
76
+ childCtx: childExecCtxs[index],
77
+ childLog: childContexts[index].itemLog,
78
+ result,
79
+ store: layerStore,
80
+ }));
81
+ },
82
+ cleanup(index) {
83
+ layerStore.cleanup(childExecCtxs[index].executionId);
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Wraps the interpreter's `executeStep` so every forked path runs inside the
89
+ * layer child boundary. Contexts that are not one of this fork's children
90
+ * (nested steps within a path) pass straight through.
91
+ */
92
+ function withForkLayers(executeStep, childContexts, bridge) {
93
+ if (!bridge) {
94
+ return executeStep;
95
+ }
96
+ const indexByContext = new Map(childContexts.map((child, i) => [
97
+ child,
98
+ i,
99
+ ]));
100
+ return async (step, input, childCtx) => {
101
+ const index = indexByContext.get(frameworkCast(childCtx));
102
+ if (index === undefined) {
103
+ return executeStep(step, input, childCtx);
104
+ }
105
+ await bridge.seed(index);
106
+ try {
107
+ const output = await executeStep(step, input, childCtx);
108
+ return await bridge.settle(index, output);
109
+ }
110
+ finally {
111
+ bridge.cleanup(index);
112
+ }
113
+ };
114
+ }
115
+ export async function executeFork(step, input, ctx, rawExecuteStep, opts) {
41
116
  const baseCtx = frameworkCast(ctx);
42
117
  const paths = step.paths(input, ctx);
43
118
  if (paths.length === 0) {
@@ -56,6 +131,11 @@ export async function executeFork(step, input, ctx, executeStep) {
56
131
  }
57
132
  const childContexts = createChildContexts(baseCtx, paths.length, step.id);
58
133
  const concurrency = step.concurrency ?? paths.length;
134
+ const executeStep = withForkLayers(rawExecuteStep, childContexts, createForkLayerBridge({
135
+ ctx: baseCtx,
136
+ childContexts,
137
+ opts,
138
+ }));
59
139
  switch (step.mode) {
60
140
  case 'all':
61
141
  return executeAll(step, paths, input, ctx, childContexts, executeStep, concurrency);
@@ -194,7 +194,12 @@ export async function execute(step, input, ctx) {
194
194
  result = await executeBranch(step, input, ctx, (s, i, c) => execute(s, i, c));
195
195
  break;
196
196
  case 'fork':
197
- result = await executeFork(step, input, ctx, (s, i, c) => execute(s, i, c));
197
+ result = await executeFork(step, input, ctx, (s, i, c) => execute(s, i, c), {
198
+ // Fork paths are child executions: the layer store lets their
199
+ // `onSpawn`/`onReturn` boundary run (see `createForkLayerBridge`).
200
+ layerStore: harnessWithLayerStore(baseCtx).layerStateStore,
201
+ itemSchemas: baseCtx.itemSchemas,
202
+ });
198
203
  break;
199
204
  case 'spawn':
200
205
  result = await dispatchViaAdapter(step, input, ctx, () => executeSpawn(step, input, ctx, (s, i, c) => execute(s, i, c), buildSpawnOpts(ctx)));
@@ -1,33 +1,33 @@
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";
6
- readonly COST: "gen_ai.cost";
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
+ readonly COST: 'gen_ai.cost';
7
7
  };
8
8
  export declare const ToolAttr: {
9
- readonly NAME: "tool.name";
10
- readonly NEEDS_APPROVAL: "tool.needs_approval";
9
+ readonly NAME: 'tool.name';
10
+ readonly NEEDS_APPROVAL: 'tool.needs_approval';
11
11
  };
12
12
  export declare const NoeticAttr: {
13
13
  /** Full JSON-serialised `WorkflowDocument` (the DAG, lossless). */
14
- readonly WORKFLOW_DOCUMENT: "noetic.workflow.document";
14
+ readonly WORKFLOW_DOCUMENT: 'noetic.workflow.document';
15
15
  /** Workflow document schema version. */
16
- readonly WORKFLOW_VERSION: "noetic.workflow.version";
16
+ readonly WORKFLOW_VERSION: 'noetic.workflow.version';
17
17
  /** Count of declared nodes in the workflow tree. */
18
- readonly WORKFLOW_NODE_COUNT: "noetic.workflow.node_count";
18
+ readonly WORKFLOW_NODE_COUNT: 'noetic.workflow.node_count';
19
19
  /** JSON array of `{ id, kind }` for every declared node (flattened graph). */
20
- readonly WORKFLOW_NODES: "noetic.workflow.nodes";
20
+ readonly WORKFLOW_NODES: 'noetic.workflow.nodes';
21
21
  /** JSON array of `{ from, to }` parent→child edges between declared nodes. */
22
- readonly WORKFLOW_EDGES: "noetic.workflow.edges";
22
+ readonly WORKFLOW_EDGES: 'noetic.workflow.edges';
23
23
  /** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
24
- readonly NODE_ID: "noetic.node.id";
24
+ readonly NODE_ID: 'noetic.node.id';
25
25
  /**
26
26
  * Conversation/session this run belongs to (the run's `ctx.threadId`). Stamped
27
27
  * on the root `workflow.run` span so every turn of a multi-turn session shares
28
28
  * one id, letting a consumer group per-run traces back into their session.
29
29
  */
30
- readonly SESSION_ID: "noetic.session.id";
30
+ readonly SESSION_ID: 'noetic.session.id';
31
31
  /** Resource the session is scoped to (the run's `ctx.resourceId`), when set. */
32
- readonly RESOURCE_ID: "noetic.resource.id";
32
+ readonly RESOURCE_ID: 'noetic.resource.id';
33
33
  };
@@ -2,7 +2,7 @@
2
2
  * Pattern that lets an LLM generate a workflow as JSON during execution,
3
3
  * then hydrates and runs it within the same harness session.
4
4
  */
5
- import type { ContextMemory } from '@noetic-tools/memory';
5
+ import type { ContextMemory, MemoryLayer } from '@noetic-tools/memory';
6
6
  import type { AgentHarnessContract, Context, Step, Tool } from '@noetic-tools/types';
7
7
  /** @public Options for creating a dynamic workflow step. */
8
8
  export interface DynamicWorkflowOpts {
@@ -34,6 +34,12 @@ export interface ParseAndRunWorkflowOpts {
34
34
  tools: Tool[];
35
35
  input?: string;
36
36
  maxDepth?: number;
37
+ /**
38
+ * Memory layers the document's `provide` / `spawn` nodes may reference by name.
39
+ * Without a registry those nodes resolve to NO layers rather than failing, so a
40
+ * host that runs layer-bearing workflows must pass its layers here.
41
+ */
42
+ layers?: ReadonlyMap<string, MemoryLayer>;
37
43
  }
38
44
  /**
39
45
  * Parses raw JSON into a `WorkflowDocument`, hydrates it into a live step
@@ -142,6 +142,7 @@ export async function parseAndRunWorkflow(opts) {
142
142
  const hydrationCtx = {
143
143
  tools: buildToolMap(opts.tools),
144
144
  executeStep,
145
+ layers: opts.layers,
145
146
  };
146
147
  const hydrated = hydrateWorkflow(parseResult.doc, hydrationCtx);
147
148
  const runSpan = beginWorkflowRunSpan(opts.harness, opts.ctx, parseResult.doc);
@@ -44,17 +44,17 @@ export type UntilPredicate = {
44
44
  export declare const UntilPredicateSchema: z.ZodType<UntilPredicate>;
45
45
  /** @public ESM literal enum for merge strategies. */
46
46
  export declare const MergeStrategy: {
47
- readonly Last: "last";
48
- readonly First: "first";
49
- readonly Concat: "concat";
47
+ readonly Last: 'last';
48
+ readonly First: 'first';
49
+ readonly Concat: 'concat';
50
50
  };
51
51
  /** @public Named merge strategy for fork nodes. */
52
52
  export type MergeStrategy = (typeof MergeStrategy)[keyof typeof MergeStrategy];
53
53
  /** @public Zod schema for the named merge strategy used by `fork` nodes. */
54
54
  export declare const MergeStrategySchema: z.ZodEnum<{
55
55
  concat: "concat";
56
- last: "last";
57
56
  first: "first";
57
+ last: "last";
58
58
  }>;
59
59
  declare const ModelParamsSchema: z.ZodObject<{
60
60
  temperature: z.ZodOptional<z.ZodNumber>;
@@ -84,9 +84,9 @@ declare const LlmToolEntrySchema: z.ZodObject<{
84
84
  declare const RetryPolicySchema: z.ZodObject<{
85
85
  maxAttempts: z.ZodNumber;
86
86
  backoff: z.ZodEnum<{
87
+ exponential: "exponential";
87
88
  fixed: "fixed";
88
89
  linear: "linear";
89
- exponential: "exponential";
90
90
  }>;
91
91
  initialDelay: z.ZodNumber;
92
92
  maxDelay: z.ZodOptional<z.ZodNumber>;
@@ -94,10 +94,10 @@ declare const RetryPolicySchema: z.ZodObject<{
94
94
  declare const HarnessSettingsSchema: z.ZodObject<{
95
95
  model: z.ZodOptional<z.ZodString>;
96
96
  permissionMode: z.ZodOptional<z.ZodEnum<{
97
- default: "default";
98
- plan: "plan";
99
97
  acceptEdits: "acceptEdits";
100
98
  bypassPermissions: "bypassPermissions";
99
+ default: "default";
100
+ plan: "plan";
101
101
  }>>;
102
102
  maxTurns: z.ZodOptional<z.ZodNumber>;
103
103
  allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -106,9 +106,9 @@ declare const HarnessSettingsSchema: z.ZodObject<{
106
106
  declare const HarnessSessionPolicySchema: z.ZodObject<{
107
107
  reuse: z.ZodOptional<z.ZodString>;
108
108
  onComplete: z.ZodOptional<z.ZodEnum<{
109
- stop: "stop";
110
- detach: "detach";
111
109
  destroy: "destroy";
110
+ detach: "detach";
111
+ stop: "stop";
112
112
  }>>;
113
113
  }, z.core.$strip>;
114
114
  interface WorkflowNodeBase {
@@ -200,6 +200,13 @@ export interface SpawnWorkflowNode extends WorkflowNodeBase {
200
200
  kind: 'spawn';
201
201
  child: WorkflowNode;
202
202
  timeout?: number;
203
+ /**
204
+ * Memory layers the child runs with, by registered name (resolved from
205
+ * `HydrationContext.layers`, like `provide`). Omit to inherit the parent's
206
+ * layers — the default spawn behaviour. Naming layers here REPLACES the
207
+ * inherited set for the child, so list every layer the child needs.
208
+ */
209
+ layers?: string[];
203
210
  }
204
211
  export interface ProvideWorkflowNode extends WorkflowNodeBase {
205
212
  kind: 'provide';
@@ -210,6 +210,7 @@ const SpawnNodeSchema = z.object({
210
210
  ...SHARED_FIELDS,
211
211
  child: WorkflowNodeRef,
212
212
  timeout: z.number().positive().optional(),
213
+ layers: z.array(z.string().min(1)).min(1).optional(),
213
214
  });
214
215
  const ProvideNodeSchema = z.object({
215
216
  kind: z.literal('provide'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and memory layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",
@@ -66,8 +66,8 @@
66
66
  }
67
67
  },
68
68
  "dependencies": {
69
- "@noetic-tools/memory": "^1.0.1",
70
- "@noetic-tools/types": "^1.3.0",
69
+ "@noetic-tools/memory": "^1.1.0",
70
+ "@noetic-tools/types": "^1.4.0",
71
71
  "@openrouter/agent": "^0.6.0",
72
72
  "zod": "^4.0.0"
73
73
  },
@@ -316,6 +316,14 @@
316
316
  "timeout": {
317
317
  "type": "number",
318
318
  "exclusiveMinimum": 0
319
+ },
320
+ "layers": {
321
+ "minItems": 1,
322
+ "type": "array",
323
+ "items": {
324
+ "type": "string",
325
+ "minLength": 1
326
+ }
319
327
  }
320
328
  },
321
329
  "required": [