@poncho-ai/harness 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-test.log +22 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +416 -0
- package/dist/index.js +3015 -0
- package/package.json +53 -0
- package/src/agent-parser.ts +127 -0
- package/src/anthropic-client.ts +134 -0
- package/src/config.ts +141 -0
- package/src/default-tools.ts +89 -0
- package/src/harness.ts +522 -0
- package/src/index.ts +17 -0
- package/src/latitude-capture.ts +108 -0
- package/src/local-tools.ts +108 -0
- package/src/mcp.ts +287 -0
- package/src/memory.ts +700 -0
- package/src/model-client.ts +44 -0
- package/src/model-factory.ts +14 -0
- package/src/openai-client.ts +169 -0
- package/src/skill-context.ts +259 -0
- package/src/skill-tools.ts +357 -0
- package/src/state.ts +1017 -0
- package/src/telemetry.ts +108 -0
- package/src/tool-dispatcher.ts +69 -0
- package/test/agent-parser.test.ts +39 -0
- package/test/harness.test.ts +716 -0
- package/test/mcp.test.ts +82 -0
- package/test/memory.test.ts +50 -0
- package/test/model-factory.test.ts +16 -0
- package/test/state.test.ts +43 -0
- package/test/telemetry.test.ts +57 -0
- package/tsconfig.json +8 -0
package/src/state.ts
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import type { Message } from "@poncho-ai/sdk";
|
|
6
|
+
|
|
7
|
+
export interface ConversationState {
|
|
8
|
+
runId: string;
|
|
9
|
+
messages: Message[];
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StateStore {
|
|
14
|
+
get(runId: string): Promise<ConversationState | undefined>;
|
|
15
|
+
set(state: ConversationState): Promise<void>;
|
|
16
|
+
delete(runId: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Conversation {
|
|
20
|
+
conversationId: string;
|
|
21
|
+
title: string;
|
|
22
|
+
messages: Message[];
|
|
23
|
+
runtimeRunId?: string;
|
|
24
|
+
ownerId: string;
|
|
25
|
+
tenantId: string | null;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ConversationStore {
|
|
31
|
+
list(ownerId?: string): Promise<Conversation[]>;
|
|
32
|
+
get(conversationId: string): Promise<Conversation | undefined>;
|
|
33
|
+
create(ownerId?: string, title?: string): Promise<Conversation>;
|
|
34
|
+
update(conversation: Conversation): Promise<void>;
|
|
35
|
+
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
36
|
+
delete(conversationId: string): Promise<boolean>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type StateProviderName =
|
|
40
|
+
| "local"
|
|
41
|
+
| "memory"
|
|
42
|
+
| "redis"
|
|
43
|
+
| "upstash"
|
|
44
|
+
| "dynamodb";
|
|
45
|
+
|
|
46
|
+
export interface StateConfig {
|
|
47
|
+
provider?: StateProviderName;
|
|
48
|
+
ttl?: number;
|
|
49
|
+
url?: string;
|
|
50
|
+
token?: string;
|
|
51
|
+
table?: string;
|
|
52
|
+
region?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_OWNER = "local-owner";
|
|
56
|
+
const CONVERSATIONS_STATE_KEY = "__poncho_conversations__";
|
|
57
|
+
const LOCAL_CONVERSATIONS_FILE = "local-conversations.json";
|
|
58
|
+
const LOCAL_STATE_FILE = "local-state.json";
|
|
59
|
+
|
|
60
|
+
const getStateDirectory = (): string => {
|
|
61
|
+
const cwd = process.cwd();
|
|
62
|
+
const home = homedir();
|
|
63
|
+
const isServerless =
|
|
64
|
+
process.env.VERCEL === "1" ||
|
|
65
|
+
process.env.VERCEL_ENV !== undefined ||
|
|
66
|
+
process.env.VERCEL_URL !== undefined ||
|
|
67
|
+
process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined ||
|
|
68
|
+
process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true ||
|
|
69
|
+
process.env.LAMBDA_TASK_ROOT !== undefined ||
|
|
70
|
+
process.env.NOW_REGION !== undefined ||
|
|
71
|
+
cwd.startsWith("/var/task") ||
|
|
72
|
+
home.startsWith("/var/task") ||
|
|
73
|
+
process.env.SERVERLESS === "1";
|
|
74
|
+
if (isServerless) {
|
|
75
|
+
return "/tmp/.poncho/state";
|
|
76
|
+
}
|
|
77
|
+
return resolve(homedir(), ".poncho", "state");
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const projectScopedFilePath = (workingDir: string, suffix: string): string => {
|
|
81
|
+
const projectName = basename(workingDir).replace(/[^a-zA-Z0-9_-]+/g, "-") || "project";
|
|
82
|
+
const projectHash = createHash("sha256").update(workingDir).digest("hex").slice(0, 12);
|
|
83
|
+
return resolve(getStateDirectory(), `${projectName}-${projectHash}-${suffix}`);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const normalizeTitle = (title?: string): string => {
|
|
87
|
+
return title && title.trim().length > 0 ? title.trim() : "New conversation";
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export class InMemoryStateStore implements StateStore {
|
|
91
|
+
private readonly store = new Map<string, ConversationState>();
|
|
92
|
+
private readonly ttlMs?: number;
|
|
93
|
+
|
|
94
|
+
constructor(ttlSeconds?: number) {
|
|
95
|
+
this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1000 : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private isExpired(state: ConversationState): boolean {
|
|
99
|
+
return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async get(runId: string): Promise<ConversationState | undefined> {
|
|
103
|
+
const state = this.store.get(runId);
|
|
104
|
+
if (!state) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
if (this.isExpired(state)) {
|
|
108
|
+
this.store.delete(runId);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async set(state: ConversationState): Promise<void> {
|
|
115
|
+
this.store.set(state.runId, { ...state, updatedAt: Date.now() });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async delete(runId: string): Promise<void> {
|
|
119
|
+
this.store.delete(runId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class UpstashStateStore implements StateStore {
|
|
124
|
+
private readonly baseUrl: string;
|
|
125
|
+
private readonly token: string;
|
|
126
|
+
private readonly ttl?: number;
|
|
127
|
+
|
|
128
|
+
constructor(baseUrl: string, token: string, ttl?: number) {
|
|
129
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
130
|
+
this.token = token;
|
|
131
|
+
this.ttl = ttl;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private headers(): HeadersInit {
|
|
135
|
+
return {
|
|
136
|
+
Authorization: `Bearer ${this.token}`,
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async get(runId: string): Promise<ConversationState | undefined> {
|
|
142
|
+
const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(runId)}`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: this.headers(),
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const payload = (await response.json()) as { result?: string | null };
|
|
150
|
+
if (!payload.result) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return JSON.parse(payload.result) as ConversationState;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async set(state: ConversationState): Promise<void> {
|
|
157
|
+
const serialized = JSON.stringify({ ...state, updatedAt: Date.now() });
|
|
158
|
+
const path =
|
|
159
|
+
typeof this.ttl === "number"
|
|
160
|
+
? `${this.baseUrl}/setex/${encodeURIComponent(state.runId)}/${Math.max(
|
|
161
|
+
1,
|
|
162
|
+
this.ttl,
|
|
163
|
+
)}/${encodeURIComponent(serialized)}`
|
|
164
|
+
: `${this.baseUrl}/set/${encodeURIComponent(state.runId)}/${encodeURIComponent(
|
|
165
|
+
serialized,
|
|
166
|
+
)}`;
|
|
167
|
+
await fetch(path, { method: "POST", headers: this.headers() });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async delete(runId: string): Promise<void> {
|
|
171
|
+
await fetch(`${this.baseUrl}/del/${encodeURIComponent(runId)}`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: this.headers(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export class InMemoryConversationStore implements ConversationStore {
|
|
179
|
+
private readonly conversations = new Map<string, Conversation>();
|
|
180
|
+
private readonly ttlMs?: number;
|
|
181
|
+
|
|
182
|
+
constructor(ttlSeconds?: number) {
|
|
183
|
+
this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1000 : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private isExpired(updatedAt: number): boolean {
|
|
187
|
+
return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private purgeExpired(): void {
|
|
191
|
+
for (const [conversationId, conversation] of this.conversations.entries()) {
|
|
192
|
+
if (this.isExpired(conversation.updatedAt)) {
|
|
193
|
+
this.conversations.delete(conversationId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async list(ownerId = DEFAULT_OWNER): Promise<Conversation[]> {
|
|
199
|
+
this.purgeExpired();
|
|
200
|
+
return Array.from(this.conversations.values())
|
|
201
|
+
.filter((conversation) => conversation.ownerId === ownerId)
|
|
202
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async get(conversationId: string): Promise<Conversation | undefined> {
|
|
206
|
+
this.purgeExpired();
|
|
207
|
+
return this.conversations.get(conversationId);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
const conversation: Conversation = {
|
|
213
|
+
conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
|
|
214
|
+
title: normalizeTitle(title),
|
|
215
|
+
messages: [],
|
|
216
|
+
ownerId,
|
|
217
|
+
tenantId: null,
|
|
218
|
+
createdAt: now,
|
|
219
|
+
updatedAt: now,
|
|
220
|
+
};
|
|
221
|
+
this.conversations.set(conversation.conversationId, conversation);
|
|
222
|
+
return conversation;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async update(conversation: Conversation): Promise<void> {
|
|
226
|
+
this.conversations.set(conversation.conversationId, {
|
|
227
|
+
...conversation,
|
|
228
|
+
updatedAt: Date.now(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async rename(conversationId: string, title: string): Promise<Conversation | undefined> {
|
|
233
|
+
const existing = await this.get(conversationId);
|
|
234
|
+
if (!existing) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const updated: Conversation = {
|
|
238
|
+
...existing,
|
|
239
|
+
title: normalizeTitle(title || existing.title),
|
|
240
|
+
updatedAt: Date.now(),
|
|
241
|
+
};
|
|
242
|
+
this.conversations.set(conversationId, updated);
|
|
243
|
+
return updated;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async delete(conversationId: string): Promise<boolean> {
|
|
247
|
+
return this.conversations.delete(conversationId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type ConversationStoreFile = {
|
|
252
|
+
conversations: Conversation[];
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
class FileConversationStore implements ConversationStore {
|
|
256
|
+
private readonly filePath: string;
|
|
257
|
+
private readonly conversations = new Map<string, Conversation>();
|
|
258
|
+
private loaded = false;
|
|
259
|
+
private writing = Promise.resolve();
|
|
260
|
+
|
|
261
|
+
constructor(workingDir: string) {
|
|
262
|
+
this.filePath = projectScopedFilePath(workingDir, LOCAL_CONVERSATIONS_FILE);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async ensureLoaded(): Promise<void> {
|
|
266
|
+
if (this.loaded) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.loaded = true;
|
|
270
|
+
try {
|
|
271
|
+
const raw = await readFile(this.filePath, "utf8");
|
|
272
|
+
const parsed = JSON.parse(raw) as ConversationStoreFile;
|
|
273
|
+
for (const conversation of parsed.conversations ?? []) {
|
|
274
|
+
this.conversations.set(conversation.conversationId, conversation);
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// Missing or invalid file should not crash local mode.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async persist(): Promise<void> {
|
|
282
|
+
const payload: ConversationStoreFile = {
|
|
283
|
+
conversations: Array.from(this.conversations.values()),
|
|
284
|
+
};
|
|
285
|
+
this.writing = this.writing.then(async () => {
|
|
286
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
287
|
+
await writeFile(this.filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
288
|
+
});
|
|
289
|
+
await this.writing;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async list(ownerId = DEFAULT_OWNER): Promise<Conversation[]> {
|
|
293
|
+
await this.ensureLoaded();
|
|
294
|
+
return Array.from(this.conversations.values())
|
|
295
|
+
.filter((conversation) => conversation.ownerId === ownerId)
|
|
296
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async get(conversationId: string): Promise<Conversation | undefined> {
|
|
300
|
+
await this.ensureLoaded();
|
|
301
|
+
return this.conversations.get(conversationId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
|
|
305
|
+
await this.ensureLoaded();
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
const conversation: Conversation = {
|
|
308
|
+
conversationId: randomUUID(),
|
|
309
|
+
title: normalizeTitle(title),
|
|
310
|
+
messages: [],
|
|
311
|
+
ownerId,
|
|
312
|
+
tenantId: null,
|
|
313
|
+
createdAt: now,
|
|
314
|
+
updatedAt: now,
|
|
315
|
+
};
|
|
316
|
+
this.conversations.set(conversation.conversationId, conversation);
|
|
317
|
+
await this.persist();
|
|
318
|
+
return conversation;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async update(conversation: Conversation): Promise<void> {
|
|
322
|
+
await this.ensureLoaded();
|
|
323
|
+
this.conversations.set(conversation.conversationId, {
|
|
324
|
+
...conversation,
|
|
325
|
+
updatedAt: Date.now(),
|
|
326
|
+
});
|
|
327
|
+
await this.persist();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async rename(conversationId: string, title: string): Promise<Conversation | undefined> {
|
|
331
|
+
await this.ensureLoaded();
|
|
332
|
+
const existing = this.conversations.get(conversationId);
|
|
333
|
+
if (!existing) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const updated: Conversation = {
|
|
337
|
+
...existing,
|
|
338
|
+
title: normalizeTitle(title || existing.title),
|
|
339
|
+
updatedAt: Date.now(),
|
|
340
|
+
};
|
|
341
|
+
this.conversations.set(conversationId, updated);
|
|
342
|
+
await this.persist();
|
|
343
|
+
return updated;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async delete(conversationId: string): Promise<boolean> {
|
|
347
|
+
await this.ensureLoaded();
|
|
348
|
+
const removed = this.conversations.delete(conversationId);
|
|
349
|
+
if (removed) {
|
|
350
|
+
await this.persist();
|
|
351
|
+
}
|
|
352
|
+
return removed;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
type LocalStateFile = {
|
|
357
|
+
states: ConversationState[];
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
class FileStateStore implements StateStore {
|
|
361
|
+
private readonly filePath: string;
|
|
362
|
+
private readonly states = new Map<string, ConversationState>();
|
|
363
|
+
private readonly ttlMs?: number;
|
|
364
|
+
private loaded = false;
|
|
365
|
+
private writing = Promise.resolve();
|
|
366
|
+
|
|
367
|
+
constructor(workingDir: string, ttlSeconds?: number) {
|
|
368
|
+
this.filePath = projectScopedFilePath(workingDir, LOCAL_STATE_FILE);
|
|
369
|
+
this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1000 : undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private isExpired(state: ConversationState): boolean {
|
|
373
|
+
return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async ensureLoaded(): Promise<void> {
|
|
377
|
+
if (this.loaded) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.loaded = true;
|
|
381
|
+
try {
|
|
382
|
+
const raw = await readFile(this.filePath, "utf8");
|
|
383
|
+
const parsed = JSON.parse(raw) as LocalStateFile;
|
|
384
|
+
for (const state of parsed.states ?? []) {
|
|
385
|
+
this.states.set(state.runId, state);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// Missing/invalid file should not crash local mode.
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async persist(): Promise<void> {
|
|
393
|
+
const payload: LocalStateFile = {
|
|
394
|
+
states: Array.from(this.states.values()),
|
|
395
|
+
};
|
|
396
|
+
this.writing = this.writing.then(async () => {
|
|
397
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
398
|
+
await writeFile(this.filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
399
|
+
});
|
|
400
|
+
await this.writing;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async get(runId: string): Promise<ConversationState | undefined> {
|
|
404
|
+
await this.ensureLoaded();
|
|
405
|
+
const state = this.states.get(runId);
|
|
406
|
+
if (!state) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
if (this.isExpired(state)) {
|
|
410
|
+
this.states.delete(runId);
|
|
411
|
+
await this.persist();
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
return state;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async set(state: ConversationState): Promise<void> {
|
|
418
|
+
await this.ensureLoaded();
|
|
419
|
+
this.states.set(state.runId, { ...state, updatedAt: Date.now() });
|
|
420
|
+
await this.persist();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async delete(runId: string): Promise<void> {
|
|
424
|
+
await this.ensureLoaded();
|
|
425
|
+
this.states.delete(runId);
|
|
426
|
+
await this.persist();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
431
|
+
protected readonly memoryFallback: InMemoryConversationStore;
|
|
432
|
+
protected readonly ttl?: number;
|
|
433
|
+
|
|
434
|
+
constructor(ttl?: number) {
|
|
435
|
+
this.ttl = ttl;
|
|
436
|
+
this.memoryFallback = new InMemoryConversationStore(ttl);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
protected abstract getRaw(key: string): Promise<string | undefined>;
|
|
440
|
+
protected abstract setRaw(key: string, value: string): Promise<void>;
|
|
441
|
+
protected abstract setRawWithTtl(key: string, value: string, ttl: number): Promise<void>;
|
|
442
|
+
protected abstract delRaw(key: string): Promise<void>;
|
|
443
|
+
|
|
444
|
+
private async readAllConversations(): Promise<Conversation[]> {
|
|
445
|
+
try {
|
|
446
|
+
const raw = await this.getRaw(CONVERSATIONS_STATE_KEY);
|
|
447
|
+
if (!raw) {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
const parsed = JSON.parse(raw) as { conversations?: Conversation[] };
|
|
451
|
+
return Array.isArray(parsed.conversations) ? parsed.conversations : [];
|
|
452
|
+
} catch {
|
|
453
|
+
return await this.memoryFallback.list(DEFAULT_OWNER);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async writeAllConversations(conversations: Conversation[]): Promise<void> {
|
|
458
|
+
const payload = JSON.stringify({ conversations });
|
|
459
|
+
try {
|
|
460
|
+
if (typeof this.ttl === "number") {
|
|
461
|
+
await this.setRawWithTtl(CONVERSATIONS_STATE_KEY, payload, Math.max(1, this.ttl));
|
|
462
|
+
} else {
|
|
463
|
+
await this.setRaw(CONVERSATIONS_STATE_KEY, payload);
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Fallback keeps local dev usable when provider is temporarily unavailable.
|
|
467
|
+
for (const conversation of conversations) {
|
|
468
|
+
await this.memoryFallback.update(conversation);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async list(ownerId = DEFAULT_OWNER): Promise<Conversation[]> {
|
|
474
|
+
const conversations = await this.readAllConversations();
|
|
475
|
+
return conversations
|
|
476
|
+
.filter((conversation) => conversation.ownerId === ownerId)
|
|
477
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async get(conversationId: string): Promise<Conversation | undefined> {
|
|
481
|
+
const conversations = await this.readAllConversations();
|
|
482
|
+
return conversations.find((conversation) => conversation.conversationId === conversationId);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
|
|
486
|
+
const now = Date.now();
|
|
487
|
+
const conversation: Conversation = {
|
|
488
|
+
conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
|
|
489
|
+
title: normalizeTitle(title),
|
|
490
|
+
messages: [],
|
|
491
|
+
ownerId,
|
|
492
|
+
tenantId: null,
|
|
493
|
+
createdAt: now,
|
|
494
|
+
updatedAt: now,
|
|
495
|
+
};
|
|
496
|
+
const conversations = await this.readAllConversations();
|
|
497
|
+
conversations.push(conversation);
|
|
498
|
+
await this.writeAllConversations(conversations);
|
|
499
|
+
return conversation;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async update(conversation: Conversation): Promise<void> {
|
|
503
|
+
const conversations = await this.readAllConversations();
|
|
504
|
+
const next = conversations.map((item) =>
|
|
505
|
+
item.conversationId === conversation.conversationId
|
|
506
|
+
? { ...conversation, updatedAt: Date.now() }
|
|
507
|
+
: item,
|
|
508
|
+
);
|
|
509
|
+
await this.writeAllConversations(next);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async rename(conversationId: string, title: string): Promise<Conversation | undefined> {
|
|
513
|
+
const conversations = await this.readAllConversations();
|
|
514
|
+
let updated: Conversation | undefined;
|
|
515
|
+
const next = conversations.map((item) => {
|
|
516
|
+
if (item.conversationId !== conversationId) {
|
|
517
|
+
return item;
|
|
518
|
+
}
|
|
519
|
+
updated = {
|
|
520
|
+
...item,
|
|
521
|
+
title: normalizeTitle(title || item.title),
|
|
522
|
+
updatedAt: Date.now(),
|
|
523
|
+
};
|
|
524
|
+
return updated;
|
|
525
|
+
});
|
|
526
|
+
if (!updated) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
await this.writeAllConversations(next);
|
|
530
|
+
return updated;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async delete(conversationId: string): Promise<boolean> {
|
|
534
|
+
const conversations = await this.readAllConversations();
|
|
535
|
+
const next = conversations.filter((item) => item.conversationId !== conversationId);
|
|
536
|
+
if (next.length === conversations.length) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
if (next.length === 0) {
|
|
540
|
+
try {
|
|
541
|
+
await this.delRaw(CONVERSATIONS_STATE_KEY);
|
|
542
|
+
} catch {
|
|
543
|
+
// Fall through to write empty payload for resilience.
|
|
544
|
+
await this.writeAllConversations(next);
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
await this.writeAllConversations(next);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
class UpstashConversationStore extends KeyValueConversationStoreBase {
|
|
554
|
+
private readonly baseUrl: string;
|
|
555
|
+
private readonly token: string;
|
|
556
|
+
|
|
557
|
+
constructor(baseUrl: string, token: string, ttl?: number) {
|
|
558
|
+
super(ttl);
|
|
559
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
560
|
+
this.token = token;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private headers(): HeadersInit {
|
|
564
|
+
return {
|
|
565
|
+
Authorization: `Bearer ${this.token}`,
|
|
566
|
+
"Content-Type": "application/json",
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
protected async getRaw(key: string): Promise<string | undefined> {
|
|
571
|
+
const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
|
|
572
|
+
method: "POST",
|
|
573
|
+
headers: this.headers(),
|
|
574
|
+
});
|
|
575
|
+
if (!response.ok) {
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
const payload = (await response.json()) as { result?: string | null };
|
|
579
|
+
return payload.result ?? undefined;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
protected async setRaw(key: string, value: string): Promise<void> {
|
|
583
|
+
await fetch(
|
|
584
|
+
`${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
|
|
585
|
+
{ method: "POST", headers: this.headers() },
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
|
|
590
|
+
await fetch(
|
|
591
|
+
`${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(
|
|
592
|
+
value,
|
|
593
|
+
)}`,
|
|
594
|
+
{ method: "POST", headers: this.headers() },
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
protected async delRaw(key: string): Promise<void> {
|
|
599
|
+
await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
|
|
600
|
+
method: "POST",
|
|
601
|
+
headers: this.headers(),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
class RedisLikeStateStore implements StateStore {
|
|
607
|
+
private readonly memoryFallback: InMemoryStateStore;
|
|
608
|
+
private readonly ttl?: number;
|
|
609
|
+
private readonly clientPromise: Promise<
|
|
610
|
+
| {
|
|
611
|
+
get: (key: string) => Promise<string | null>;
|
|
612
|
+
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
613
|
+
del: (key: string) => Promise<unknown>;
|
|
614
|
+
}
|
|
615
|
+
| undefined
|
|
616
|
+
>;
|
|
617
|
+
|
|
618
|
+
constructor(url: string, ttl?: number) {
|
|
619
|
+
this.ttl = ttl;
|
|
620
|
+
this.memoryFallback = new InMemoryStateStore(ttl);
|
|
621
|
+
this.clientPromise = (async () => {
|
|
622
|
+
try {
|
|
623
|
+
const redisModule = (await import("redis")) as unknown as {
|
|
624
|
+
createClient: (options: { url: string }) => {
|
|
625
|
+
connect: () => Promise<unknown>;
|
|
626
|
+
get: (key: string) => Promise<string | null>;
|
|
627
|
+
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
628
|
+
del: (key: string) => Promise<unknown>;
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
const client = redisModule.createClient({ url });
|
|
632
|
+
await client.connect();
|
|
633
|
+
return client;
|
|
634
|
+
} catch {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
})();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async get(runId: string): Promise<ConversationState | undefined> {
|
|
641
|
+
const client = await this.clientPromise;
|
|
642
|
+
if (!client) {
|
|
643
|
+
return await this.memoryFallback.get(runId);
|
|
644
|
+
}
|
|
645
|
+
const raw = await client.get(runId);
|
|
646
|
+
return raw ? (JSON.parse(raw) as ConversationState) : undefined;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async set(state: ConversationState): Promise<void> {
|
|
650
|
+
const client = await this.clientPromise;
|
|
651
|
+
if (!client) {
|
|
652
|
+
await this.memoryFallback.set(state);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const serialized = JSON.stringify({ ...state, updatedAt: Date.now() });
|
|
656
|
+
if (typeof this.ttl === "number") {
|
|
657
|
+
await client.set(state.runId, serialized, { EX: Math.max(1, this.ttl) });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
await client.set(state.runId, serialized);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async delete(runId: string): Promise<void> {
|
|
664
|
+
const client = await this.clientPromise;
|
|
665
|
+
if (!client) {
|
|
666
|
+
await this.memoryFallback.delete(runId);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await client.del(runId);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
class RedisLikeConversationStore extends KeyValueConversationStoreBase {
|
|
674
|
+
private readonly clientPromise: Promise<
|
|
675
|
+
| {
|
|
676
|
+
get: (key: string) => Promise<string | null>;
|
|
677
|
+
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
678
|
+
del: (key: string) => Promise<unknown>;
|
|
679
|
+
}
|
|
680
|
+
| undefined
|
|
681
|
+
>;
|
|
682
|
+
|
|
683
|
+
constructor(url: string, ttl?: number) {
|
|
684
|
+
super(ttl);
|
|
685
|
+
this.clientPromise = (async () => {
|
|
686
|
+
try {
|
|
687
|
+
const redisModule = (await import("redis")) as unknown as {
|
|
688
|
+
createClient: (options: { url: string }) => {
|
|
689
|
+
connect: () => Promise<unknown>;
|
|
690
|
+
get: (key: string) => Promise<string | null>;
|
|
691
|
+
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
692
|
+
del: (key: string) => Promise<unknown>;
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
const client = redisModule.createClient({ url });
|
|
696
|
+
await client.connect();
|
|
697
|
+
return client;
|
|
698
|
+
} catch {
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
})();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
protected async getRaw(key: string): Promise<string | undefined> {
|
|
705
|
+
const client = await this.clientPromise;
|
|
706
|
+
if (!client) {
|
|
707
|
+
throw new Error("Redis unavailable");
|
|
708
|
+
}
|
|
709
|
+
const value = await client.get(key);
|
|
710
|
+
return value ?? undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
protected async setRaw(key: string, value: string): Promise<void> {
|
|
714
|
+
const client = await this.clientPromise;
|
|
715
|
+
if (!client) {
|
|
716
|
+
throw new Error("Redis unavailable");
|
|
717
|
+
}
|
|
718
|
+
await client.set(key, value);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
|
|
722
|
+
const client = await this.clientPromise;
|
|
723
|
+
if (!client) {
|
|
724
|
+
throw new Error("Redis unavailable");
|
|
725
|
+
}
|
|
726
|
+
await client.set(key, value, { EX: Math.max(1, ttl) });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
protected async delRaw(key: string): Promise<void> {
|
|
730
|
+
const client = await this.clientPromise;
|
|
731
|
+
if (!client) {
|
|
732
|
+
throw new Error("Redis unavailable");
|
|
733
|
+
}
|
|
734
|
+
await client.del(key);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
class DynamoDbStateStore implements StateStore {
|
|
739
|
+
private readonly memoryFallback: InMemoryStateStore;
|
|
740
|
+
private readonly table: string;
|
|
741
|
+
private readonly ttl?: number;
|
|
742
|
+
private readonly clientPromise: Promise<
|
|
743
|
+
| {
|
|
744
|
+
send: (command: unknown) => Promise<unknown>;
|
|
745
|
+
GetItemCommand: new (input: unknown) => unknown;
|
|
746
|
+
PutItemCommand: new (input: unknown) => unknown;
|
|
747
|
+
DeleteItemCommand: new (input: unknown) => unknown;
|
|
748
|
+
}
|
|
749
|
+
| undefined
|
|
750
|
+
>;
|
|
751
|
+
|
|
752
|
+
constructor(table: string, region?: string, ttl?: number) {
|
|
753
|
+
this.table = table;
|
|
754
|
+
this.ttl = ttl;
|
|
755
|
+
this.memoryFallback = new InMemoryStateStore(ttl);
|
|
756
|
+
this.clientPromise = (async () => {
|
|
757
|
+
try {
|
|
758
|
+
const module = (await import("@aws-sdk/client-dynamodb")) as {
|
|
759
|
+
DynamoDBClient: new (input: { region?: string }) => { send: (command: unknown) => Promise<unknown> };
|
|
760
|
+
GetItemCommand: new (input: unknown) => unknown;
|
|
761
|
+
PutItemCommand: new (input: unknown) => unknown;
|
|
762
|
+
DeleteItemCommand: new (input: unknown) => unknown;
|
|
763
|
+
};
|
|
764
|
+
return {
|
|
765
|
+
send: module.DynamoDBClient
|
|
766
|
+
? new module.DynamoDBClient({ region }).send.bind(
|
|
767
|
+
new module.DynamoDBClient({ region }),
|
|
768
|
+
)
|
|
769
|
+
: async () => ({}),
|
|
770
|
+
GetItemCommand: module.GetItemCommand,
|
|
771
|
+
PutItemCommand: module.PutItemCommand,
|
|
772
|
+
DeleteItemCommand: module.DeleteItemCommand,
|
|
773
|
+
};
|
|
774
|
+
} catch {
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
})();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async get(runId: string): Promise<ConversationState | undefined> {
|
|
781
|
+
const client = await this.clientPromise;
|
|
782
|
+
if (!client) {
|
|
783
|
+
return await this.memoryFallback.get(runId);
|
|
784
|
+
}
|
|
785
|
+
const result = (await client.send(
|
|
786
|
+
new client.GetItemCommand({
|
|
787
|
+
TableName: this.table,
|
|
788
|
+
Key: { runId: { S: runId } },
|
|
789
|
+
}),
|
|
790
|
+
)) as {
|
|
791
|
+
Item?: {
|
|
792
|
+
value?: { S?: string };
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
const raw = result.Item?.value?.S;
|
|
796
|
+
return raw ? (JSON.parse(raw) as ConversationState) : undefined;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async set(state: ConversationState): Promise<void> {
|
|
800
|
+
const client = await this.clientPromise;
|
|
801
|
+
if (!client) {
|
|
802
|
+
await this.memoryFallback.set(state);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const updatedState = { ...state, updatedAt: Date.now() };
|
|
806
|
+
const ttlEpoch =
|
|
807
|
+
typeof this.ttl === "number"
|
|
808
|
+
? Math.floor(Date.now() / 1000) + Math.max(1, this.ttl)
|
|
809
|
+
: undefined;
|
|
810
|
+
await client.send(
|
|
811
|
+
new client.PutItemCommand({
|
|
812
|
+
TableName: this.table,
|
|
813
|
+
Item: {
|
|
814
|
+
runId: { S: state.runId },
|
|
815
|
+
value: { S: JSON.stringify(updatedState) },
|
|
816
|
+
...(typeof ttlEpoch === "number" ? { ttl: { N: String(ttlEpoch) } } : {}),
|
|
817
|
+
},
|
|
818
|
+
}),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async delete(runId: string): Promise<void> {
|
|
823
|
+
const client = await this.clientPromise;
|
|
824
|
+
if (!client) {
|
|
825
|
+
await this.memoryFallback.delete(runId);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
await client.send(
|
|
829
|
+
new client.DeleteItemCommand({
|
|
830
|
+
TableName: this.table,
|
|
831
|
+
Key: { runId: { S: runId } },
|
|
832
|
+
}),
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
class DynamoDbConversationStore extends KeyValueConversationStoreBase {
|
|
838
|
+
private readonly table: string;
|
|
839
|
+
private readonly clientPromise: Promise<
|
|
840
|
+
| {
|
|
841
|
+
send: (command: unknown) => Promise<unknown>;
|
|
842
|
+
GetItemCommand: new (input: unknown) => unknown;
|
|
843
|
+
PutItemCommand: new (input: unknown) => unknown;
|
|
844
|
+
DeleteItemCommand: new (input: unknown) => unknown;
|
|
845
|
+
}
|
|
846
|
+
| undefined
|
|
847
|
+
>;
|
|
848
|
+
|
|
849
|
+
constructor(table: string, region?: string, ttl?: number) {
|
|
850
|
+
super(ttl);
|
|
851
|
+
this.table = table;
|
|
852
|
+
this.clientPromise = (async () => {
|
|
853
|
+
try {
|
|
854
|
+
const module = (await import("@aws-sdk/client-dynamodb")) as {
|
|
855
|
+
DynamoDBClient: new (input: { region?: string }) => { send: (command: unknown) => Promise<unknown> };
|
|
856
|
+
GetItemCommand: new (input: unknown) => unknown;
|
|
857
|
+
PutItemCommand: new (input: unknown) => unknown;
|
|
858
|
+
DeleteItemCommand: new (input: unknown) => unknown;
|
|
859
|
+
};
|
|
860
|
+
const client = new module.DynamoDBClient({ region });
|
|
861
|
+
return {
|
|
862
|
+
send: client.send.bind(client),
|
|
863
|
+
GetItemCommand: module.GetItemCommand,
|
|
864
|
+
PutItemCommand: module.PutItemCommand,
|
|
865
|
+
DeleteItemCommand: module.DeleteItemCommand,
|
|
866
|
+
};
|
|
867
|
+
} catch {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
})();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
protected async getRaw(key: string): Promise<string | undefined> {
|
|
874
|
+
const client = await this.clientPromise;
|
|
875
|
+
if (!client) {
|
|
876
|
+
throw new Error("DynamoDB unavailable");
|
|
877
|
+
}
|
|
878
|
+
const result = (await client.send(
|
|
879
|
+
new client.GetItemCommand({
|
|
880
|
+
TableName: this.table,
|
|
881
|
+
Key: { runId: { S: key } },
|
|
882
|
+
}),
|
|
883
|
+
)) as {
|
|
884
|
+
Item?: {
|
|
885
|
+
value?: { S?: string };
|
|
886
|
+
};
|
|
887
|
+
};
|
|
888
|
+
return result.Item?.value?.S;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
protected async setRaw(key: string, value: string): Promise<void> {
|
|
892
|
+
const client = await this.clientPromise;
|
|
893
|
+
if (!client) {
|
|
894
|
+
throw new Error("DynamoDB unavailable");
|
|
895
|
+
}
|
|
896
|
+
await client.send(
|
|
897
|
+
new client.PutItemCommand({
|
|
898
|
+
TableName: this.table,
|
|
899
|
+
Item: {
|
|
900
|
+
runId: { S: key },
|
|
901
|
+
value: { S: value },
|
|
902
|
+
},
|
|
903
|
+
}),
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
|
|
908
|
+
const client = await this.clientPromise;
|
|
909
|
+
if (!client) {
|
|
910
|
+
throw new Error("DynamoDB unavailable");
|
|
911
|
+
}
|
|
912
|
+
const ttlEpoch = Math.floor(Date.now() / 1000) + Math.max(1, ttl);
|
|
913
|
+
await client.send(
|
|
914
|
+
new client.PutItemCommand({
|
|
915
|
+
TableName: this.table,
|
|
916
|
+
Item: {
|
|
917
|
+
runId: { S: key },
|
|
918
|
+
value: { S: value },
|
|
919
|
+
ttl: { N: String(ttlEpoch) },
|
|
920
|
+
},
|
|
921
|
+
}),
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
protected async delRaw(key: string): Promise<void> {
|
|
926
|
+
const client = await this.clientPromise;
|
|
927
|
+
if (!client) {
|
|
928
|
+
throw new Error("DynamoDB unavailable");
|
|
929
|
+
}
|
|
930
|
+
await client.send(
|
|
931
|
+
new client.DeleteItemCommand({
|
|
932
|
+
TableName: this.table,
|
|
933
|
+
Key: { runId: { S: key } },
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export const createStateStore = (
|
|
940
|
+
config?: StateConfig,
|
|
941
|
+
options?: { workingDir?: string },
|
|
942
|
+
): StateStore => {
|
|
943
|
+
const provider = config?.provider ?? "local";
|
|
944
|
+
const ttl = config?.ttl;
|
|
945
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
946
|
+
if (provider === "local") {
|
|
947
|
+
return new FileStateStore(workingDir, ttl);
|
|
948
|
+
}
|
|
949
|
+
if (provider === "memory") {
|
|
950
|
+
return new InMemoryStateStore(ttl);
|
|
951
|
+
}
|
|
952
|
+
if (provider === "upstash") {
|
|
953
|
+
const url = config?.url ?? process.env.UPSTASH_REDIS_REST_URL ?? "";
|
|
954
|
+
const token = config?.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? "";
|
|
955
|
+
if (url && token) {
|
|
956
|
+
return new UpstashStateStore(url, token, ttl);
|
|
957
|
+
}
|
|
958
|
+
return new InMemoryStateStore(ttl);
|
|
959
|
+
}
|
|
960
|
+
if (provider === "redis") {
|
|
961
|
+
const url = config?.url ?? process.env.REDIS_URL ?? "";
|
|
962
|
+
if (url) {
|
|
963
|
+
return new RedisLikeStateStore(url, ttl);
|
|
964
|
+
}
|
|
965
|
+
return new InMemoryStateStore(ttl);
|
|
966
|
+
}
|
|
967
|
+
if (provider === "dynamodb") {
|
|
968
|
+
const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
|
|
969
|
+
if (table) {
|
|
970
|
+
return new DynamoDbStateStore(table, config?.region as string | undefined, ttl);
|
|
971
|
+
}
|
|
972
|
+
return new InMemoryStateStore(ttl);
|
|
973
|
+
}
|
|
974
|
+
return new InMemoryStateStore(ttl);
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
export const createConversationStore = (
|
|
978
|
+
config?: StateConfig,
|
|
979
|
+
options?: { workingDir?: string },
|
|
980
|
+
): ConversationStore => {
|
|
981
|
+
const provider = config?.provider ?? "local";
|
|
982
|
+
const ttl = config?.ttl;
|
|
983
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
984
|
+
if (provider === "local") {
|
|
985
|
+
return new FileConversationStore(workingDir);
|
|
986
|
+
}
|
|
987
|
+
if (provider === "memory") {
|
|
988
|
+
return new InMemoryConversationStore(ttl);
|
|
989
|
+
}
|
|
990
|
+
if (provider === "upstash") {
|
|
991
|
+
const url =
|
|
992
|
+
config?.url ??
|
|
993
|
+
(process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL || "");
|
|
994
|
+
const token =
|
|
995
|
+
config?.token ??
|
|
996
|
+
(process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN || "");
|
|
997
|
+
if (url && token) {
|
|
998
|
+
return new UpstashConversationStore(url, token, ttl);
|
|
999
|
+
}
|
|
1000
|
+
return new InMemoryConversationStore(ttl);
|
|
1001
|
+
}
|
|
1002
|
+
if (provider === "redis") {
|
|
1003
|
+
const url = config?.url ?? process.env.REDIS_URL ?? "";
|
|
1004
|
+
if (url) {
|
|
1005
|
+
return new RedisLikeConversationStore(url, ttl);
|
|
1006
|
+
}
|
|
1007
|
+
return new InMemoryConversationStore(ttl);
|
|
1008
|
+
}
|
|
1009
|
+
if (provider === "dynamodb") {
|
|
1010
|
+
const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
|
|
1011
|
+
if (table) {
|
|
1012
|
+
return new DynamoDbConversationStore(table, config?.region as string | undefined, ttl);
|
|
1013
|
+
}
|
|
1014
|
+
return new InMemoryConversationStore(ttl);
|
|
1015
|
+
}
|
|
1016
|
+
return new InMemoryConversationStore(ttl);
|
|
1017
|
+
};
|