@noetic-tools/context 1.0.0 → 1.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.
@@ -12,6 +12,9 @@ export function staticContent(opts) {
12
12
  id: opts.id ?? 'static-content',
13
13
  slot: opts.slot ?? Slot.WORKING_MEMORY + 5,
14
14
  scope: opts.scope ?? 'resource',
15
+ // Loaded once in `init` and never rewritten, so it can be pinned outright
16
+ // rather than waiting for churn telemetry to work that out.
17
+ placement: 'anchor',
15
18
  hooks: {
16
19
  async init() {
17
20
  const raw = await opts.load();
@@ -11,6 +11,7 @@ export declare function steering(config: SteeringConfig): {
11
11
  name: string;
12
12
  slot: 90;
13
13
  scope: ContextScope;
14
+ placement: "live";
14
15
  budget: {
15
16
  min: number;
16
17
  max: number;
@@ -162,6 +162,11 @@ export function steering(config) {
162
162
  name: 'Steering',
163
163
  slot: Slot.STEERING,
164
164
  scope,
165
+ // `recall` drains the pending queue as it renders, so its output can never
166
+ // be replayed from a pin — the drain is already committed, and serving an
167
+ // older render would lose the feedback for good. Rendering after history
168
+ // also puts guidance where the model weighs it most.
169
+ placement: 'live',
165
170
  budget: {
166
171
  min: 0,
167
172
  max: 500,
@@ -211,6 +211,12 @@ export function temporalContext(config) {
211
211
  // Grounding sits near the top of the window, before reasoning content.
212
212
  slot: Slot.REMINDER,
213
213
  scope: config?.scope ?? 'resource',
214
+ // The `<current_datetime>` block changes every turn by construction, so
215
+ // anchoring it would re-bill the whole window each time. Churn telemetry
216
+ // would reach the same verdict — this just skips the learning cost. With
217
+ // grounding off, only the slow-moving fact ledger remains, which is worth
218
+ // anchoring, so leave that to `'auto'`.
219
+ placement: groundDateTime ? 'live' : 'auto',
214
220
  budget: {
215
221
  min: 0,
216
222
  max: injectLedger ? 800 : 200,
@@ -1,19 +1,38 @@
1
1
  import type { Item, ProjectionPolicy } from '@noetic-tools/types';
2
2
  interface AssembleViewParams {
3
3
  systemPromptItems: Item[];
4
+ /** Anchor band — layer output rendered before history, where a prompt cache can hold it. */
4
5
  layerOutputItems: Item[];
5
6
  historyItems: Item[];
7
+ /** Live band — layer output rendered after history, slot-ascending. */
8
+ liveLayerItems?: Item[];
9
+ /** Supersedes for anchored layers whose pinned output went stale. */
10
+ deltaItems?: Item[];
11
+ /** Items that must land last, after everything else. Never dropped. */
12
+ tailItems?: Item[];
6
13
  policy?: ProjectionPolicy;
7
14
  }
8
15
  /**
9
- * Assemble the model's context window from system prompt items, context-layer
10
- * recall output (slot-ascending), and conversation history.
16
+ * Assemble the model's context window in bands:
11
17
  *
12
- * Without a `policy` the inputs are concatenated as-is (optionally sliding the
13
- * history window by `windowSize`). With a `policy` the assembled view is held to
14
- * a hard token budget: system items are always kept; layer output is kept
15
- * low-slot-first (highest-slot dropped when tight); history takes the remainder,
16
- * keeping the most recent turns.
18
+ * ```
19
+ * system | anchor layers | history | live layers | supersedes | tail
20
+ * ```
21
+ *
22
+ * The split exists for the prompt cache, which matches on a prefix. Stable
23
+ * layer output sits ahead of history where it can be cached; volatile output
24
+ * sits after it, where re-rendering costs almost nothing. Both bands arrive
25
+ * slot-ascending.
26
+ *
27
+ * Without a `policy` the bands are concatenated as-is (optionally sliding the
28
+ * history window by `windowSize`). With a `policy` the view is held to a hard
29
+ * token budget, claimed in this order: system items, then anchor output, then
30
+ * live output, then supersedes, then the tail — with history taking whatever
31
+ * remains and keeping the most recent turns.
32
+ *
33
+ * Supersedes are claimed ahead of history, and all together or not at all: a
34
+ * dropped supersede would leave the model reading a pinned block the runtime
35
+ * knows is stale. The caller re-anchors instead — see `prepareBandedView`.
17
36
  */
18
- export declare function assembleView({ systemPromptItems, layerOutputItems, historyItems, policy, }: AssembleViewParams): Item[];
37
+ export declare function assembleView({ systemPromptItems, layerOutputItems, historyItems, liveLayerItems, deltaItems, tailItems, policy, }: AssembleViewParams): Item[];
19
38
  export {};
@@ -57,33 +57,62 @@ function keepRecentWithinBudget(items, budget, windowSize) {
57
57
  //#endregion
58
58
  //#region Public API
59
59
  /**
60
- * Assemble the model's context window from system prompt items, context-layer
61
- * recall output (slot-ascending), and conversation history.
60
+ * Assemble the model's context window in bands:
62
61
  *
63
- * Without a `policy` the inputs are concatenated as-is (optionally sliding the
64
- * history window by `windowSize`). With a `policy` the assembled view is held to
65
- * a hard token budget: system items are always kept; layer output is kept
66
- * low-slot-first (highest-slot dropped when tight); history takes the remainder,
67
- * keeping the most recent turns.
62
+ * ```
63
+ * system | anchor layers | history | live layers | supersedes | tail
64
+ * ```
65
+ *
66
+ * The split exists for the prompt cache, which matches on a prefix. Stable
67
+ * layer output sits ahead of history where it can be cached; volatile output
68
+ * sits after it, where re-rendering costs almost nothing. Both bands arrive
69
+ * slot-ascending.
70
+ *
71
+ * Without a `policy` the bands are concatenated as-is (optionally sliding the
72
+ * history window by `windowSize`). With a `policy` the view is held to a hard
73
+ * token budget, claimed in this order: system items, then anchor output, then
74
+ * live output, then supersedes, then the tail — with history taking whatever
75
+ * remains and keeping the most recent turns.
76
+ *
77
+ * Supersedes are claimed ahead of history, and all together or not at all: a
78
+ * dropped supersede would leave the model reading a pinned block the runtime
79
+ * knows is stale. The caller re-anchors instead — see `prepareBandedView`.
68
80
  */
69
- export function assembleView({ systemPromptItems, layerOutputItems, historyItems, policy, }) {
81
+ export function assembleView({ systemPromptItems, layerOutputItems, historyItems, liveLayerItems = [], deltaItems = [], tailItems = [], policy, }) {
70
82
  if (!policy) {
71
83
  return [
72
84
  ...systemPromptItems,
73
85
  ...layerOutputItems,
74
86
  ...historyItems,
87
+ ...liveLayerItems,
88
+ ...deltaItems,
89
+ ...tailItems,
75
90
  ];
76
91
  }
77
92
  const budget = Math.max(0, policy.tokenBudget - policy.responseReserve);
78
93
  // System items are never dropped — they anchor the conversation.
79
- const afterSystem = Math.max(0, budget - totalTokens(systemPromptItems));
80
- const keptLayers = keepFrontWithinBudget(layerOutputItems, afterSystem);
81
- const afterLayers = Math.max(0, afterSystem - totalTokens(keptLayers));
82
- const keptHistory = keepRecentWithinBudget(historyItems, afterLayers, policy.windowSize);
94
+ let left = Math.max(0, budget - totalTokens(systemPromptItems));
95
+ const keptAnchor = keepFrontWithinBudget(layerOutputItems, left);
96
+ left = Math.max(0, left - totalTokens(keptAnchor));
97
+ const keptLive = keepFrontWithinBudget(liveLayerItems, left);
98
+ left = Math.max(0, left - totalTokens(keptLive));
99
+ // The tail carries steering guidance, which must reach the model whatever the
100
+ // budget says — it is the correction the retry exists to deliver.
101
+ left = Math.max(0, left - totalTokens(tailItems));
102
+ // Supersedes are never dropped either. Each one corrects a pinned block that
103
+ // is already in the view, so dropping one leaves the model reading content
104
+ // the runtime knows is stale — silent corruption, and far worse than losing
105
+ // a turn of history. History absorbs the cost; `deltaBudgetFraction` keeps
106
+ // the supersedes from growing large enough for that to hurt.
107
+ left = Math.max(0, left - totalTokens(deltaItems));
108
+ const keptHistory = keepRecentWithinBudget(historyItems, left, policy.windowSize);
83
109
  return [
84
110
  ...systemPromptItems,
85
- ...keptLayers,
111
+ ...keptAnchor,
86
112
  ...keptHistory,
113
+ ...keptLive,
114
+ ...deltaItems,
115
+ ...tailItems,
87
116
  ];
88
117
  }
89
118
  //#endregion
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  export type { ContextScope, ExecutionContext, ExecutionOutcome, LayerCallModelRequest, } from '@noetic-tools/types';
7
7
  export * from '@noetic-tools/types/contract';
8
8
  export * from './context/budget.js';
9
+ export * from './context/cache-anchoring.js';
9
10
  export * from './context/exec-context-factory.js';
10
11
  export * from './context/function-call-utils.js';
11
12
  export * from './context/layer-api.js';
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@
8
8
  // @noetic-tools/context remains the one-stop import for context-layer authoring.
9
9
  export * from '@noetic-tools/types/contract';
10
10
  export * from './context/budget.js';
11
+ export * from './context/cache-anchoring.js';
11
12
  export * from './context/exec-context-factory.js';
12
13
  export * from './context/function-call-utils.js';
13
14
  export * from './context/layer-api.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/context",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Context layer system for Noetic agents: the ContextLayer contract, lifecycle, budget/projection machinery, and built-in layer implementations.",
6
6
  "license": "Apache-2.0",
@@ -44,7 +44,7 @@
44
44
  "zod": "^4.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@noetic-tools/types": "^2.3.0",
47
+ "@noetic-tools/types": "^3.1.0",
48
48
  "zod": "^4.0.0"
49
49
  },
50
50
  "scripts": {