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