@animalabs/context-manager 0.1.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.
- package/dist/src/blob-manager.d.ts +37 -0
- package/dist/src/blob-manager.d.ts.map +1 -0
- package/dist/src/blob-manager.js +128 -0
- package/dist/src/blob-manager.js.map +1 -0
- package/dist/src/context-log.d.ts +99 -0
- package/dist/src/context-log.d.ts.map +1 -0
- package/dist/src/context-log.js +277 -0
- package/dist/src/context-log.js.map +1 -0
- package/dist/src/context-manager.d.ts +245 -0
- package/dist/src/context-manager.d.ts.map +1 -0
- package/dist/src/context-manager.js +553 -0
- package/dist/src/context-manager.js.map +1 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/message-store.d.ts +135 -0
- package/dist/src/message-store.d.ts.map +1 -0
- package/dist/src/message-store.js +372 -0
- package/dist/src/message-store.js.map +1 -0
- package/dist/src/strategies/autobiographical.d.ts +199 -0
- package/dist/src/strategies/autobiographical.d.ts.map +1 -0
- package/dist/src/strategies/autobiographical.js +1122 -0
- package/dist/src/strategies/autobiographical.js.map +1 -0
- package/dist/src/strategies/index.d.ts +3 -0
- package/dist/src/strategies/index.d.ts.map +1 -0
- package/dist/src/strategies/index.js +3 -0
- package/dist/src/strategies/index.js.map +1 -0
- package/dist/src/strategies/knowledge.d.ts +46 -0
- package/dist/src/strategies/knowledge.d.ts.map +1 -0
- package/dist/src/strategies/knowledge.js +270 -0
- package/dist/src/strategies/knowledge.js.map +1 -0
- package/dist/src/strategies/passthrough.d.ts +17 -0
- package/dist/src/strategies/passthrough.d.ts.map +1 -0
- package/dist/src/strategies/passthrough.js +69 -0
- package/dist/src/strategies/passthrough.js.map +1 -0
- package/dist/src/types/context.d.ts +108 -0
- package/dist/src/types/context.d.ts.map +1 -0
- package/dist/src/types/context.js +2 -0
- package/dist/src/types/context.js.map +1 -0
- package/dist/src/types/index.d.ts +5 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/message.d.ts +129 -0
- package/dist/src/types/message.d.ts.map +1 -0
- package/dist/src/types/message.js +2 -0
- package/dist/src/types/message.js.map +1 -0
- package/dist/src/types/strategy.d.ts +233 -0
- package/dist/src/types/strategy.d.ts.map +1 -0
- package/dist/src/types/strategy.js +32 -0
- package/dist/src/types/strategy.js.map +1 -0
- package/dist/test/autobiographical.test.d.ts +2 -0
- package/dist/test/autobiographical.test.d.ts.map +1 -0
- package/dist/test/autobiographical.test.js +46 -0
- package/dist/test/autobiographical.test.js.map +1 -0
- package/dist/test/head-window-reset.test.d.ts +17 -0
- package/dist/test/head-window-reset.test.d.ts.map +1 -0
- package/dist/test/head-window-reset.test.js +342 -0
- package/dist/test/head-window-reset.test.js.map +1 -0
- package/dist/test/integration.test.d.ts +2 -0
- package/dist/test/integration.test.d.ts.map +1 -0
- package/dist/test/integration.test.js +1341 -0
- package/dist/test/integration.test.js.map +1 -0
- package/dist/test/knowledge.test.d.ts +2 -0
- package/dist/test/knowledge.test.d.ts.map +1 -0
- package/dist/test/knowledge.test.js +617 -0
- package/dist/test/knowledge.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/src/blob-manager.ts +155 -0
- package/src/context-log.ts +342 -0
- package/src/context-manager.ts +726 -0
- package/src/index.ts +50 -0
- package/src/message-store.ts +479 -0
- package/src/strategies/autobiographical.ts +1355 -0
- package/src/strategies/index.ts +2 -0
- package/src/strategies/knowledge.ts +336 -0
- package/src/strategies/passthrough.ts +98 -0
- package/src/types/context.ts +119 -0
- package/src/types/index.ts +42 -0
- package/src/types/message.ts +140 -0
- package/src/types/strategy.ts +282 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StoredMessage,
|
|
3
|
+
MessageStoreView,
|
|
4
|
+
KnowledgeConfig,
|
|
5
|
+
SummaryEntry,
|
|
6
|
+
SummaryLevel,
|
|
7
|
+
PhaseType,
|
|
8
|
+
} from '../types/index.js';
|
|
9
|
+
import { AutobiographicalStrategy, type Chunk } from './autobiographical.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_RESEARCH_PREFIXES = ['mcpl:', 'zulip:'];
|
|
12
|
+
const DEFAULT_SUBAGENT_PREFIXES = ['subagent:'];
|
|
13
|
+
const DEFAULT_LESSON_NAMES = ['lessons:create', 'lessons:update'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Domain-aware compression strategy for knowledge extraction workflows.
|
|
17
|
+
*
|
|
18
|
+
* Builds on AutobiographicalStrategy's hierarchical pyramid (L1/L2/L3)
|
|
19
|
+
* but changes three things:
|
|
20
|
+
* 1. Semantic chunking — chunks at phase transitions instead of fixed token counts
|
|
21
|
+
* 2. Phase-aware compression — different prompts per phase type
|
|
22
|
+
* 3. Asymmetric budget — caps research, prioritizes synthesis in L1 selection
|
|
23
|
+
*
|
|
24
|
+
* Also adds [LEAD] tracking: unresolved questions/leads are flagged in summaries
|
|
25
|
+
* and preserved across compression levels.
|
|
26
|
+
*/
|
|
27
|
+
export class KnowledgeStrategy extends AutobiographicalStrategy {
|
|
28
|
+
readonly name = 'knowledge' as const;
|
|
29
|
+
|
|
30
|
+
private knowledgeConfig: KnowledgeConfig;
|
|
31
|
+
|
|
32
|
+
constructor(config: Partial<KnowledgeConfig> = {}) {
|
|
33
|
+
// Force hierarchical mode
|
|
34
|
+
super({ ...config, hierarchical: true });
|
|
35
|
+
// Merge knowledge-specific fields onto the base config so all KnowledgeConfig
|
|
36
|
+
// fields are actually present (super() only stores AutobiographicalConfig fields).
|
|
37
|
+
this.knowledgeConfig = { ...this.config, ...config } as KnowledgeConfig;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Override: Semantic chunking
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
protected rebuildChunks(store: MessageStoreView): void {
|
|
45
|
+
const messagesToChunk = this.getCompressibleMessages(store);
|
|
46
|
+
|
|
47
|
+
// Preserve existing compressed chunks
|
|
48
|
+
const existingCompressed = new Map<string, Chunk>();
|
|
49
|
+
for (const chunk of this.chunks) {
|
|
50
|
+
if (chunk.compressed) {
|
|
51
|
+
existingCompressed.set(this.chunkKey(chunk), chunk);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.chunks = [];
|
|
56
|
+
this.compressionQueue = [];
|
|
57
|
+
|
|
58
|
+
let currentChunk: StoredMessage[] = [];
|
|
59
|
+
let currentTokens = 0;
|
|
60
|
+
let currentPhase: PhaseType | null = null;
|
|
61
|
+
let lastToolPhase: PhaseType = 'synthesis';
|
|
62
|
+
let chunkFilteredStart = 0;
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < messagesToChunk.length; i++) {
|
|
65
|
+
const msg = messagesToChunk[i];
|
|
66
|
+
const phase = this.classifyMessage(msg, lastToolPhase);
|
|
67
|
+
|
|
68
|
+
// Track tool phase for tool_result inheritance
|
|
69
|
+
if (this.getToolNames(msg).length > 0) {
|
|
70
|
+
lastToolPhase = phase;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let msgTokens = store.estimateTokens(msg);
|
|
74
|
+
if (this.config.attachmentsIgnoreSize) {
|
|
75
|
+
msgTokens = this.estimateTextOnlyTokens(msg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Semantic chunking uses a minimum of 2 messages (vs parent's 4) because
|
|
79
|
+
// phase transitions are the primary chunk boundary signal. A tool_use + tool_result
|
|
80
|
+
// pair is already a meaningful unit of work worth compressing independently.
|
|
81
|
+
const phaseChanged = currentPhase !== null && phase !== currentPhase;
|
|
82
|
+
const maxTokens = this.getMaxChunkTokens(currentPhase ?? phase);
|
|
83
|
+
const sizeExceeded = currentTokens >= maxTokens;
|
|
84
|
+
const shouldClose = currentChunk.length >= 2 && (phaseChanged || sizeExceeded);
|
|
85
|
+
|
|
86
|
+
if (shouldClose) {
|
|
87
|
+
const chunk = this.createChunk(
|
|
88
|
+
this.chunks.length,
|
|
89
|
+
chunkFilteredStart,
|
|
90
|
+
i,
|
|
91
|
+
currentChunk,
|
|
92
|
+
currentTokens,
|
|
93
|
+
existingCompressed
|
|
94
|
+
);
|
|
95
|
+
chunk.phaseType = currentPhase!;
|
|
96
|
+
this.chunks.push(chunk);
|
|
97
|
+
if (!chunk.compressed) this.compressionQueue.push(chunk.index);
|
|
98
|
+
|
|
99
|
+
currentChunk = [];
|
|
100
|
+
currentTokens = 0;
|
|
101
|
+
chunkFilteredStart = i;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
currentChunk.push(msg);
|
|
105
|
+
currentTokens += msgTokens;
|
|
106
|
+
currentPhase = phase;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Final remaining chunk (minimum 1 message to avoid silent drops)
|
|
110
|
+
if (currentChunk.length >= 1 && currentPhase) {
|
|
111
|
+
const chunk = this.createChunk(
|
|
112
|
+
this.chunks.length,
|
|
113
|
+
chunkFilteredStart,
|
|
114
|
+
messagesToChunk.length,
|
|
115
|
+
currentChunk,
|
|
116
|
+
currentTokens,
|
|
117
|
+
existingCompressed
|
|
118
|
+
);
|
|
119
|
+
chunk.phaseType = currentPhase;
|
|
120
|
+
this.chunks.push(chunk);
|
|
121
|
+
if (!chunk.compressed) this.compressionQueue.push(chunk.index);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Override: Phase-aware compression prompts
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
protected getCompressionInstruction(chunk: Chunk, targetTokens: number): string {
|
|
130
|
+
const leadSuffix = 'If any leads or open questions were identified but not yet pursued, mark them with [LEAD].';
|
|
131
|
+
|
|
132
|
+
switch (chunk.phaseType) {
|
|
133
|
+
case 'research':
|
|
134
|
+
return (
|
|
135
|
+
`Summarize the research performed. What was searched for, what sources were consulted, ` +
|
|
136
|
+
`and what was found? Note any leads discovered. Be terse — the raw data is not needed, ` +
|
|
137
|
+
`only what was learned. ${leadSuffix} Aim for about ${targetTokens} tokens.`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
case 'synthesis':
|
|
141
|
+
return (
|
|
142
|
+
`Capture the reasoning and conclusions from this discussion. What connections were drawn? ` +
|
|
143
|
+
`What hypotheses were formed or rejected? What decisions were made about what to ` +
|
|
144
|
+
`investigate next? Preserve the logic chain. ${leadSuffix} Aim for about ${targetTokens} tokens.`
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
case 'lesson':
|
|
148
|
+
return (
|
|
149
|
+
`Record what knowledge was captured. What lessons were created or updated? ` +
|
|
150
|
+
`What was the evidence basis? Note any confidence gaps or unresolved questions. ` +
|
|
151
|
+
`${leadSuffix} Aim for about ${targetTokens} tokens.`
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
case 'subagent':
|
|
155
|
+
return (
|
|
156
|
+
`Summarize the subagent work. What tasks were dispatched, what did each return, ` +
|
|
157
|
+
`and what was the coordinator's synthesis of the results? Collapse dispatch/wait mechanics. ` +
|
|
158
|
+
`${leadSuffix} Aim for about ${targetTokens} tokens.`
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
return super.getCompressionInstruction(chunk, targetTokens);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
protected getMergeInstruction(
|
|
167
|
+
targetLevel: SummaryLevel,
|
|
168
|
+
sources: SummaryEntry[],
|
|
169
|
+
targetTokens: number
|
|
170
|
+
): string {
|
|
171
|
+
// Describe the phase composition of the sources
|
|
172
|
+
const phaseCounts = new Map<string, number>();
|
|
173
|
+
for (const s of sources) {
|
|
174
|
+
const phase = s.phaseType ?? 'unknown';
|
|
175
|
+
phaseCounts.set(phase, (phaseCounts.get(phase) ?? 0) + 1);
|
|
176
|
+
}
|
|
177
|
+
const phaseDesc = [...phaseCounts.entries()]
|
|
178
|
+
.map(([phase, count]) => `${count} ${phase}`)
|
|
179
|
+
.join(', ');
|
|
180
|
+
|
|
181
|
+
const levelLabel = targetLevel === 2 ? 'L1 summaries' : 'L2 summaries';
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
`Please consolidate these ${sources.length} ${levelLabel} (${phaseDesc}) ` +
|
|
185
|
+
`into a single cohesive memory. ` +
|
|
186
|
+
`Aim for about ${targetTokens} tokens. Write as you would to yourself — this is your ` +
|
|
187
|
+
`autobiography, capturing the arc of what happened. ` +
|
|
188
|
+
`IMPORTANT: Preserve any items marked with [LEAD] — these are unresolved questions ` +
|
|
189
|
+
`that must survive consolidation until explicitly resolved or dropped.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Override: Asymmetric L1 budget allocation
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
protected selectL1Summaries(
|
|
198
|
+
shownL1: SummaryEntry[],
|
|
199
|
+
budget: number,
|
|
200
|
+
maxTokens: number
|
|
201
|
+
): { selected: SummaryEntry[]; tokensUsed: number } {
|
|
202
|
+
const effectiveBudget = Math.min(budget, maxTokens);
|
|
203
|
+
|
|
204
|
+
const researchCap = effectiveBudget * (this.knowledgeConfig.researchL1BudgetCap ?? 0.3);
|
|
205
|
+
const synthesisFloor = effectiveBudget * (this.knowledgeConfig.synthesisL1BudgetFloor ?? 0.4);
|
|
206
|
+
|
|
207
|
+
// Partition by phase
|
|
208
|
+
const synthesis: SummaryEntry[] = [];
|
|
209
|
+
const lessons: SummaryEntry[] = [];
|
|
210
|
+
const subagent: SummaryEntry[] = [];
|
|
211
|
+
const research: SummaryEntry[] = [];
|
|
212
|
+
const other: SummaryEntry[] = [];
|
|
213
|
+
|
|
214
|
+
for (const s of shownL1) {
|
|
215
|
+
switch (s.phaseType) {
|
|
216
|
+
case 'synthesis': synthesis.push(s); break;
|
|
217
|
+
case 'lesson': lessons.push(s); break;
|
|
218
|
+
case 'subagent': subagent.push(s); break;
|
|
219
|
+
case 'research': research.push(s); break;
|
|
220
|
+
default: other.push(s); break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const selected: SummaryEntry[] = [];
|
|
225
|
+
let used = 0;
|
|
226
|
+
|
|
227
|
+
// Priority 1: Synthesis — guaranteed floor, can go higher if budget allows
|
|
228
|
+
for (const s of synthesis) {
|
|
229
|
+
if (used + s.tokens > effectiveBudget) break;
|
|
230
|
+
// Keep going past the floor, but cap synthesis to avoid starving other phases
|
|
231
|
+
const synthesisCap = effectiveBudget * (this.knowledgeConfig.synthesisL1BudgetCap ?? 0.7);
|
|
232
|
+
if (used >= synthesisFloor && used + s.tokens > synthesisCap) break;
|
|
233
|
+
selected.push(s);
|
|
234
|
+
used += s.tokens;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Priority 2: Lessons — high value, no cap
|
|
238
|
+
for (const s of lessons) {
|
|
239
|
+
if (used + s.tokens > effectiveBudget) break;
|
|
240
|
+
selected.push(s);
|
|
241
|
+
used += s.tokens;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Priority 3: Subagent — moderate value, no cap
|
|
245
|
+
for (const s of [...subagent, ...other]) {
|
|
246
|
+
if (used + s.tokens > effectiveBudget) break;
|
|
247
|
+
selected.push(s);
|
|
248
|
+
used += s.tokens;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Priority 4: Research — capped
|
|
252
|
+
let researchUsed = 0;
|
|
253
|
+
for (const s of research) {
|
|
254
|
+
if (researchUsed + s.tokens > researchCap) break;
|
|
255
|
+
if (used + s.tokens > effectiveBudget) break;
|
|
256
|
+
selected.push(s);
|
|
257
|
+
researchUsed += s.tokens;
|
|
258
|
+
used += s.tokens;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { selected, tokensUsed: used };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Phase detection
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Classify a single message by the phase it belongs to.
|
|
270
|
+
* Uses tool_use block names for classification.
|
|
271
|
+
* Messages with only tool_result (no tool_use) inherit from the last tool phase.
|
|
272
|
+
*/
|
|
273
|
+
private classifyMessage(msg: StoredMessage, lastToolPhase: PhaseType): PhaseType {
|
|
274
|
+
const toolNames = this.getToolNames(msg);
|
|
275
|
+
|
|
276
|
+
if (toolNames.length > 0) {
|
|
277
|
+
return this.classifyByToolNames(toolNames);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// tool_result without tool_use → inherit from preceding tool phase
|
|
281
|
+
if (this.hasToolResult(msg)) {
|
|
282
|
+
return lastToolPhase;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Pure dialogue
|
|
286
|
+
return 'synthesis';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Classify by tool names with priority: lesson > subagent > research > synthesis.
|
|
291
|
+
*/
|
|
292
|
+
private classifyByToolNames(toolNames: string[]): PhaseType {
|
|
293
|
+
const lessonNames = this.knowledgeConfig.lessonToolNames ?? DEFAULT_LESSON_NAMES;
|
|
294
|
+
const subagentPrefixes = this.knowledgeConfig.subagentToolPrefixes ?? DEFAULT_SUBAGENT_PREFIXES;
|
|
295
|
+
const researchPrefixes = this.knowledgeConfig.researchToolPrefixes ?? DEFAULT_RESEARCH_PREFIXES;
|
|
296
|
+
|
|
297
|
+
if (toolNames.some(n => lessonNames.includes(n))) return 'lesson';
|
|
298
|
+
if (toolNames.some(n => subagentPrefixes.some(p => n.startsWith(p)))) return 'subagent';
|
|
299
|
+
if (toolNames.some(n => researchPrefixes.some(p => n.startsWith(p)))) return 'research';
|
|
300
|
+
|
|
301
|
+
return 'synthesis';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Extract tool names from tool_use blocks in a message.
|
|
306
|
+
*/
|
|
307
|
+
private getToolNames(msg: StoredMessage): string[] {
|
|
308
|
+
const names: string[] = [];
|
|
309
|
+
for (const block of msg.content) {
|
|
310
|
+
if (block.type === 'tool_use') {
|
|
311
|
+
names.push((block as { type: 'tool_use'; name: string }).name);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return names;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get the maximum chunk token size for a given phase.
|
|
319
|
+
* Research and subagent chunks can grow larger since they compress aggressively.
|
|
320
|
+
*/
|
|
321
|
+
private getMaxChunkTokens(phase: PhaseType): number {
|
|
322
|
+
const base = this.config.targetChunkTokens;
|
|
323
|
+
switch (phase) {
|
|
324
|
+
case 'research':
|
|
325
|
+
return this.knowledgeConfig.maxResearchChunkTokens ?? base * 2;
|
|
326
|
+
case 'subagent':
|
|
327
|
+
return this.knowledgeConfig.maxSubagentChunkTokens ?? base * 2;
|
|
328
|
+
case 'synthesis':
|
|
329
|
+
return this.knowledgeConfig.maxSynthesisChunkTokens ?? Math.round(base * 1.5);
|
|
330
|
+
case 'lesson':
|
|
331
|
+
return this.knowledgeConfig.maxLessonChunkTokens ?? base;
|
|
332
|
+
default:
|
|
333
|
+
return base;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContextStrategy,
|
|
3
|
+
StrategyContext,
|
|
4
|
+
ReadinessState,
|
|
5
|
+
MessageStoreView,
|
|
6
|
+
ContextLogView,
|
|
7
|
+
TokenBudget,
|
|
8
|
+
ContextEntry,
|
|
9
|
+
StoredMessage,
|
|
10
|
+
} from '../types/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple passthrough strategy that copies all messages to context.
|
|
14
|
+
* No compression, no background work. Always ready.
|
|
15
|
+
* Good for testing and simple use cases.
|
|
16
|
+
*/
|
|
17
|
+
export class PassthroughStrategy implements ContextStrategy {
|
|
18
|
+
readonly name = 'passthrough';
|
|
19
|
+
|
|
20
|
+
checkReadiness(): ReadinessState {
|
|
21
|
+
return { ready: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async onNewMessage(message: StoredMessage, ctx: StrategyContext): Promise<void> {
|
|
25
|
+
// No-op: context is rebuilt on select()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
select(
|
|
29
|
+
store: MessageStoreView,
|
|
30
|
+
_log: ContextLogView,
|
|
31
|
+
budget: TokenBudget
|
|
32
|
+
): ContextEntry[] {
|
|
33
|
+
const messages = store.getAll();
|
|
34
|
+
const entries: ContextEntry[] = [];
|
|
35
|
+
let totalTokens = 0;
|
|
36
|
+
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
37
|
+
|
|
38
|
+
// Add messages until we hit the budget
|
|
39
|
+
for (let i = 0; i < messages.length; i++) {
|
|
40
|
+
const msg = messages[i];
|
|
41
|
+
const tokens = store.estimateTokens(msg);
|
|
42
|
+
|
|
43
|
+
if (totalTokens + tokens > maxTokens) {
|
|
44
|
+
// If we're over budget, start from the end and work backwards
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
entries.push({
|
|
49
|
+
index: i,
|
|
50
|
+
sourceMessageId: msg.id,
|
|
51
|
+
sourceRelation: 'copy',
|
|
52
|
+
participant: msg.participant,
|
|
53
|
+
content: msg.content,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
totalTokens += tokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If we hit the limit before adding all messages,
|
|
60
|
+
// keep the most recent ones that fit
|
|
61
|
+
if (entries.length < messages.length) {
|
|
62
|
+
return this.selectFromEnd(store, maxTokens);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return entries;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Select messages from the end when we can't fit everything.
|
|
70
|
+
*/
|
|
71
|
+
private selectFromEnd(store: MessageStoreView, maxTokens: number): ContextEntry[] {
|
|
72
|
+
const messages = store.getAll();
|
|
73
|
+
const entries: ContextEntry[] = [];
|
|
74
|
+
let totalTokens = 0;
|
|
75
|
+
|
|
76
|
+
// Work backwards from the most recent
|
|
77
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
78
|
+
const msg = messages[i];
|
|
79
|
+
const tokens = store.estimateTokens(msg);
|
|
80
|
+
|
|
81
|
+
if (totalTokens + tokens > maxTokens) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
entries.unshift({
|
|
86
|
+
index: i,
|
|
87
|
+
sourceMessageId: msg.id,
|
|
88
|
+
sourceRelation: 'copy',
|
|
89
|
+
participant: msg.participant,
|
|
90
|
+
content: msg.content,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
totalTokens += tokens;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ContentBlock, NormalizedMessage } from '@animalabs/membrane';
|
|
2
|
+
import type { MessageId, StoredContentBlock } from './message.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Describes how a context entry relates to its source message.
|
|
6
|
+
* This determines edit propagation behavior.
|
|
7
|
+
*/
|
|
8
|
+
export type SourceRelation =
|
|
9
|
+
/** Direct copy - edits MUST propagate */
|
|
10
|
+
| 'copy'
|
|
11
|
+
/** Summary/compression - edits MAY be ignored (stale is acceptable) */
|
|
12
|
+
| 'derived'
|
|
13
|
+
/** Just mentions source - edits DON'T propagate */
|
|
14
|
+
| 'referenced';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An entry in the context log.
|
|
18
|
+
* The context log is a materialized, editable working set derived from the message store.
|
|
19
|
+
*/
|
|
20
|
+
export interface ContextEntry {
|
|
21
|
+
/** Index in the context log */
|
|
22
|
+
index: number;
|
|
23
|
+
/** Source message ID (if derived from message store) */
|
|
24
|
+
sourceMessageId?: MessageId;
|
|
25
|
+
/** How this entry relates to its source */
|
|
26
|
+
sourceRelation?: SourceRelation;
|
|
27
|
+
/** Participant name */
|
|
28
|
+
participant: string;
|
|
29
|
+
/** Materialized content blocks */
|
|
30
|
+
content: ContentBlock[];
|
|
31
|
+
/** For prompt caching (future) */
|
|
32
|
+
cacheMarker?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Internal representation with blob references for storage.
|
|
37
|
+
*/
|
|
38
|
+
export interface ContextEntryInternal {
|
|
39
|
+
index: number;
|
|
40
|
+
sourceMessageId?: MessageId;
|
|
41
|
+
sourceRelation?: SourceRelation;
|
|
42
|
+
participant: string;
|
|
43
|
+
content: StoredContentBlock[];
|
|
44
|
+
cacheMarker?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Token budget for context compilation.
|
|
49
|
+
*/
|
|
50
|
+
export interface TokenBudget {
|
|
51
|
+
/** Maximum tokens for the context window */
|
|
52
|
+
maxTokens: number;
|
|
53
|
+
/** Reserve this many tokens for model response */
|
|
54
|
+
reserveForResponse: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Information about pending background work.
|
|
59
|
+
*/
|
|
60
|
+
export interface PendingWork {
|
|
61
|
+
/** Human-readable description */
|
|
62
|
+
description: string;
|
|
63
|
+
/** Estimated time to completion in ms */
|
|
64
|
+
estimatedMs?: number;
|
|
65
|
+
/** When the work started */
|
|
66
|
+
started: Date;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An injection into the compiled context.
|
|
71
|
+
* Source-agnostic: may come from MCPL servers, local strategies, or application code.
|
|
72
|
+
*/
|
|
73
|
+
export interface ContextInjection {
|
|
74
|
+
/** Server-defined namespace (e.g., "memory", "compliance") */
|
|
75
|
+
namespace: string;
|
|
76
|
+
|
|
77
|
+
/** Where to inject in the message array */
|
|
78
|
+
position: 'system' | 'beforeUser' | 'afterUser';
|
|
79
|
+
|
|
80
|
+
/** Content blocks to inject (multimodal) */
|
|
81
|
+
content: ContentBlock[];
|
|
82
|
+
|
|
83
|
+
/** Arbitrary metadata (passed through, not interpreted) */
|
|
84
|
+
metadata?: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Result of context compilation.
|
|
89
|
+
* Separates system-position injections from message-level content.
|
|
90
|
+
*/
|
|
91
|
+
export interface CompileResult {
|
|
92
|
+
/** Compiled messages (includes beforeUser/afterUser injections merged in) */
|
|
93
|
+
messages: NormalizedMessage[];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* System-position injections, grouped by namespace.
|
|
97
|
+
* Caller should append these to the system prompt.
|
|
98
|
+
* Separated because the system prompt is outside context-manager's scope.
|
|
99
|
+
*/
|
|
100
|
+
systemInjections: ContentBlock[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Branch information.
|
|
105
|
+
*/
|
|
106
|
+
export interface BranchInfo {
|
|
107
|
+
/** Branch identifier */
|
|
108
|
+
id: string;
|
|
109
|
+
/** Branch name */
|
|
110
|
+
name: string;
|
|
111
|
+
/** Current head sequence */
|
|
112
|
+
head: number;
|
|
113
|
+
/** Parent branch ID */
|
|
114
|
+
parentId?: string;
|
|
115
|
+
/** Sequence at which branch was created */
|
|
116
|
+
branchPoint?: number;
|
|
117
|
+
/** Creation timestamp */
|
|
118
|
+
created: Date;
|
|
119
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Message types
|
|
2
|
+
export type {
|
|
3
|
+
MessageId,
|
|
4
|
+
Sequence,
|
|
5
|
+
BranchId,
|
|
6
|
+
MessageMetadata,
|
|
7
|
+
StoredMessage,
|
|
8
|
+
BlobReference,
|
|
9
|
+
StoredContentBlock,
|
|
10
|
+
StoredMessageInternal,
|
|
11
|
+
MessageQuery,
|
|
12
|
+
MessageQueryResult,
|
|
13
|
+
} from './message.js';
|
|
14
|
+
|
|
15
|
+
// Context types
|
|
16
|
+
export type {
|
|
17
|
+
SourceRelation,
|
|
18
|
+
ContextEntry,
|
|
19
|
+
ContextEntryInternal,
|
|
20
|
+
TokenBudget,
|
|
21
|
+
PendingWork,
|
|
22
|
+
BranchInfo,
|
|
23
|
+
ContextInjection,
|
|
24
|
+
CompileResult,
|
|
25
|
+
} from './context.js';
|
|
26
|
+
|
|
27
|
+
// Strategy types
|
|
28
|
+
export type {
|
|
29
|
+
MessageStoreView,
|
|
30
|
+
ContextLogView,
|
|
31
|
+
StrategyContext,
|
|
32
|
+
ReadinessState,
|
|
33
|
+
ContextStrategy,
|
|
34
|
+
AutobiographicalConfig,
|
|
35
|
+
SummaryLevel,
|
|
36
|
+
SummaryEntry,
|
|
37
|
+
PhaseType,
|
|
38
|
+
KnowledgeConfig,
|
|
39
|
+
ResettableStrategy,
|
|
40
|
+
} from './strategy.js';
|
|
41
|
+
|
|
42
|
+
export { DEFAULT_AUTOBIOGRAPHICAL_CONFIG, isResettableStrategy } from './strategy.js';
|