@noetic-tools/core 1.1.2 → 1.2.1

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.
@@ -105,6 +105,13 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
105
105
  readonly layerStateStore: LayerStateStore;
106
106
  /** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
107
107
  readonly recallCache: RecallCache;
108
+ /**
109
+ * Execution ids whose layer `init` hooks have already run, so repeated/nested
110
+ * `run()` calls and the session turn path never re-init (which would clobber
111
+ * accumulated in-memory state by re-hydrating from storage). Keyed by
112
+ * `ctx.id` (the layer-state store's executionId).
113
+ */
114
+ private readonly initializedExecutions;
108
115
  readonly traceExporter: TraceExporter;
109
116
  /**
110
117
  * Long-lived shared cwd state. The same reference is seeded into every
@@ -143,6 +150,14 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
143
150
  /** Run all layer `init` hooks before the first step executes. Per spec 11,
144
151
  * init MUST complete before any recall fires so layer state is populated. */
145
152
  private initAndRun;
153
+ /**
154
+ * Idempotently run layer `init` hooks for `ctx` (keyed by `ctx.id`). Init
155
+ * MUST complete before any recall/store fires so init-bearing layers are not
156
+ * treated as disabled. Guarded so nested/repeated `run()` calls and the
157
+ * session turn path never re-init — re-init would re-hydrate from storage and
158
+ * clobber accumulated in-memory state.
159
+ */
160
+ private ensureLayersInit;
146
161
  detachedSpawn<I, O>(s: Step<ContextMemory, I, O>, input: I, parentCtx: Context, overrides?: DetachedSpawnOverrides): DetachedHandle<O>;
147
162
  createContext(opts?: {
148
163
  parent?: Context;
@@ -110,6 +110,13 @@ export class AgentHarness {
110
110
  layerStateStore;
111
111
  /** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
112
112
  recallCache;
113
+ /**
114
+ * Execution ids whose layer `init` hooks have already run, so repeated/nested
115
+ * `run()` calls and the session turn path never re-init (which would clobber
116
+ * accumulated in-memory state by re-hydrating from storage). Keyed by
117
+ * `ctx.id` (the layer-state store's executionId).
118
+ */
119
+ initializedExecutions = new Set();
113
120
  traceExporter;
114
121
  /**
115
122
  * Long-lived shared cwd state. The same reference is seeded into every
@@ -346,17 +353,46 @@ export class AgentHarness {
346
353
  }
347
354
  //#endregion
348
355
  async run(s, input, ctx) {
356
+ // Populate the unified tool pool when an embedder drives a step directly on
357
+ // a bare `createContext()` context (the turn pipeline does this per turn).
358
+ // Without it, steps that resolve tools dynamically from `ctx.unifiedTools`
359
+ // — and any sub-agents spawned from them — would see no tools.
360
+ if (!ctx.unifiedTools && (s !== undefined || this.harnessTools.length > 0)) {
361
+ this.setUnifiedTools(ctx, [
362
+ ...collectAllTools(s),
363
+ ...this.harnessTools,
364
+ ]);
365
+ }
366
+ // Hydrate/recall/persist memory on the bare `run()` path too (issue #48):
367
+ // configuring `memory` + `storage` must "just work" without going through
368
+ // `execute()`/`seedSessionHistory`. Idempotent — see `ensureLayersInit`.
369
+ await this.ensureLayersInit(ctx);
349
370
  return execute(s, input, ctx);
350
371
  }
351
372
  /** Run all layer `init` hooks before the first step executes. Per spec 11,
352
373
  * init MUST complete before any recall fires so layer state is populated. */
353
374
  async initAndRun(s, input, ctx) {
375
+ await this.ensureLayersInit(ctx);
376
+ return execute(s, input, ctx);
377
+ }
378
+ /**
379
+ * Idempotently run layer `init` hooks for `ctx` (keyed by `ctx.id`). Init
380
+ * MUST complete before any recall/store fires so init-bearing layers are not
381
+ * treated as disabled. Guarded so nested/repeated `run()` calls and the
382
+ * session turn path never re-init — re-init would re-hydrate from storage and
383
+ * clobber accumulated in-memory state.
384
+ */
385
+ async ensureLayersInit(ctx) {
354
386
  const layers = ctx.layers;
355
387
  const storage = this.config.storage;
356
- if (layers && layers.length > 0 && storage) {
357
- await this.initLayers(layers, ctx, storage);
388
+ if (!layers || layers.length === 0 || !storage) {
389
+ return;
358
390
  }
359
- return execute(s, input, ctx);
391
+ if (this.initializedExecutions.has(ctx.id)) {
392
+ return;
393
+ }
394
+ this.initializedExecutions.add(ctx.id);
395
+ await this.initLayers(layers, ctx, storage);
360
396
  }
361
397
  detachedSpawn(s, input, parentCtx, overrides) {
362
398
  return dispatchStepThroughAdapter(this, s, input, parentCtx, overrides);
@@ -528,6 +564,9 @@ export class AgentHarness {
528
564
  });
529
565
  }
530
566
  async disposeLayers(layers, ctx) {
567
+ // Drop the init guard so a deliberate dispose→run cycle re-hydrates from
568
+ // storage (disposeLayers also clears the layer-state store for this id).
569
+ this.initializedExecutions.delete(ctx.id);
531
570
  await disposeLayers({
532
571
  layers,
533
572
  ctx: this.toExecCtx(ctx),
@@ -411,6 +411,19 @@ async function collectSpawnItems({ layers, parentExecutionCtx, childExecutionCtx
411
411
  });
412
412
  return spawnResults.flatMap((r) => r.items);
413
413
  }
414
+ /** Adds a spawned child's accumulated token/cost usage to its parent. */
415
+ function rollUpSpawnUsage(parent, child) {
416
+ if (!isMutableContext(parent)) {
417
+ return;
418
+ }
419
+ parent.tokens.input += child.tokens.input;
420
+ parent.tokens.output += child.tokens.output;
421
+ parent.tokens.total += child.tokens.total;
422
+ if (child.tokens.cached !== undefined) {
423
+ parent.tokens.cached = (parent.tokens.cached ?? 0) + child.tokens.cached;
424
+ }
425
+ parent.cost += child.cost;
426
+ }
414
427
  export async function executeSpawn(step, input, ctx, executeStep, opts) {
415
428
  const baseCtx = frameworkCast(ctx);
416
429
  const layers = resolveLayersForSpawn(step, opts?.parentLayers);
@@ -441,12 +454,20 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
441
454
  itemSchemas: opts?.itemSchemas,
442
455
  });
443
456
  }
444
- // Build unified tool set for child from its step tree + layers
457
+ // Build unified tool set for child from its step tree + layers, plus the
458
+ // parent's unified tools. Sub-agents that read tools dynamically (via
459
+ // `ctx.unifiedTools`) otherwise lose the harness tool pool at the spawn
460
+ // boundary — a child whose `step.llm` resolves `tools` from context would
461
+ // see nothing. Inheriting the parent's tools keeps the harness toolset
462
+ // available across spawns; the child's own step/layer tools take precedence
463
+ // on name collision (dedup keeps the first occurrence), and children that
464
+ // need a restricted set filter explicitly.
445
465
  const childStepTools = collectAllTools(step.child);
446
466
  const childLayerTools = layers.length > 0 ? resolveLayerTools(layers, baseCtx.harness, baseCtx) : [];
447
467
  const childUnifiedTools = deduplicateTools([
448
468
  ...childStepTools,
449
469
  ...childLayerTools,
470
+ ...(baseCtx.unifiedTools ?? []),
450
471
  ]);
451
472
  // Create child context — empty by default, layers provide items via onSpawn.
452
473
  // `id: childId` keeps the ContextImpl's id in sync with the executionId
@@ -468,6 +489,12 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
468
489
  try {
469
490
  // Execute the child step
470
491
  const childOutput = await executeStep(step.child, input, frameworkCast(childCtx));
492
+ // Roll the child's token/cost usage up into the parent. A sub-agent's
493
+ // spend is the parent's spend; without this it would be stranded on the
494
+ // child context and invisible to `ctx.tokens` / `ctx.cost` / until.maxCost.
495
+ // Because every spawn boundary rolls up, nested sub-agents propagate to the
496
+ // root recursively.
497
+ rollUpSpawnUsage(baseCtx, childCtx);
471
498
  // Pipeline result through layer onReturn hooks
472
499
  if (!hasLayers || !parentExecutionCtx) {
473
500
  return childOutput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and memory layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",