@noetic-tools/core 5.0.0 → 5.2.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.
@@ -253,9 +253,12 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
253
253
  * the same arrangement `executeLLM` builds: harness-level context layers'
254
254
  * recall outputs concatenated with the session's accumulated history.
255
255
  *
256
- * Read-mostly: `recallLayers` writes layer-state snapshots to
257
- * `layerStateStore` exactly as a real turn would, so successive previews
258
- * remain consistent with what the next real turn produces.
256
+ * Runs layer `init` on the throwaway preview context first — exactly what
257
+ * the next real turn's `ensureLayersInit` would do, re-hydrating thread/
258
+ * resource-scoped state from storage. Without it, every init-bearing layer
259
+ * is treated as disabled by the recall lifecycle and the preview silently
260
+ * degenerates to bare history. The preview execution's store entries are
261
+ * dropped afterwards so repeated previews don't grow the layer-state store.
259
262
  */
260
263
  previewRequestItems(scope?: SessionScope): Promise<ReadonlyArray<Item>>;
261
264
  storeLayers(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<void>;
@@ -631,9 +631,12 @@ export class AgentHarness {
631
631
  * the same arrangement `executeLLM` builds: harness-level context layers'
632
632
  * recall outputs concatenated with the session's accumulated history.
633
633
  *
634
- * Read-mostly: `recallLayers` writes layer-state snapshots to
635
- * `layerStateStore` exactly as a real turn would, so successive previews
636
- * remain consistent with what the next real turn produces.
634
+ * Runs layer `init` on the throwaway preview context first — exactly what
635
+ * the next real turn's `ensureLayersInit` would do, re-hydrating thread/
636
+ * resource-scoped state from storage. Without it, every init-bearing layer
637
+ * is treated as disabled by the recall lifecycle and the preview silently
638
+ * degenerates to bare history. The preview execution's store entries are
639
+ * dropped afterwards so repeated previews don't grow the layer-state store.
637
640
  */
638
641
  async previewRequestItems(scope) {
639
642
  const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
@@ -654,16 +657,24 @@ export class AgentHarness {
654
657
  if (layers.length === 0) {
655
658
  return historyItems;
656
659
  }
657
- const recallResults = await this.recallLayers(layers, '', ctx);
658
- const layerOutputItems = recallResults.flatMap((r) => r.items);
659
- if (layerOutputItems.length === 0) {
660
- return historyItems;
660
+ try {
661
+ await this.ensureLayersInit(ctx);
662
+ const recallResults = await this.recallLayers(layers, '', ctx);
663
+ const layerOutputItems = recallResults.flatMap((r) => r.items);
664
+ if (layerOutputItems.length === 0) {
665
+ return historyItems;
666
+ }
667
+ return assembleView({
668
+ systemPromptItems: [],
669
+ layerOutputItems,
670
+ historyItems,
671
+ });
672
+ }
673
+ finally {
674
+ await this.layerStateStore.flush?.(ctx.id);
675
+ this.layerStateStore.cleanup(ctx.id);
676
+ this.initializedExecutions.delete(ctx.id);
661
677
  }
662
- return assembleView({
663
- systemPromptItems: [],
664
- layerOutputItems,
665
- historyItems,
666
- });
667
678
  }
668
679
  async storeLayers(layers, response, ctx) {
669
680
  const storage = this.config.storage;
@@ -34,6 +34,13 @@ export declare class AgentHarnessModelCaller {
34
34
  private readonly opts;
35
35
  constructor(opts: AgentHarnessModelCallerOpts);
36
36
  private newSpanCollector;
37
+ /**
38
+ * Broadcast a framework-authored item so `getItemStream` carries it — tool
39
+ * results and injected messages never appear in the SDK stream. Emitted
40
+ * unconditionally: item streaming, like the SDK item events, is not subject
41
+ * to `step.emit` filtering.
42
+ */
43
+ private emitItemAppended;
37
44
  private callOverriddenModel;
38
45
  private requireModelClient;
39
46
  callModel(request: CallModelRequest): Promise<LLMResponse>;
@@ -302,6 +302,22 @@ export class AgentHarnessModelCaller {
302
302
  newSpanCollector(request) {
303
303
  return new CallSpanCollector(this.opts.harness, this.opts.traceExporter, request.parentSpan ?? request.ctx?.span ?? null, request.nodeId);
304
304
  }
305
+ /**
306
+ * Broadcast a framework-authored item so `getItemStream` carries it — tool
307
+ * results and injected messages never appear in the SDK stream. Emitted
308
+ * unconditionally: item streaming, like the SDK item events, is not subject
309
+ * to `step.emit` filtering.
310
+ */
311
+ emitItemAppended(ctx, item) {
312
+ emitFrameworkEvent({
313
+ broadcaster: getBroadcaster(ctx),
314
+ agentName: this.opts.agentName,
315
+ eventType: 'item_appended',
316
+ data: {
317
+ item,
318
+ },
319
+ });
320
+ }
305
321
  async callOverriddenModel(request) {
306
322
  if (!this.opts.callModelOverride) {
307
323
  throw new Error('No callModel override configured.');
@@ -557,6 +573,7 @@ export class AgentHarnessModelCaller {
557
573
  ],
558
574
  });
559
575
  ctx.itemLog.append(userItem);
576
+ this.emitItemAppended(ctx, userItem);
560
577
  }
561
578
  emitIfAllowed('inbox_injected', {
562
579
  round,
@@ -706,6 +723,7 @@ export class AgentHarnessModelCaller {
706
723
  callId: fc.callId,
707
724
  output: toolResult.output,
708
725
  });
726
+ this.emitItemAppended(request.ctx, outputItem);
709
727
  params.emitIfAllowed('tool_call_completed', {
710
728
  name: fc.name,
711
729
  callId: fc.callId,
@@ -731,6 +749,7 @@ export class AgentHarnessModelCaller {
731
749
  callId: fc.callId,
732
750
  output: errorOutput,
733
751
  });
752
+ this.emitItemAppended(request.ctx, outputItem);
734
753
  params.emitIfAllowed('tool_call_completed', {
735
754
  name: fc.name,
736
755
  callId: fc.callId,
package/dist/index.d.ts CHANGED
@@ -70,7 +70,7 @@ export type { DurableTaskState, FactExtractor, FactSearcher, HistoryWindowConfig
70
70
  /** @public */
71
71
  /** @public */
72
72
  /** @public */
73
- export { durableTaskState, fileReference, findFunctionCall, historyWindow, observationalContext, PlanPhase, planContext, staticContent, steering, storageGetMany, stripUnresolvedToolCalls, temporalContext, toolContextLayer, workingMemoryContext, } from '@noetic-tools/context';
73
+ export { durableTaskState, fileReference, findFunctionCall, historyWindow, observationalContext, PlanPhase, PlanStyle, planContext, staticContent, steering, storageGetMany, stripUnresolvedToolCalls, temporalContext, toolContextLayer, workingMemoryContext, } from '@noetic-tools/context';
74
74
  /** @public */
75
75
  export { createMessage, estimateTokens } from '@noetic-tools/types';
76
76
  /** @public */
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ export { execute } from './interpreter/execute.js';
58
58
  /** @public */
59
59
  /** @public */
60
60
  /** @public */
61
- export { durableTaskState, fileReference, findFunctionCall, historyWindow, observationalContext, PlanPhase, planContext, staticContent, steering, storageGetMany, stripUnresolvedToolCalls, temporalContext, toolContextLayer, workingMemoryContext, } from '@noetic-tools/context';
61
+ export { durableTaskState, fileReference, findFunctionCall, historyWindow, observationalContext, PlanPhase, PlanStyle, planContext, staticContent, steering, storageGetMany, stripUnresolvedToolCalls, temporalContext, toolContextLayer, workingMemoryContext, } from '@noetic-tools/context';
62
62
  //#endregion
63
63
  //#region Message Utilities
64
64
  /** @public */
@@ -174,6 +174,18 @@ export class SessionRunner {
174
174
  messageIds: messages.map((m) => m.id),
175
175
  },
176
176
  });
177
+ // Input items never appear in the SDK stream — item_appended is what
178
+ // carries them into getItemStream (and any other log-faithful consumer).
179
+ for (const item of items) {
180
+ emitFrameworkEvent({
181
+ broadcaster: this.broadcaster,
182
+ agentName: this.agentName,
183
+ eventType: 'item_appended',
184
+ data: {
185
+ item,
186
+ },
187
+ });
188
+ }
177
189
  const turn = {
178
190
  turnId,
179
191
  session: this,
@@ -6,5 +6,9 @@ export declare function filterTextStream(broadcaster: EventBroadcaster): AsyncIt
6
6
  export declare function filterReasoningStream(broadcaster: EventBroadcaster): AsyncIterable<string>;
7
7
  /** @internal Yield cumulative StreamingItem snapshots. Uses roundOffset keyed by
8
8
  * response.created so accumulators across multiple tool rounds and across
9
- * multiple turns within a session don't collide. */
9
+ * multiple turns within a session don't collide.
10
+ *
11
+ * SDK events cover model output only; framework-authored items — turn input
12
+ * messages, between-rounds injections, tool results — arrive via the
13
+ * `item_appended` framework event and are yielded already complete. */
10
14
  export declare function buildItemStream(broadcaster: EventBroadcaster, itemSchemas?: ItemSchemaRegistry): AsyncIterable<StreamingItem>;
@@ -1,4 +1,4 @@
1
- import { defaultItemSchemaRegistry } from '@noetic-tools/types';
1
+ import { defaultItemSchemaRegistry, frameworkCast } from '@noetic-tools/types';
2
2
  //#endregion
3
3
  //#region Helpers
4
4
  function isItemRecord(value) {
@@ -138,11 +138,26 @@ export async function* filterReasoningStream(broadcaster) {
138
138
  }
139
139
  /** @internal Yield cumulative StreamingItem snapshots. Uses roundOffset keyed by
140
140
  * response.created so accumulators across multiple tool rounds and across
141
- * multiple turns within a session don't collide. */
141
+ * multiple turns within a session don't collide.
142
+ *
143
+ * SDK events cover model output only; framework-authored items — turn input
144
+ * messages, between-rounds injections, tool results — arrive via the
145
+ * `item_appended` framework event and are yielded already complete. */
142
146
  export async function* buildItemStream(broadcaster, itemSchemas = defaultItemSchemaRegistry) {
143
147
  const accumulators = new Map();
144
148
  let roundOffset = 0;
145
149
  for await (const event of broadcaster) {
150
+ if (event.source === 'framework') {
151
+ if (event.type.endsWith(':item_appended') &&
152
+ typeof event.data.item === 'object' &&
153
+ event.data.item !== null) {
154
+ yield {
155
+ ...frameworkCast(event.data.item),
156
+ isComplete: true,
157
+ };
158
+ }
159
+ continue;
160
+ }
146
161
  if (event.source !== 'sdk') {
147
162
  continue;
148
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and context layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "dependencies": {
69
- "@noetic-tools/context": "^1.0.0",
69
+ "@noetic-tools/context": "^1.1.0",
70
70
  "@noetic-tools/types": "^3.0.0",
71
71
  "@openrouter/agent": "^0.6.0",
72
72
  "zod": "^4.0.0"