@poncho-ai/harness 0.35.0 → 0.36.1
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 +12 -11
- package/CHANGELOG.md +25 -0
- package/dist/index.d.ts +485 -29
- package/dist/index.js +2839 -2114
- package/dist/isolate-TCWTUVG4.js +1532 -0
- package/package.json +23 -4
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +106 -1
- package/src/harness.ts +226 -91
- package/src/index.ts +5 -0
- package/src/isolate/bindings.ts +206 -0
- package/src/isolate/bundler.ts +179 -0
- package/src/isolate/index.ts +10 -0
- package/src/isolate/polyfills.ts +796 -0
- package/src/isolate/run-code-tool.ts +220 -0
- package/src/isolate/runtime.ts +286 -0
- package/src/isolate/type-stubs.ts +196 -0
- package/src/memory.ts +129 -198
- package/src/reminder-store.ts +3 -237
- package/src/secrets-store.ts +2 -91
- package/src/state.ts +11 -1302
- package/src/storage/engine.ts +106 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/memory-engine.ts +588 -0
- package/src/storage/postgres-engine.ts +139 -0
- package/src/storage/schema.ts +145 -0
- package/src/storage/sql-dialect.ts +963 -0
- package/src/storage/sqlite-engine.ts +99 -0
- package/src/storage/store-adapters.ts +100 -0
- package/src/todo-tools.ts +1 -136
- package/src/upload-store.ts +1 -0
- package/src/vfs/bash-manager.ts +120 -0
- package/src/vfs/bash-tool.ts +59 -0
- package/src/vfs/create-bash-fs.ts +32 -0
- package/src/vfs/edit-file-tool.ts +72 -0
- package/src/vfs/index.ts +5 -0
- package/src/vfs/poncho-fs-adapter.ts +267 -0
- package/src/vfs/protected-fs.ts +177 -0
- package/src/vfs/read-file-tool.ts +103 -0
- package/src/vfs/write-file-tool.ts +49 -0
- package/test/harness.test.ts +30 -36
- package/test/isolate-vfs.test.ts +453 -0
- package/test/isolate.test.ts +252 -0
- package/test/state.test.ts +4 -27
- package/test/storage-engine.test.ts +250 -0
- package/test/vfs.test.ts +242 -0
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -11931
- package/src/kv-store.ts +0 -216
package/src/state.ts
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
1
|
import type { Message } from "@poncho-ai/sdk";
|
|
5
|
-
import {
|
|
6
|
-
ensureAgentIdentity,
|
|
7
|
-
getAgentStoreDirectory,
|
|
8
|
-
slugifyStorageComponent,
|
|
9
|
-
STORAGE_SCHEMA_VERSION,
|
|
10
|
-
} from "./agent-identity.js";
|
|
11
2
|
|
|
12
3
|
export interface ConversationState {
|
|
13
4
|
runId: string;
|
|
@@ -78,31 +69,15 @@ export interface Conversation {
|
|
|
78
69
|
subagentCallbackCount?: number;
|
|
79
70
|
runningCallbackSince?: number;
|
|
80
71
|
lastActivityAt?: number;
|
|
81
|
-
/** Harness-internal message chain preserved across continuation runs.
|
|
82
|
-
* Cleared when a run completes without continuation. */
|
|
83
72
|
_continuationMessages?: Message[];
|
|
84
|
-
/** Number of continuation pickups for the current multi-step run.
|
|
85
|
-
* Reset when a run completes without continuation. Used to enforce
|
|
86
|
-
* a maximum continuation count across all entry points. */
|
|
87
73
|
_continuationCount?: number;
|
|
88
|
-
/** Full structured message chain from the last harness run, including
|
|
89
|
-
* tool-call and tool-result messages the model needs for context.
|
|
90
|
-
* Unlike `_continuationMessages`, this is always set after a run
|
|
91
|
-
* and does NOT signal that a continuation is pending. */
|
|
92
74
|
_harnessMessages?: Message[];
|
|
93
|
-
/** Archived full-fidelity tool results keyed by toolResultId. */
|
|
94
75
|
_toolResultArchive?: Record<string, ArchivedToolResult>;
|
|
95
76
|
createdAt: number;
|
|
96
77
|
updatedAt: number;
|
|
97
78
|
}
|
|
98
79
|
|
|
99
80
|
export interface ConversationStore {
|
|
100
|
-
/**
|
|
101
|
-
* List conversations. tenantId semantics:
|
|
102
|
-
* undefined = no filter (builder/admin sees everything)
|
|
103
|
-
* null = legacy single-user only
|
|
104
|
-
* string = tenant-scoped
|
|
105
|
-
*/
|
|
106
81
|
list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]>;
|
|
107
82
|
listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
|
|
108
83
|
get(conversationId: string): Promise<Conversation | undefined>;
|
|
@@ -111,17 +86,14 @@ export interface ConversationStore {
|
|
|
111
86
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
112
87
|
delete(conversationId: string): Promise<boolean>;
|
|
113
88
|
appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
|
|
114
|
-
/**
|
|
115
|
-
* Atomically clear `runningCallbackSince` without clobbering other fields.
|
|
116
|
-
* Returns the conversation as it exists after the clear (with current
|
|
117
|
-
* `pendingSubagentResults`).
|
|
118
|
-
*/
|
|
119
89
|
clearCallbackLock(conversationId: string): Promise<Conversation | undefined>;
|
|
120
90
|
}
|
|
121
91
|
|
|
122
92
|
export type StateProviderName =
|
|
123
93
|
| "local"
|
|
124
94
|
| "memory"
|
|
95
|
+
| "sqlite"
|
|
96
|
+
| "postgresql"
|
|
125
97
|
| "redis"
|
|
126
98
|
| "upstash"
|
|
127
99
|
| "dynamodb";
|
|
@@ -136,38 +108,6 @@ export interface StateConfig {
|
|
|
136
108
|
}
|
|
137
109
|
|
|
138
110
|
const DEFAULT_OWNER = "local-owner";
|
|
139
|
-
const LOCAL_STATE_FILE = "state.json";
|
|
140
|
-
const CONVERSATIONS_DIRECTORY = "conversations";
|
|
141
|
-
const LOCAL_CONVERSATION_INDEX_FILE = "index.json";
|
|
142
|
-
|
|
143
|
-
type StoreIdentityOptions = {
|
|
144
|
-
workingDir: string;
|
|
145
|
-
agentId?: string;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const toStoreIdentity = async ({
|
|
149
|
-
workingDir,
|
|
150
|
-
agentId,
|
|
151
|
-
}: StoreIdentityOptions): Promise<{ name: string; id: string }> => {
|
|
152
|
-
const ensured = await ensureAgentIdentity(workingDir);
|
|
153
|
-
if (!agentId) {
|
|
154
|
-
return ensured;
|
|
155
|
-
}
|
|
156
|
-
return { name: ensured.name, id: agentId };
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
|
|
160
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
161
|
-
const tmpPath = `${filePath}.tmp`;
|
|
162
|
-
await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
|
|
163
|
-
await rename(tmpPath, filePath);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const formatUtcTimestamp = (value: number): string =>
|
|
167
|
-
new Date(value)
|
|
168
|
-
.toISOString()
|
|
169
|
-
.replace(/[-:]/g, "")
|
|
170
|
-
.replace(/\.\d{3}Z$/, "Z");
|
|
171
111
|
|
|
172
112
|
const normalizeTitle = (title?: string): string => {
|
|
173
113
|
return title && title.trim().length > 0 ? title.trim() : "New conversation";
|
|
@@ -187,9 +127,7 @@ export class InMemoryStateStore implements StateStore {
|
|
|
187
127
|
|
|
188
128
|
async get(runId: string): Promise<ConversationState | undefined> {
|
|
189
129
|
const state = this.store.get(runId);
|
|
190
|
-
if (!state)
|
|
191
|
-
return undefined;
|
|
192
|
-
}
|
|
130
|
+
if (!state) return undefined;
|
|
193
131
|
if (this.isExpired(state)) {
|
|
194
132
|
this.store.delete(runId);
|
|
195
133
|
return undefined;
|
|
@@ -206,61 +144,6 @@ export class InMemoryStateStore implements StateStore {
|
|
|
206
144
|
}
|
|
207
145
|
}
|
|
208
146
|
|
|
209
|
-
class UpstashStateStore implements StateStore {
|
|
210
|
-
private readonly baseUrl: string;
|
|
211
|
-
private readonly token: string;
|
|
212
|
-
private readonly ttl?: number;
|
|
213
|
-
|
|
214
|
-
constructor(baseUrl: string, token: string, ttl?: number) {
|
|
215
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
216
|
-
this.token = token;
|
|
217
|
-
this.ttl = ttl;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private headers(): HeadersInit {
|
|
221
|
-
return {
|
|
222
|
-
Authorization: `Bearer ${this.token}`,
|
|
223
|
-
"Content-Type": "application/json",
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async get(runId: string): Promise<ConversationState | undefined> {
|
|
228
|
-
const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(runId)}`, {
|
|
229
|
-
method: "POST",
|
|
230
|
-
headers: this.headers(),
|
|
231
|
-
});
|
|
232
|
-
if (!response.ok) {
|
|
233
|
-
return undefined;
|
|
234
|
-
}
|
|
235
|
-
const payload = (await response.json()) as { result?: string | null };
|
|
236
|
-
if (!payload.result) {
|
|
237
|
-
return undefined;
|
|
238
|
-
}
|
|
239
|
-
return JSON.parse(payload.result) as ConversationState;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async set(state: ConversationState): Promise<void> {
|
|
243
|
-
const serialized = JSON.stringify({ ...state, updatedAt: Date.now() });
|
|
244
|
-
const path =
|
|
245
|
-
typeof this.ttl === "number"
|
|
246
|
-
? `${this.baseUrl}/setex/${encodeURIComponent(state.runId)}/${Math.max(
|
|
247
|
-
1,
|
|
248
|
-
this.ttl,
|
|
249
|
-
)}/${encodeURIComponent(serialized)}`
|
|
250
|
-
: `${this.baseUrl}/set/${encodeURIComponent(state.runId)}/${encodeURIComponent(
|
|
251
|
-
serialized,
|
|
252
|
-
)}`;
|
|
253
|
-
await fetch(path, { method: "POST", headers: this.headers() });
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async delete(runId: string): Promise<void> {
|
|
257
|
-
await fetch(`${this.baseUrl}/del/${encodeURIComponent(runId)}`, {
|
|
258
|
-
method: "POST",
|
|
259
|
-
headers: this.headers(),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
147
|
export class InMemoryConversationStore implements ConversationStore {
|
|
265
148
|
private readonly conversations = new Map<string, Conversation>();
|
|
266
149
|
private readonly ttlMs?: number;
|
|
@@ -338,9 +221,7 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
338
221
|
|
|
339
222
|
async rename(conversationId: string, title: string): Promise<Conversation | undefined> {
|
|
340
223
|
const existing = await this.get(conversationId);
|
|
341
|
-
if (!existing)
|
|
342
|
-
return undefined;
|
|
343
|
-
}
|
|
224
|
+
if (!existing) return undefined;
|
|
344
225
|
const updated: Conversation = {
|
|
345
226
|
...existing,
|
|
346
227
|
title: normalizeTitle(title || existing.title),
|
|
@@ -388,1196 +269,24 @@ export type ConversationSummary = {
|
|
|
388
269
|
};
|
|
389
270
|
};
|
|
390
271
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
updatedAt: number;
|
|
397
|
-
createdAt?: number;
|
|
398
|
-
ownerId: string;
|
|
399
|
-
tenantId?: string | null;
|
|
400
|
-
fileName: string;
|
|
401
|
-
parentConversationId?: string;
|
|
402
|
-
messageCount?: number;
|
|
403
|
-
hasPendingApprovals?: boolean;
|
|
404
|
-
channelMeta?: {
|
|
405
|
-
platform: string;
|
|
406
|
-
channelId: string;
|
|
407
|
-
platformThreadId: string;
|
|
408
|
-
};
|
|
409
|
-
}>;
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
class FileConversationStore implements ConversationStore {
|
|
413
|
-
private readonly workingDir: string;
|
|
414
|
-
private readonly agentId?: string;
|
|
415
|
-
private readonly conversations = new Map<string, ConversationStoreFile["conversations"][number]>();
|
|
416
|
-
private loaded = false;
|
|
417
|
-
private writing = Promise.resolve();
|
|
418
|
-
private paths?: { conversationsDir: string; indexPath: string };
|
|
419
|
-
|
|
420
|
-
constructor(workingDir: string, agentId?: string) {
|
|
421
|
-
this.workingDir = workingDir;
|
|
422
|
-
this.agentId = agentId;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
private async resolvePaths(): Promise<{ conversationsDir: string; indexPath: string }> {
|
|
426
|
-
if (this.paths) {
|
|
427
|
-
return this.paths;
|
|
428
|
-
}
|
|
429
|
-
const identity = await toStoreIdentity({
|
|
430
|
-
workingDir: this.workingDir,
|
|
431
|
-
agentId: this.agentId,
|
|
432
|
-
});
|
|
433
|
-
const agentDir = getAgentStoreDirectory(identity);
|
|
434
|
-
const conversationsDir = resolve(agentDir, CONVERSATIONS_DIRECTORY);
|
|
435
|
-
const indexPath = resolve(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
|
|
436
|
-
this.paths = { conversationsDir, indexPath };
|
|
437
|
-
return this.paths;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
private async writeIndex(): Promise<void> {
|
|
441
|
-
const { indexPath } = await this.resolvePaths();
|
|
442
|
-
const payload: ConversationStoreFile = {
|
|
443
|
-
schemaVersion: STORAGE_SCHEMA_VERSION,
|
|
444
|
-
conversations: Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt),
|
|
445
|
-
};
|
|
446
|
-
await writeJsonAtomic(indexPath, payload);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private async readConversationFile(fileName: string): Promise<Conversation | undefined> {
|
|
450
|
-
const { conversationsDir } = await this.resolvePaths();
|
|
451
|
-
const filePath = resolve(conversationsDir, fileName);
|
|
452
|
-
try {
|
|
453
|
-
const raw = await readFile(filePath, "utf8");
|
|
454
|
-
return JSON.parse(raw) as Conversation;
|
|
455
|
-
} catch {
|
|
456
|
-
return undefined;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
private async rebuildIndexFromFiles(): Promise<void> {
|
|
461
|
-
const { conversationsDir } = await this.resolvePaths();
|
|
462
|
-
this.conversations.clear();
|
|
463
|
-
try {
|
|
464
|
-
const entries = await readdir(conversationsDir, { withFileTypes: true });
|
|
465
|
-
for (const entry of entries) {
|
|
466
|
-
if (!entry.isFile() || entry.name === LOCAL_CONVERSATION_INDEX_FILE) {
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
const conversation = await this.readConversationFile(entry.name);
|
|
470
|
-
if (!conversation) {
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
this.conversations.set(conversation.conversationId, {
|
|
474
|
-
conversationId: conversation.conversationId,
|
|
475
|
-
title: conversation.title,
|
|
476
|
-
updatedAt: conversation.updatedAt,
|
|
477
|
-
createdAt: conversation.createdAt,
|
|
478
|
-
ownerId: conversation.ownerId,
|
|
479
|
-
tenantId: conversation.tenantId,
|
|
480
|
-
fileName: entry.name,
|
|
481
|
-
parentConversationId: conversation.parentConversationId,
|
|
482
|
-
messageCount: conversation.messages.length,
|
|
483
|
-
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
} catch {
|
|
487
|
-
// Missing directory should behave like empty.
|
|
488
|
-
}
|
|
489
|
-
await this.writeIndex();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private resolveConversationFileName(conversation: Conversation): string {
|
|
493
|
-
const ts = formatUtcTimestamp(conversation.createdAt || Date.now());
|
|
494
|
-
return `${ts}--${slugifyStorageComponent(conversation.conversationId)}.json`;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
private async ensureLoaded(): Promise<void> {
|
|
498
|
-
if (this.loaded) {
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
this.loaded = true;
|
|
502
|
-
const { indexPath } = await this.resolvePaths();
|
|
503
|
-
try {
|
|
504
|
-
const raw = await readFile(indexPath, "utf8");
|
|
505
|
-
const parsed = JSON.parse(raw) as ConversationStoreFile;
|
|
506
|
-
for (const conversation of parsed.conversations ?? []) {
|
|
507
|
-
this.conversations.set(conversation.conversationId, conversation);
|
|
508
|
-
}
|
|
509
|
-
// Rebuild if any entry is from an older index format (missing messageCount)
|
|
510
|
-
let needsRebuild = false;
|
|
511
|
-
for (const entry of this.conversations.values()) {
|
|
512
|
-
if (entry.messageCount === undefined) {
|
|
513
|
-
needsRebuild = true;
|
|
514
|
-
break;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (needsRebuild) {
|
|
518
|
-
await this.rebuildIndexFromFiles();
|
|
519
|
-
}
|
|
520
|
-
} catch {
|
|
521
|
-
await this.rebuildIndexFromFiles();
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
private async persistConversation(conversation: Conversation): Promise<void> {
|
|
526
|
-
const { conversationsDir } = await this.resolvePaths();
|
|
527
|
-
const existing = this.conversations.get(conversation.conversationId);
|
|
528
|
-
const fileName = existing?.fileName ?? this.resolveConversationFileName(conversation);
|
|
529
|
-
const filePath = resolve(conversationsDir, fileName);
|
|
530
|
-
this.writing = this.writing.then(async () => {
|
|
531
|
-
await writeJsonAtomic(filePath, conversation);
|
|
532
|
-
this.conversations.set(conversation.conversationId, {
|
|
533
|
-
conversationId: conversation.conversationId,
|
|
534
|
-
title: conversation.title,
|
|
535
|
-
updatedAt: conversation.updatedAt,
|
|
536
|
-
createdAt: conversation.createdAt,
|
|
537
|
-
ownerId: conversation.ownerId,
|
|
538
|
-
tenantId: conversation.tenantId,
|
|
539
|
-
fileName,
|
|
540
|
-
parentConversationId: conversation.parentConversationId,
|
|
541
|
-
messageCount: conversation.messages.length,
|
|
542
|
-
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
|
|
543
|
-
channelMeta: conversation.channelMeta,
|
|
544
|
-
});
|
|
545
|
-
await this.writeIndex();
|
|
546
|
-
});
|
|
547
|
-
await this.writing;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
|
|
551
|
-
await this.ensureLoaded();
|
|
552
|
-
const summaries = Array.from(this.conversations.values())
|
|
553
|
-
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
554
|
-
.filter((c) => tenantId === undefined || (c.tenantId ?? null) === tenantId)
|
|
555
|
-
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
556
|
-
const conversations: Conversation[] = [];
|
|
557
|
-
for (const summary of summaries) {
|
|
558
|
-
const loaded = await this.readConversationFile(summary.fileName);
|
|
559
|
-
if (loaded) {
|
|
560
|
-
conversations.push(loaded);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
return conversations;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
|
|
567
|
-
await this.ensureLoaded();
|
|
568
|
-
return Array.from(this.conversations.values())
|
|
569
|
-
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
570
|
-
.filter((c) => tenantId === undefined || (c.tenantId ?? null) === tenantId)
|
|
571
|
-
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
572
|
-
.map((c) => ({
|
|
573
|
-
conversationId: c.conversationId,
|
|
574
|
-
title: c.title,
|
|
575
|
-
updatedAt: c.updatedAt,
|
|
576
|
-
createdAt: c.createdAt,
|
|
577
|
-
ownerId: c.ownerId,
|
|
578
|
-
tenantId: c.tenantId,
|
|
579
|
-
parentConversationId: c.parentConversationId,
|
|
580
|
-
messageCount: c.messageCount,
|
|
581
|
-
hasPendingApprovals: c.hasPendingApprovals,
|
|
582
|
-
channelMeta: c.channelMeta,
|
|
583
|
-
}));
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async get(conversationId: string): Promise<Conversation | undefined> {
|
|
587
|
-
await this.ensureLoaded();
|
|
588
|
-
const summary = this.conversations.get(conversationId);
|
|
589
|
-
if (!summary) {
|
|
590
|
-
return undefined;
|
|
591
|
-
}
|
|
592
|
-
return await this.readConversationFile(summary.fileName);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
|
|
596
|
-
await this.ensureLoaded();
|
|
597
|
-
const now = Date.now();
|
|
598
|
-
const conversation: Conversation = {
|
|
599
|
-
conversationId: randomUUID(),
|
|
600
|
-
title: normalizeTitle(title),
|
|
601
|
-
messages: [],
|
|
602
|
-
ownerId,
|
|
603
|
-
tenantId,
|
|
604
|
-
createdAt: now,
|
|
605
|
-
updatedAt: now,
|
|
606
|
-
};
|
|
607
|
-
await this.persistConversation(conversation);
|
|
608
|
-
return conversation;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async update(conversation: Conversation): Promise<void> {
|
|
612
|
-
await this.ensureLoaded();
|
|
613
|
-
const next = {
|
|
614
|
-
...conversation,
|
|
615
|
-
updatedAt: Date.now(),
|
|
616
|
-
};
|
|
617
|
-
await this.persistConversation(next);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
async rename(conversationId: string, title: string): Promise<Conversation | undefined> {
|
|
621
|
-
await this.ensureLoaded();
|
|
622
|
-
const existing = await this.get(conversationId);
|
|
623
|
-
if (!existing) {
|
|
624
|
-
return undefined;
|
|
625
|
-
}
|
|
626
|
-
const updated: Conversation = {
|
|
627
|
-
...existing,
|
|
628
|
-
title: normalizeTitle(title || existing.title),
|
|
629
|
-
updatedAt: Date.now(),
|
|
630
|
-
};
|
|
631
|
-
await this.persistConversation(updated);
|
|
632
|
-
return updated;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async delete(conversationId: string): Promise<boolean> {
|
|
636
|
-
await this.ensureLoaded();
|
|
637
|
-
const { conversationsDir } = await this.resolvePaths();
|
|
638
|
-
const existing = this.conversations.get(conversationId);
|
|
639
|
-
const removed = this.conversations.delete(conversationId);
|
|
640
|
-
if (removed) {
|
|
641
|
-
this.writing = this.writing.then(async () => {
|
|
642
|
-
if (existing) {
|
|
643
|
-
await rm(resolve(conversationsDir, existing.fileName), { force: true });
|
|
644
|
-
}
|
|
645
|
-
await this.writeIndex();
|
|
646
|
-
});
|
|
647
|
-
await this.writing;
|
|
648
|
-
}
|
|
649
|
-
return removed;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
|
|
653
|
-
await this.ensureLoaded();
|
|
654
|
-
const conversation = await this.get(conversationId);
|
|
655
|
-
if (!conversation) return;
|
|
656
|
-
if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
|
|
657
|
-
conversation.pendingSubagentResults.push(result);
|
|
658
|
-
conversation.updatedAt = Date.now();
|
|
659
|
-
await this.update(conversation);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async clearCallbackLock(conversationId: string): Promise<Conversation | undefined> {
|
|
663
|
-
await this.ensureLoaded();
|
|
664
|
-
const summary = this.conversations.get(conversationId);
|
|
665
|
-
if (!summary) return undefined;
|
|
666
|
-
const { conversationsDir } = await this.resolvePaths();
|
|
667
|
-
const filePath = resolve(conversationsDir, summary.fileName);
|
|
668
|
-
let result: Conversation | undefined;
|
|
669
|
-
// Read inside the writing chain so we see the latest state after any
|
|
670
|
-
// pending appendSubagentResult writes have flushed.
|
|
671
|
-
this.writing = this.writing.then(async () => {
|
|
672
|
-
const conv = await this.readConversationFile(summary.fileName);
|
|
673
|
-
if (!conv) return;
|
|
674
|
-
conv.runningCallbackSince = undefined;
|
|
675
|
-
conv.updatedAt = Date.now();
|
|
676
|
-
await writeJsonAtomic(filePath, conv);
|
|
677
|
-
this.conversations.set(conversationId, {
|
|
678
|
-
...summary,
|
|
679
|
-
updatedAt: conv.updatedAt,
|
|
680
|
-
});
|
|
681
|
-
result = conv;
|
|
682
|
-
});
|
|
683
|
-
await this.writing;
|
|
684
|
-
return result;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
type LocalStateFile = {
|
|
689
|
-
states: ConversationState[];
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
class FileStateStore implements StateStore {
|
|
693
|
-
private readonly workingDir: string;
|
|
694
|
-
private readonly agentId?: string;
|
|
695
|
-
private filePath = "";
|
|
696
|
-
private readonly states = new Map<string, ConversationState>();
|
|
697
|
-
private readonly ttlMs?: number;
|
|
698
|
-
private loaded = false;
|
|
699
|
-
private writing = Promise.resolve();
|
|
700
|
-
|
|
701
|
-
constructor(workingDir: string, ttlSeconds?: number, agentId?: string) {
|
|
702
|
-
this.workingDir = workingDir;
|
|
703
|
-
this.agentId = agentId;
|
|
704
|
-
this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1000 : undefined;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
private async ensureFilePath(): Promise<void> {
|
|
708
|
-
if (this.filePath) {
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
const identity = await toStoreIdentity({
|
|
712
|
-
workingDir: this.workingDir,
|
|
713
|
-
agentId: this.agentId,
|
|
714
|
-
});
|
|
715
|
-
this.filePath = resolve(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
private isExpired(state: ConversationState): boolean {
|
|
719
|
-
return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
private async ensureLoaded(): Promise<void> {
|
|
723
|
-
await this.ensureFilePath();
|
|
724
|
-
if (this.loaded) {
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
this.loaded = true;
|
|
728
|
-
try {
|
|
729
|
-
const raw = await readFile(this.filePath, "utf8");
|
|
730
|
-
const parsed = JSON.parse(raw) as LocalStateFile;
|
|
731
|
-
for (const state of parsed.states ?? []) {
|
|
732
|
-
this.states.set(state.runId, state);
|
|
733
|
-
}
|
|
734
|
-
} catch {
|
|
735
|
-
// Missing/invalid file should not crash local mode.
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private async persist(): Promise<void> {
|
|
740
|
-
const payload: LocalStateFile = {
|
|
741
|
-
states: Array.from(this.states.values()),
|
|
742
|
-
};
|
|
743
|
-
this.writing = this.writing.then(async () => {
|
|
744
|
-
await writeJsonAtomic(this.filePath, payload);
|
|
745
|
-
});
|
|
746
|
-
await this.writing;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
async get(runId: string): Promise<ConversationState | undefined> {
|
|
750
|
-
await this.ensureLoaded();
|
|
751
|
-
const state = this.states.get(runId);
|
|
752
|
-
if (!state) {
|
|
753
|
-
return undefined;
|
|
754
|
-
}
|
|
755
|
-
if (this.isExpired(state)) {
|
|
756
|
-
this.states.delete(runId);
|
|
757
|
-
await this.persist();
|
|
758
|
-
return undefined;
|
|
759
|
-
}
|
|
760
|
-
return state;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
async set(state: ConversationState): Promise<void> {
|
|
764
|
-
await this.ensureLoaded();
|
|
765
|
-
this.states.set(state.runId, { ...state, updatedAt: Date.now() });
|
|
766
|
-
await this.persist();
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
async delete(runId: string): Promise<void> {
|
|
770
|
-
await this.ensureLoaded();
|
|
771
|
-
this.states.delete(runId);
|
|
772
|
-
await this.persist();
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
interface RawKeyValueClient {
|
|
777
|
-
get(key: string): Promise<string | undefined>;
|
|
778
|
-
mget(keys: string[]): Promise<(string | undefined)[]>;
|
|
779
|
-
set(key: string, value: string, ttl?: number): Promise<void>;
|
|
780
|
-
del(key: string): Promise<void>;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
type ConversationMeta = {
|
|
784
|
-
conversationId: string;
|
|
785
|
-
title: string;
|
|
786
|
-
updatedAt: number;
|
|
787
|
-
createdAt?: number;
|
|
788
|
-
ownerId: string;
|
|
789
|
-
tenantId?: string | null;
|
|
790
|
-
parentConversationId?: string;
|
|
791
|
-
messageCount?: number;
|
|
792
|
-
hasPendingApprovals?: boolean;
|
|
793
|
-
channelMeta?: {
|
|
794
|
-
platform: string;
|
|
795
|
-
channelId: string;
|
|
796
|
-
platformThreadId: string;
|
|
797
|
-
};
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
801
|
-
protected readonly ttl?: number;
|
|
802
|
-
private readonly agentIdPromise: Promise<string>;
|
|
803
|
-
private readonly ownerLocks = new Map<string, Promise<void>>();
|
|
804
|
-
private readonly appendLocks = new Map<string, Promise<void>>();
|
|
805
|
-
protected readonly memoryFallback: InMemoryConversationStore;
|
|
806
|
-
|
|
807
|
-
constructor(ttl: number | undefined, workingDir: string, agentId?: string) {
|
|
808
|
-
this.ttl = ttl;
|
|
809
|
-
this.memoryFallback = new InMemoryConversationStore(ttl);
|
|
810
|
-
this.agentIdPromise = toStoreIdentity({ workingDir, agentId }).then((identity) => identity.id);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
protected abstract client(): Promise<RawKeyValueClient | undefined>;
|
|
814
|
-
|
|
815
|
-
private async withOwnerLock(ownerId: string, task: () => Promise<void>): Promise<void> {
|
|
816
|
-
const prev = this.ownerLocks.get(ownerId) ?? Promise.resolve();
|
|
817
|
-
const next = prev.then(task, task);
|
|
818
|
-
this.ownerLocks.set(ownerId, next);
|
|
819
|
-
try {
|
|
820
|
-
await next;
|
|
821
|
-
} finally {
|
|
822
|
-
if (this.ownerLocks.get(ownerId) === next) {
|
|
823
|
-
this.ownerLocks.delete(ownerId);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
private async withAppendLock(conversationId: string, task: () => Promise<void>): Promise<void> {
|
|
829
|
-
const prev = this.appendLocks.get(conversationId) ?? Promise.resolve();
|
|
830
|
-
const next = prev.then(task, task);
|
|
831
|
-
this.appendLocks.set(conversationId, next);
|
|
832
|
-
try {
|
|
833
|
-
await next;
|
|
834
|
-
} finally {
|
|
835
|
-
if (this.appendLocks.get(conversationId) === next) {
|
|
836
|
-
this.appendLocks.delete(conversationId);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
private async namespace(): Promise<string> {
|
|
842
|
-
const agentId = await this.agentIdPromise;
|
|
843
|
-
return `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
private async conversationKey(conversationId: string): Promise<string> {
|
|
847
|
-
return `${await this.namespace()}:conv:${conversationId}`;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
private async conversationMetaKey(conversationId: string): Promise<string> {
|
|
851
|
-
return `${await this.namespace()}:convmeta:${conversationId}`;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
private async ownerIndexKey(ownerId: string): Promise<string> {
|
|
855
|
-
return `${await this.namespace()}:owner:${slugifyStorageComponent(ownerId)}:conversations`;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
private async getOwnerConversationIds(ownerId: string): Promise<string[]> {
|
|
859
|
-
const kv = await this.client();
|
|
860
|
-
if (!kv) {
|
|
861
|
-
return [];
|
|
862
|
-
}
|
|
863
|
-
try {
|
|
864
|
-
const raw = await kv.get(await this.ownerIndexKey(ownerId));
|
|
865
|
-
if (!raw) {
|
|
866
|
-
return [];
|
|
867
|
-
}
|
|
868
|
-
const parsed = JSON.parse(raw) as { ids?: string[] };
|
|
869
|
-
return Array.isArray(parsed.ids)
|
|
870
|
-
? parsed.ids.filter((value): value is string => typeof value === "string")
|
|
871
|
-
: [];
|
|
872
|
-
} catch {
|
|
873
|
-
return [];
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
private async setOwnerConversationIds(ownerId: string, ids: string[]): Promise<void> {
|
|
878
|
-
const kv = await this.client();
|
|
879
|
-
if (!kv) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
const key = await this.ownerIndexKey(ownerId);
|
|
883
|
-
const payload = JSON.stringify({ schemaVersion: STORAGE_SCHEMA_VERSION, ids });
|
|
884
|
-
if (ids.length === 0) {
|
|
885
|
-
await kv.del(key);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
await kv.set(key, payload, this.ttl);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
private async getConversationMeta(conversationId: string): Promise<ConversationMeta | undefined> {
|
|
892
|
-
const kv = await this.client();
|
|
893
|
-
if (!kv) {
|
|
894
|
-
return undefined;
|
|
895
|
-
}
|
|
896
|
-
try {
|
|
897
|
-
const raw = await kv.get(await this.conversationMetaKey(conversationId));
|
|
898
|
-
if (!raw) {
|
|
899
|
-
return undefined;
|
|
900
|
-
}
|
|
901
|
-
return JSON.parse(raw) as ConversationMeta;
|
|
902
|
-
} catch {
|
|
903
|
-
return undefined;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
|
|
908
|
-
const kv = await this.client();
|
|
909
|
-
if (!kv) {
|
|
910
|
-
return await this.memoryFallback.list(ownerId, tenantId);
|
|
911
|
-
}
|
|
912
|
-
if (!ownerId) {
|
|
913
|
-
return [];
|
|
914
|
-
}
|
|
915
|
-
const ids = await this.getOwnerConversationIds(ownerId);
|
|
916
|
-
if (ids.length === 0) return [];
|
|
917
|
-
const convKeys = await Promise.all(ids.map((id) => this.conversationKey(id)));
|
|
918
|
-
const rawValues = await kv.mget(convKeys);
|
|
919
|
-
const conversations: Conversation[] = [];
|
|
920
|
-
for (const raw of rawValues) {
|
|
921
|
-
if (!raw) continue;
|
|
922
|
-
try {
|
|
923
|
-
const conv = JSON.parse(raw) as Conversation;
|
|
924
|
-
if (tenantId === undefined || conv.tenantId === tenantId) {
|
|
925
|
-
conversations.push(conv);
|
|
926
|
-
}
|
|
927
|
-
} catch { /* skip invalid records */ }
|
|
928
|
-
}
|
|
929
|
-
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
|
|
933
|
-
const kv = await this.client();
|
|
934
|
-
if (!kv) {
|
|
935
|
-
return await this.memoryFallback.listSummaries(ownerId, tenantId);
|
|
936
|
-
}
|
|
937
|
-
if (!ownerId) {
|
|
938
|
-
return [];
|
|
939
|
-
}
|
|
940
|
-
const ids = await this.getOwnerConversationIds(ownerId);
|
|
941
|
-
if (ids.length === 0) return [];
|
|
942
|
-
const metaKeys = await Promise.all(ids.map((id) => this.conversationMetaKey(id)));
|
|
943
|
-
const rawValues = await kv.mget(metaKeys);
|
|
944
|
-
const summaries: ConversationSummary[] = [];
|
|
945
|
-
for (const raw of rawValues) {
|
|
946
|
-
if (!raw) continue;
|
|
947
|
-
try {
|
|
948
|
-
const meta = JSON.parse(raw) as ConversationMeta;
|
|
949
|
-
if (meta.ownerId === ownerId && (tenantId === undefined || (meta.tenantId ?? null) === tenantId)) {
|
|
950
|
-
summaries.push({
|
|
951
|
-
conversationId: meta.conversationId,
|
|
952
|
-
title: meta.title,
|
|
953
|
-
updatedAt: meta.updatedAt,
|
|
954
|
-
createdAt: meta.createdAt,
|
|
955
|
-
ownerId: meta.ownerId,
|
|
956
|
-
tenantId: meta.tenantId,
|
|
957
|
-
parentConversationId: meta.parentConversationId,
|
|
958
|
-
messageCount: meta.messageCount,
|
|
959
|
-
hasPendingApprovals: meta.hasPendingApprovals,
|
|
960
|
-
channelMeta: meta.channelMeta,
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
} catch { /* skip invalid records */ }
|
|
964
|
-
}
|
|
965
|
-
return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
async get(conversationId: string): Promise<Conversation | undefined> {
|
|
969
|
-
const kv = await this.client();
|
|
970
|
-
if (!kv) {
|
|
971
|
-
return await this.memoryFallback.get(conversationId);
|
|
972
|
-
}
|
|
973
|
-
const raw = await kv.get(await this.conversationKey(conversationId));
|
|
974
|
-
if (!raw) {
|
|
975
|
-
return undefined;
|
|
976
|
-
}
|
|
977
|
-
try {
|
|
978
|
-
return JSON.parse(raw) as Conversation;
|
|
979
|
-
} catch {
|
|
980
|
-
return undefined;
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
|
|
985
|
-
const now = Date.now();
|
|
986
|
-
const conversation: Conversation = {
|
|
987
|
-
conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
|
|
988
|
-
title: normalizeTitle(title),
|
|
989
|
-
messages: [],
|
|
990
|
-
ownerId,
|
|
991
|
-
tenantId,
|
|
992
|
-
createdAt: now,
|
|
993
|
-
updatedAt: now,
|
|
994
|
-
};
|
|
995
|
-
await this.update(conversation);
|
|
996
|
-
return conversation;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
async update(conversation: Conversation): Promise<void> {
|
|
1000
|
-
const kv = await this.client();
|
|
1001
|
-
if (!kv) {
|
|
1002
|
-
await this.memoryFallback.update(conversation);
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
const existing = await this.get(conversation.conversationId);
|
|
1006
|
-
const nextConversation: Conversation = {
|
|
1007
|
-
...conversation,
|
|
1008
|
-
updatedAt: Date.now(),
|
|
1009
|
-
};
|
|
1010
|
-
const convKey = await this.conversationKey(nextConversation.conversationId);
|
|
1011
|
-
const metaKey = await this.conversationMetaKey(nextConversation.conversationId);
|
|
1012
|
-
await kv.set(convKey, JSON.stringify(nextConversation), this.ttl);
|
|
1013
|
-
await kv.set(
|
|
1014
|
-
metaKey,
|
|
1015
|
-
JSON.stringify({
|
|
1016
|
-
conversationId: nextConversation.conversationId,
|
|
1017
|
-
title: nextConversation.title,
|
|
1018
|
-
updatedAt: nextConversation.updatedAt,
|
|
1019
|
-
createdAt: nextConversation.createdAt,
|
|
1020
|
-
ownerId: nextConversation.ownerId,
|
|
1021
|
-
tenantId: nextConversation.tenantId,
|
|
1022
|
-
parentConversationId: nextConversation.parentConversationId,
|
|
1023
|
-
messageCount: nextConversation.messages.length,
|
|
1024
|
-
hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
|
|
1025
|
-
channelMeta: nextConversation.channelMeta,
|
|
1026
|
-
} satisfies ConversationMeta),
|
|
1027
|
-
this.ttl,
|
|
1028
|
-
);
|
|
1029
|
-
if (existing && existing.ownerId !== nextConversation.ownerId) {
|
|
1030
|
-
await this.withOwnerLock(existing.ownerId, async () => {
|
|
1031
|
-
const ids = await this.getOwnerConversationIds(existing.ownerId);
|
|
1032
|
-
await this.setOwnerConversationIds(
|
|
1033
|
-
existing.ownerId,
|
|
1034
|
-
ids.filter((id) => id !== nextConversation.conversationId),
|
|
1035
|
-
);
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
await this.withOwnerLock(nextConversation.ownerId, async () => {
|
|
1039
|
-
const ids = await this.getOwnerConversationIds(nextConversation.ownerId);
|
|
1040
|
-
const deduped = [nextConversation.conversationId, ...ids.filter((id) => id !== nextConversation.conversationId)];
|
|
1041
|
-
await this.setOwnerConversationIds(nextConversation.ownerId, deduped);
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
async rename(conversationId: string, title: string): Promise<Conversation | undefined> {
|
|
1046
|
-
const existing = await this.get(conversationId);
|
|
1047
|
-
if (!existing) {
|
|
1048
|
-
return undefined;
|
|
1049
|
-
}
|
|
1050
|
-
const updated: Conversation = {
|
|
1051
|
-
...existing,
|
|
1052
|
-
title: normalizeTitle(title || existing.title),
|
|
1053
|
-
updatedAt: Date.now(),
|
|
1054
|
-
};
|
|
1055
|
-
await this.update(updated);
|
|
1056
|
-
return updated;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
async delete(conversationId: string): Promise<boolean> {
|
|
1060
|
-
const kv = await this.client();
|
|
1061
|
-
if (!kv) {
|
|
1062
|
-
return await this.memoryFallback.delete(conversationId);
|
|
1063
|
-
}
|
|
1064
|
-
const existing = await this.get(conversationId);
|
|
1065
|
-
if (!existing) {
|
|
1066
|
-
return false;
|
|
1067
|
-
}
|
|
1068
|
-
await kv.del(await this.conversationKey(conversationId));
|
|
1069
|
-
await kv.del(await this.conversationMetaKey(conversationId));
|
|
1070
|
-
await this.withOwnerLock(existing.ownerId, async () => {
|
|
1071
|
-
const ids = await this.getOwnerConversationIds(existing.ownerId);
|
|
1072
|
-
await this.setOwnerConversationIds(
|
|
1073
|
-
existing.ownerId,
|
|
1074
|
-
ids.filter((id) => id !== conversationId),
|
|
1075
|
-
);
|
|
1076
|
-
});
|
|
1077
|
-
return true;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
|
|
1081
|
-
await this.withAppendLock(conversationId, async () => {
|
|
1082
|
-
const conversation = await this.get(conversationId);
|
|
1083
|
-
if (!conversation) return;
|
|
1084
|
-
if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
|
|
1085
|
-
conversation.pendingSubagentResults.push(result);
|
|
1086
|
-
conversation.updatedAt = Date.now();
|
|
1087
|
-
await this.update(conversation);
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
async clearCallbackLock(conversationId: string): Promise<Conversation | undefined> {
|
|
1092
|
-
let result: Conversation | undefined;
|
|
1093
|
-
await this.withAppendLock(conversationId, async () => {
|
|
1094
|
-
const conversation = await this.get(conversationId);
|
|
1095
|
-
if (!conversation) return;
|
|
1096
|
-
conversation.runningCallbackSince = undefined;
|
|
1097
|
-
conversation.updatedAt = Date.now();
|
|
1098
|
-
await this.update(conversation);
|
|
1099
|
-
result = conversation;
|
|
1100
|
-
});
|
|
1101
|
-
return result;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
class UpstashConversationStore extends KeyValueConversationStoreBase {
|
|
1106
|
-
private readonly baseUrl: string;
|
|
1107
|
-
private readonly token: string;
|
|
1108
|
-
|
|
1109
|
-
constructor(baseUrl: string, token: string, workingDir: string, ttl?: number, agentId?: string) {
|
|
1110
|
-
super(ttl, workingDir, agentId);
|
|
1111
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1112
|
-
this.token = token;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
private headers(): HeadersInit {
|
|
1116
|
-
return {
|
|
1117
|
-
Authorization: `Bearer ${this.token}`,
|
|
1118
|
-
"Content-Type": "application/json",
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
protected async client(): Promise<RawKeyValueClient | undefined> {
|
|
1123
|
-
return {
|
|
1124
|
-
get: async (key: string) => {
|
|
1125
|
-
const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
|
|
1126
|
-
method: "POST",
|
|
1127
|
-
headers: this.headers(),
|
|
1128
|
-
});
|
|
1129
|
-
if (!response.ok) {
|
|
1130
|
-
return undefined;
|
|
1131
|
-
}
|
|
1132
|
-
const payload = (await response.json()) as { result?: string | null };
|
|
1133
|
-
return payload.result ?? undefined;
|
|
1134
|
-
},
|
|
1135
|
-
mget: async (keys: string[]) => {
|
|
1136
|
-
if (keys.length === 0) return [];
|
|
1137
|
-
const path = keys.map((k) => encodeURIComponent(k)).join("/");
|
|
1138
|
-
const response = await fetch(`${this.baseUrl}/mget/${path}`, {
|
|
1139
|
-
method: "POST",
|
|
1140
|
-
headers: this.headers(),
|
|
1141
|
-
});
|
|
1142
|
-
if (!response.ok) {
|
|
1143
|
-
return keys.map(() => undefined);
|
|
1144
|
-
}
|
|
1145
|
-
const payload = (await response.json()) as { result?: (string | null)[] };
|
|
1146
|
-
return (payload.result ?? []).map((v) => v ?? undefined);
|
|
1147
|
-
},
|
|
1148
|
-
set: async (key: string, value: string, ttl?: number) => {
|
|
1149
|
-
const command = typeof ttl === "number"
|
|
1150
|
-
? ["SETEX", key, Math.max(1, ttl), value]
|
|
1151
|
-
: ["SET", key, value];
|
|
1152
|
-
const response = await fetch(this.baseUrl, {
|
|
1153
|
-
method: "POST",
|
|
1154
|
-
headers: this.headers(),
|
|
1155
|
-
body: JSON.stringify(command),
|
|
1156
|
-
});
|
|
1157
|
-
if (!response.ok) {
|
|
1158
|
-
const text = await response.text().catch(() => "");
|
|
1159
|
-
console.error(`[store][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
|
|
1160
|
-
}
|
|
1161
|
-
},
|
|
1162
|
-
del: async (key: string) => {
|
|
1163
|
-
const response = await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
|
|
1164
|
-
method: "POST",
|
|
1165
|
-
headers: this.headers(),
|
|
1166
|
-
});
|
|
1167
|
-
if (!response.ok) {
|
|
1168
|
-
const text = await response.text().catch(() => "");
|
|
1169
|
-
console.error(`[store][upstash] DEL failed (${response.status}): ${text.slice(0, 200)}`);
|
|
1170
|
-
}
|
|
1171
|
-
},
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
class RedisLikeStateStore implements StateStore {
|
|
1177
|
-
private readonly memoryFallback: InMemoryStateStore;
|
|
1178
|
-
private readonly ttl?: number;
|
|
1179
|
-
private readonly clientPromise: Promise<
|
|
1180
|
-
| {
|
|
1181
|
-
get: (key: string) => Promise<string | null>;
|
|
1182
|
-
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
1183
|
-
del: (key: string) => Promise<unknown>;
|
|
1184
|
-
}
|
|
1185
|
-
| undefined
|
|
1186
|
-
>;
|
|
1187
|
-
|
|
1188
|
-
constructor(url: string, ttl?: number) {
|
|
1189
|
-
this.ttl = ttl;
|
|
1190
|
-
this.memoryFallback = new InMemoryStateStore(ttl);
|
|
1191
|
-
this.clientPromise = (async () => {
|
|
1192
|
-
try {
|
|
1193
|
-
const redisModule = (await import("redis")) as unknown as {
|
|
1194
|
-
createClient: (options: { url: string }) => {
|
|
1195
|
-
connect: () => Promise<unknown>;
|
|
1196
|
-
get: (key: string) => Promise<string | null>;
|
|
1197
|
-
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
1198
|
-
del: (key: string) => Promise<unknown>;
|
|
1199
|
-
};
|
|
1200
|
-
};
|
|
1201
|
-
const client = redisModule.createClient({ url });
|
|
1202
|
-
await client.connect();
|
|
1203
|
-
return client;
|
|
1204
|
-
} catch {
|
|
1205
|
-
return undefined;
|
|
1206
|
-
}
|
|
1207
|
-
})();
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
async get(runId: string): Promise<ConversationState | undefined> {
|
|
1211
|
-
const client = await this.clientPromise;
|
|
1212
|
-
if (!client) {
|
|
1213
|
-
return await this.memoryFallback.get(runId);
|
|
1214
|
-
}
|
|
1215
|
-
const raw = await client.get(runId);
|
|
1216
|
-
return raw ? (JSON.parse(raw) as ConversationState) : undefined;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
async set(state: ConversationState): Promise<void> {
|
|
1220
|
-
const client = await this.clientPromise;
|
|
1221
|
-
if (!client) {
|
|
1222
|
-
await this.memoryFallback.set(state);
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
const serialized = JSON.stringify({ ...state, updatedAt: Date.now() });
|
|
1226
|
-
if (typeof this.ttl === "number") {
|
|
1227
|
-
await client.set(state.runId, serialized, { EX: Math.max(1, this.ttl) });
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
await client.set(state.runId, serialized);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
async delete(runId: string): Promise<void> {
|
|
1234
|
-
const client = await this.clientPromise;
|
|
1235
|
-
if (!client) {
|
|
1236
|
-
await this.memoryFallback.delete(runId);
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
await client.del(runId);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
class RedisLikeConversationStore extends KeyValueConversationStoreBase {
|
|
1244
|
-
private readonly clientPromise: Promise<
|
|
1245
|
-
| {
|
|
1246
|
-
get: (key: string) => Promise<string | null>;
|
|
1247
|
-
mGet: (keys: readonly string[]) => Promise<(string | null)[]>;
|
|
1248
|
-
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
1249
|
-
del: (key: string) => Promise<unknown>;
|
|
1250
|
-
}
|
|
1251
|
-
| undefined
|
|
1252
|
-
>;
|
|
1253
|
-
|
|
1254
|
-
constructor(url: string, workingDir: string, ttl?: number, agentId?: string) {
|
|
1255
|
-
super(ttl, workingDir, agentId);
|
|
1256
|
-
this.clientPromise = (async () => {
|
|
1257
|
-
try {
|
|
1258
|
-
const redisModule = (await import("redis")) as unknown as {
|
|
1259
|
-
createClient: (options: { url: string }) => {
|
|
1260
|
-
connect: () => Promise<unknown>;
|
|
1261
|
-
get: (key: string) => Promise<string | null>;
|
|
1262
|
-
mGet: (keys: readonly string[]) => Promise<(string | null)[]>;
|
|
1263
|
-
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
1264
|
-
del: (key: string) => Promise<unknown>;
|
|
1265
|
-
};
|
|
1266
|
-
};
|
|
1267
|
-
const client = redisModule.createClient({ url });
|
|
1268
|
-
await client.connect();
|
|
1269
|
-
return client;
|
|
1270
|
-
} catch {
|
|
1271
|
-
return undefined;
|
|
1272
|
-
}
|
|
1273
|
-
})();
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
protected async client(): Promise<RawKeyValueClient | undefined> {
|
|
1277
|
-
const client = await this.clientPromise;
|
|
1278
|
-
if (!client) {
|
|
1279
|
-
return undefined;
|
|
1280
|
-
}
|
|
1281
|
-
return {
|
|
1282
|
-
get: async (key: string) => {
|
|
1283
|
-
const value = await client.get(key);
|
|
1284
|
-
return value ?? undefined;
|
|
1285
|
-
},
|
|
1286
|
-
mget: async (keys: string[]) => {
|
|
1287
|
-
if (keys.length === 0) return [];
|
|
1288
|
-
const values = await client.mGet(keys);
|
|
1289
|
-
return values.map((v: string | null) => v ?? undefined);
|
|
1290
|
-
},
|
|
1291
|
-
set: async (key: string, value: string, ttl?: number) => {
|
|
1292
|
-
if (typeof ttl === "number") {
|
|
1293
|
-
await client.set(key, value, { EX: Math.max(1, ttl) });
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
await client.set(key, value);
|
|
1297
|
-
},
|
|
1298
|
-
del: async (key: string) => {
|
|
1299
|
-
await client.del(key);
|
|
1300
|
-
},
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
class DynamoDbStateStore implements StateStore {
|
|
1306
|
-
private readonly memoryFallback: InMemoryStateStore;
|
|
1307
|
-
private readonly table: string;
|
|
1308
|
-
private readonly ttl?: number;
|
|
1309
|
-
private readonly clientPromise: Promise<
|
|
1310
|
-
| {
|
|
1311
|
-
send: (command: unknown) => Promise<unknown>;
|
|
1312
|
-
GetItemCommand: new (input: unknown) => unknown;
|
|
1313
|
-
PutItemCommand: new (input: unknown) => unknown;
|
|
1314
|
-
DeleteItemCommand: new (input: unknown) => unknown;
|
|
1315
|
-
}
|
|
1316
|
-
| undefined
|
|
1317
|
-
>;
|
|
1318
|
-
|
|
1319
|
-
constructor(table: string, region?: string, ttl?: number) {
|
|
1320
|
-
this.table = table;
|
|
1321
|
-
this.ttl = ttl;
|
|
1322
|
-
this.memoryFallback = new InMemoryStateStore(ttl);
|
|
1323
|
-
this.clientPromise = (async () => {
|
|
1324
|
-
try {
|
|
1325
|
-
const module = (await import("@aws-sdk/client-dynamodb")) as {
|
|
1326
|
-
DynamoDBClient: new (input: { region?: string }) => { send: (command: unknown) => Promise<unknown> };
|
|
1327
|
-
GetItemCommand: new (input: unknown) => unknown;
|
|
1328
|
-
PutItemCommand: new (input: unknown) => unknown;
|
|
1329
|
-
DeleteItemCommand: new (input: unknown) => unknown;
|
|
1330
|
-
};
|
|
1331
|
-
return {
|
|
1332
|
-
send: module.DynamoDBClient
|
|
1333
|
-
? new module.DynamoDBClient({ region }).send.bind(
|
|
1334
|
-
new module.DynamoDBClient({ region }),
|
|
1335
|
-
)
|
|
1336
|
-
: async () => ({}),
|
|
1337
|
-
GetItemCommand: module.GetItemCommand,
|
|
1338
|
-
PutItemCommand: module.PutItemCommand,
|
|
1339
|
-
DeleteItemCommand: module.DeleteItemCommand,
|
|
1340
|
-
};
|
|
1341
|
-
} catch {
|
|
1342
|
-
return undefined;
|
|
1343
|
-
}
|
|
1344
|
-
})();
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
async get(runId: string): Promise<ConversationState | undefined> {
|
|
1348
|
-
const client = await this.clientPromise;
|
|
1349
|
-
if (!client) {
|
|
1350
|
-
return await this.memoryFallback.get(runId);
|
|
1351
|
-
}
|
|
1352
|
-
const result = (await client.send(
|
|
1353
|
-
new client.GetItemCommand({
|
|
1354
|
-
TableName: this.table,
|
|
1355
|
-
Key: { runId: { S: runId } },
|
|
1356
|
-
}),
|
|
1357
|
-
)) as {
|
|
1358
|
-
Item?: {
|
|
1359
|
-
value?: { S?: string };
|
|
1360
|
-
};
|
|
1361
|
-
};
|
|
1362
|
-
const raw = result.Item?.value?.S;
|
|
1363
|
-
return raw ? (JSON.parse(raw) as ConversationState) : undefined;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
async set(state: ConversationState): Promise<void> {
|
|
1367
|
-
const client = await this.clientPromise;
|
|
1368
|
-
if (!client) {
|
|
1369
|
-
await this.memoryFallback.set(state);
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
const updatedState = { ...state, updatedAt: Date.now() };
|
|
1373
|
-
const ttlEpoch =
|
|
1374
|
-
typeof this.ttl === "number"
|
|
1375
|
-
? Math.floor(Date.now() / 1000) + Math.max(1, this.ttl)
|
|
1376
|
-
: undefined;
|
|
1377
|
-
await client.send(
|
|
1378
|
-
new client.PutItemCommand({
|
|
1379
|
-
TableName: this.table,
|
|
1380
|
-
Item: {
|
|
1381
|
-
runId: { S: state.runId },
|
|
1382
|
-
value: { S: JSON.stringify(updatedState) },
|
|
1383
|
-
...(typeof ttlEpoch === "number" ? { ttl: { N: String(ttlEpoch) } } : {}),
|
|
1384
|
-
},
|
|
1385
|
-
}),
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
async delete(runId: string): Promise<void> {
|
|
1390
|
-
const client = await this.clientPromise;
|
|
1391
|
-
if (!client) {
|
|
1392
|
-
await this.memoryFallback.delete(runId);
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
await client.send(
|
|
1396
|
-
new client.DeleteItemCommand({
|
|
1397
|
-
TableName: this.table,
|
|
1398
|
-
Key: { runId: { S: runId } },
|
|
1399
|
-
}),
|
|
1400
|
-
);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
class DynamoDbConversationStore extends KeyValueConversationStoreBase {
|
|
1405
|
-
private readonly table: string;
|
|
1406
|
-
private readonly clientPromise: Promise<
|
|
1407
|
-
| {
|
|
1408
|
-
send: (command: unknown) => Promise<unknown>;
|
|
1409
|
-
GetItemCommand: new (input: unknown) => unknown;
|
|
1410
|
-
PutItemCommand: new (input: unknown) => unknown;
|
|
1411
|
-
DeleteItemCommand: new (input: unknown) => unknown;
|
|
1412
|
-
}
|
|
1413
|
-
| undefined
|
|
1414
|
-
>;
|
|
1415
|
-
|
|
1416
|
-
constructor(table: string, workingDir: string, region?: string, ttl?: number, agentId?: string) {
|
|
1417
|
-
super(ttl, workingDir, agentId);
|
|
1418
|
-
this.table = table;
|
|
1419
|
-
this.clientPromise = (async () => {
|
|
1420
|
-
try {
|
|
1421
|
-
const module = (await import("@aws-sdk/client-dynamodb")) as {
|
|
1422
|
-
DynamoDBClient: new (input: { region?: string }) => { send: (command: unknown) => Promise<unknown> };
|
|
1423
|
-
GetItemCommand: new (input: unknown) => unknown;
|
|
1424
|
-
PutItemCommand: new (input: unknown) => unknown;
|
|
1425
|
-
DeleteItemCommand: new (input: unknown) => unknown;
|
|
1426
|
-
};
|
|
1427
|
-
const client = new module.DynamoDBClient({ region });
|
|
1428
|
-
return {
|
|
1429
|
-
send: client.send.bind(client),
|
|
1430
|
-
GetItemCommand: module.GetItemCommand,
|
|
1431
|
-
PutItemCommand: module.PutItemCommand,
|
|
1432
|
-
DeleteItemCommand: module.DeleteItemCommand,
|
|
1433
|
-
};
|
|
1434
|
-
} catch {
|
|
1435
|
-
return undefined;
|
|
1436
|
-
}
|
|
1437
|
-
})();
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
protected async client(): Promise<RawKeyValueClient | undefined> {
|
|
1441
|
-
const client = await this.clientPromise;
|
|
1442
|
-
if (!client) {
|
|
1443
|
-
return undefined;
|
|
1444
|
-
}
|
|
1445
|
-
return {
|
|
1446
|
-
get: async (key: string) => {
|
|
1447
|
-
const result = (await client.send(
|
|
1448
|
-
new client.GetItemCommand({
|
|
1449
|
-
TableName: this.table,
|
|
1450
|
-
Key: { runId: { S: key } },
|
|
1451
|
-
}),
|
|
1452
|
-
)) as {
|
|
1453
|
-
Item?: {
|
|
1454
|
-
value?: { S?: string };
|
|
1455
|
-
};
|
|
1456
|
-
};
|
|
1457
|
-
return result.Item?.value?.S;
|
|
1458
|
-
},
|
|
1459
|
-
set: async (key: string, value: string, ttl?: number) => {
|
|
1460
|
-
const ttlEpoch =
|
|
1461
|
-
typeof ttl === "number" ? Math.floor(Date.now() / 1000) + Math.max(1, ttl) : undefined;
|
|
1462
|
-
await client.send(
|
|
1463
|
-
new client.PutItemCommand({
|
|
1464
|
-
TableName: this.table,
|
|
1465
|
-
Item: {
|
|
1466
|
-
runId: { S: key },
|
|
1467
|
-
value: { S: value },
|
|
1468
|
-
...(typeof ttlEpoch === "number" ? { ttl: { N: String(ttlEpoch) } } : {}),
|
|
1469
|
-
},
|
|
1470
|
-
}),
|
|
1471
|
-
);
|
|
1472
|
-
},
|
|
1473
|
-
mget: async (keys: string[]) => {
|
|
1474
|
-
if (keys.length === 0) return [];
|
|
1475
|
-
return Promise.all(keys.map(async (key) => {
|
|
1476
|
-
const result = (await client.send(
|
|
1477
|
-
new client.GetItemCommand({
|
|
1478
|
-
TableName: this.table,
|
|
1479
|
-
Key: { runId: { S: key } },
|
|
1480
|
-
}),
|
|
1481
|
-
)) as { Item?: { value?: { S?: string } } };
|
|
1482
|
-
return result.Item?.value?.S;
|
|
1483
|
-
}));
|
|
1484
|
-
},
|
|
1485
|
-
del: async (key: string) => {
|
|
1486
|
-
await client.send(
|
|
1487
|
-
new client.DeleteItemCommand({
|
|
1488
|
-
TableName: this.table,
|
|
1489
|
-
Key: { runId: { S: key } },
|
|
1490
|
-
}),
|
|
1491
|
-
);
|
|
1492
|
-
},
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Legacy factories — return InMemory stores. The harness now uses
|
|
274
|
+
// engine-backed stores via storage/store-adapters.ts. These factories
|
|
275
|
+
// exist only for backward compatibility with external callers and tests.
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
1496
277
|
|
|
1497
278
|
export const createStateStore = (
|
|
1498
279
|
config?: StateConfig,
|
|
1499
|
-
|
|
280
|
+
_options?: { workingDir?: string; agentId?: string },
|
|
1500
281
|
): StateStore => {
|
|
1501
|
-
const provider = config?.provider ?? "local";
|
|
1502
282
|
const ttl = config?.ttl;
|
|
1503
|
-
const workingDir = options?.workingDir ?? process.cwd();
|
|
1504
|
-
if (provider === "local") {
|
|
1505
|
-
return new FileStateStore(workingDir, ttl, options?.agentId);
|
|
1506
|
-
}
|
|
1507
|
-
if (provider === "memory") {
|
|
1508
|
-
return new InMemoryStateStore(ttl);
|
|
1509
|
-
}
|
|
1510
|
-
if (provider === "upstash") {
|
|
1511
|
-
const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
|
|
1512
|
-
const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
|
|
1513
|
-
const url = process.env[urlEnv] ?? "";
|
|
1514
|
-
const token = process.env[tokenEnv] ?? "";
|
|
1515
|
-
if (url && token) {
|
|
1516
|
-
return new UpstashStateStore(url, token, ttl);
|
|
1517
|
-
}
|
|
1518
|
-
return new InMemoryStateStore(ttl);
|
|
1519
|
-
}
|
|
1520
|
-
if (provider === "redis") {
|
|
1521
|
-
const urlEnv = config?.urlEnv ?? "REDIS_URL";
|
|
1522
|
-
const url = process.env[urlEnv] ?? "";
|
|
1523
|
-
if (url) {
|
|
1524
|
-
return new RedisLikeStateStore(url, ttl);
|
|
1525
|
-
}
|
|
1526
|
-
return new InMemoryStateStore(ttl);
|
|
1527
|
-
}
|
|
1528
|
-
if (provider === "dynamodb") {
|
|
1529
|
-
const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
|
|
1530
|
-
if (table) {
|
|
1531
|
-
return new DynamoDbStateStore(table, config?.region as string | undefined, ttl);
|
|
1532
|
-
}
|
|
1533
|
-
return new InMemoryStateStore(ttl);
|
|
1534
|
-
}
|
|
1535
283
|
return new InMemoryStateStore(ttl);
|
|
1536
284
|
};
|
|
1537
285
|
|
|
1538
286
|
export const createConversationStore = (
|
|
1539
287
|
config?: StateConfig,
|
|
1540
|
-
|
|
288
|
+
_options?: { workingDir?: string; agentId?: string },
|
|
1541
289
|
): ConversationStore => {
|
|
1542
|
-
const provider = config?.provider ?? "local";
|
|
1543
290
|
const ttl = config?.ttl;
|
|
1544
|
-
const workingDir = options?.workingDir ?? process.cwd();
|
|
1545
|
-
if (provider === "local") {
|
|
1546
|
-
return new FileConversationStore(workingDir, options?.agentId);
|
|
1547
|
-
}
|
|
1548
|
-
if (provider === "memory") {
|
|
1549
|
-
return new InMemoryConversationStore(ttl);
|
|
1550
|
-
}
|
|
1551
|
-
if (provider === "upstash") {
|
|
1552
|
-
const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
|
|
1553
|
-
const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
|
|
1554
|
-
const url = process.env[urlEnv] ?? "";
|
|
1555
|
-
const token = process.env[tokenEnv] ?? "";
|
|
1556
|
-
if (url && token) {
|
|
1557
|
-
return new UpstashConversationStore(url, token, workingDir, ttl, options?.agentId);
|
|
1558
|
-
}
|
|
1559
|
-
return new InMemoryConversationStore(ttl);
|
|
1560
|
-
}
|
|
1561
|
-
if (provider === "redis") {
|
|
1562
|
-
const urlEnv = config?.urlEnv ?? "REDIS_URL";
|
|
1563
|
-
const url = process.env[urlEnv] ?? "";
|
|
1564
|
-
if (url) {
|
|
1565
|
-
return new RedisLikeConversationStore(url, workingDir, ttl, options?.agentId);
|
|
1566
|
-
}
|
|
1567
|
-
return new InMemoryConversationStore(ttl);
|
|
1568
|
-
}
|
|
1569
|
-
if (provider === "dynamodb") {
|
|
1570
|
-
const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
|
|
1571
|
-
if (table) {
|
|
1572
|
-
return new DynamoDbConversationStore(
|
|
1573
|
-
table,
|
|
1574
|
-
workingDir,
|
|
1575
|
-
config?.region as string | undefined,
|
|
1576
|
-
ttl,
|
|
1577
|
-
options?.agentId,
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1580
|
-
return new InMemoryConversationStore(ttl);
|
|
1581
|
-
}
|
|
1582
291
|
return new InMemoryConversationStore(ttl);
|
|
1583
292
|
};
|