@poncho-ai/harness 0.35.0 → 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.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +6 -5
  2. package/.turbo/turbo-test.log +15169 -0
  3. package/CHANGELOG.md +18 -0
  4. package/dist/chunk-MCKGQKYU.js +15 -0
  5. package/dist/dist-3KMQR4IO.js +27092 -0
  6. package/dist/index.d.ts +485 -29
  7. package/dist/index.js +2839 -2114
  8. package/dist/isolate-5MISBSUK.js +733 -0
  9. package/dist/isolate-5R6762YA.js +605 -0
  10. package/dist/isolate-KUZ5NOPG.js +727 -0
  11. package/dist/isolate-LOL3T7RA.js +729 -0
  12. package/dist/isolate-N22X4TCE.js +740 -0
  13. package/dist/isolate-T7WXM7IL.js +1490 -0
  14. package/dist/isolate-TCWTUVG4.js +1532 -0
  15. package/dist/isolate-WFOLANOB.js +768 -0
  16. package/package.json +22 -3
  17. package/scripts/migrate-to-engine.mjs +556 -0
  18. package/src/config.ts +106 -1
  19. package/src/harness.ts +226 -91
  20. package/src/index.ts +5 -0
  21. package/src/isolate/bindings.ts +206 -0
  22. package/src/isolate/bundler.ts +179 -0
  23. package/src/isolate/index.ts +10 -0
  24. package/src/isolate/polyfills.ts +796 -0
  25. package/src/isolate/run-code-tool.ts +220 -0
  26. package/src/isolate/runtime.ts +286 -0
  27. package/src/isolate/type-stubs.ts +196 -0
  28. package/src/memory.ts +129 -198
  29. package/src/reminder-store.ts +3 -237
  30. package/src/secrets-store.ts +2 -91
  31. package/src/state.ts +11 -1302
  32. package/src/storage/engine.ts +106 -0
  33. package/src/storage/index.ts +59 -0
  34. package/src/storage/memory-engine.ts +588 -0
  35. package/src/storage/postgres-engine.ts +139 -0
  36. package/src/storage/schema.ts +145 -0
  37. package/src/storage/sql-dialect.ts +963 -0
  38. package/src/storage/sqlite-engine.ts +99 -0
  39. package/src/storage/store-adapters.ts +100 -0
  40. package/src/todo-tools.ts +1 -136
  41. package/src/upload-store.ts +1 -0
  42. package/src/vfs/bash-manager.ts +120 -0
  43. package/src/vfs/bash-tool.ts +59 -0
  44. package/src/vfs/create-bash-fs.ts +32 -0
  45. package/src/vfs/edit-file-tool.ts +72 -0
  46. package/src/vfs/index.ts +5 -0
  47. package/src/vfs/poncho-fs-adapter.ts +267 -0
  48. package/src/vfs/protected-fs.ts +177 -0
  49. package/src/vfs/read-file-tool.ts +103 -0
  50. package/src/vfs/write-file-tool.ts +49 -0
  51. package/test/harness.test.ts +30 -36
  52. package/test/isolate-vfs.test.ts +453 -0
  53. package/test/isolate.test.ts +252 -0
  54. package/test/state.test.ts +4 -27
  55. package/test/storage-engine.test.ts +250 -0
  56. package/test/vfs.test.ts +242 -0
  57. package/src/kv-store.ts +0 -216
package/src/memory.ts CHANGED
@@ -1,14 +1,5 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
1
  import { defineTool, type ToolContext, type ToolDefinition } from "@poncho-ai/sdk";
4
2
  import type { StateProviderName } from "./state.js";
5
- import {
6
- ensureAgentIdentity,
7
- getAgentStoreDirectory,
8
- slugifyStorageComponent,
9
- STORAGE_SCHEMA_VERSION,
10
- } from "./agent-identity.js";
11
- import { createRawKVStore, type RawKVStore } from "./kv-store.js";
12
3
 
13
4
  export interface MainMemory {
14
5
  content: string;
@@ -31,10 +22,6 @@ export interface MemoryStore {
31
22
  updateMainMemory(input: { content: string }): Promise<MainMemory>;
32
23
  }
33
24
 
34
- type MainMemoryPayload = {
35
- main: MainMemory;
36
- };
37
-
38
25
  type RecallItem = {
39
26
  conversationId: string;
40
27
  title: string;
@@ -42,18 +29,27 @@ type RecallItem = {
42
29
  content: string;
43
30
  };
44
31
 
32
+ type ConversationListItem = {
33
+ conversationId: string;
34
+ title: string;
35
+ createdAt?: number;
36
+ updatedAt: number;
37
+ messageCount?: number;
38
+ };
39
+
40
+ type ConversationDetail = {
41
+ conversationId: string;
42
+ title: string;
43
+ createdAt?: number;
44
+ updatedAt: number;
45
+ messages: Array<{ role: string; content: string }>;
46
+ };
47
+
48
+
45
49
  const DEFAULT_MAIN_MEMORY: MainMemory = {
46
50
  content: "",
47
51
  updatedAt: 0,
48
52
  };
49
- const LOCAL_MEMORY_FILE = "memory.json";
50
-
51
- const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
52
- await mkdir(dirname(filePath), { recursive: true });
53
- const tmpPath = `${filePath}.tmp`;
54
- await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
55
- await rename(tmpPath, filePath);
56
- };
57
53
 
58
54
  const scoreText = (text: string, query: string): number => {
59
55
  const normalized = query.trim().toLowerCase();
@@ -99,167 +95,12 @@ class InMemoryMemoryStore implements MemoryStore {
99
95
  }
100
96
  }
101
97
 
102
- class FileMainMemoryStore implements MemoryStore {
103
- private readonly workingDir: string;
104
- private filePath = "";
105
- private readonly customRelPath?: string;
106
- private readonly ttlMs?: number;
107
- private loaded = false;
108
- private writing = Promise.resolve();
109
- private mainMemory: MainMemory = { ...DEFAULT_MAIN_MEMORY };
110
-
111
- constructor(workingDir: string, ttlSeconds?: number, customRelPath?: string) {
112
- this.workingDir = workingDir;
113
- this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1000 : undefined;
114
- this.customRelPath = customRelPath;
115
- }
116
-
117
- private async ensureFilePath(): Promise<void> {
118
- if (this.filePath) {
119
- return;
120
- }
121
- const identity = await ensureAgentIdentity(this.workingDir);
122
- this.filePath = resolve(
123
- getAgentStoreDirectory(identity),
124
- this.customRelPath ?? LOCAL_MEMORY_FILE,
125
- );
126
- }
127
-
128
- private isExpired(updatedAt: number): boolean {
129
- return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
130
- }
131
-
132
- private async ensureLoaded(): Promise<void> {
133
- await this.ensureFilePath();
134
- if (this.loaded) {
135
- return;
136
- }
137
- this.loaded = true;
138
- try {
139
- const raw = await readFile(this.filePath, "utf8");
140
- const parsed = JSON.parse(raw) as MainMemoryPayload;
141
- const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
142
- const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
143
- this.mainMemory = { content, updatedAt };
144
- } catch {
145
- // Missing or invalid file should not crash local mode.
146
- }
147
- }
148
-
149
- private async persist(): Promise<void> {
150
- const payload: MainMemoryPayload = { main: this.mainMemory };
151
- this.writing = this.writing.then(async () => {
152
- await writeJsonAtomic(this.filePath, payload);
153
- });
154
- await this.writing;
155
- }
156
-
157
- async getMainMemory(): Promise<MainMemory> {
158
- await this.ensureLoaded();
159
- if (this.mainMemory.updatedAt > 0 && this.isExpired(this.mainMemory.updatedAt)) {
160
- this.mainMemory = { ...DEFAULT_MAIN_MEMORY };
161
- await this.persist();
162
- }
163
- return this.mainMemory;
164
- }
165
-
166
- async updateMainMemory(input: { content: string }): Promise<MainMemory> {
167
- await this.ensureLoaded();
168
- this.mainMemory = {
169
- content: input.content.trim(),
170
- updatedAt: Date.now(),
171
- };
172
- await this.persist();
173
- return this.mainMemory;
174
- }
175
- }
176
-
177
- class KVBackedMemoryStore implements MemoryStore {
178
- private readonly kv: RawKVStore;
179
- private readonly storageKey: string;
180
- private readonly ttl?: number;
181
- private readonly memoryFallback: InMemoryMemoryStore;
182
-
183
- constructor(kv: RawKVStore, storageKey: string, ttl?: number) {
184
- this.kv = kv;
185
- this.storageKey = storageKey;
186
- this.ttl = ttl;
187
- this.memoryFallback = new InMemoryMemoryStore(ttl);
188
- }
189
-
190
- private async readPayload(): Promise<MainMemoryPayload> {
191
- try {
192
- const raw = await this.kv.get(this.storageKey);
193
- if (!raw) return { main: { ...DEFAULT_MAIN_MEMORY } };
194
- const parsed = JSON.parse(raw) as MainMemoryPayload;
195
- const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
196
- const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
197
- return { main: { content, updatedAt } };
198
- } catch {
199
- const main = await this.memoryFallback.getMainMemory();
200
- return { main };
201
- }
202
- }
203
-
204
- private async writePayload(payload: MainMemoryPayload): Promise<void> {
205
- try {
206
- const serialized = JSON.stringify(payload);
207
- if (typeof this.ttl === "number") {
208
- await this.kv.setWithTtl(this.storageKey, serialized, Math.max(1, this.ttl));
209
- } else {
210
- await this.kv.set(this.storageKey, serialized);
211
- }
212
- } catch {
213
- await this.memoryFallback.updateMainMemory({ content: payload.main.content });
214
- }
215
- }
216
-
217
- async getMainMemory(): Promise<MainMemory> {
218
- const payload = await this.readPayload();
219
- return payload.main;
220
- }
221
-
222
- async updateMainMemory(input: { content: string }): Promise<MainMemory> {
223
- const payload = await this.readPayload();
224
- payload.main = { content: input.content.trim(), updatedAt: Date.now() };
225
- await this.writePayload(payload);
226
- return payload.main;
227
- }
228
- }
229
-
230
98
  export const createMemoryStore = (
231
99
  agentId: string,
232
100
  config?: MemoryConfig,
233
101
  options?: { workingDir?: string; tenantId?: string },
234
102
  ): MemoryStore => {
235
- const provider = config?.provider ?? "local";
236
103
  const ttl = config?.ttl;
237
- const workingDir = options?.workingDir ?? process.cwd();
238
- const tenantId = options?.tenantId;
239
-
240
- if (provider === "local") {
241
- if (tenantId) {
242
- // Tenant-scoped memory: store under tenants/{tenantId}/memory.json
243
- return new FileMainMemoryStore(
244
- workingDir,
245
- ttl,
246
- `tenants/${slugifyStorageComponent(tenantId)}/${LOCAL_MEMORY_FILE}`,
247
- );
248
- }
249
- return new FileMainMemoryStore(workingDir, ttl);
250
- }
251
- if (provider === "memory") {
252
- return new InMemoryMemoryStore(ttl);
253
- }
254
-
255
- const kv = createRawKVStore(config);
256
- if (kv) {
257
- const base = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
258
- const storageKey = tenantId
259
- ? `${base}:t:${slugifyStorageComponent(tenantId)}:memory:main`
260
- : `${base}:memory:main`;
261
- return new KVBackedMemoryStore(kv, storageKey, ttl);
262
- }
263
104
  return new InMemoryMemoryStore(ttl);
264
105
  };
265
106
 
@@ -397,56 +238,146 @@ export const createMemoryTools = (
397
238
  defineTool({
398
239
  name: "conversation_recall",
399
240
  description:
400
- "Recall relevant snippets from previous conversations when prior context is likely important (for example: 'as we discussed', 'last time', or ambiguous references).",
241
+ "Recall past conversations. Three modes:\n" +
242
+ "- Search: provide 'query' to keyword-search past conversations for relevant snippets.\n" +
243
+ "- List: provide 'after' and/or 'before' dates to browse conversations by date range.\n" +
244
+ "- Fetch: provide 'conversationId' to load the full message history of a specific conversation.\n" +
245
+ "Modes can be combined (e.g. query + date range to search within a time window).",
401
246
  inputSchema: {
402
247
  type: "object",
403
248
  properties: {
404
249
  query: {
405
250
  type: "string",
406
- description: "Search query for past conversation recall",
251
+ description: "Keyword search query for past conversations",
407
252
  },
408
- limit: {
409
- type: "number",
410
- description: "Maximum snippets to return",
253
+ after: {
254
+ type: "string",
255
+ description:
256
+ "ISO 8601 date string. Only return conversations updated after this date (e.g. '2025-03-01').",
411
257
  },
412
- excludeConversationId: {
258
+ before: {
413
259
  type: "string",
414
- description: "Optional conversation id to exclude from recall",
260
+ description:
261
+ "ISO 8601 date string. Only return conversations updated before this date.",
262
+ },
263
+ conversationId: {
264
+ type: "string",
265
+ description: "Fetch the full message history of a specific conversation by ID",
266
+ },
267
+ lastN: {
268
+ type: "number",
269
+ description: "When fetching a conversation, only return the last N messages (max 50)",
270
+ },
271
+ limit: {
272
+ type: "number",
273
+ description: "Maximum results to return (default 20 for list, 3 for search, max 50)",
415
274
  },
416
275
  },
417
- required: ["query"],
418
276
  additionalProperties: false,
419
277
  },
420
278
  handler: async (input, context) => {
421
279
  const query = typeof input.query === "string" ? input.query.trim() : "";
422
- if (!query) {
423
- throw new Error("query is required");
280
+ const after = typeof input.after === "string" ? input.after : "";
281
+ const before = typeof input.before === "string" ? input.before : "";
282
+ const fetchId = typeof input.conversationId === "string" ? input.conversationId.trim() : "";
283
+ const hasDateFilter = after !== "" || before !== "";
284
+
285
+ // Determine mode
286
+ const mode = fetchId ? "fetch" : (query && !hasDateFilter) ? "search" : "list";
287
+
288
+ // --- Fetch mode: load full conversation by ID ---
289
+ if (mode === "fetch") {
290
+ const rawFetchFn = context.parameters.__conversationFetchFn;
291
+ if (typeof rawFetchFn !== "function") {
292
+ throw new Error("Conversation fetching is not available in this environment.");
293
+ }
294
+ const conversation = (await (rawFetchFn as (id: string) => Promise<unknown>)(fetchId)) as
295
+ | ConversationDetail
296
+ | undefined;
297
+ if (!conversation) {
298
+ throw new Error(`Conversation '${fetchId}' not found.`);
299
+ }
300
+ const lastN = typeof input.lastN === "number" ? Math.max(1, Math.min(50, input.lastN)) : undefined;
301
+ const messages = lastN
302
+ ? conversation.messages.slice(-lastN)
303
+ : conversation.messages.slice(-50);
304
+ return {
305
+ mode: "fetch",
306
+ conversationId: conversation.conversationId,
307
+ title: conversation.title,
308
+ createdAt: conversation.createdAt
309
+ ? new Date(conversation.createdAt).toISOString()
310
+ : undefined,
311
+ updatedAt: new Date(conversation.updatedAt).toISOString(),
312
+ messageCount: conversation.messages.length,
313
+ messages: messages.map((m) => ({
314
+ role: m.role,
315
+ content: m.content.slice(0, 4000),
316
+ })),
317
+ };
424
318
  }
319
+
320
+ // --- List mode: browse by date, optionally with keyword filtering ---
321
+ if (mode === "list") {
322
+ const rawListFn = context.parameters.__conversationListFn;
323
+ if (typeof rawListFn !== "function") {
324
+ throw new Error("Conversation listing is not available in this environment.");
325
+ }
326
+ const allConversations = (await (rawListFn as () => Promise<unknown>)()) as ConversationListItem[];
327
+ const limit = Math.max(1, Math.min(50, typeof input.limit === "number" ? input.limit : 20));
328
+ const afterMs = after ? new Date(after).getTime() : 0;
329
+ const beforeMs = before ? new Date(before).getTime() : Infinity;
330
+
331
+ let filtered = allConversations.filter((item) => {
332
+ const ts = item.updatedAt;
333
+ return ts >= afterMs && ts <= beforeMs;
334
+ });
335
+
336
+ // If query is also provided, score and rank by relevance
337
+ if (query) {
338
+ filtered = filtered
339
+ .map((item) => ({
340
+ ...item,
341
+ _score: scoreText(item.title, query),
342
+ }))
343
+ .filter((item) => item._score > 0)
344
+ .sort((a, b) => {
345
+ if (b._score === a._score) return b.updatedAt - a.updatedAt;
346
+ return b._score - a._score;
347
+ });
348
+ }
349
+
350
+ return {
351
+ mode: "list",
352
+ conversations: filtered.slice(0, limit).map((item) => ({
353
+ conversationId: item.conversationId,
354
+ title: item.title,
355
+ createdAt: item.createdAt
356
+ ? new Date(item.createdAt).toISOString()
357
+ : undefined,
358
+ updatedAt: new Date(item.updatedAt).toISOString(),
359
+ messageCount: item.messageCount,
360
+ })),
361
+ };
362
+ }
363
+
364
+ // --- Search mode: keyword search across conversation content ---
425
365
  const limit = Math.max(
426
366
  1,
427
- Math.min(5, typeof input.limit === "number" ? input.limit : 3),
367
+ Math.min(10, typeof input.limit === "number" ? input.limit : 3),
428
368
  );
429
- const excludeConversationId =
430
- typeof input.excludeConversationId === "string"
431
- ? input.excludeConversationId
432
- : "";
433
369
  const rawCorpus = context.parameters.__conversationRecallCorpus;
434
370
  const resolvedCorpus =
435
371
  typeof rawCorpus === "function" ? await (rawCorpus as () => Promise<unknown>)() : rawCorpus;
436
372
  const corpus = asRecallCorpus(resolvedCorpus).slice(0, maxRecallConversations);
437
373
  const results = corpus
438
- .filter((item) =>
439
- excludeConversationId ? item.conversationId !== excludeConversationId : true,
440
- )
441
374
  .map((item) => ({
442
375
  ...item,
443
376
  score: scoreText(`${item.title}\n${item.content}`, query),
444
377
  }))
445
378
  .filter((item) => item.score > 0)
446
379
  .sort((a, b) => {
447
- if (b.score === a.score) {
448
- return b.updatedAt - a.updatedAt;
449
- }
380
+ if (b.score === a.score) return b.updatedAt - a.updatedAt;
450
381
  return b.score - a.score;
451
382
  })
452
383
  .slice(0, limit)
@@ -456,7 +387,7 @@ export const createMemoryTools = (
456
387
  updatedAt: item.updatedAt,
457
388
  snippet: buildRecallSnippet(item.content, query),
458
389
  }));
459
- return { results };
390
+ return { mode: "search", results };
460
391
  },
461
392
  }),
462
393
  ];
@@ -1,13 +1,4 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
1
  import type { StateConfig } from "./state.js";
4
- import {
5
- ensureAgentIdentity,
6
- getAgentStoreDirectory,
7
- slugifyStorageComponent,
8
- STORAGE_SCHEMA_VERSION,
9
- } from "./agent-identity.js";
10
- import { createRawKVStore, type RawKVStore } from "./kv-store.js";
11
2
 
12
3
  // ---------------------------------------------------------------------------
13
4
  // Data model
@@ -45,29 +36,8 @@ export interface ReminderStore {
45
36
  // Helpers
46
37
  // ---------------------------------------------------------------------------
47
38
 
48
- const REMINDERS_FILE = "reminders.json";
49
39
  const STALE_CANCELLED_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
50
40
 
51
- const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
52
- await mkdir(dirname(filePath), { recursive: true });
53
- const tmpPath = `${filePath}.tmp`;
54
- await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
55
- await rename(tmpPath, filePath);
56
- };
57
-
58
- const isValidReminder = (item: unknown): item is Reminder =>
59
- typeof item === "object" &&
60
- item !== null &&
61
- typeof (item as Record<string, unknown>).id === "string" &&
62
- typeof (item as Record<string, unknown>).task === "string" &&
63
- typeof (item as Record<string, unknown>).scheduledAt === "number" &&
64
- typeof (item as Record<string, unknown>).status === "string";
65
-
66
- const parseReminderList = (raw: unknown): Reminder[] => {
67
- if (!Array.isArray(raw)) return [];
68
- return raw.filter(isValidReminder);
69
- };
70
-
71
41
  /** Remove all fired reminders and cancelled reminders older than 7 days. */
72
42
  const pruneStale = (reminders: Reminder[]): Reminder[] => {
73
43
  const cutoff = Date.now() - STALE_CANCELLED_MS;
@@ -132,218 +102,14 @@ class InMemoryReminderStore implements ReminderStore {
132
102
  }
133
103
  }
134
104
 
135
- // ---------------------------------------------------------------------------
136
- // FileReminderStore — single JSON file for all reminders
137
- // ---------------------------------------------------------------------------
138
-
139
- class FileReminderStore implements ReminderStore {
140
- private readonly workingDir: string;
141
- private filePath = "";
142
-
143
- constructor(workingDir: string) {
144
- this.workingDir = workingDir;
145
- }
146
-
147
- private async ensureFilePath(): Promise<string> {
148
- if (this.filePath) return this.filePath;
149
- const identity = await ensureAgentIdentity(this.workingDir);
150
- this.filePath = resolve(getAgentStoreDirectory(identity), REMINDERS_FILE);
151
- return this.filePath;
152
- }
153
-
154
- private async readAll(): Promise<Reminder[]> {
155
- try {
156
- const fp = await this.ensureFilePath();
157
- const raw = await readFile(fp, "utf8");
158
- return parseReminderList(JSON.parse(raw));
159
- } catch {
160
- return [];
161
- }
162
- }
163
-
164
- private async writeAll(reminders: Reminder[]): Promise<void> {
165
- const fp = await this.ensureFilePath();
166
- await writeJsonAtomic(fp, reminders);
167
- }
168
-
169
- async list(): Promise<Reminder[]> {
170
- const all = await this.readAll();
171
- const pruned = pruneStale(all);
172
- if (pruned.length !== all.length) await this.writeAll(pruned);
173
- return pruned;
174
- }
175
-
176
- async create(input: {
177
- task: string;
178
- scheduledAt: number;
179
- timezone?: string;
180
- conversationId: string;
181
- ownerId?: string;
182
- tenantId?: string | null;
183
- }): Promise<Reminder> {
184
- const reminder: Reminder = {
185
- id: generateId(),
186
- task: input.task,
187
- scheduledAt: input.scheduledAt,
188
- timezone: input.timezone,
189
- status: "pending",
190
- createdAt: Date.now(),
191
- conversationId: input.conversationId,
192
- ownerId: input.ownerId,
193
- tenantId: input.tenantId,
194
- };
195
- let reminders = await this.readAll();
196
- reminders = pruneStale(reminders);
197
- reminders.push(reminder);
198
- await this.writeAll(reminders);
199
- return reminder;
200
- }
201
-
202
- async cancel(id: string): Promise<Reminder> {
203
- const reminders = await this.readAll();
204
- const reminder = reminders.find((r) => r.id === id);
205
- if (!reminder) throw new Error(`Reminder "${id}" not found`);
206
- if (reminder.status !== "pending") {
207
- throw new Error(`Reminder "${id}" is already ${reminder.status}`);
208
- }
209
- reminder.status = "cancelled";
210
- await this.writeAll(reminders);
211
- return reminder;
212
- }
213
-
214
- async delete(id: string): Promise<void> {
215
- const reminders = await this.readAll();
216
- await this.writeAll(reminders.filter((r) => r.id !== id));
217
- }
218
- }
219
-
220
- // ---------------------------------------------------------------------------
221
- // KVBackedReminderStore — wraps any RawKVStore (Upstash, Redis, DynamoDB)
222
- // ---------------------------------------------------------------------------
223
-
224
- class KVBackedReminderStore implements ReminderStore {
225
- private readonly kv: RawKVStore;
226
- private readonly key: string;
227
- private readonly ttl?: number;
228
- private readonly memoryFallback = new InMemoryReminderStore();
229
-
230
- constructor(kv: RawKVStore, key: string, ttl?: number) {
231
- this.kv = kv;
232
- this.key = key;
233
- this.ttl = ttl;
234
- }
235
-
236
- private async readAll(): Promise<Reminder[]> {
237
- try {
238
- const raw = await this.kv.get(this.key);
239
- if (!raw) return [];
240
- return parseReminderList(JSON.parse(raw));
241
- } catch {
242
- return this.memoryFallback.list();
243
- }
244
- }
245
-
246
- private async writeAll(reminders: Reminder[]): Promise<void> {
247
- try {
248
- const serialized = JSON.stringify(reminders);
249
- if (typeof this.ttl === "number") {
250
- await this.kv.setWithTtl(this.key, serialized, Math.max(1, this.ttl));
251
- } else {
252
- await this.kv.set(this.key, serialized);
253
- }
254
- } catch {
255
- // KV write failed; operations already applied in-memory via caller
256
- }
257
- }
258
-
259
- async list(): Promise<Reminder[]> {
260
- const all = await this.readAll();
261
- const pruned = pruneStale(all);
262
- if (pruned.length !== all.length) await this.writeAll(pruned);
263
- return pruned;
264
- }
265
-
266
- async create(input: {
267
- task: string;
268
- scheduledAt: number;
269
- timezone?: string;
270
- conversationId: string;
271
- ownerId?: string;
272
- }): Promise<Reminder> {
273
- let reminders: Reminder[];
274
- try {
275
- reminders = await this.readAll();
276
- } catch {
277
- return this.memoryFallback.create(input);
278
- }
279
- const reminder: Reminder = {
280
- id: generateId(),
281
- task: input.task,
282
- scheduledAt: input.scheduledAt,
283
- timezone: input.timezone,
284
- status: "pending",
285
- createdAt: Date.now(),
286
- conversationId: input.conversationId,
287
- ownerId: input.ownerId,
288
- };
289
- reminders = pruneStale(reminders);
290
- reminders.push(reminder);
291
- await this.writeAll(reminders);
292
- return reminder;
293
- }
294
-
295
- async cancel(id: string): Promise<Reminder> {
296
- let reminders: Reminder[];
297
- try {
298
- reminders = await this.readAll();
299
- } catch {
300
- return this.memoryFallback.cancel(id);
301
- }
302
- const reminder = reminders.find((r) => r.id === id);
303
- if (!reminder) throw new Error(`Reminder "${id}" not found`);
304
- if (reminder.status !== "pending") {
305
- throw new Error(`Reminder "${id}" is already ${reminder.status}`);
306
- }
307
- reminder.status = "cancelled";
308
- await this.writeAll(reminders);
309
- return reminder;
310
- }
311
-
312
- async delete(id: string): Promise<void> {
313
- let reminders: Reminder[];
314
- try {
315
- reminders = await this.readAll();
316
- } catch {
317
- return this.memoryFallback.delete(id);
318
- }
319
- await this.writeAll(reminders.filter((r) => r.id !== id));
320
- }
321
- }
322
-
323
105
  // ---------------------------------------------------------------------------
324
106
  // Factory
325
107
  // ---------------------------------------------------------------------------
326
108
 
327
109
  export const createReminderStore = (
328
- agentId: string,
329
- config?: StateConfig,
330
- options?: { workingDir?: string },
110
+ _agentId: string,
111
+ _config?: StateConfig,
112
+ _options?: { workingDir?: string },
331
113
  ): ReminderStore => {
332
- const provider = config?.provider ?? "local";
333
- const ttl = config?.ttl;
334
- const workingDir = options?.workingDir ?? process.cwd();
335
-
336
- if (provider === "local") {
337
- return new FileReminderStore(workingDir);
338
- }
339
- if (provider === "memory") {
340
- return new InMemoryReminderStore();
341
- }
342
-
343
- const kv = createRawKVStore(config);
344
- if (kv) {
345
- const key = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:reminders`;
346
- return new KVBackedReminderStore(kv, key, ttl);
347
- }
348
114
  return new InMemoryReminderStore();
349
115
  };