@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Main class
|
|
2
|
+
export { ContextManager } from './context-manager.js';
|
|
3
|
+
export type { ContextManagerConfig } from './context-manager.js';
|
|
4
|
+
|
|
5
|
+
// Storage
|
|
6
|
+
export { MessageStore } from './message-store.js';
|
|
7
|
+
export type { MessageStoreEvent, MessageStoreListener } from './message-store.js';
|
|
8
|
+
export { ContextLog } from './context-log.js';
|
|
9
|
+
export { BlobManager } from './blob-manager.js';
|
|
10
|
+
|
|
11
|
+
// Strategies
|
|
12
|
+
export { PassthroughStrategy } from './strategies/passthrough.js';
|
|
13
|
+
export { AutobiographicalStrategy } from './strategies/autobiographical.js';
|
|
14
|
+
export { KnowledgeStrategy } from './strategies/knowledge.js';
|
|
15
|
+
|
|
16
|
+
// Types
|
|
17
|
+
export type {
|
|
18
|
+
// Message types
|
|
19
|
+
MessageId,
|
|
20
|
+
Sequence,
|
|
21
|
+
BranchId,
|
|
22
|
+
MessageMetadata,
|
|
23
|
+
StoredMessage,
|
|
24
|
+
BlobReference,
|
|
25
|
+
StoredContentBlock,
|
|
26
|
+
MessageQuery,
|
|
27
|
+
MessageQueryResult,
|
|
28
|
+
// Context types
|
|
29
|
+
SourceRelation,
|
|
30
|
+
ContextEntry,
|
|
31
|
+
TokenBudget,
|
|
32
|
+
PendingWork,
|
|
33
|
+
BranchInfo,
|
|
34
|
+
ContextInjection,
|
|
35
|
+
CompileResult,
|
|
36
|
+
// Strategy types
|
|
37
|
+
MessageStoreView,
|
|
38
|
+
ContextLogView,
|
|
39
|
+
StrategyContext,
|
|
40
|
+
ReadinessState,
|
|
41
|
+
ContextStrategy,
|
|
42
|
+
AutobiographicalConfig,
|
|
43
|
+
SummaryLevel,
|
|
44
|
+
SummaryEntry,
|
|
45
|
+
PhaseType,
|
|
46
|
+
KnowledgeConfig,
|
|
47
|
+
ResettableStrategy,
|
|
48
|
+
} from './types/index.js';
|
|
49
|
+
|
|
50
|
+
export { DEFAULT_AUTOBIOGRAPHICAL_CONFIG, isResettableStrategy } from './types/index.js';
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import type { JsStore } from '@animalabs/chronicle';
|
|
2
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
3
|
+
import type {
|
|
4
|
+
MessageId,
|
|
5
|
+
Sequence,
|
|
6
|
+
MessageMetadata,
|
|
7
|
+
StoredMessage,
|
|
8
|
+
StoredMessageInternal,
|
|
9
|
+
MessageStoreView,
|
|
10
|
+
MessageQuery,
|
|
11
|
+
MessageQueryResult,
|
|
12
|
+
} from './types/index.js';
|
|
13
|
+
import { BlobManager } from './blob-manager.js';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MESSAGE_STATE_ID = 'messages';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Event emitted when the message store changes.
|
|
19
|
+
*/
|
|
20
|
+
export type MessageStoreEvent =
|
|
21
|
+
| { type: 'add'; message: StoredMessage }
|
|
22
|
+
| { type: 'edit'; messageId: MessageId; oldContent: ContentBlock[]; newContent: ContentBlock[] }
|
|
23
|
+
| { type: 'remove'; messageId: MessageId }
|
|
24
|
+
| { type: 'removeRange'; fromId: MessageId; toId: MessageId };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Listener for message store events.
|
|
28
|
+
*/
|
|
29
|
+
export type MessageStoreListener = (event: MessageStoreEvent) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options for token estimation.
|
|
33
|
+
*/
|
|
34
|
+
export interface TokenEstimatorOptions {
|
|
35
|
+
/** Custom token estimator function */
|
|
36
|
+
estimator?: (text: string) => number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wrapper around Chronicle append_log state for message storage.
|
|
41
|
+
* Handles blob extraction and provides a clean interface for message operations.
|
|
42
|
+
*/
|
|
43
|
+
export class MessageStore {
|
|
44
|
+
private blobManager: BlobManager;
|
|
45
|
+
private listeners: Set<MessageStoreListener> = new Set();
|
|
46
|
+
private idToIndex: Map<MessageId, number> = new Map();
|
|
47
|
+
private tokenEstimator: (text: string) => number;
|
|
48
|
+
private stateId: string;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private store: JsStore,
|
|
52
|
+
options: TokenEstimatorOptions & {
|
|
53
|
+
/** Namespace for multi-agent support. Creates state ID: `{namespace}/messages` */
|
|
54
|
+
namespace?: string;
|
|
55
|
+
} = {}
|
|
56
|
+
) {
|
|
57
|
+
this.stateId = options.namespace
|
|
58
|
+
? `${options.namespace}/messages`
|
|
59
|
+
: DEFAULT_MESSAGE_STATE_ID;
|
|
60
|
+
this.blobManager = new BlobManager(store);
|
|
61
|
+
this.tokenEstimator = options.estimator ?? defaultTokenEstimator;
|
|
62
|
+
this.rebuildIndex();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register the message store state in Chronicle.
|
|
67
|
+
* Should be called once when setting up the store.
|
|
68
|
+
*
|
|
69
|
+
* @param store The Chronicle store
|
|
70
|
+
* @param namespace Optional namespace for multi-agent support
|
|
71
|
+
*/
|
|
72
|
+
static register(store: JsStore, namespace?: string): void {
|
|
73
|
+
const stateId = namespace ? `${namespace}/messages` : DEFAULT_MESSAGE_STATE_ID;
|
|
74
|
+
store.registerState({
|
|
75
|
+
id: stateId,
|
|
76
|
+
strategy: 'append_log',
|
|
77
|
+
deltaSnapshotEvery: 50,
|
|
78
|
+
fullSnapshotEvery: 10,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add a listener for store events.
|
|
84
|
+
*/
|
|
85
|
+
addListener(listener: MessageStoreListener): () => void {
|
|
86
|
+
this.listeners.add(listener);
|
|
87
|
+
return () => this.listeners.delete(listener);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private emit(event: MessageStoreEvent): void {
|
|
91
|
+
for (const listener of this.listeners) {
|
|
92
|
+
listener(event);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private rebuildIndex(): void {
|
|
97
|
+
this.idToIndex.clear();
|
|
98
|
+
const messages = this.getAllInternal();
|
|
99
|
+
for (let i = 0; i < messages.length; i++) {
|
|
100
|
+
this.idToIndex.set(messages[i].id, i);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Append a new message to the store.
|
|
106
|
+
*/
|
|
107
|
+
append(
|
|
108
|
+
participant: string,
|
|
109
|
+
content: ContentBlock[],
|
|
110
|
+
metadata?: MessageMetadata,
|
|
111
|
+
causedBy?: MessageId[]
|
|
112
|
+
): StoredMessage {
|
|
113
|
+
// Extract blobs from content
|
|
114
|
+
const storedContent = this.blobManager.extractBlobs(content);
|
|
115
|
+
|
|
116
|
+
// Append to Chronicle first to get the record ID
|
|
117
|
+
const partialInternal = {
|
|
118
|
+
participant,
|
|
119
|
+
content: storedContent,
|
|
120
|
+
metadata,
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
causedBy,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const record = this.store.appendToStateJson(this.stateId, partialInternal);
|
|
126
|
+
const index = this.length() - 1;
|
|
127
|
+
|
|
128
|
+
// Now update the stored item to include the id (for rebuildIndex)
|
|
129
|
+
const fullInternal: StoredMessageInternal = {
|
|
130
|
+
id: record.id,
|
|
131
|
+
sequence: record.sequence,
|
|
132
|
+
...partialInternal,
|
|
133
|
+
};
|
|
134
|
+
this.store.editStateItem(this.stateId, index, Buffer.from(JSON.stringify(fullInternal)));
|
|
135
|
+
|
|
136
|
+
// Build full message with ID and sequence from record
|
|
137
|
+
const message: StoredMessage = {
|
|
138
|
+
id: record.id,
|
|
139
|
+
sequence: record.sequence,
|
|
140
|
+
participant,
|
|
141
|
+
content, // Original content with inline data
|
|
142
|
+
metadata,
|
|
143
|
+
timestamp: new Date(partialInternal.timestamp),
|
|
144
|
+
causedBy,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Update index
|
|
148
|
+
this.idToIndex.set(message.id, index);
|
|
149
|
+
|
|
150
|
+
this.emit({ type: 'add', message });
|
|
151
|
+
return message;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Edit a message's content.
|
|
156
|
+
*/
|
|
157
|
+
edit(messageId: MessageId, newContent: ContentBlock[]): void {
|
|
158
|
+
const index = this.idToIndex.get(messageId);
|
|
159
|
+
if (index === undefined) {
|
|
160
|
+
throw new Error(`Message not found: ${messageId}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const oldMessage = this.getInternal(index);
|
|
164
|
+
if (!oldMessage) {
|
|
165
|
+
throw new Error(`Message not found at index: ${index}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const oldContent = this.blobManager.resolveBlobs(oldMessage.content);
|
|
169
|
+
const storedContent = this.blobManager.extractBlobs(newContent);
|
|
170
|
+
|
|
171
|
+
// Update the stored message
|
|
172
|
+
const updated: StoredMessageInternal = {
|
|
173
|
+
...oldMessage,
|
|
174
|
+
content: storedContent,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
this.store.editStateItem(this.stateId, index, Buffer.from(JSON.stringify(updated)));
|
|
178
|
+
|
|
179
|
+
this.emit({ type: 'edit', messageId, oldContent, newContent });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Remove a message from the store.
|
|
184
|
+
*/
|
|
185
|
+
remove(messageId: MessageId): void {
|
|
186
|
+
const index = this.idToIndex.get(messageId);
|
|
187
|
+
if (index === undefined) {
|
|
188
|
+
throw new Error(`Message not found: ${messageId}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.store.redactStateItems(this.stateId, index, index + 1);
|
|
192
|
+
this.rebuildIndex();
|
|
193
|
+
|
|
194
|
+
this.emit({ type: 'remove', messageId });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Remove a range of messages from the store.
|
|
199
|
+
*/
|
|
200
|
+
removeRange(fromId: MessageId, toId: MessageId): void {
|
|
201
|
+
const fromIndex = this.idToIndex.get(fromId);
|
|
202
|
+
const toIndex = this.idToIndex.get(toId);
|
|
203
|
+
|
|
204
|
+
if (fromIndex === undefined) {
|
|
205
|
+
throw new Error(`Message not found: ${fromId}`);
|
|
206
|
+
}
|
|
207
|
+
if (toIndex === undefined) {
|
|
208
|
+
throw new Error(`Message not found: ${toId}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.store.redactStateItems(this.stateId, fromIndex, toIndex + 1);
|
|
212
|
+
this.rebuildIndex();
|
|
213
|
+
|
|
214
|
+
this.emit({ type: 'removeRange', fromId, toId });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get a message by ID.
|
|
219
|
+
*/
|
|
220
|
+
get(messageId: MessageId): StoredMessage | null {
|
|
221
|
+
const index = this.idToIndex.get(messageId);
|
|
222
|
+
if (index === undefined) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const internal = this.getInternal(index);
|
|
227
|
+
if (!internal) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this.internalToStored(internal, messageId, index);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get a message as it was at a specific sequence (time travel).
|
|
236
|
+
*/
|
|
237
|
+
getAt(messageId: MessageId, atSequence: Sequence): StoredMessage | null {
|
|
238
|
+
// Get historical state
|
|
239
|
+
const historicalState = this.store.getStateJsonAt(this.stateId, atSequence);
|
|
240
|
+
if (!historicalState || !Array.isArray(historicalState)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Find the message in historical state
|
|
245
|
+
for (let i = 0; i < historicalState.length; i++) {
|
|
246
|
+
const internal = historicalState[i] as StoredMessageInternal;
|
|
247
|
+
if (internal.id === messageId) {
|
|
248
|
+
return this.internalToStored(internal, messageId, i);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get all messages.
|
|
257
|
+
*/
|
|
258
|
+
getAll(): StoredMessage[] {
|
|
259
|
+
const internals = this.getAllInternal();
|
|
260
|
+
return internals.map((internal, i) =>
|
|
261
|
+
this.internalToStored(internal, internal.id, i)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get messages from a specific index.
|
|
267
|
+
*/
|
|
268
|
+
getFrom(index: number): StoredMessage[] {
|
|
269
|
+
return this.getAll().slice(index);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the last N messages.
|
|
274
|
+
*/
|
|
275
|
+
getTail(count: number): StoredMessage[] {
|
|
276
|
+
const all = this.getAll();
|
|
277
|
+
return all.slice(Math.max(0, all.length - count));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get the total number of messages.
|
|
282
|
+
*/
|
|
283
|
+
length(): number {
|
|
284
|
+
return this.store.getStateLen(this.stateId) ?? 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Estimate tokens for a message.
|
|
289
|
+
*/
|
|
290
|
+
estimateTokens(message: StoredMessage): number {
|
|
291
|
+
let tokens = 0;
|
|
292
|
+
for (const block of message.content) {
|
|
293
|
+
tokens += this.estimateBlockTokens(block);
|
|
294
|
+
}
|
|
295
|
+
return tokens;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private estimateBlockTokens(block: ContentBlock): number {
|
|
299
|
+
switch (block.type) {
|
|
300
|
+
case 'text':
|
|
301
|
+
return this.tokenEstimator(block.text);
|
|
302
|
+
case 'thinking':
|
|
303
|
+
return this.tokenEstimator(block.thinking);
|
|
304
|
+
case 'tool_use':
|
|
305
|
+
return this.tokenEstimator(JSON.stringify(block.input)) + 20; // overhead for name, id
|
|
306
|
+
case 'tool_result':
|
|
307
|
+
if (!block.content) return 0;
|
|
308
|
+
if (typeof block.content === 'string') {
|
|
309
|
+
return this.tokenEstimator(block.content);
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(block.content)) {
|
|
312
|
+
return block.content.reduce((sum, b) => sum + this.estimateBlockTokens(b), 0);
|
|
313
|
+
}
|
|
314
|
+
return 0;
|
|
315
|
+
case 'image':
|
|
316
|
+
return block.tokenEstimate ?? 1000; // Default estimate for images
|
|
317
|
+
case 'document':
|
|
318
|
+
case 'audio':
|
|
319
|
+
case 'video':
|
|
320
|
+
return 1000; // Default estimate for media
|
|
321
|
+
default:
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Create a read-only view of the store for strategies.
|
|
328
|
+
*/
|
|
329
|
+
createView(): MessageStoreView {
|
|
330
|
+
return {
|
|
331
|
+
getAll: () => this.getAll(),
|
|
332
|
+
get: (id) => this.get(id),
|
|
333
|
+
getFrom: (index) => this.getFrom(index),
|
|
334
|
+
getTail: (count) => this.getTail(count),
|
|
335
|
+
length: () => this.length(),
|
|
336
|
+
estimateTokens: (msg) => this.estimateTokens(msg),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Query messages by filter criteria.
|
|
342
|
+
* Useful for finding messages from external sources, by participant, etc.
|
|
343
|
+
*/
|
|
344
|
+
query(filter: MessageQuery): MessageQueryResult {
|
|
345
|
+
let messages = this.getAll();
|
|
346
|
+
let totalCount = 0;
|
|
347
|
+
|
|
348
|
+
// Apply filters
|
|
349
|
+
const filtered: StoredMessage[] = [];
|
|
350
|
+
for (const msg of messages) {
|
|
351
|
+
if (this.matchesFilter(msg, filter)) {
|
|
352
|
+
filtered.push(msg);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
totalCount = filtered.length;
|
|
357
|
+
|
|
358
|
+
// Apply reverse if requested
|
|
359
|
+
let result = filter.reverse ? filtered.reverse() : filtered;
|
|
360
|
+
|
|
361
|
+
// Apply limit if specified
|
|
362
|
+
if (filter.limit !== undefined && filter.limit < result.length) {
|
|
363
|
+
result = result.slice(0, filter.limit);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { messages: result, totalCount };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Find a message by external source and ID.
|
|
371
|
+
* Convenience method for common lookup pattern.
|
|
372
|
+
*/
|
|
373
|
+
findByExternalId(source: string, externalId: string): StoredMessage | null {
|
|
374
|
+
const result = this.query({
|
|
375
|
+
source,
|
|
376
|
+
externalIds: [externalId],
|
|
377
|
+
limit: 1,
|
|
378
|
+
});
|
|
379
|
+
return result.messages[0] ?? null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Check if a message matches the query filter.
|
|
384
|
+
*/
|
|
385
|
+
private matchesFilter(msg: StoredMessage, filter: MessageQuery): boolean {
|
|
386
|
+
// Filter by source
|
|
387
|
+
if (filter.source !== undefined) {
|
|
388
|
+
const external = msg.metadata?.external as { source?: string } | undefined;
|
|
389
|
+
if (external?.source !== filter.source) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Filter by external IDs
|
|
395
|
+
if (filter.externalIds !== undefined && filter.externalIds.length > 0) {
|
|
396
|
+
const external = msg.metadata?.external as { id?: string } | undefined;
|
|
397
|
+
if (!external?.id || !filter.externalIds.includes(external.id)) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Filter by participant
|
|
403
|
+
if (filter.participant !== undefined) {
|
|
404
|
+
if (msg.participant !== filter.participant) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Filter by metadata fields
|
|
410
|
+
if (filter.metadata !== undefined) {
|
|
411
|
+
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
412
|
+
const actual = this.getNestedValue(msg.metadata, key);
|
|
413
|
+
if (actual !== value) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get a nested value from an object using dot notation.
|
|
424
|
+
* e.g., getNestedValue(obj, 'external.channelId')
|
|
425
|
+
*/
|
|
426
|
+
private getNestedValue(obj: unknown, path: string): unknown {
|
|
427
|
+
if (obj === undefined || obj === null) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const parts = path.split('.');
|
|
432
|
+
let current: unknown = obj;
|
|
433
|
+
|
|
434
|
+
for (const part of parts) {
|
|
435
|
+
if (current === undefined || current === null || typeof current !== 'object') {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
current = (current as Record<string, unknown>)[part];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return current;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private getAllInternal(): StoredMessageInternal[] {
|
|
445
|
+
const state = this.store.getStateJson(this.stateId);
|
|
446
|
+
if (!state || !Array.isArray(state)) {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
return state as StoredMessageInternal[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private getInternal(index: number): StoredMessageInternal | null {
|
|
453
|
+
const all = this.getAllInternal();
|
|
454
|
+
return all[index] ?? null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private internalToStored(
|
|
458
|
+
internal: StoredMessageInternal,
|
|
459
|
+
id: MessageId,
|
|
460
|
+
index: number
|
|
461
|
+
): StoredMessage {
|
|
462
|
+
return {
|
|
463
|
+
id,
|
|
464
|
+
sequence: index, // Use index as sequence for now
|
|
465
|
+
participant: internal.participant,
|
|
466
|
+
content: this.blobManager.resolveBlobs(internal.content),
|
|
467
|
+
metadata: internal.metadata,
|
|
468
|
+
timestamp: new Date(internal.timestamp),
|
|
469
|
+
causedBy: internal.causedBy,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Default token estimator: chars / 4
|
|
476
|
+
*/
|
|
477
|
+
function defaultTokenEstimator(text: string): number {
|
|
478
|
+
return Math.ceil(text.length / 4);
|
|
479
|
+
}
|