@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.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +12 -11
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.d.ts +485 -29
  4. package/dist/index.js +2839 -2114
  5. package/dist/isolate-TCWTUVG4.js +1532 -0
  6. package/package.json +23 -4
  7. package/scripts/migrate-to-engine.mjs +556 -0
  8. package/src/config.ts +106 -1
  9. package/src/harness.ts +226 -91
  10. package/src/index.ts +5 -0
  11. package/src/isolate/bindings.ts +206 -0
  12. package/src/isolate/bundler.ts +179 -0
  13. package/src/isolate/index.ts +10 -0
  14. package/src/isolate/polyfills.ts +796 -0
  15. package/src/isolate/run-code-tool.ts +220 -0
  16. package/src/isolate/runtime.ts +286 -0
  17. package/src/isolate/type-stubs.ts +196 -0
  18. package/src/memory.ts +129 -198
  19. package/src/reminder-store.ts +3 -237
  20. package/src/secrets-store.ts +2 -91
  21. package/src/state.ts +11 -1302
  22. package/src/storage/engine.ts +106 -0
  23. package/src/storage/index.ts +59 -0
  24. package/src/storage/memory-engine.ts +588 -0
  25. package/src/storage/postgres-engine.ts +139 -0
  26. package/src/storage/schema.ts +145 -0
  27. package/src/storage/sql-dialect.ts +963 -0
  28. package/src/storage/sqlite-engine.ts +99 -0
  29. package/src/storage/store-adapters.ts +100 -0
  30. package/src/todo-tools.ts +1 -136
  31. package/src/upload-store.ts +1 -0
  32. package/src/vfs/bash-manager.ts +120 -0
  33. package/src/vfs/bash-tool.ts +59 -0
  34. package/src/vfs/create-bash-fs.ts +32 -0
  35. package/src/vfs/edit-file-tool.ts +72 -0
  36. package/src/vfs/index.ts +5 -0
  37. package/src/vfs/poncho-fs-adapter.ts +267 -0
  38. package/src/vfs/protected-fs.ts +177 -0
  39. package/src/vfs/read-file-tool.ts +103 -0
  40. package/src/vfs/write-file-tool.ts +49 -0
  41. package/test/harness.test.ts +30 -36
  42. package/test/isolate-vfs.test.ts +453 -0
  43. package/test/isolate.test.ts +252 -0
  44. package/test/state.test.ts +4 -27
  45. package/test/storage-engine.test.ts +250 -0
  46. package/test/vfs.test.ts +242 -0
  47. package/.turbo/turbo-lint.log +0 -6
  48. package/.turbo/turbo-test.log +0 -11931
  49. 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
- type ConversationStoreFile = {
392
- schemaVersion: string;
393
- conversations: Array<{
394
- conversationId: string;
395
- title: string;
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
- options?: { workingDir?: string; agentId?: string },
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
- options?: { workingDir?: string; agentId?: string },
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
  };