@mnemonik/shared 7.41.0 → 7.42.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.
@@ -0,0 +1,47 @@
1
+ export { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
2
+ export type ContextBudgetUnits = 'characters' | 'utf8-bytes';
3
+ export interface ContextBudgetItem {
4
+ /** Exact text emitted when the item fits in full. */
5
+ text: string;
6
+ /** Stable envelope name used by continuation/not-reached pointers. */
7
+ name: string;
8
+ /** Memory identity when this item is one pointer-terminated memory precis. */
9
+ memoryId?: string;
10
+ /** Memory identities when one rendered envelope represents several memories. */
11
+ memoryIds?: readonly string[];
12
+ }
13
+ export type ContextBudgetPiece = string | ContextBudgetItem;
14
+ export interface BudgetedContextFit {
15
+ value: string;
16
+ fullyDeliveredItems: number;
17
+ partialItemIndex: number | null;
18
+ partialUnitsDelivered: number;
19
+ }
20
+ /** Measure rendered context in the host's native budget unit. */
21
+ export declare function measureContext(text: string, units?: ContextBudgetUnits): number;
22
+ /**
23
+ * Compose ordered items within a fixed budget. Full items are emitted exactly;
24
+ * the first item that does not fit is packed by whole natural units, and all
25
+ * later identities share one trailing not-reached pointer.
26
+ */
27
+ export declare function fitContextItems(items: readonly ContextBudgetItem[], opts: {
28
+ budget: number;
29
+ units?: ContextBudgetUnits;
30
+ reservedSuffix?: readonly ContextBudgetItem[];
31
+ }): BudgetedContextFit;
32
+ /**
33
+ * Turn one rendered envelope into budget items. Pointer-terminated memories
34
+ * retain their IDs; an unrecognised shape remains one named envelope. Callers
35
+ * may provide response metadata IDs for envelopes (such as JIT) whose visible
36
+ * wire format does not carry trailing memory pointers.
37
+ */
38
+ export declare function contextItemsFromText(text: string, envelopeName: string, memoryIds?: readonly string[]): ContextBudgetItem[];
39
+ /** Fit one rendered envelope while preserving its exact under-budget bytes. */
40
+ export declare function fitContextText(text: string, envelopeName: string, opts: {
41
+ budget: number;
42
+ units?: ContextBudgetUnits;
43
+ memoryIds?: readonly string[];
44
+ }): string;
45
+ /** Split an ambient body into whole pointer-terminated items. */
46
+ export declare function splitAmbientItems(body: string): string[];
47
+ //# sourceMappingURL=contextBudget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contextBudget.d.ts","sourceRoot":"","sources":["../src/contextBudget.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAgCD,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,kBAAiC,GAAG,MAAM,CAE7F;AAqDD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,cAAc,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C,GACA,kBAAkB,CAmEpB;AA4BD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,iBAAiB,EAAE,CAMrB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAClF,MAAM,CAIR;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD"}
@@ -0,0 +1,193 @@
1
+ import { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
2
+ export { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
3
+ const JOINER = '\n\n';
4
+ const CONTINUATION_PREFIX = '… continues: ';
5
+ const NOT_REACHED_PREFIX = '… not reached: ';
6
+ function itemMemoryIds(item) {
7
+ if (item.memoryIds && item.memoryIds.length > 0)
8
+ return item.memoryIds;
9
+ return item.memoryId ? [item.memoryId] : [];
10
+ }
11
+ function identity(item) {
12
+ const memoryIds = itemMemoryIds(item);
13
+ if (memoryIds.length > 0)
14
+ return memoryIds.map((id) => `memory_get ${id}`).join(', ');
15
+ return item.name.replace(/\s+/g, ' ').trim();
16
+ }
17
+ function continuationPointer(item) {
18
+ return `${CONTINUATION_PREFIX}${identity(item)}`;
19
+ }
20
+ function notReachedPointer(items) {
21
+ if (items.length === 0)
22
+ return null;
23
+ return `${NOT_REACHED_PREFIX}${items.map(identity).join(', ')}`;
24
+ }
25
+ function joinSections(sections) {
26
+ return sections
27
+ .filter((section) => section !== null && section.length > 0)
28
+ .join(JOINER);
29
+ }
30
+ /** Measure rendered context in the host's native budget unit. */
31
+ export function measureContext(text, units = 'characters') {
32
+ return units === 'utf8-bytes' ? Buffer.byteLength(text, 'utf8') : text.length;
33
+ }
34
+ function withoutTerminalMemoryPointer(item) {
35
+ if (!item.memoryId)
36
+ return item.text;
37
+ const terminalPointer = new RegExp(`\\s*${AMBIENT_ITEM_POINTER_SOURCE}\\s*$`);
38
+ return item.text.replace(terminalPointer, '').trimEnd();
39
+ }
40
+ /**
41
+ * Prefer bullet lines whenever the item contains them. Text ahead of the first
42
+ * bullet is framing and travels with the first admitted fact. Otherwise each
43
+ * sentence (including a final sentence ending at end-of-item) is one unit.
44
+ */
45
+ function naturalUnits(item) {
46
+ const text = withoutTerminalMemoryPointer(item);
47
+ const bulletStarts = [];
48
+ const bulletPattern = /(^|\n)(?=[ \t]*(?:•|[-*])[ \t]+)/g;
49
+ let bulletMatch;
50
+ while ((bulletMatch = bulletPattern.exec(text)) !== null) {
51
+ bulletStarts.push(bulletMatch.index + (bulletMatch[1] ?? '').length);
52
+ if (bulletPattern.lastIndex === bulletMatch.index)
53
+ bulletPattern.lastIndex += 1;
54
+ }
55
+ if (bulletStarts.length > 0) {
56
+ return {
57
+ prefix: text.slice(0, bulletStarts[0]),
58
+ units: bulletStarts.map((start, index) => text.slice(start, bulletStarts[index + 1])),
59
+ };
60
+ }
61
+ const units = [];
62
+ const sentenceEnd = /[.!?](?:["')\]]*)?(?=\s|$)/g;
63
+ let cursor = 0;
64
+ let sentenceMatch;
65
+ while ((sentenceMatch = sentenceEnd.exec(text)) !== null) {
66
+ const end = sentenceMatch.index + sentenceMatch[0].length;
67
+ units.push(text.slice(cursor, end));
68
+ cursor = end;
69
+ }
70
+ if (cursor < text.length)
71
+ units.push(text.slice(cursor));
72
+ return { prefix: '', units: units.filter((unit) => unit.trim().length > 0) };
73
+ }
74
+ function partialText(item, plan, count) {
75
+ const content = `${plan.prefix}${plan.units.slice(0, count).join('')}`.trimEnd();
76
+ return `${content}\n${continuationPointer(item)}`;
77
+ }
78
+ /**
79
+ * Compose ordered items within a fixed budget. Full items are emitted exactly;
80
+ * the first item that does not fit is packed by whole natural units, and all
81
+ * later identities share one trailing not-reached pointer.
82
+ */
83
+ export function fitContextItems(items, opts) {
84
+ const units = opts.units ?? 'characters';
85
+ const suffix = opts.reservedSuffix ?? [];
86
+ const fullValue = [...items, ...suffix].map((item) => item.text).join(JOINER);
87
+ if (measureContext(fullValue, units) <= opts.budget) {
88
+ return {
89
+ value: fullValue,
90
+ fullyDeliveredItems: items.length,
91
+ partialItemIndex: null,
92
+ partialUnitsDelivered: 0,
93
+ };
94
+ }
95
+ const accepted = [];
96
+ for (let index = 0; index < items.length; index += 1) {
97
+ const item = items[index];
98
+ if (item === undefined)
99
+ continue;
100
+ const later = items.slice(index + 1);
101
+ const withFullItem = joinSections([
102
+ ...accepted,
103
+ item.text,
104
+ ...suffix.map((entry) => entry.text),
105
+ notReachedPointer(later),
106
+ ]);
107
+ if (measureContext(withFullItem, units) <= opts.budget) {
108
+ accepted.push(item.text);
109
+ continue;
110
+ }
111
+ const plan = naturalUnits(item);
112
+ let partialUnitsDelivered = 0;
113
+ for (let count = 1; count <= plan.units.length; count += 1) {
114
+ const candidate = joinSections([
115
+ ...accepted,
116
+ partialText(item, plan, count),
117
+ ...suffix.map((entry) => entry.text),
118
+ notReachedPointer(later),
119
+ ]);
120
+ if (measureContext(candidate, units) > opts.budget)
121
+ break;
122
+ partialUnitsDelivered = count;
123
+ }
124
+ // A one-fact excerpt reads as though it were the whole memory. Require two
125
+ // complete units before admitting a partial item; otherwise name it only.
126
+ const meaningfulMinimum = Math.min(2, plan.units.length);
127
+ const partialAccepted = partialUnitsDelivered >= meaningfulMinimum && partialUnitsDelivered > 0;
128
+ const partial = partialAccepted ? partialText(item, plan, partialUnitsDelivered) : null;
129
+ const unreached = partialAccepted ? later : items.slice(index);
130
+ return {
131
+ value: joinSections([
132
+ ...accepted,
133
+ partial,
134
+ ...suffix.map((entry) => entry.text),
135
+ notReachedPointer(unreached),
136
+ ]),
137
+ fullyDeliveredItems: index,
138
+ partialItemIndex: partialAccepted ? index : null,
139
+ partialUnitsDelivered: partialAccepted ? partialUnitsDelivered : 0,
140
+ };
141
+ }
142
+ return {
143
+ value: joinSections([...accepted, ...suffix.map((entry) => entry.text)]),
144
+ fullyDeliveredItems: items.length,
145
+ partialItemIndex: null,
146
+ partialUnitsDelivered: 0,
147
+ };
148
+ }
149
+ function parsePointerItems(body, envelopeName) {
150
+ const regex = new RegExp(AMBIENT_ITEM_POINTER_SOURCE, 'g');
151
+ const memories = [];
152
+ let cursor = 0;
153
+ let match;
154
+ while ((match = regex.exec(body)) !== null) {
155
+ const end = match.index + match[0].length;
156
+ const text = body.slice(cursor, end).trim();
157
+ const memoryId = match[1];
158
+ if (text.length > 0 && memoryId !== undefined) {
159
+ memories.push({ text, name: envelopeName, memoryId });
160
+ }
161
+ cursor = end;
162
+ }
163
+ const trailerText = body.slice(cursor).trim();
164
+ return {
165
+ memories,
166
+ trailer: trailerText ? { text: trailerText, name: `${envelopeName} trailer` } : null,
167
+ };
168
+ }
169
+ /**
170
+ * Turn one rendered envelope into budget items. Pointer-terminated memories
171
+ * retain their IDs; an unrecognised shape remains one named envelope. Callers
172
+ * may provide response metadata IDs for envelopes (such as JIT) whose visible
173
+ * wire format does not carry trailing memory pointers.
174
+ */
175
+ export function contextItemsFromText(text, envelopeName, memoryIds = []) {
176
+ const parsed = parsePointerItems(text, envelopeName);
177
+ if (parsed.memories.length === 0) {
178
+ return [{ text, name: envelopeName, ...(memoryIds.length > 0 ? { memoryIds } : {}) }];
179
+ }
180
+ return [...parsed.memories, ...(parsed.trailer ? [parsed.trailer] : [])];
181
+ }
182
+ /** Fit one rendered envelope while preserving its exact under-budget bytes. */
183
+ export function fitContextText(text, envelopeName, opts) {
184
+ const units = opts.units ?? 'characters';
185
+ if (measureContext(text, units) <= opts.budget)
186
+ return text;
187
+ return fitContextItems(contextItemsFromText(text, envelopeName, opts.memoryIds), opts).value;
188
+ }
189
+ /** Split an ambient body into whole pointer-terminated items. */
190
+ export function splitAmbientItems(body) {
191
+ return parsePointerItems(body, 'ambient recall').memories.map((item) => item.text);
192
+ }
193
+ //# sourceMappingURL=contextBudget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contextBudget.js","sourceRoot":"","sources":["../src/contextBudget.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemonik/shared",
3
- "version": "7.41.0",
3
+ "version": "7.42.0",
4
4
  "description": "Shared constants and utilities for Mnemonik packages",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,6 +26,10 @@
26
26
  "./ambient-pointer": {
27
27
  "import": "./dist/ambientPointer.js",
28
28
  "types": "./dist/ambientPointer.d.ts"
29
+ },
30
+ "./context-budget": {
31
+ "import": "./dist/contextBudget.js",
32
+ "types": "./dist/contextBudget.d.ts"
29
33
  }
30
34
  },
31
35
  "scripts": {
@@ -0,0 +1,252 @@
1
+ import { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
2
+
3
+ export { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
4
+
5
+ export type ContextBudgetUnits = 'characters' | 'utf8-bytes';
6
+
7
+ export interface ContextBudgetItem {
8
+ /** Exact text emitted when the item fits in full. */
9
+ text: string;
10
+ /** Stable envelope name used by continuation/not-reached pointers. */
11
+ name: string;
12
+ /** Memory identity when this item is one pointer-terminated memory precis. */
13
+ memoryId?: string;
14
+ /** Memory identities when one rendered envelope represents several memories. */
15
+ memoryIds?: readonly string[];
16
+ }
17
+
18
+ export type ContextBudgetPiece = string | ContextBudgetItem;
19
+
20
+ export interface BudgetedContextFit {
21
+ value: string;
22
+ fullyDeliveredItems: number;
23
+ partialItemIndex: number | null;
24
+ partialUnitsDelivered: number;
25
+ }
26
+
27
+ const JOINER = '\n\n';
28
+ const CONTINUATION_PREFIX = '… continues: ';
29
+ const NOT_REACHED_PREFIX = '… not reached: ';
30
+
31
+ function itemMemoryIds(item: ContextBudgetItem): readonly string[] {
32
+ if (item.memoryIds && item.memoryIds.length > 0) return item.memoryIds;
33
+ return item.memoryId ? [item.memoryId] : [];
34
+ }
35
+
36
+ function identity(item: ContextBudgetItem): string {
37
+ const memoryIds = itemMemoryIds(item);
38
+ if (memoryIds.length > 0) return memoryIds.map((id) => `memory_get ${id}`).join(', ');
39
+ return item.name.replace(/\s+/g, ' ').trim();
40
+ }
41
+
42
+ function continuationPointer(item: ContextBudgetItem): string {
43
+ return `${CONTINUATION_PREFIX}${identity(item)}`;
44
+ }
45
+
46
+ function notReachedPointer(items: readonly ContextBudgetItem[]): string | null {
47
+ if (items.length === 0) return null;
48
+ return `${NOT_REACHED_PREFIX}${items.map(identity).join(', ')}`;
49
+ }
50
+
51
+ function joinSections(sections: readonly (string | null)[]): string {
52
+ return sections
53
+ .filter((section): section is string => section !== null && section.length > 0)
54
+ .join(JOINER);
55
+ }
56
+
57
+ /** Measure rendered context in the host's native budget unit. */
58
+ export function measureContext(text: string, units: ContextBudgetUnits = 'characters'): number {
59
+ return units === 'utf8-bytes' ? Buffer.byteLength(text, 'utf8') : text.length;
60
+ }
61
+
62
+ interface NaturalUnits {
63
+ prefix: string;
64
+ units: string[];
65
+ }
66
+
67
+ function withoutTerminalMemoryPointer(item: ContextBudgetItem): string {
68
+ if (!item.memoryId) return item.text;
69
+ const terminalPointer = new RegExp(`\\s*${AMBIENT_ITEM_POINTER_SOURCE}\\s*$`);
70
+ return item.text.replace(terminalPointer, '').trimEnd();
71
+ }
72
+
73
+ /**
74
+ * Prefer bullet lines whenever the item contains them. Text ahead of the first
75
+ * bullet is framing and travels with the first admitted fact. Otherwise each
76
+ * sentence (including a final sentence ending at end-of-item) is one unit.
77
+ */
78
+ function naturalUnits(item: ContextBudgetItem): NaturalUnits {
79
+ const text = withoutTerminalMemoryPointer(item);
80
+ const bulletStarts: number[] = [];
81
+ const bulletPattern = /(^|\n)(?=[ \t]*(?:•|[-*])[ \t]+)/g;
82
+ let bulletMatch: RegExpExecArray | null;
83
+ while ((bulletMatch = bulletPattern.exec(text)) !== null) {
84
+ bulletStarts.push(bulletMatch.index + (bulletMatch[1] ?? '').length);
85
+ if (bulletPattern.lastIndex === bulletMatch.index) bulletPattern.lastIndex += 1;
86
+ }
87
+
88
+ if (bulletStarts.length > 0) {
89
+ return {
90
+ prefix: text.slice(0, bulletStarts[0]),
91
+ units: bulletStarts.map((start, index) => text.slice(start, bulletStarts[index + 1])),
92
+ };
93
+ }
94
+
95
+ const units: string[] = [];
96
+ const sentenceEnd = /[.!?](?:["')\]]*)?(?=\s|$)/g;
97
+ let cursor = 0;
98
+ let sentenceMatch: RegExpExecArray | null;
99
+ while ((sentenceMatch = sentenceEnd.exec(text)) !== null) {
100
+ const end = sentenceMatch.index + sentenceMatch[0].length;
101
+ units.push(text.slice(cursor, end));
102
+ cursor = end;
103
+ }
104
+ if (cursor < text.length) units.push(text.slice(cursor));
105
+ return { prefix: '', units: units.filter((unit) => unit.trim().length > 0) };
106
+ }
107
+
108
+ function partialText(item: ContextBudgetItem, plan: NaturalUnits, count: number): string {
109
+ const content = `${plan.prefix}${plan.units.slice(0, count).join('')}`.trimEnd();
110
+ return `${content}\n${continuationPointer(item)}`;
111
+ }
112
+
113
+ /**
114
+ * Compose ordered items within a fixed budget. Full items are emitted exactly;
115
+ * the first item that does not fit is packed by whole natural units, and all
116
+ * later identities share one trailing not-reached pointer.
117
+ */
118
+ export function fitContextItems(
119
+ items: readonly ContextBudgetItem[],
120
+ opts: {
121
+ budget: number;
122
+ units?: ContextBudgetUnits;
123
+ reservedSuffix?: readonly ContextBudgetItem[];
124
+ }
125
+ ): BudgetedContextFit {
126
+ const units = opts.units ?? 'characters';
127
+ const suffix = opts.reservedSuffix ?? [];
128
+ const fullValue = [...items, ...suffix].map((item) => item.text).join(JOINER);
129
+ if (measureContext(fullValue, units) <= opts.budget) {
130
+ return {
131
+ value: fullValue,
132
+ fullyDeliveredItems: items.length,
133
+ partialItemIndex: null,
134
+ partialUnitsDelivered: 0,
135
+ };
136
+ }
137
+
138
+ const accepted: string[] = [];
139
+ for (let index = 0; index < items.length; index += 1) {
140
+ const item = items[index];
141
+ if (item === undefined) continue;
142
+ const later = items.slice(index + 1);
143
+ const withFullItem = joinSections([
144
+ ...accepted,
145
+ item.text,
146
+ ...suffix.map((entry) => entry.text),
147
+ notReachedPointer(later),
148
+ ]);
149
+ if (measureContext(withFullItem, units) <= opts.budget) {
150
+ accepted.push(item.text);
151
+ continue;
152
+ }
153
+
154
+ const plan = naturalUnits(item);
155
+ let partialUnitsDelivered = 0;
156
+ for (let count = 1; count <= plan.units.length; count += 1) {
157
+ const candidate = joinSections([
158
+ ...accepted,
159
+ partialText(item, plan, count),
160
+ ...suffix.map((entry) => entry.text),
161
+ notReachedPointer(later),
162
+ ]);
163
+ if (measureContext(candidate, units) > opts.budget) break;
164
+ partialUnitsDelivered = count;
165
+ }
166
+
167
+ // A one-fact excerpt reads as though it were the whole memory. Require two
168
+ // complete units before admitting a partial item; otherwise name it only.
169
+ const meaningfulMinimum = Math.min(2, plan.units.length);
170
+ const partialAccepted = partialUnitsDelivered >= meaningfulMinimum && partialUnitsDelivered > 0;
171
+ const partial = partialAccepted ? partialText(item, plan, partialUnitsDelivered) : null;
172
+ const unreached = partialAccepted ? later : items.slice(index);
173
+ return {
174
+ value: joinSections([
175
+ ...accepted,
176
+ partial,
177
+ ...suffix.map((entry) => entry.text),
178
+ notReachedPointer(unreached),
179
+ ]),
180
+ fullyDeliveredItems: index,
181
+ partialItemIndex: partialAccepted ? index : null,
182
+ partialUnitsDelivered: partialAccepted ? partialUnitsDelivered : 0,
183
+ };
184
+ }
185
+
186
+ return {
187
+ value: joinSections([...accepted, ...suffix.map((entry) => entry.text)]),
188
+ fullyDeliveredItems: items.length,
189
+ partialItemIndex: null,
190
+ partialUnitsDelivered: 0,
191
+ };
192
+ }
193
+
194
+ interface ParsedPointerItems {
195
+ memories: ContextBudgetItem[];
196
+ trailer: ContextBudgetItem | null;
197
+ }
198
+
199
+ function parsePointerItems(body: string, envelopeName: string): ParsedPointerItems {
200
+ const regex = new RegExp(AMBIENT_ITEM_POINTER_SOURCE, 'g');
201
+ const memories: ContextBudgetItem[] = [];
202
+ let cursor = 0;
203
+ let match: RegExpExecArray | null;
204
+ while ((match = regex.exec(body)) !== null) {
205
+ const end = match.index + match[0].length;
206
+ const text = body.slice(cursor, end).trim();
207
+ const memoryId = match[1];
208
+ if (text.length > 0 && memoryId !== undefined) {
209
+ memories.push({ text, name: envelopeName, memoryId });
210
+ }
211
+ cursor = end;
212
+ }
213
+ const trailerText = body.slice(cursor).trim();
214
+ return {
215
+ memories,
216
+ trailer: trailerText ? { text: trailerText, name: `${envelopeName} trailer` } : null,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Turn one rendered envelope into budget items. Pointer-terminated memories
222
+ * retain their IDs; an unrecognised shape remains one named envelope. Callers
223
+ * may provide response metadata IDs for envelopes (such as JIT) whose visible
224
+ * wire format does not carry trailing memory pointers.
225
+ */
226
+ export function contextItemsFromText(
227
+ text: string,
228
+ envelopeName: string,
229
+ memoryIds: readonly string[] = []
230
+ ): ContextBudgetItem[] {
231
+ const parsed = parsePointerItems(text, envelopeName);
232
+ if (parsed.memories.length === 0) {
233
+ return [{ text, name: envelopeName, ...(memoryIds.length > 0 ? { memoryIds } : {}) }];
234
+ }
235
+ return [...parsed.memories, ...(parsed.trailer ? [parsed.trailer] : [])];
236
+ }
237
+
238
+ /** Fit one rendered envelope while preserving its exact under-budget bytes. */
239
+ export function fitContextText(
240
+ text: string,
241
+ envelopeName: string,
242
+ opts: { budget: number; units?: ContextBudgetUnits; memoryIds?: readonly string[] }
243
+ ): string {
244
+ const units = opts.units ?? 'characters';
245
+ if (measureContext(text, units) <= opts.budget) return text;
246
+ return fitContextItems(contextItemsFromText(text, envelopeName, opts.memoryIds), opts).value;
247
+ }
248
+
249
+ /** Split an ambient body into whole pointer-terminated items. */
250
+ export function splitAmbientItems(body: string): string[] {
251
+ return parsePointerItems(body, 'ambient recall').memories.map((item) => item.text);
252
+ }