@poncho-ai/harness 0.23.0 → 0.25.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 +20 -2
- package/dist/index.js +228 -80
- package/package.json +1 -1
- package/src/config.ts +1 -0
- package/src/default-tools.ts +53 -0
- package/src/harness.ts +129 -41
- package/src/memory.ts +63 -46
- package/test/harness.test.ts +193 -3
- package/test/memory.test.ts +100 -15
package/src/default-tools.ts
CHANGED
|
@@ -89,6 +89,59 @@ export const createWriteTool = (workingDir: string): ToolDefinition =>
|
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
export const createEditTool = (workingDir: string): ToolDefinition =>
|
|
93
|
+
defineTool({
|
|
94
|
+
name: "edit_file",
|
|
95
|
+
description:
|
|
96
|
+
"Edit a file by replacing an exact string match with new content. " +
|
|
97
|
+
"The old_str must match exactly one location in the file (including whitespace and indentation). " +
|
|
98
|
+
"Use an empty new_str to delete matched content.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
path: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "File path relative to working directory",
|
|
105
|
+
},
|
|
106
|
+
old_str: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description:
|
|
109
|
+
"The exact text to find and replace (must be unique in the file). " +
|
|
110
|
+
"Include surrounding context lines if needed to ensure uniqueness.",
|
|
111
|
+
},
|
|
112
|
+
new_str: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "The replacement text (use empty string to delete the matched content)",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ["path", "old_str", "new_str"],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
handler: async (input) => {
|
|
121
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
122
|
+
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
123
|
+
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
124
|
+
if (!oldStr) throw new Error("old_str must not be empty.");
|
|
125
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
126
|
+
const content = await readFile(resolved, "utf8");
|
|
127
|
+
const first = content.indexOf(oldStr);
|
|
128
|
+
if (first === -1) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"old_str not found in file. Make sure it matches exactly, including whitespace and line breaks.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const last = content.lastIndexOf(oldStr);
|
|
134
|
+
if (first !== last) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"old_str appears multiple times in the file. Please provide more context to ensure a unique match.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
140
|
+
await writeFile(resolved, newContent, "utf8");
|
|
141
|
+
return { path, edited: true };
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
92
145
|
export const createDeleteTool = (workingDir: string): ToolDefinition =>
|
|
93
146
|
defineTool({
|
|
94
147
|
name: "delete_file",
|
package/src/harness.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
2
4
|
import type {
|
|
3
5
|
AgentEvent,
|
|
4
6
|
ContentPart,
|
|
@@ -13,9 +15,9 @@ import type {
|
|
|
13
15
|
import { getTextContent } from "@poncho-ai/sdk";
|
|
14
16
|
import type { UploadStore } from "./upload-store.js";
|
|
15
17
|
import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
16
|
-
import { parseAgentFile, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
|
|
18
|
+
import { parseAgentFile, parseAgentMarkdown, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
|
|
17
19
|
import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig, type ToolAccess, type BuiltInToolToggles } from "./config.js";
|
|
18
|
-
import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createWriteTool, ponchoDocsTool } from "./default-tools.js";
|
|
20
|
+
import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createWriteTool, ponchoDocsTool } from "./default-tools.js";
|
|
19
21
|
import {
|
|
20
22
|
createMemoryStore,
|
|
21
23
|
createMemoryTools,
|
|
@@ -224,7 +226,7 @@ You are running locally in development mode. Treat this as an editable agent wor
|
|
|
224
226
|
## Understanding Your Environment
|
|
225
227
|
|
|
226
228
|
- Built-in tools: \`list_directory\` and \`read_file\`
|
|
227
|
-
- \`write_file\`
|
|
229
|
+
- \`write_file\` and \`edit_file\` are available in development (disabled by default in production)
|
|
228
230
|
- A starter local skill is included (\`starter-echo\`)
|
|
229
231
|
- Bash/shell commands are **not** available unless you install and enable a shell tool/skill
|
|
230
232
|
- Git operations are only available if a git-capable tool/skill is configured
|
|
@@ -485,6 +487,7 @@ Since all fields have defaults, you only need to specify \`*Env\` when your env
|
|
|
485
487
|
- If shell/CLI access is unavailable, ask the user to run needed commands and provide exact copy-paste commands.
|
|
486
488
|
- For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
|
|
487
489
|
- Prefer quoting concrete commands and examples from \`README.md\` over guessing.
|
|
490
|
+
- Prefer \`edit_file\` for targeted changes to existing files (uses exact string matching); use \`write_file\` only for creating new files or full rewrites.
|
|
488
491
|
- Keep edits minimal, preserve unrelated settings/code, and summarize what changed.
|
|
489
492
|
|
|
490
493
|
## Detailed Documentation
|
|
@@ -562,6 +565,7 @@ export class AgentHarness {
|
|
|
562
565
|
};
|
|
563
566
|
|
|
564
567
|
private parsedAgent?: ParsedAgent;
|
|
568
|
+
private agentFileFingerprint = "";
|
|
565
569
|
private mcpBridge?: LocalMcpBridge;
|
|
566
570
|
private subagentManager?: SubagentManager;
|
|
567
571
|
|
|
@@ -585,7 +589,7 @@ export class AgentHarness {
|
|
|
585
589
|
private isToolEnabled(name: string): boolean {
|
|
586
590
|
const access = this.resolveToolAccess(name);
|
|
587
591
|
if (access === false) return false;
|
|
588
|
-
if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
|
|
592
|
+
if (name === "write_file" || name === "edit_file" || name === "delete_file" || name === "delete_directory") {
|
|
589
593
|
return this.shouldEnableWriteTool();
|
|
590
594
|
}
|
|
591
595
|
return true;
|
|
@@ -633,6 +637,9 @@ export class AgentHarness {
|
|
|
633
637
|
if (this.isToolEnabled("write_file")) {
|
|
634
638
|
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
635
639
|
}
|
|
640
|
+
if (this.isToolEnabled("edit_file")) {
|
|
641
|
+
this.registerIfMissing(createEditTool(this.workingDir));
|
|
642
|
+
}
|
|
636
643
|
if (this.isToolEnabled("delete_file")) {
|
|
637
644
|
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
638
645
|
}
|
|
@@ -692,20 +699,17 @@ export class AgentHarness {
|
|
|
692
699
|
}
|
|
693
700
|
|
|
694
701
|
private getRequestedMcpPatterns(): string[] {
|
|
695
|
-
const
|
|
702
|
+
const patterns = new Set<string>(this.getAgentMcpIntent());
|
|
696
703
|
for (const skillName of this.activeSkillNames) {
|
|
697
704
|
const skill = this.loadedSkills.find((entry) => entry.name === skillName);
|
|
698
705
|
if (!skill) {
|
|
699
706
|
continue;
|
|
700
707
|
}
|
|
701
708
|
for (const pattern of skill.allowedTools.mcp) {
|
|
702
|
-
|
|
709
|
+
patterns.add(pattern);
|
|
703
710
|
}
|
|
704
711
|
}
|
|
705
|
-
|
|
706
|
-
return [...skillPatterns];
|
|
707
|
-
}
|
|
708
|
-
return this.getAgentMcpIntent();
|
|
712
|
+
return [...patterns];
|
|
709
713
|
}
|
|
710
714
|
|
|
711
715
|
private getRequestedScriptPatterns(): string[] {
|
|
@@ -723,20 +727,17 @@ export class AgentHarness {
|
|
|
723
727
|
}
|
|
724
728
|
|
|
725
729
|
private getRequestedMcpApprovalPatterns(): string[] {
|
|
726
|
-
const
|
|
730
|
+
const patterns = new Set<string>(this.getAgentMcpApprovalPatterns());
|
|
727
731
|
for (const skillName of this.activeSkillNames) {
|
|
728
732
|
const skill = this.loadedSkills.find((entry) => entry.name === skillName);
|
|
729
733
|
if (!skill) {
|
|
730
734
|
continue;
|
|
731
735
|
}
|
|
732
736
|
for (const pattern of skill.approvalRequired.mcp) {
|
|
733
|
-
|
|
737
|
+
patterns.add(pattern);
|
|
734
738
|
}
|
|
735
739
|
}
|
|
736
|
-
|
|
737
|
-
return [...skillPatterns];
|
|
738
|
-
}
|
|
739
|
-
return this.getAgentMcpApprovalPatterns();
|
|
740
|
+
return [...patterns];
|
|
740
741
|
}
|
|
741
742
|
|
|
742
743
|
private getRequestedScriptApprovalPatterns(): string[] {
|
|
@@ -887,13 +888,59 @@ export class AgentHarness {
|
|
|
887
888
|
|
|
888
889
|
private static readonly SKILL_REFRESH_DEBOUNCE_MS = 3000;
|
|
889
890
|
|
|
890
|
-
|
|
891
|
+
/**
|
|
892
|
+
* Re-read AGENT.md and update the parsed agent when the file has changed
|
|
893
|
+
* on disk. Returns `true` when the agent was actually re-parsed.
|
|
894
|
+
*
|
|
895
|
+
* Preserves the agent identity (id) across reloads so conversation
|
|
896
|
+
* continuity isn't broken.
|
|
897
|
+
*/
|
|
898
|
+
private async refreshAgentIfChanged(): Promise<boolean> {
|
|
891
899
|
if (this.environment !== "development") {
|
|
892
|
-
return;
|
|
900
|
+
return false;
|
|
893
901
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
902
|
+
try {
|
|
903
|
+
const agentFilePath = resolve(this.workingDir, "AGENT.md");
|
|
904
|
+
const rawContent = await readFile(agentFilePath, "utf8");
|
|
905
|
+
if (rawContent === this.agentFileFingerprint) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
const parsed = parseAgentMarkdown(rawContent);
|
|
909
|
+
// Preserve the resolved agent identity so existing conversations
|
|
910
|
+
// keep working after an AGENT.md edit.
|
|
911
|
+
if (!parsed.frontmatter.id && this.parsedAgent?.frontmatter.id) {
|
|
912
|
+
parsed.frontmatter.id = this.parsedAgent.frontmatter.id;
|
|
913
|
+
}
|
|
914
|
+
this.parsedAgent = parsed;
|
|
915
|
+
this.agentFileFingerprint = rawContent;
|
|
916
|
+
return true;
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.warn(
|
|
919
|
+
`[poncho][agent] Failed to refresh AGENT.md in development mode: ${
|
|
920
|
+
error instanceof Error ? error.message : String(error)
|
|
921
|
+
}`,
|
|
922
|
+
);
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Re-scan skill directories and update metadata, tools, and context window
|
|
929
|
+
* when skills have changed on disk. Returns `true` when the skill set was
|
|
930
|
+
* actually updated.
|
|
931
|
+
*
|
|
932
|
+
* @param force - bypass the time-based debounce (used for mid-run refreshes
|
|
933
|
+
* after the agent may have written new skill files).
|
|
934
|
+
*/
|
|
935
|
+
private async refreshSkillsIfChanged(force = false): Promise<boolean> {
|
|
936
|
+
if (this.environment !== "development") {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
if (!force) {
|
|
940
|
+
const elapsed = Date.now() - this.lastSkillRefreshAt;
|
|
941
|
+
if (this.lastSkillRefreshAt > 0 && elapsed < AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
897
944
|
}
|
|
898
945
|
this.lastSkillRefreshAt = Date.now();
|
|
899
946
|
try {
|
|
@@ -903,27 +950,44 @@ export class AgentHarness {
|
|
|
903
950
|
);
|
|
904
951
|
const nextFingerprint = this.buildSkillFingerprint(latestSkills);
|
|
905
952
|
if (nextFingerprint === this.skillFingerprint) {
|
|
906
|
-
return;
|
|
953
|
+
return false;
|
|
907
954
|
}
|
|
908
955
|
this.loadedSkills = latestSkills;
|
|
909
956
|
this.skillContextWindow = buildSkillContextWindow(latestSkills);
|
|
910
957
|
this.skillFingerprint = nextFingerprint;
|
|
911
958
|
this.registerSkillTools(latestSkills);
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
|
|
959
|
+
// Prune active skills that no longer exist in the updated metadata,
|
|
960
|
+
// but preserve ones that were merely updated (same name). This keeps
|
|
961
|
+
// MCP tools from active skills registered when their allowed-tools
|
|
962
|
+
// list changes, instead of forcing the agent to re-activate.
|
|
963
|
+
const latestSkillNames = new Set(latestSkills.map(s => s.name));
|
|
964
|
+
for (const name of this.activeSkillNames) {
|
|
965
|
+
if (!latestSkillNames.has(name)) {
|
|
966
|
+
this.activeSkillNames.delete(name);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// Re-discover MCP server catalogs so newly advertised tools are visible,
|
|
970
|
+
// then refresh the registered tool set with updated skill patterns.
|
|
971
|
+
if (this.mcpBridge) {
|
|
972
|
+
await this.mcpBridge.discoverTools();
|
|
973
|
+
}
|
|
915
974
|
await this.refreshMcpTools("skills:changed");
|
|
975
|
+
return true;
|
|
916
976
|
} catch (error) {
|
|
917
977
|
console.warn(
|
|
918
978
|
`[poncho][skills] Failed to refresh skills in development mode: ${
|
|
919
979
|
error instanceof Error ? error.message : String(error)
|
|
920
980
|
}`,
|
|
921
981
|
);
|
|
982
|
+
return false;
|
|
922
983
|
}
|
|
923
984
|
}
|
|
924
985
|
|
|
925
986
|
async initialize(): Promise<void> {
|
|
926
|
-
|
|
987
|
+
const agentFilePath = resolve(this.workingDir, "AGENT.md");
|
|
988
|
+
const agentRawContent = await readFile(agentFilePath, "utf8");
|
|
989
|
+
this.parsedAgent = parseAgentMarkdown(agentRawContent);
|
|
990
|
+
this.agentFileFingerprint = agentRawContent;
|
|
927
991
|
const identity = await ensureAgentIdentity(this.workingDir);
|
|
928
992
|
if (!this.parsedAgent.frontmatter.id) {
|
|
929
993
|
this.parsedAgent.frontmatter.id = identity.id;
|
|
@@ -1282,10 +1346,11 @@ export class AgentHarness {
|
|
|
1282
1346
|
if (!this.parsedAgent) {
|
|
1283
1347
|
await this.initialize();
|
|
1284
1348
|
}
|
|
1285
|
-
// Start memory fetch early so it overlaps with
|
|
1349
|
+
// Start memory fetch early so it overlaps with refresh I/O
|
|
1286
1350
|
const memoryPromise = this.memoryStore
|
|
1287
1351
|
? this.memoryStore.getMainMemory()
|
|
1288
1352
|
: undefined;
|
|
1353
|
+
await this.refreshAgentIfChanged();
|
|
1289
1354
|
await this.refreshSkillsIfChanged();
|
|
1290
1355
|
|
|
1291
1356
|
// Track which conversation/owner this run belongs to so browser & subagent tools resolve correctly
|
|
@@ -1295,7 +1360,7 @@ export class AgentHarness {
|
|
|
1295
1360
|
this._currentRunOwnerId = ownerParam;
|
|
1296
1361
|
}
|
|
1297
1362
|
|
|
1298
|
-
|
|
1363
|
+
let agent = this.parsedAgent as ParsedAgent;
|
|
1299
1364
|
const runId = `run_${randomUUID()}`;
|
|
1300
1365
|
const start = now();
|
|
1301
1366
|
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
|
|
@@ -1311,15 +1376,16 @@ export class AgentHarness {
|
|
|
1311
1376
|
const inputMessageCount = messages.length;
|
|
1312
1377
|
const events: AgentEvent[] = [];
|
|
1313
1378
|
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1379
|
+
const renderCurrentAgentPrompt = (): string =>
|
|
1380
|
+
renderAgentPrompt(this.parsedAgent!, {
|
|
1381
|
+
parameters: input.parameters,
|
|
1382
|
+
runtime: {
|
|
1383
|
+
runId,
|
|
1384
|
+
agentId: this.parsedAgent!.frontmatter.id ?? this.parsedAgent!.frontmatter.name,
|
|
1385
|
+
environment: this.environment,
|
|
1386
|
+
workingDir: this.workingDir,
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1323
1389
|
const developmentContext =
|
|
1324
1390
|
this.environment === "development" ? `\n\n${DEVELOPMENT_MODE_CONTEXT}` : "";
|
|
1325
1391
|
const browserContext = this._browserSession
|
|
@@ -1342,9 +1408,6 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
|
|
|
1342
1408
|
### Tabs and resources
|
|
1343
1409
|
Each conversation gets its own browser tab sharing a single browser instance. Call \`browser_close\` when done to free the tab. If you don't close it, the tab stays open and the user can continue interacting with it.`
|
|
1344
1410
|
: "";
|
|
1345
|
-
const promptWithSkills = this.skillContextWindow
|
|
1346
|
-
? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
|
|
1347
|
-
: `${systemPrompt}${developmentContext}${browserContext}`;
|
|
1348
1411
|
const mainMemory = await memoryPromise;
|
|
1349
1412
|
const boundedMainMemory =
|
|
1350
1413
|
mainMemory && mainMemory.content.length > 4000
|
|
@@ -1357,7 +1420,13 @@ Each conversation gets its own browser tab sharing a single browser instance. Ca
|
|
|
1357
1420
|
|
|
1358
1421
|
${boundedMainMemory.trim()}`
|
|
1359
1422
|
: "";
|
|
1360
|
-
|
|
1423
|
+
|
|
1424
|
+
const buildSystemPrompt = (): string => {
|
|
1425
|
+
const agentPrompt = renderCurrentAgentPrompt();
|
|
1426
|
+
const promptWithSkills = this.skillContextWindow
|
|
1427
|
+
? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
|
|
1428
|
+
: `${agentPrompt}${developmentContext}${browserContext}`;
|
|
1429
|
+
return `${promptWithSkills}${memoryContext}
|
|
1361
1430
|
|
|
1362
1431
|
## Execution Integrity
|
|
1363
1432
|
|
|
@@ -1365,6 +1434,9 @@ ${boundedMainMemory.trim()}`
|
|
|
1365
1434
|
- Do not fabricate "Tool Used" or "Tool Result" logs as plain text.
|
|
1366
1435
|
- Never output faux execution transcripts, markdown tool logs, or "Tool Used/Result" sections.
|
|
1367
1436
|
- If no suitable tool is available, explicitly say that and ask for guidance.`;
|
|
1437
|
+
};
|
|
1438
|
+
let integrityPrompt = buildSystemPrompt();
|
|
1439
|
+
let lastPromptFingerprint = `${this.agentFileFingerprint}\n${this.skillFingerprint}`;
|
|
1368
1440
|
|
|
1369
1441
|
const pushEvent = (event: AgentEvent): AgentEvent => {
|
|
1370
1442
|
events.push(event);
|
|
@@ -2256,6 +2328,22 @@ ${boundedMainMemory.trim()}`
|
|
|
2256
2328
|
metadata: toolMsgMeta as Message["metadata"],
|
|
2257
2329
|
});
|
|
2258
2330
|
|
|
2331
|
+
// In development, re-read AGENT.md and re-scan skills after tool
|
|
2332
|
+
// execution so changes are available on the next step without
|
|
2333
|
+
// requiring a server restart.
|
|
2334
|
+
if (this.environment === "development") {
|
|
2335
|
+
const agentChanged = await this.refreshAgentIfChanged();
|
|
2336
|
+
const skillsChanged = await this.refreshSkillsIfChanged(true);
|
|
2337
|
+
if (agentChanged || skillsChanged) {
|
|
2338
|
+
agent = this.parsedAgent as ParsedAgent;
|
|
2339
|
+
const currentFingerprint = `${this.agentFileFingerprint}\n${this.skillFingerprint}`;
|
|
2340
|
+
if (currentFingerprint !== lastPromptFingerprint) {
|
|
2341
|
+
integrityPrompt = buildSystemPrompt();
|
|
2342
|
+
lastPromptFingerprint = currentFingerprint;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2259
2347
|
yield pushEvent({
|
|
2260
2348
|
type: "step:completed",
|
|
2261
2349
|
step,
|
package/src/memory.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface MemoryConfig {
|
|
|
27
27
|
|
|
28
28
|
export interface MemoryStore {
|
|
29
29
|
getMainMemory(): Promise<MainMemory>;
|
|
30
|
-
updateMainMemory(input: { content: string
|
|
30
|
+
updateMainMemory(input: { content: string }): Promise<MainMemory>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
type MainMemoryPayload = {
|
|
@@ -89,19 +89,10 @@ class InMemoryMemoryStore implements MemoryStore {
|
|
|
89
89
|
return this.mainMemory;
|
|
90
90
|
}
|
|
91
91
|
|
|
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;
|
|
92
|
+
async updateMainMemory(input: { content: string }): Promise<MainMemory> {
|
|
102
93
|
this.mainMemory = {
|
|
103
|
-
content:
|
|
104
|
-
updatedAt: now,
|
|
94
|
+
content: input.content.trim(),
|
|
95
|
+
updatedAt: Date.now(),
|
|
105
96
|
};
|
|
106
97
|
return this.mainMemory;
|
|
107
98
|
}
|
|
@@ -166,18 +157,10 @@ class FileMainMemoryStore implements MemoryStore {
|
|
|
166
157
|
return this.mainMemory;
|
|
167
158
|
}
|
|
168
159
|
|
|
169
|
-
async updateMainMemory(input: {
|
|
170
|
-
content: string;
|
|
171
|
-
mode?: "replace" | "append";
|
|
172
|
-
}): Promise<MainMemory> {
|
|
160
|
+
async updateMainMemory(input: { content: string }): Promise<MainMemory> {
|
|
173
161
|
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
162
|
this.mainMemory = {
|
|
180
|
-
content:
|
|
163
|
+
content: input.content.trim(),
|
|
181
164
|
updatedAt: Date.now(),
|
|
182
165
|
};
|
|
183
166
|
await this.persist();
|
|
@@ -225,7 +208,6 @@ abstract class KeyValueMainMemoryStoreBase implements MemoryStore {
|
|
|
225
208
|
} catch {
|
|
226
209
|
await this.memoryFallback.updateMainMemory({
|
|
227
210
|
content: payload.main.content,
|
|
228
|
-
mode: "replace",
|
|
229
211
|
});
|
|
230
212
|
}
|
|
231
213
|
}
|
|
@@ -235,18 +217,11 @@ abstract class KeyValueMainMemoryStoreBase implements MemoryStore {
|
|
|
235
217
|
return payload.main;
|
|
236
218
|
}
|
|
237
219
|
|
|
238
|
-
async updateMainMemory(input: {
|
|
239
|
-
content: string;
|
|
240
|
-
mode?: "replace" | "append";
|
|
241
|
-
}): Promise<MainMemory> {
|
|
220
|
+
async updateMainMemory(input: { content: string }): Promise<MainMemory> {
|
|
242
221
|
const key = this.key();
|
|
243
222
|
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
223
|
payload.main = {
|
|
249
|
-
content:
|
|
224
|
+
content: input.content.trim(),
|
|
250
225
|
updatedAt: Date.now(),
|
|
251
226
|
};
|
|
252
227
|
await this.writePayload(key, payload);
|
|
@@ -590,20 +565,17 @@ export const createMemoryTools = (
|
|
|
590
565
|
},
|
|
591
566
|
}),
|
|
592
567
|
defineTool({
|
|
593
|
-
name: "
|
|
568
|
+
name: "memory_main_write",
|
|
594
569
|
description:
|
|
595
|
-
"
|
|
570
|
+
"Overwrite the entire persistent main memory document. " +
|
|
571
|
+
"Use for initial writes or full rewrites. " +
|
|
572
|
+
"Prefer memory_main_edit for targeted changes to existing memory.",
|
|
596
573
|
inputSchema: {
|
|
597
574
|
type: "object",
|
|
598
575
|
properties: {
|
|
599
|
-
mode: {
|
|
600
|
-
type: "string",
|
|
601
|
-
enum: ["replace", "append"],
|
|
602
|
-
description: "replace overwrites memory; append adds content to the end",
|
|
603
|
-
},
|
|
604
576
|
content: {
|
|
605
577
|
type: "string",
|
|
606
|
-
description: "The memory content to write",
|
|
578
|
+
description: "The full memory content to write",
|
|
607
579
|
},
|
|
608
580
|
},
|
|
609
581
|
required: ["content"],
|
|
@@ -614,11 +586,56 @@ export const createMemoryTools = (
|
|
|
614
586
|
if (!content) {
|
|
615
587
|
throw new Error("content is required");
|
|
616
588
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
589
|
+
const memory = await store.updateMainMemory({ content });
|
|
590
|
+
return { ok: true, memory };
|
|
591
|
+
},
|
|
592
|
+
}),
|
|
593
|
+
defineTool({
|
|
594
|
+
name: "memory_main_edit",
|
|
595
|
+
description:
|
|
596
|
+
"Edit persistent main memory by replacing an exact string match with new content. " +
|
|
597
|
+
"The old_str must match exactly one location in memory. " +
|
|
598
|
+
"Use an empty new_str to delete matched content. " +
|
|
599
|
+
"Proactively evaluate every turn whether memory should be updated.",
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: "object",
|
|
602
|
+
properties: {
|
|
603
|
+
old_str: {
|
|
604
|
+
type: "string",
|
|
605
|
+
description:
|
|
606
|
+
"The exact text to find and replace (must be unique in memory). " +
|
|
607
|
+
"Include surrounding context if needed to ensure uniqueness.",
|
|
608
|
+
},
|
|
609
|
+
new_str: {
|
|
610
|
+
type: "string",
|
|
611
|
+
description: "The replacement text (use empty string to delete the matched content)",
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
required: ["old_str", "new_str"],
|
|
615
|
+
additionalProperties: false,
|
|
616
|
+
},
|
|
617
|
+
handler: async (input) => {
|
|
618
|
+
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
619
|
+
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
620
|
+
if (!oldStr) {
|
|
621
|
+
throw new Error("old_str must not be empty.");
|
|
622
|
+
}
|
|
623
|
+
const current = await store.getMainMemory();
|
|
624
|
+
const content = current.content;
|
|
625
|
+
const first = content.indexOf(oldStr);
|
|
626
|
+
if (first === -1) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
"old_str not found in memory. Make sure it matches exactly, including whitespace and line breaks.",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const last = content.lastIndexOf(oldStr);
|
|
632
|
+
if (first !== last) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
"old_str appears multiple times in memory. Please provide more context to ensure a unique match.",
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
638
|
+
const memory = await store.updateMainMemory({ content: newContent });
|
|
622
639
|
return { ok: true, memory };
|
|
623
640
|
},
|
|
624
641
|
}),
|