@robota-sdk/agent-plugin 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +1724 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +48 -0
- package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
- package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
- package/src/conversation-history/conversation-history-helpers.ts +120 -0
- package/src/conversation-history/conversation-history-plugin.ts +294 -0
- package/src/conversation-history/index.ts +11 -0
- package/src/conversation-history/storages/database-storage.ts +96 -0
- package/src/conversation-history/storages/file-storage.ts +95 -0
- package/src/conversation-history/storages/index.ts +3 -0
- package/src/conversation-history/storages/memory-storage.ts +44 -0
- package/src/conversation-history/types.ts +64 -0
- package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
- package/src/error-handling/context-adapter.ts +48 -0
- package/src/error-handling/error-handling-helpers.ts +53 -0
- package/src/error-handling/error-handling-plugin.ts +293 -0
- package/src/error-handling/index.ts +9 -0
- package/src/error-handling/types.ts +82 -0
- package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
- package/src/execution-analytics/analytics-aggregation.ts +88 -0
- package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
- package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
- package/src/execution-analytics/index.ts +9 -0
- package/src/execution-analytics/types.ts +97 -0
- package/src/index.ts +8 -0
- package/src/limits/__tests__/limits-plugin.test.ts +712 -0
- package/src/limits/index.ts +9 -0
- package/src/limits/limits-helpers.ts +185 -0
- package/src/limits/limits-plugin.ts +196 -0
- package/src/limits/types.ts +73 -0
- package/src/limits/validation.ts +81 -0
- package/src/logging/__tests__/formatters.test.ts +48 -0
- package/src/logging/__tests__/logging-plugin.test.ts +464 -0
- package/src/logging/__tests__/logging-storages.test.ts +95 -0
- package/src/logging/formatters.ts +28 -0
- package/src/logging/index.ts +15 -0
- package/src/logging/logging-helpers.ts +223 -0
- package/src/logging/logging-plugin.ts +288 -0
- package/src/logging/storages/console-storage.ts +44 -0
- package/src/logging/storages/file-storage.ts +44 -0
- package/src/logging/storages/index.ts +4 -0
- package/src/logging/storages/remote-storage.ts +78 -0
- package/src/logging/storages/silent-storage.ts +18 -0
- package/src/logging/types.ts +106 -0
- package/src/performance/__tests__/memory-storage.test.ts +86 -0
- package/src/performance/__tests__/performance-plugin.test.ts +208 -0
- package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
- package/src/performance/collectors/system-metrics-collector.ts +69 -0
- package/src/performance/index.ts +12 -0
- package/src/performance/performance-helpers.ts +86 -0
- package/src/performance/performance-plugin.ts +274 -0
- package/src/performance/storages/index.ts +1 -0
- package/src/performance/storages/memory-storage.ts +88 -0
- package/src/performance/types.ts +160 -0
- package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
- package/src/usage/__tests__/memory-storage.test.ts +83 -0
- package/src/usage/__tests__/silent-storage.test.ts +44 -0
- package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
- package/src/usage/__tests__/usage-plugin.test.ts +358 -0
- package/src/usage/aggregate-usage-stats.ts +142 -0
- package/src/usage/index.ts +14 -0
- package/src/usage/storages/file-storage.ts +115 -0
- package/src/usage/storages/index.ts +4 -0
- package/src/usage/storages/memory-storage.ts +61 -0
- package/src/usage/storages/remote-storage.ts +143 -0
- package/src/usage/storages/silent-storage.ts +38 -0
- package/src/usage/types.ts +132 -0
- package/src/usage/usage-plugin-helpers.ts +116 -0
- package/src/usage/usage-plugin.ts +296 -0
- package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
- package/src/webhook/http-client.ts +141 -0
- package/src/webhook/index.ts +9 -0
- package/src/webhook/transformer.ts +209 -0
- package/src/webhook/types.ts +201 -0
- package/src/webhook/webhook-helpers.ts +60 -0
- package/src/webhook/webhook-plugin.ts +298 -0
- package/src/webhook/webhook-queue.ts +148 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
3
|
+
import { ConversationHistoryPlugin } from '../conversation-history-plugin';
|
|
4
|
+
import { ConfigurationError, PluginError } from '@robota-sdk/agent-core';
|
|
5
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
6
|
+
|
|
7
|
+
function createUserMessage(content: string): TUniversalMessage {
|
|
8
|
+
return {
|
|
9
|
+
id: randomUUID(),
|
|
10
|
+
role: 'user',
|
|
11
|
+
content,
|
|
12
|
+
state: 'complete',
|
|
13
|
+
timestamp: new Date(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createAssistantMessage(content: string): TUniversalMessage {
|
|
18
|
+
return {
|
|
19
|
+
id: randomUUID(),
|
|
20
|
+
role: 'assistant',
|
|
21
|
+
content,
|
|
22
|
+
state: 'complete',
|
|
23
|
+
timestamp: new Date(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('ConversationHistoryPlugin', () => {
|
|
28
|
+
let plugin: ConversationHistoryPlugin;
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
if (plugin) {
|
|
32
|
+
await plugin.destroy();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('constructor', () => {
|
|
37
|
+
it('initializes with memory storage', () => {
|
|
38
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
39
|
+
expect(plugin.name).toBe('ConversationHistoryPlugin');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws on missing storage strategy', () => {
|
|
43
|
+
expect(() => new ConversationHistoryPlugin({ storage: '' as any })).toThrow(
|
|
44
|
+
ConfigurationError,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('throws on invalid storage strategy', () => {
|
|
49
|
+
expect(() => new ConversationHistoryPlugin({ storage: 'redis' as any })).toThrow(
|
|
50
|
+
ConfigurationError,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('throws on non-positive maxConversations', () => {
|
|
55
|
+
expect(
|
|
56
|
+
() => new ConversationHistoryPlugin({ storage: 'memory', maxConversations: 0 }),
|
|
57
|
+
).toThrow(ConfigurationError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('throws on non-positive maxMessagesPerConversation', () => {
|
|
61
|
+
expect(
|
|
62
|
+
() => new ConversationHistoryPlugin({ storage: 'memory', maxMessagesPerConversation: -1 }),
|
|
63
|
+
).toThrow(ConfigurationError);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('throws when file storage is missing filePath', () => {
|
|
67
|
+
expect(() => new ConversationHistoryPlugin({ storage: 'file', filePath: '' })).toThrow(
|
|
68
|
+
ConfigurationError,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('throws when database storage is missing connectionString', () => {
|
|
73
|
+
expect(
|
|
74
|
+
() => new ConversationHistoryPlugin({ storage: 'database', connectionString: '' }),
|
|
75
|
+
).toThrow(ConfigurationError);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('conversation CRUD', () => {
|
|
80
|
+
it('starts a conversation and retrieves it', async () => {
|
|
81
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
82
|
+
await plugin.startConversation('conv-1');
|
|
83
|
+
|
|
84
|
+
const entry = await plugin.loadConversation('conv-1');
|
|
85
|
+
expect(entry).toBeDefined();
|
|
86
|
+
expect(entry!.conversationId).toBe('conv-1');
|
|
87
|
+
expect(entry!.messages).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('adds messages to a conversation', async () => {
|
|
91
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
92
|
+
await plugin.startConversation('conv-1');
|
|
93
|
+
|
|
94
|
+
await plugin.addMessage(createUserMessage('hello'));
|
|
95
|
+
await plugin.addMessage(createAssistantMessage('hi there'));
|
|
96
|
+
|
|
97
|
+
const messages = await plugin.getHistory('conv-1');
|
|
98
|
+
expect(messages).toHaveLength(2);
|
|
99
|
+
expect(messages[0].role).toBe('user');
|
|
100
|
+
expect(messages[1].role).toBe('assistant');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('throws when adding message without active conversation', async () => {
|
|
104
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
105
|
+
|
|
106
|
+
await expect(plugin.addMessage(createUserMessage('hello'))).rejects.toThrow(PluginError);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('lists conversations', async () => {
|
|
110
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
111
|
+
await plugin.startConversation('conv-1');
|
|
112
|
+
await plugin.startConversation('conv-2');
|
|
113
|
+
|
|
114
|
+
const ids = await plugin.listConversations();
|
|
115
|
+
expect(ids).toContain('conv-1');
|
|
116
|
+
expect(ids).toContain('conv-2');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('deletes a conversation', async () => {
|
|
120
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
121
|
+
await plugin.startConversation('conv-1');
|
|
122
|
+
|
|
123
|
+
const deleted = await plugin.deleteConversation('conv-1');
|
|
124
|
+
expect(deleted).toBe(true);
|
|
125
|
+
|
|
126
|
+
const entry = await plugin.loadConversation('conv-1');
|
|
127
|
+
expect(entry).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('clears all conversations', async () => {
|
|
131
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
132
|
+
await plugin.startConversation('conv-1');
|
|
133
|
+
await plugin.startConversation('conv-2');
|
|
134
|
+
|
|
135
|
+
await plugin.clearAllConversations();
|
|
136
|
+
|
|
137
|
+
const ids = await plugin.listConversations();
|
|
138
|
+
expect(ids).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('message trimming', () => {
|
|
143
|
+
it('trims messages when exceeding maxMessagesPerConversation', async () => {
|
|
144
|
+
plugin = new ConversationHistoryPlugin({
|
|
145
|
+
storage: 'memory',
|
|
146
|
+
maxMessagesPerConversation: 3,
|
|
147
|
+
});
|
|
148
|
+
await plugin.startConversation('conv-1');
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < 5; i++) {
|
|
151
|
+
await plugin.addMessage(createUserMessage(`msg-${i}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const messages = await plugin.getHistory('conv-1');
|
|
155
|
+
expect(messages).toHaveLength(3);
|
|
156
|
+
// Should keep the last 3 messages
|
|
157
|
+
expect(messages[0].content).toBe('msg-2');
|
|
158
|
+
expect(messages[2].content).toBe('msg-4');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('getHistory', () => {
|
|
163
|
+
it('returns empty array for unknown conversation', async () => {
|
|
164
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
165
|
+
|
|
166
|
+
const messages = await plugin.getHistory('nonexistent');
|
|
167
|
+
expect(messages).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('autoSave: false (batch mode)', () => {
|
|
172
|
+
it('startConversation persists to storage and queues conversationId in pendingSaves', async () => {
|
|
173
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory', autoSave: false });
|
|
174
|
+
await plugin.startConversation('conv-1');
|
|
175
|
+
// always persisted immediately so subsequent loads can find it
|
|
176
|
+
const entry = await plugin.loadConversation('conv-1');
|
|
177
|
+
expect(entry).toBeDefined();
|
|
178
|
+
expect(entry!.conversationId).toBe('conv-1');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('addMessage persists to storage and queues conversationId in pendingSaves', async () => {
|
|
182
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory', autoSave: false });
|
|
183
|
+
await plugin.startConversation('conv-1');
|
|
184
|
+
await plugin.addMessage(createUserMessage('hello'));
|
|
185
|
+
// always persisted immediately so reads return current state
|
|
186
|
+
const msgs = await plugin.getHistory('conv-1');
|
|
187
|
+
expect(msgs).toHaveLength(1);
|
|
188
|
+
expect(msgs[0].content).toBe('hello');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('savePending flushes pending saves', async () => {
|
|
192
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory', autoSave: false });
|
|
193
|
+
await plugin.startConversation('conv-1');
|
|
194
|
+
await plugin.savePending();
|
|
195
|
+
// no error — empty pendingSaves is a no-op
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('setupBatchSaving timer runs on autoSave: false', async () => {
|
|
199
|
+
plugin = new ConversationHistoryPlugin({
|
|
200
|
+
storage: 'memory',
|
|
201
|
+
autoSave: false,
|
|
202
|
+
saveInterval: 50,
|
|
203
|
+
});
|
|
204
|
+
expect(plugin).toBeDefined();
|
|
205
|
+
// timer cleaned up in afterEach via destroy()
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('destroy', () => {
|
|
210
|
+
it('completes without error', async () => {
|
|
211
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory' });
|
|
212
|
+
await expect(plugin.destroy()).resolves.not.toThrow();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('completes without error in batch mode', async () => {
|
|
216
|
+
plugin = new ConversationHistoryPlugin({ storage: 'memory', autoSave: false });
|
|
217
|
+
await plugin.startConversation('conv-1');
|
|
218
|
+
await expect(plugin.destroy()).resolves.not.toThrow();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DatabaseHistoryStorage } from '../storages/database-storage';
|
|
3
|
+
import { FileHistoryStorage } from '../storages/file-storage';
|
|
4
|
+
import { MemoryHistoryStorage } from '../storages/memory-storage';
|
|
5
|
+
import type { IConversationHistoryEntry } from '../types';
|
|
6
|
+
|
|
7
|
+
const entry: IConversationHistoryEntry = {
|
|
8
|
+
conversationId: 'conv-1',
|
|
9
|
+
messages: [
|
|
10
|
+
{
|
|
11
|
+
id: 'msg-1',
|
|
12
|
+
role: 'user',
|
|
13
|
+
content: 'hello',
|
|
14
|
+
state: 'complete' as const,
|
|
15
|
+
timestamp: new Date(),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
startTime: new Date(),
|
|
19
|
+
lastUpdated: new Date(),
|
|
20
|
+
metadata: {},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('MemoryHistoryStorage', () => {
|
|
24
|
+
it('saves and loads entries', async () => {
|
|
25
|
+
const storage = new MemoryHistoryStorage();
|
|
26
|
+
await storage.save('conv-1', entry);
|
|
27
|
+
const loaded = await storage.load('conv-1');
|
|
28
|
+
expect(loaded).toBeDefined();
|
|
29
|
+
expect(loaded!.conversationId).toBe('conv-1');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns undefined for missing conversation', async () => {
|
|
33
|
+
const storage = new MemoryHistoryStorage();
|
|
34
|
+
expect(await storage.load('missing')).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('lists conversation IDs', async () => {
|
|
38
|
+
const storage = new MemoryHistoryStorage();
|
|
39
|
+
await storage.save('conv-1', entry);
|
|
40
|
+
await storage.save('conv-2', { ...entry, conversationId: 'conv-2' });
|
|
41
|
+
const list = await storage.list();
|
|
42
|
+
expect(list).toContain('conv-1');
|
|
43
|
+
expect(list).toContain('conv-2');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('deletes a conversation', async () => {
|
|
47
|
+
const storage = new MemoryHistoryStorage();
|
|
48
|
+
await storage.save('conv-1', entry);
|
|
49
|
+
const deleted = await storage.delete('conv-1');
|
|
50
|
+
expect(deleted).toBe(true);
|
|
51
|
+
expect(await storage.load('conv-1')).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns false when deleting non-existent', async () => {
|
|
55
|
+
const storage = new MemoryHistoryStorage();
|
|
56
|
+
expect(await storage.delete('missing')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('clears all conversations', async () => {
|
|
60
|
+
const storage = new MemoryHistoryStorage();
|
|
61
|
+
await storage.save('conv-1', entry);
|
|
62
|
+
await storage.clear();
|
|
63
|
+
expect(await storage.list()).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('DatabaseHistoryStorage', () => {
|
|
68
|
+
// Placeholder implementation - tests verify it does not throw
|
|
69
|
+
const storage = new DatabaseHistoryStorage('postgres://user:pass@host/db');
|
|
70
|
+
|
|
71
|
+
it('save does not throw', async () => {
|
|
72
|
+
await expect(storage.save('conv-1', entry)).resolves.toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('load returns undefined', async () => {
|
|
76
|
+
expect(await storage.load('conv-1')).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('list returns empty array', async () => {
|
|
80
|
+
expect(await storage.list()).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('delete returns false', async () => {
|
|
84
|
+
expect(await storage.delete('conv-1')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('clear does not throw', async () => {
|
|
88
|
+
await expect(storage.clear()).resolves.toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('FileHistoryStorage', () => {
|
|
93
|
+
// Placeholder implementation - tests verify it does not throw
|
|
94
|
+
const storage = new FileHistoryStorage('/tmp/history.json');
|
|
95
|
+
|
|
96
|
+
it('save does not throw', async () => {
|
|
97
|
+
await expect(storage.save('conv-1', entry)).resolves.toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('load returns undefined', async () => {
|
|
101
|
+
expect(await storage.load('conv-1')).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('list returns empty array', async () => {
|
|
105
|
+
expect(await storage.list()).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('delete returns false', async () => {
|
|
109
|
+
expect(await storage.delete('conv-1')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('clear does not throw', async () => {
|
|
113
|
+
await expect(storage.clear()).resolves.toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation History Plugin - Validation and storage factory helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from conversation-history-plugin.ts to keep each file under 300 lines.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ConfigurationError, PluginError, type ILogger } from '@robota-sdk/agent-core';
|
|
9
|
+
import { MemoryHistoryStorage, FileHistoryStorage, DatabaseHistoryStorage } from './storages/index';
|
|
10
|
+
import type {
|
|
11
|
+
IConversationHistoryPluginOptions,
|
|
12
|
+
IHistoryStorage,
|
|
13
|
+
IConversationHistoryEntry,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
/** Validate ConversationHistoryPlugin constructor options. @internal */
|
|
17
|
+
export function validateConversationHistoryOptions(
|
|
18
|
+
options: IConversationHistoryPluginOptions,
|
|
19
|
+
): void {
|
|
20
|
+
if (!options.storage) {
|
|
21
|
+
throw new ConfigurationError('Storage strategy is required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!['memory', 'file', 'database'].includes(options.storage)) {
|
|
25
|
+
throw new ConfigurationError('Invalid storage strategy', {
|
|
26
|
+
validStrategies: ['memory', 'file', 'database'],
|
|
27
|
+
provided: options.storage,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.storage === 'file' && !options.filePath) {
|
|
32
|
+
throw new ConfigurationError('File path is required for file storage strategy');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.storage === 'database' && !options.connectionString) {
|
|
36
|
+
throw new ConfigurationError('Connection string is required for database storage strategy');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.maxConversations !== undefined && options.maxConversations <= 0) {
|
|
40
|
+
throw new ConfigurationError('Max conversations must be positive');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.maxMessagesPerConversation !== undefined && options.maxMessagesPerConversation <= 0) {
|
|
44
|
+
throw new ConfigurationError('Max messages per conversation must be positive');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load a conversation entry from storage with error wrapping. @internal
|
|
50
|
+
*/
|
|
51
|
+
export async function loadConversationEntry(
|
|
52
|
+
storage: IHistoryStorage,
|
|
53
|
+
conversationId: string,
|
|
54
|
+
pluginName: string,
|
|
55
|
+
logger: ILogger,
|
|
56
|
+
): Promise<IConversationHistoryEntry | undefined> {
|
|
57
|
+
try {
|
|
58
|
+
const entry = await storage.load(conversationId);
|
|
59
|
+
logger.debug('Loaded conversation', {
|
|
60
|
+
conversationId,
|
|
61
|
+
found: !!entry,
|
|
62
|
+
messageCount: entry?.messages.length ?? 0,
|
|
63
|
+
});
|
|
64
|
+
return entry;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new PluginError('Failed to load conversation', pluginName, {
|
|
67
|
+
conversationId,
|
|
68
|
+
error: error instanceof Error ? error.message : String(error),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Flush all pending conversation saves to storage, logging individual failures. @internal
|
|
75
|
+
*/
|
|
76
|
+
export async function savePendingConversations(
|
|
77
|
+
storage: IHistoryStorage,
|
|
78
|
+
pendingSaves: Set<string>,
|
|
79
|
+
pluginName: string,
|
|
80
|
+
logger: ILogger,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (pendingSaves.size === 0) return;
|
|
83
|
+
|
|
84
|
+
const conversationIds = Array.from(pendingSaves);
|
|
85
|
+
logger.debug('Saving pending conversations', { count: conversationIds.length });
|
|
86
|
+
|
|
87
|
+
for (const conversationId of conversationIds) {
|
|
88
|
+
try {
|
|
89
|
+
const entry = await storage.load(conversationId);
|
|
90
|
+
if (entry) {
|
|
91
|
+
await storage.save(conversationId, entry);
|
|
92
|
+
}
|
|
93
|
+
pendingSaves.delete(conversationId);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.error('Failed to save pending conversation', {
|
|
96
|
+
conversationId,
|
|
97
|
+
error: error instanceof Error ? error.message : String(error),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Create IHistoryStorage instance for the given strategy. @internal */
|
|
104
|
+
export function createHistoryStorage(
|
|
105
|
+
strategy: string,
|
|
106
|
+
maxConversations: number,
|
|
107
|
+
filePath: string,
|
|
108
|
+
connectionString: string,
|
|
109
|
+
): IHistoryStorage {
|
|
110
|
+
switch (strategy) {
|
|
111
|
+
case 'memory':
|
|
112
|
+
return new MemoryHistoryStorage(maxConversations);
|
|
113
|
+
case 'file':
|
|
114
|
+
return new FileHistoryStorage(filePath);
|
|
115
|
+
case 'database':
|
|
116
|
+
return new DatabaseHistoryStorage(connectionString);
|
|
117
|
+
default:
|
|
118
|
+
throw new ConfigurationError('Unknown storage strategy', { strategy });
|
|
119
|
+
}
|
|
120
|
+
}
|