@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.
- package/.turbo/turbo-build.log +6 -5
- package/.turbo/turbo-test.log +15169 -0
- package/CHANGELOG.md +18 -0
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/dist-3KMQR4IO.js +27092 -0
- package/dist/index.d.ts +485 -29
- package/dist/index.js +2839 -2114
- 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 +22 -3
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +106 -1
- package/src/harness.ts +226 -91
- package/src/index.ts +5 -0
- package/src/isolate/bindings.ts +206 -0
- package/src/isolate/bundler.ts +179 -0
- package/src/isolate/index.ts +10 -0
- package/src/isolate/polyfills.ts +796 -0
- package/src/isolate/run-code-tool.ts +220 -0
- package/src/isolate/runtime.ts +286 -0
- package/src/isolate/type-stubs.ts +196 -0
- package/src/memory.ts +129 -198
- package/src/reminder-store.ts +3 -237
- package/src/secrets-store.ts +2 -91
- package/src/state.ts +11 -1302
- package/src/storage/engine.ts +106 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/memory-engine.ts +588 -0
- package/src/storage/postgres-engine.ts +139 -0
- package/src/storage/schema.ts +145 -0
- package/src/storage/sql-dialect.ts +963 -0
- package/src/storage/sqlite-engine.ts +99 -0
- package/src/storage/store-adapters.ts +100 -0
- package/src/todo-tools.ts +1 -136
- package/src/upload-store.ts +1 -0
- package/src/vfs/bash-manager.ts +120 -0
- package/src/vfs/bash-tool.ts +59 -0
- package/src/vfs/create-bash-fs.ts +32 -0
- package/src/vfs/edit-file-tool.ts +72 -0
- package/src/vfs/index.ts +5 -0
- package/src/vfs/poncho-fs-adapter.ts +267 -0
- package/src/vfs/protected-fs.ts +177 -0
- package/src/vfs/read-file-tool.ts +103 -0
- package/src/vfs/write-file-tool.ts +49 -0
- package/test/harness.test.ts +30 -36
- package/test/isolate-vfs.test.ts +453 -0
- package/test/isolate.test.ts +252 -0
- package/test/state.test.ts +4 -27
- package/test/storage-engine.test.ts +250 -0
- package/test/vfs.test.ts +242 -0
- package/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
|
|
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: "
|
|
251
|
+
description: "Keyword search query for past conversations",
|
|
407
252
|
},
|
|
408
|
-
|
|
409
|
-
type: "
|
|
410
|
-
description:
|
|
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
|
-
|
|
258
|
+
before: {
|
|
413
259
|
type: "string",
|
|
414
|
-
description:
|
|
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
|
-
|
|
423
|
-
|
|
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(
|
|
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
|
];
|
package/src/reminder-store.ts
CHANGED
|
@@ -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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
};
|