@poncho-ai/harness 0.24.0 → 0.26.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 +5 -5
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.js +645 -337
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/harness.ts +140 -40
- package/src/kv-store.ts +206 -0
- package/src/memory.ts +88 -336
- package/src/todo-tools.ts +363 -0
- package/test/harness.test.ts +129 -2
- package/test/memory.test.ts +100 -15
package/src/memory.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
slugifyStorageComponent,
|
|
9
9
|
STORAGE_SCHEMA_VERSION,
|
|
10
10
|
} from "./agent-identity.js";
|
|
11
|
+
import { createRawKVStore, type RawKVStore } from "./kv-store.js";
|
|
11
12
|
|
|
12
13
|
export interface MainMemory {
|
|
13
14
|
content: string;
|
|
@@ -27,7 +28,7 @@ export interface MemoryConfig {
|
|
|
27
28
|
|
|
28
29
|
export interface MemoryStore {
|
|
29
30
|
getMainMemory(): Promise<MainMemory>;
|
|
30
|
-
updateMainMemory(input: { content: string
|
|
31
|
+
updateMainMemory(input: { content: string }): Promise<MainMemory>;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
type MainMemoryPayload = {
|
|
@@ -89,19 +90,10 @@ class InMemoryMemoryStore implements MemoryStore {
|
|
|
89
90
|
return this.mainMemory;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
async updateMainMemory(input: {
|
|
93
|
-
content: string;
|
|
94
|
-
mode?: "replace" | "append";
|
|
95
|
-
}): Promise<MainMemory> {
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
const existing = await this.getMainMemory();
|
|
98
|
-
const nextContent =
|
|
99
|
-
input.mode === "append" && existing.content
|
|
100
|
-
? `${existing.content}\n\n${input.content}`.trim()
|
|
101
|
-
: input.content;
|
|
93
|
+
async updateMainMemory(input: { content: string }): Promise<MainMemory> {
|
|
102
94
|
this.mainMemory = {
|
|
103
|
-
content:
|
|
104
|
-
updatedAt: now,
|
|
95
|
+
content: input.content.trim(),
|
|
96
|
+
updatedAt: Date.now(),
|
|
105
97
|
};
|
|
106
98
|
return this.mainMemory;
|
|
107
99
|
}
|
|
@@ -166,18 +158,10 @@ class FileMainMemoryStore implements MemoryStore {
|
|
|
166
158
|
return this.mainMemory;
|
|
167
159
|
}
|
|
168
160
|
|
|
169
|
-
async updateMainMemory(input: {
|
|
170
|
-
content: string;
|
|
171
|
-
mode?: "replace" | "append";
|
|
172
|
-
}): Promise<MainMemory> {
|
|
161
|
+
async updateMainMemory(input: { content: string }): Promise<MainMemory> {
|
|
173
162
|
await this.ensureLoaded();
|
|
174
|
-
const existing = await this.getMainMemory();
|
|
175
|
-
const nextContent =
|
|
176
|
-
input.mode === "append" && existing.content
|
|
177
|
-
? `${existing.content}\n\n${input.content}`.trim()
|
|
178
|
-
: input.content;
|
|
179
163
|
this.mainMemory = {
|
|
180
|
-
content:
|
|
164
|
+
content: input.content.trim(),
|
|
181
165
|
updatedAt: Date.now(),
|
|
182
166
|
};
|
|
183
167
|
await this.persist();
|
|
@@ -185,25 +169,23 @@ class FileMainMemoryStore implements MemoryStore {
|
|
|
185
169
|
}
|
|
186
170
|
}
|
|
187
171
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
class KVBackedMemoryStore implements MemoryStore {
|
|
173
|
+
private readonly kv: RawKVStore;
|
|
174
|
+
private readonly storageKey: string;
|
|
175
|
+
private readonly ttl?: number;
|
|
176
|
+
private readonly memoryFallback: InMemoryMemoryStore;
|
|
191
177
|
|
|
192
|
-
constructor(ttl?: number) {
|
|
178
|
+
constructor(kv: RawKVStore, storageKey: string, ttl?: number) {
|
|
179
|
+
this.kv = kv;
|
|
180
|
+
this.storageKey = storageKey;
|
|
193
181
|
this.ttl = ttl;
|
|
194
182
|
this.memoryFallback = new InMemoryMemoryStore(ttl);
|
|
195
183
|
}
|
|
196
184
|
|
|
197
|
-
|
|
198
|
-
protected abstract setRaw(key: string, value: string): Promise<void>;
|
|
199
|
-
protected abstract setRawWithTtl(key: string, value: string, ttl: number): Promise<void>;
|
|
200
|
-
|
|
201
|
-
protected async readPayload(key: string): Promise<MainMemoryPayload> {
|
|
185
|
+
private async readPayload(): Promise<MainMemoryPayload> {
|
|
202
186
|
try {
|
|
203
|
-
const raw = await this.
|
|
204
|
-
if (!raw) {
|
|
205
|
-
return { main: { ...DEFAULT_MAIN_MEMORY } };
|
|
206
|
-
}
|
|
187
|
+
const raw = await this.kv.get(this.storageKey);
|
|
188
|
+
if (!raw) return { main: { ...DEFAULT_MAIN_MEMORY } };
|
|
207
189
|
const parsed = JSON.parse(raw) as MainMemoryPayload;
|
|
208
190
|
const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
|
|
209
191
|
const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
|
|
@@ -214,267 +196,30 @@ abstract class KeyValueMainMemoryStoreBase implements MemoryStore {
|
|
|
214
196
|
}
|
|
215
197
|
}
|
|
216
198
|
|
|
217
|
-
|
|
199
|
+
private async writePayload(payload: MainMemoryPayload): Promise<void> {
|
|
218
200
|
try {
|
|
219
201
|
const serialized = JSON.stringify(payload);
|
|
220
202
|
if (typeof this.ttl === "number") {
|
|
221
|
-
await this.
|
|
203
|
+
await this.kv.setWithTtl(this.storageKey, serialized, Math.max(1, this.ttl));
|
|
222
204
|
} else {
|
|
223
|
-
await this.
|
|
205
|
+
await this.kv.set(this.storageKey, serialized);
|
|
224
206
|
}
|
|
225
207
|
} catch {
|
|
226
|
-
await this.memoryFallback.updateMainMemory({
|
|
227
|
-
content: payload.main.content,
|
|
228
|
-
mode: "replace",
|
|
229
|
-
});
|
|
208
|
+
await this.memoryFallback.updateMainMemory({ content: payload.main.content });
|
|
230
209
|
}
|
|
231
210
|
}
|
|
232
211
|
|
|
233
212
|
async getMainMemory(): Promise<MainMemory> {
|
|
234
|
-
const payload = await this.readPayload(
|
|
213
|
+
const payload = await this.readPayload();
|
|
235
214
|
return payload.main;
|
|
236
215
|
}
|
|
237
216
|
|
|
238
|
-
async updateMainMemory(input: {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const key = this.key();
|
|
243
|
-
const payload = await this.readPayload(key);
|
|
244
|
-
const nextContent =
|
|
245
|
-
input.mode === "append" && payload.main.content
|
|
246
|
-
? `${payload.main.content}\n\n${input.content}`.trim()
|
|
247
|
-
: input.content;
|
|
248
|
-
payload.main = {
|
|
249
|
-
content: nextContent.trim(),
|
|
250
|
-
updatedAt: Date.now(),
|
|
251
|
-
};
|
|
252
|
-
await this.writePayload(key, payload);
|
|
217
|
+
async updateMainMemory(input: { content: string }): Promise<MainMemory> {
|
|
218
|
+
const payload = await this.readPayload();
|
|
219
|
+
payload.main = { content: input.content.trim(), updatedAt: Date.now() };
|
|
220
|
+
await this.writePayload(payload);
|
|
253
221
|
return payload.main;
|
|
254
222
|
}
|
|
255
|
-
|
|
256
|
-
protected abstract key(): string;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
class UpstashMemoryStore extends KeyValueMainMemoryStoreBase {
|
|
260
|
-
private readonly baseUrl: string;
|
|
261
|
-
private readonly token: string;
|
|
262
|
-
private readonly storageKey: string;
|
|
263
|
-
|
|
264
|
-
constructor(options: {
|
|
265
|
-
baseUrl: string;
|
|
266
|
-
token: string;
|
|
267
|
-
storageKey: string;
|
|
268
|
-
ttl?: number;
|
|
269
|
-
}) {
|
|
270
|
-
super(options.ttl);
|
|
271
|
-
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
272
|
-
this.token = options.token;
|
|
273
|
-
this.storageKey = options.storageKey;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
protected key(): string {
|
|
277
|
-
return this.storageKey;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private headers(): HeadersInit {
|
|
281
|
-
return {
|
|
282
|
-
Authorization: `Bearer ${this.token}`,
|
|
283
|
-
"Content-Type": "application/json",
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
protected async getRaw(key: string): Promise<string | undefined> {
|
|
288
|
-
const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
|
|
289
|
-
method: "POST",
|
|
290
|
-
headers: this.headers(),
|
|
291
|
-
});
|
|
292
|
-
if (!response.ok) {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
const payload = (await response.json()) as { result?: string | null };
|
|
296
|
-
return payload.result ?? undefined;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
protected async setRaw(key: string, value: string): Promise<void> {
|
|
300
|
-
await fetch(
|
|
301
|
-
`${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
|
|
302
|
-
{ method: "POST", headers: this.headers() },
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
|
|
307
|
-
await fetch(
|
|
308
|
-
`${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(
|
|
309
|
-
value,
|
|
310
|
-
)}`,
|
|
311
|
-
{ method: "POST", headers: this.headers() },
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
class RedisMemoryStore extends KeyValueMainMemoryStoreBase {
|
|
317
|
-
private readonly storageKey: string;
|
|
318
|
-
private readonly clientPromise: Promise<
|
|
319
|
-
| {
|
|
320
|
-
get: (key: string) => Promise<string | null>;
|
|
321
|
-
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
322
|
-
}
|
|
323
|
-
| undefined
|
|
324
|
-
>;
|
|
325
|
-
|
|
326
|
-
constructor(options: {
|
|
327
|
-
url: string;
|
|
328
|
-
storageKey: string;
|
|
329
|
-
ttl?: number;
|
|
330
|
-
}) {
|
|
331
|
-
super(options.ttl);
|
|
332
|
-
this.storageKey = options.storageKey;
|
|
333
|
-
this.clientPromise = (async () => {
|
|
334
|
-
try {
|
|
335
|
-
const redisModule = (await import("redis")) as unknown as {
|
|
336
|
-
createClient: (args: { url: string }) => {
|
|
337
|
-
connect: () => Promise<unknown>;
|
|
338
|
-
get: (key: string) => Promise<string | null>;
|
|
339
|
-
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
340
|
-
};
|
|
341
|
-
};
|
|
342
|
-
const client = redisModule.createClient({ url: options.url });
|
|
343
|
-
await client.connect();
|
|
344
|
-
return client;
|
|
345
|
-
} catch {
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
})();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
protected key(): string {
|
|
352
|
-
return this.storageKey;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
protected async getRaw(key: string): Promise<string | undefined> {
|
|
356
|
-
const client = await this.clientPromise;
|
|
357
|
-
if (!client) {
|
|
358
|
-
throw new Error("Redis unavailable");
|
|
359
|
-
}
|
|
360
|
-
const value = await client.get(key);
|
|
361
|
-
return value ?? undefined;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
protected async setRaw(key: string, value: string): Promise<void> {
|
|
365
|
-
const client = await this.clientPromise;
|
|
366
|
-
if (!client) {
|
|
367
|
-
throw new Error("Redis unavailable");
|
|
368
|
-
}
|
|
369
|
-
await client.set(key, value);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
|
|
373
|
-
const client = await this.clientPromise;
|
|
374
|
-
if (!client) {
|
|
375
|
-
throw new Error("Redis unavailable");
|
|
376
|
-
}
|
|
377
|
-
await client.set(key, value, { EX: Math.max(1, ttl) });
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
class DynamoDbMemoryStore extends KeyValueMainMemoryStoreBase {
|
|
382
|
-
private readonly storageKey: string;
|
|
383
|
-
private readonly table: string;
|
|
384
|
-
private readonly clientPromise: Promise<
|
|
385
|
-
| {
|
|
386
|
-
send: (command: unknown) => Promise<unknown>;
|
|
387
|
-
GetItemCommand: new (input: unknown) => unknown;
|
|
388
|
-
PutItemCommand: new (input: unknown) => unknown;
|
|
389
|
-
}
|
|
390
|
-
| undefined
|
|
391
|
-
>;
|
|
392
|
-
|
|
393
|
-
constructor(options: {
|
|
394
|
-
table: string;
|
|
395
|
-
storageKey: string;
|
|
396
|
-
region?: string;
|
|
397
|
-
ttl?: number;
|
|
398
|
-
}) {
|
|
399
|
-
super(options.ttl);
|
|
400
|
-
this.storageKey = options.storageKey;
|
|
401
|
-
this.table = options.table;
|
|
402
|
-
this.clientPromise = (async () => {
|
|
403
|
-
try {
|
|
404
|
-
const module = (await import("@aws-sdk/client-dynamodb")) as {
|
|
405
|
-
DynamoDBClient: new (input: { region?: string }) => {
|
|
406
|
-
send: (command: unknown) => Promise<unknown>;
|
|
407
|
-
};
|
|
408
|
-
GetItemCommand: new (input: unknown) => unknown;
|
|
409
|
-
PutItemCommand: new (input: unknown) => unknown;
|
|
410
|
-
};
|
|
411
|
-
const client = new module.DynamoDBClient({ region: options.region });
|
|
412
|
-
return {
|
|
413
|
-
send: client.send.bind(client),
|
|
414
|
-
GetItemCommand: module.GetItemCommand,
|
|
415
|
-
PutItemCommand: module.PutItemCommand,
|
|
416
|
-
};
|
|
417
|
-
} catch {
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
})();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
protected key(): string {
|
|
424
|
-
return this.storageKey;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
protected async getRaw(key: string): Promise<string | undefined> {
|
|
428
|
-
const client = await this.clientPromise;
|
|
429
|
-
if (!client) {
|
|
430
|
-
throw new Error("DynamoDB unavailable");
|
|
431
|
-
}
|
|
432
|
-
const result = (await client.send(
|
|
433
|
-
new client.GetItemCommand({
|
|
434
|
-
TableName: this.table,
|
|
435
|
-
Key: { runId: { S: key } },
|
|
436
|
-
}),
|
|
437
|
-
)) as {
|
|
438
|
-
Item?: {
|
|
439
|
-
value?: { S?: string };
|
|
440
|
-
};
|
|
441
|
-
};
|
|
442
|
-
return result.Item?.value?.S;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
protected async setRaw(key: string, value: string): Promise<void> {
|
|
446
|
-
const client = await this.clientPromise;
|
|
447
|
-
if (!client) {
|
|
448
|
-
throw new Error("DynamoDB unavailable");
|
|
449
|
-
}
|
|
450
|
-
await client.send(
|
|
451
|
-
new client.PutItemCommand({
|
|
452
|
-
TableName: this.table,
|
|
453
|
-
Item: {
|
|
454
|
-
runId: { S: key },
|
|
455
|
-
value: { S: value },
|
|
456
|
-
},
|
|
457
|
-
}),
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
|
|
462
|
-
const client = await this.clientPromise;
|
|
463
|
-
if (!client) {
|
|
464
|
-
throw new Error("DynamoDB unavailable");
|
|
465
|
-
}
|
|
466
|
-
const ttlEpoch = Math.floor(Date.now() / 1000) + Math.max(1, ttl);
|
|
467
|
-
await client.send(
|
|
468
|
-
new client.PutItemCommand({
|
|
469
|
-
TableName: this.table,
|
|
470
|
-
Item: {
|
|
471
|
-
runId: { S: key },
|
|
472
|
-
value: { S: value },
|
|
473
|
-
ttl: { N: String(ttlEpoch) },
|
|
474
|
-
},
|
|
475
|
-
}),
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
223
|
}
|
|
479
224
|
|
|
480
225
|
export const createMemoryStore = (
|
|
@@ -484,54 +229,19 @@ export const createMemoryStore = (
|
|
|
484
229
|
): MemoryStore => {
|
|
485
230
|
const provider = config?.provider ?? "local";
|
|
486
231
|
const ttl = config?.ttl;
|
|
487
|
-
const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(
|
|
488
|
-
agentId,
|
|
489
|
-
)}:memory:main`;
|
|
490
232
|
const workingDir = options?.workingDir ?? process.cwd();
|
|
233
|
+
|
|
491
234
|
if (provider === "local") {
|
|
492
235
|
return new FileMainMemoryStore(workingDir, ttl);
|
|
493
236
|
}
|
|
494
237
|
if (provider === "memory") {
|
|
495
238
|
return new InMemoryMemoryStore(ttl);
|
|
496
239
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
if (url && token) {
|
|
503
|
-
return new UpstashMemoryStore({
|
|
504
|
-
baseUrl: url,
|
|
505
|
-
token,
|
|
506
|
-
storageKey,
|
|
507
|
-
ttl,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
return new InMemoryMemoryStore(ttl);
|
|
511
|
-
}
|
|
512
|
-
if (provider === "redis") {
|
|
513
|
-
const urlEnv = config?.urlEnv ?? "REDIS_URL";
|
|
514
|
-
const url = process.env[urlEnv] ?? "";
|
|
515
|
-
if (url) {
|
|
516
|
-
return new RedisMemoryStore({
|
|
517
|
-
url,
|
|
518
|
-
storageKey,
|
|
519
|
-
ttl,
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
return new InMemoryMemoryStore(ttl);
|
|
523
|
-
}
|
|
524
|
-
if (provider === "dynamodb") {
|
|
525
|
-
const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
|
|
526
|
-
if (table) {
|
|
527
|
-
return new DynamoDbMemoryStore({
|
|
528
|
-
table,
|
|
529
|
-
storageKey,
|
|
530
|
-
region: config?.region,
|
|
531
|
-
ttl,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
return new InMemoryMemoryStore(ttl);
|
|
240
|
+
|
|
241
|
+
const kv = createRawKVStore(config);
|
|
242
|
+
if (kv) {
|
|
243
|
+
const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:memory:main`;
|
|
244
|
+
return new KVBackedMemoryStore(kv, storageKey, ttl);
|
|
535
245
|
}
|
|
536
246
|
return new InMemoryMemoryStore(ttl);
|
|
537
247
|
};
|
|
@@ -590,20 +300,17 @@ export const createMemoryTools = (
|
|
|
590
300
|
},
|
|
591
301
|
}),
|
|
592
302
|
defineTool({
|
|
593
|
-
name: "
|
|
303
|
+
name: "memory_main_write",
|
|
594
304
|
description:
|
|
595
|
-
"
|
|
305
|
+
"Overwrite the entire persistent main memory document. " +
|
|
306
|
+
"Use for initial writes or full rewrites. " +
|
|
307
|
+
"Prefer memory_main_edit for targeted changes to existing memory.",
|
|
596
308
|
inputSchema: {
|
|
597
309
|
type: "object",
|
|
598
310
|
properties: {
|
|
599
|
-
mode: {
|
|
600
|
-
type: "string",
|
|
601
|
-
enum: ["replace", "append"],
|
|
602
|
-
description: "replace overwrites memory; append adds content to the end",
|
|
603
|
-
},
|
|
604
311
|
content: {
|
|
605
312
|
type: "string",
|
|
606
|
-
description: "The memory content to write",
|
|
313
|
+
description: "The full memory content to write",
|
|
607
314
|
},
|
|
608
315
|
},
|
|
609
316
|
required: ["content"],
|
|
@@ -614,11 +321,56 @@ export const createMemoryTools = (
|
|
|
614
321
|
if (!content) {
|
|
615
322
|
throw new Error("content is required");
|
|
616
323
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
324
|
+
const memory = await store.updateMainMemory({ content });
|
|
325
|
+
return { ok: true, memory };
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
defineTool({
|
|
329
|
+
name: "memory_main_edit",
|
|
330
|
+
description:
|
|
331
|
+
"Edit persistent main memory by replacing an exact string match with new content. " +
|
|
332
|
+
"The old_str must match exactly one location in memory. " +
|
|
333
|
+
"Use an empty new_str to delete matched content. " +
|
|
334
|
+
"Proactively evaluate every turn whether memory should be updated.",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
old_str: {
|
|
339
|
+
type: "string",
|
|
340
|
+
description:
|
|
341
|
+
"The exact text to find and replace (must be unique in memory). " +
|
|
342
|
+
"Include surrounding context if needed to ensure uniqueness.",
|
|
343
|
+
},
|
|
344
|
+
new_str: {
|
|
345
|
+
type: "string",
|
|
346
|
+
description: "The replacement text (use empty string to delete the matched content)",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
required: ["old_str", "new_str"],
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
},
|
|
352
|
+
handler: async (input) => {
|
|
353
|
+
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
354
|
+
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
355
|
+
if (!oldStr) {
|
|
356
|
+
throw new Error("old_str must not be empty.");
|
|
357
|
+
}
|
|
358
|
+
const current = await store.getMainMemory();
|
|
359
|
+
const content = current.content;
|
|
360
|
+
const first = content.indexOf(oldStr);
|
|
361
|
+
if (first === -1) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"old_str not found in memory. Make sure it matches exactly, including whitespace and line breaks.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const last = content.lastIndexOf(oldStr);
|
|
367
|
+
if (first !== last) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
"old_str appears multiple times in memory. Please provide more context to ensure a unique match.",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
373
|
+
const memory = await store.updateMainMemory({ content: newContent });
|
|
622
374
|
return { ok: true, memory };
|
|
623
375
|
},
|
|
624
376
|
}),
|