@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/dist/index.js CHANGED
@@ -1402,7 +1402,8 @@ if (result.compacted) {
1402
1402
  When \`memory.enabled\` is true in \`poncho.config.js\`, the harness enables a simple memory model:
1403
1403
 
1404
1404
  - A single persistent main memory document is loaded at run start and interpolated into the system prompt under \`## Persistent Memory\`.
1405
- - \`memory_main_update\` can replace or append to that document. The tool description instructs the model to proactively evaluate each turn whether durable memory should be updated.
1405
+ - \`memory_main_write\` overwrites the entire memory document (for initial writes or full rewrites).
1406
+ - \`memory_main_edit\` performs targeted string-replacement edits on memory (find \`old_str\`, replace with \`new_str\`), mirroring \`edit_file\` semantics. The tool description instructs the model to proactively evaluate each turn whether durable memory should be updated.
1406
1407
  - \`conversation_recall\` can search recent prior conversations (keyword scoring) when historical context is relevant (\`as we discussed\`, \`last time\`, etc.).
1407
1408
 
1408
1409
  \`\`\`javascript
@@ -1420,9 +1421,10 @@ export default {
1420
1421
 
1421
1422
  Available memory tools:
1422
1423
 
1423
- - \`memory_main_get\`
1424
- - \`memory_main_update\`
1425
- - \`conversation_recall\`
1424
+ - \`memory_main_get\` \u2014 read the current memory document
1425
+ - \`memory_main_write\` \u2014 overwrite the entire memory document
1426
+ - \`memory_main_edit\` \u2014 edit memory via exact string replacement (\`old_str\` / \`new_str\`)
1427
+ - \`conversation_recall\` \u2014 search past conversations
1426
1428
  `,
1427
1429
  "configuration": `# Configuration & Security
1428
1430
 
@@ -1908,6 +1910,52 @@ var createWriteTool = (workingDir) => defineTool({
1908
1910
  return { path, written: true };
1909
1911
  }
1910
1912
  });
1913
+ var createEditTool = (workingDir) => defineTool({
1914
+ name: "edit_file",
1915
+ description: "Edit a file by replacing an exact string match with new content. The old_str must match exactly one location in the file (including whitespace and indentation). Use an empty new_str to delete matched content.",
1916
+ inputSchema: {
1917
+ type: "object",
1918
+ properties: {
1919
+ path: {
1920
+ type: "string",
1921
+ description: "File path relative to working directory"
1922
+ },
1923
+ old_str: {
1924
+ type: "string",
1925
+ description: "The exact text to find and replace (must be unique in the file). Include surrounding context lines if needed to ensure uniqueness."
1926
+ },
1927
+ new_str: {
1928
+ type: "string",
1929
+ description: "The replacement text (use empty string to delete the matched content)"
1930
+ }
1931
+ },
1932
+ required: ["path", "old_str", "new_str"],
1933
+ additionalProperties: false
1934
+ },
1935
+ handler: async (input) => {
1936
+ const path = typeof input.path === "string" ? input.path : "";
1937
+ const oldStr = typeof input.old_str === "string" ? input.old_str : "";
1938
+ const newStr = typeof input.new_str === "string" ? input.new_str : "";
1939
+ if (!oldStr) throw new Error("old_str must not be empty.");
1940
+ const resolved = resolveSafePath(workingDir, path);
1941
+ const content = await readFile3(resolved, "utf8");
1942
+ const first = content.indexOf(oldStr);
1943
+ if (first === -1) {
1944
+ throw new Error(
1945
+ "old_str not found in file. Make sure it matches exactly, including whitespace and line breaks."
1946
+ );
1947
+ }
1948
+ const last = content.lastIndexOf(oldStr);
1949
+ if (first !== last) {
1950
+ throw new Error(
1951
+ "old_str appears multiple times in the file. Please provide more context to ensure a unique match."
1952
+ );
1953
+ }
1954
+ const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
1955
+ await writeFile2(resolved, newContent, "utf8");
1956
+ return { path, edited: true };
1957
+ }
1958
+ });
1911
1959
  var createDeleteTool = (workingDir) => defineTool({
1912
1960
  name: "delete_file",
1913
1961
  description: "Delete a file at a path inside the working directory",
@@ -1982,6 +2030,8 @@ var ponchoDocsTool = defineTool({
1982
2030
 
1983
2031
  // src/harness.ts
1984
2032
  import { randomUUID as randomUUID3 } from "crypto";
2033
+ import { readFile as readFile7 } from "fs/promises";
2034
+ import { resolve as resolve9 } from "path";
1985
2035
  import { getTextContent as getTextContent3 } from "@poncho-ai/sdk";
1986
2036
 
1987
2037
  // src/upload-store.ts
@@ -2338,14 +2388,9 @@ var InMemoryMemoryStore = class {
2338
2388
  return this.mainMemory;
2339
2389
  }
2340
2390
  async updateMainMemory(input) {
2341
- const now2 = Date.now();
2342
- const existing = await this.getMainMemory();
2343
- const nextContent = input.mode === "append" && existing.content ? `${existing.content}
2344
-
2345
- ${input.content}`.trim() : input.content;
2346
2391
  this.mainMemory = {
2347
- content: nextContent.trim(),
2348
- updatedAt: now2
2392
+ content: input.content.trim(),
2393
+ updatedAt: Date.now()
2349
2394
  };
2350
2395
  return this.mainMemory;
2351
2396
  }
@@ -2403,12 +2448,8 @@ var FileMainMemoryStore = class {
2403
2448
  }
2404
2449
  async updateMainMemory(input) {
2405
2450
  await this.ensureLoaded();
2406
- const existing = await this.getMainMemory();
2407
- const nextContent = input.mode === "append" && existing.content ? `${existing.content}
2408
-
2409
- ${input.content}`.trim() : input.content;
2410
2451
  this.mainMemory = {
2411
- content: nextContent.trim(),
2452
+ content: input.content.trim(),
2412
2453
  updatedAt: Date.now()
2413
2454
  };
2414
2455
  await this.persist();
@@ -2447,8 +2488,7 @@ var KeyValueMainMemoryStoreBase = class {
2447
2488
  }
2448
2489
  } catch {
2449
2490
  await this.memoryFallback.updateMainMemory({
2450
- content: payload.main.content,
2451
- mode: "replace"
2491
+ content: payload.main.content
2452
2492
  });
2453
2493
  }
2454
2494
  }
@@ -2459,11 +2499,8 @@ var KeyValueMainMemoryStoreBase = class {
2459
2499
  async updateMainMemory(input) {
2460
2500
  const key = this.key();
2461
2501
  const payload = await this.readPayload(key);
2462
- const nextContent = input.mode === "append" && payload.main.content ? `${payload.main.content}
2463
-
2464
- ${input.content}`.trim() : input.content;
2465
2502
  payload.main = {
2466
- content: nextContent.trim(),
2503
+ content: input.content.trim(),
2467
2504
  updatedAt: Date.now()
2468
2505
  };
2469
2506
  await this.writePayload(key, payload);
@@ -2729,19 +2766,14 @@ var createMemoryTools = (store, options) => {
2729
2766
  }
2730
2767
  }),
2731
2768
  defineTool2({
2732
- name: "memory_main_update",
2733
- description: "Update persistent main memory when new stable preferences, long-term goals, or durable facts appear. Proactively evaluate every turn whether memory should be updated, and avoid storing ephemeral details.",
2769
+ name: "memory_main_write",
2770
+ description: "Overwrite the entire persistent main memory document. Use for initial writes or full rewrites. Prefer memory_main_edit for targeted changes to existing memory.",
2734
2771
  inputSchema: {
2735
2772
  type: "object",
2736
2773
  properties: {
2737
- mode: {
2738
- type: "string",
2739
- enum: ["replace", "append"],
2740
- description: "replace overwrites memory; append adds content to the end"
2741
- },
2742
2774
  content: {
2743
2775
  type: "string",
2744
- description: "The memory content to write"
2776
+ description: "The full memory content to write"
2745
2777
  }
2746
2778
  },
2747
2779
  required: ["content"],
@@ -2752,8 +2784,50 @@ var createMemoryTools = (store, options) => {
2752
2784
  if (!content) {
2753
2785
  throw new Error("content is required");
2754
2786
  }
2755
- const mode = input.mode === "append" || input.mode === "replace" ? input.mode : "replace";
2756
- const memory = await store.updateMainMemory({ content, mode });
2787
+ const memory = await store.updateMainMemory({ content });
2788
+ return { ok: true, memory };
2789
+ }
2790
+ }),
2791
+ defineTool2({
2792
+ name: "memory_main_edit",
2793
+ description: "Edit persistent main memory by replacing an exact string match with new content. The old_str must match exactly one location in memory. Use an empty new_str to delete matched content. Proactively evaluate every turn whether memory should be updated.",
2794
+ inputSchema: {
2795
+ type: "object",
2796
+ properties: {
2797
+ old_str: {
2798
+ type: "string",
2799
+ description: "The exact text to find and replace (must be unique in memory). Include surrounding context if needed to ensure uniqueness."
2800
+ },
2801
+ new_str: {
2802
+ type: "string",
2803
+ description: "The replacement text (use empty string to delete the matched content)"
2804
+ }
2805
+ },
2806
+ required: ["old_str", "new_str"],
2807
+ additionalProperties: false
2808
+ },
2809
+ handler: async (input) => {
2810
+ const oldStr = typeof input.old_str === "string" ? input.old_str : "";
2811
+ const newStr = typeof input.new_str === "string" ? input.new_str : "";
2812
+ if (!oldStr) {
2813
+ throw new Error("old_str must not be empty.");
2814
+ }
2815
+ const current = await store.getMainMemory();
2816
+ const content = current.content;
2817
+ const first = content.indexOf(oldStr);
2818
+ if (first === -1) {
2819
+ throw new Error(
2820
+ "old_str not found in memory. Make sure it matches exactly, including whitespace and line breaks."
2821
+ );
2822
+ }
2823
+ const last = content.lastIndexOf(oldStr);
2824
+ if (first !== last) {
2825
+ throw new Error(
2826
+ "old_str appears multiple times in memory. Please provide more context to ensure a unique match."
2827
+ );
2828
+ }
2829
+ const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
2830
+ const memory = await store.updateMainMemory({ content: newContent });
2757
2831
  return { ok: true, memory };
2758
2832
  }
2759
2833
  }),
@@ -4327,7 +4401,7 @@ You are running locally in development mode. Treat this as an editable agent wor
4327
4401
  ## Understanding Your Environment
4328
4402
 
4329
4403
  - Built-in tools: \`list_directory\` and \`read_file\`
4330
- - \`write_file\` is available in development (disabled by default in production)
4404
+ - \`write_file\` and \`edit_file\` are available in development (disabled by default in production)
4331
4405
  - A starter local skill is included (\`starter-echo\`)
4332
4406
  - Bash/shell commands are **not** available unless you install and enable a shell tool/skill
4333
4407
  - Git operations are only available if a git-capable tool/skill is configured
@@ -4588,6 +4662,7 @@ Since all fields have defaults, you only need to specify \`*Env\` when your env
4588
4662
  - If shell/CLI access is unavailable, ask the user to run needed commands and provide exact copy-paste commands.
4589
4663
  - For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
4590
4664
  - Prefer quoting concrete commands and examples from \`README.md\` over guessing.
4665
+ - Prefer \`edit_file\` for targeted changes to existing files (uses exact string matching); use \`write_file\` only for creating new files or full rewrites.
4591
4666
  - Keep edits minimal, preserve unrelated settings/code, and summarize what changed.
4592
4667
 
4593
4668
  ## Detailed Documentation
@@ -4641,6 +4716,7 @@ var AgentHarness = class _AgentHarness {
4641
4716
  _browserSession;
4642
4717
  _browserMod;
4643
4718
  parsedAgent;
4719
+ agentFileFingerprint = "";
4644
4720
  mcpBridge;
4645
4721
  subagentManager;
4646
4722
  resolveToolAccess(toolName) {
@@ -4658,7 +4734,7 @@ var AgentHarness = class _AgentHarness {
4658
4734
  isToolEnabled(name) {
4659
4735
  const access3 = this.resolveToolAccess(name);
4660
4736
  if (access3 === false) return false;
4661
- if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
4737
+ if (name === "write_file" || name === "edit_file" || name === "delete_file" || name === "delete_directory") {
4662
4738
  return this.shouldEnableWriteTool();
4663
4739
  }
4664
4740
  return true;
@@ -4701,6 +4777,9 @@ var AgentHarness = class _AgentHarness {
4701
4777
  if (this.isToolEnabled("write_file")) {
4702
4778
  this.registerIfMissing(createWriteTool(this.workingDir));
4703
4779
  }
4780
+ if (this.isToolEnabled("edit_file")) {
4781
+ this.registerIfMissing(createEditTool(this.workingDir));
4782
+ }
4704
4783
  if (this.isToolEnabled("delete_file")) {
4705
4784
  this.registerIfMissing(createDeleteTool(this.workingDir));
4706
4785
  }
@@ -4750,20 +4829,17 @@ var AgentHarness = class _AgentHarness {
4750
4829
  return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
4751
4830
  }
4752
4831
  getRequestedMcpPatterns() {
4753
- const skillPatterns = /* @__PURE__ */ new Set();
4832
+ const patterns = new Set(this.getAgentMcpIntent());
4754
4833
  for (const skillName of this.activeSkillNames) {
4755
4834
  const skill = this.loadedSkills.find((entry) => entry.name === skillName);
4756
4835
  if (!skill) {
4757
4836
  continue;
4758
4837
  }
4759
4838
  for (const pattern of skill.allowedTools.mcp) {
4760
- skillPatterns.add(pattern);
4839
+ patterns.add(pattern);
4761
4840
  }
4762
4841
  }
4763
- if (skillPatterns.size > 0) {
4764
- return [...skillPatterns];
4765
- }
4766
- return this.getAgentMcpIntent();
4842
+ return [...patterns];
4767
4843
  }
4768
4844
  getRequestedScriptPatterns() {
4769
4845
  const patterns = new Set(this.getAgentScriptIntent());
@@ -4779,20 +4855,17 @@ var AgentHarness = class _AgentHarness {
4779
4855
  return [...patterns];
4780
4856
  }
4781
4857
  getRequestedMcpApprovalPatterns() {
4782
- const skillPatterns = /* @__PURE__ */ new Set();
4858
+ const patterns = new Set(this.getAgentMcpApprovalPatterns());
4783
4859
  for (const skillName of this.activeSkillNames) {
4784
4860
  const skill = this.loadedSkills.find((entry) => entry.name === skillName);
4785
4861
  if (!skill) {
4786
4862
  continue;
4787
4863
  }
4788
4864
  for (const pattern of skill.approvalRequired.mcp) {
4789
- skillPatterns.add(pattern);
4865
+ patterns.add(pattern);
4790
4866
  }
4791
4867
  }
4792
- if (skillPatterns.size > 0) {
4793
- return [...skillPatterns];
4794
- }
4795
- return this.getAgentMcpApprovalPatterns();
4868
+ return [...patterns];
4796
4869
  }
4797
4870
  getRequestedScriptApprovalPatterns() {
4798
4871
  const patterns = new Set(this.getAgentScriptApprovalPatterns());
@@ -4923,13 +4996,54 @@ var AgentHarness = class _AgentHarness {
4923
4996
  );
4924
4997
  }
4925
4998
  static SKILL_REFRESH_DEBOUNCE_MS = 3e3;
4926
- async refreshSkillsIfChanged() {
4999
+ /**
5000
+ * Re-read AGENT.md and update the parsed agent when the file has changed
5001
+ * on disk. Returns `true` when the agent was actually re-parsed.
5002
+ *
5003
+ * Preserves the agent identity (id) across reloads so conversation
5004
+ * continuity isn't broken.
5005
+ */
5006
+ async refreshAgentIfChanged() {
4927
5007
  if (this.environment !== "development") {
4928
- return;
5008
+ return false;
4929
5009
  }
4930
- const elapsed = Date.now() - this.lastSkillRefreshAt;
4931
- if (this.lastSkillRefreshAt > 0 && elapsed < _AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
4932
- return;
5010
+ try {
5011
+ const agentFilePath = resolve9(this.workingDir, "AGENT.md");
5012
+ const rawContent = await readFile7(agentFilePath, "utf8");
5013
+ if (rawContent === this.agentFileFingerprint) {
5014
+ return false;
5015
+ }
5016
+ const parsed = parseAgentMarkdown(rawContent);
5017
+ if (!parsed.frontmatter.id && this.parsedAgent?.frontmatter.id) {
5018
+ parsed.frontmatter.id = this.parsedAgent.frontmatter.id;
5019
+ }
5020
+ this.parsedAgent = parsed;
5021
+ this.agentFileFingerprint = rawContent;
5022
+ return true;
5023
+ } catch (error) {
5024
+ console.warn(
5025
+ `[poncho][agent] Failed to refresh AGENT.md in development mode: ${error instanceof Error ? error.message : String(error)}`
5026
+ );
5027
+ return false;
5028
+ }
5029
+ }
5030
+ /**
5031
+ * Re-scan skill directories and update metadata, tools, and context window
5032
+ * when skills have changed on disk. Returns `true` when the skill set was
5033
+ * actually updated.
5034
+ *
5035
+ * @param force - bypass the time-based debounce (used for mid-run refreshes
5036
+ * after the agent may have written new skill files).
5037
+ */
5038
+ async refreshSkillsIfChanged(force = false) {
5039
+ if (this.environment !== "development") {
5040
+ return false;
5041
+ }
5042
+ if (!force) {
5043
+ const elapsed = Date.now() - this.lastSkillRefreshAt;
5044
+ if (this.lastSkillRefreshAt > 0 && elapsed < _AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
5045
+ return false;
5046
+ }
4933
5047
  }
4934
5048
  this.lastSkillRefreshAt = Date.now();
4935
5049
  try {
@@ -4939,22 +5053,35 @@ var AgentHarness = class _AgentHarness {
4939
5053
  );
4940
5054
  const nextFingerprint = this.buildSkillFingerprint(latestSkills);
4941
5055
  if (nextFingerprint === this.skillFingerprint) {
4942
- return;
5056
+ return false;
4943
5057
  }
4944
5058
  this.loadedSkills = latestSkills;
4945
5059
  this.skillContextWindow = buildSkillContextWindow(latestSkills);
4946
5060
  this.skillFingerprint = nextFingerprint;
4947
5061
  this.registerSkillTools(latestSkills);
4948
- this.activeSkillNames.clear();
5062
+ const latestSkillNames = new Set(latestSkills.map((s) => s.name));
5063
+ for (const name of this.activeSkillNames) {
5064
+ if (!latestSkillNames.has(name)) {
5065
+ this.activeSkillNames.delete(name);
5066
+ }
5067
+ }
5068
+ if (this.mcpBridge) {
5069
+ await this.mcpBridge.discoverTools();
5070
+ }
4949
5071
  await this.refreshMcpTools("skills:changed");
5072
+ return true;
4950
5073
  } catch (error) {
4951
5074
  console.warn(
4952
5075
  `[poncho][skills] Failed to refresh skills in development mode: ${error instanceof Error ? error.message : String(error)}`
4953
5076
  );
5077
+ return false;
4954
5078
  }
4955
5079
  }
4956
5080
  async initialize() {
4957
- this.parsedAgent = await parseAgentFile(this.workingDir);
5081
+ const agentFilePath = resolve9(this.workingDir, "AGENT.md");
5082
+ const agentRawContent = await readFile7(agentFilePath, "utf8");
5083
+ this.parsedAgent = parseAgentMarkdown(agentRawContent);
5084
+ this.agentFileFingerprint = agentRawContent;
4958
5085
  const identity = await ensureAgentIdentity(this.workingDir);
4959
5086
  if (!this.parsedAgent.frontmatter.id) {
4960
5087
  this.parsedAgent.frontmatter.id = identity.id;
@@ -5032,9 +5159,9 @@ var AgentHarness = class _AgentHarness {
5032
5159
  await writeFile6(filePath, json, "utf8");
5033
5160
  },
5034
5161
  async load() {
5035
- const { readFile: readFile8 } = await import("fs/promises");
5162
+ const { readFile: readFile9 } = await import("fs/promises");
5036
5163
  try {
5037
- return await readFile8(filePath, "utf8");
5164
+ return await readFile9(filePath, "utf8");
5038
5165
  } catch {
5039
5166
  return void 0;
5040
5167
  }
@@ -5198,9 +5325,9 @@ var AgentHarness = class _AgentHarness {
5198
5325
  for await (const event of this.run(input)) {
5199
5326
  eventQueue.push(event);
5200
5327
  if (queueResolve) {
5201
- const resolve10 = queueResolve;
5328
+ const resolve11 = queueResolve;
5202
5329
  queueResolve = null;
5203
- resolve10();
5330
+ resolve11();
5204
5331
  }
5205
5332
  }
5206
5333
  } catch (error) {
@@ -5219,8 +5346,8 @@ var AgentHarness = class _AgentHarness {
5219
5346
  if (eventQueue.length > 0) {
5220
5347
  yield eventQueue.shift();
5221
5348
  } else if (!generatorDone) {
5222
- await new Promise((resolve10) => {
5223
- queueResolve = resolve10;
5349
+ await new Promise((resolve11) => {
5350
+ queueResolve = resolve11;
5224
5351
  });
5225
5352
  }
5226
5353
  }
@@ -5258,13 +5385,14 @@ var AgentHarness = class _AgentHarness {
5258
5385
  await this.initialize();
5259
5386
  }
5260
5387
  const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
5388
+ await this.refreshAgentIfChanged();
5261
5389
  await this.refreshSkillsIfChanged();
5262
5390
  this._currentRunConversationId = input.conversationId;
5263
5391
  const ownerParam = input.parameters?.__ownerId;
5264
5392
  if (typeof ownerParam === "string") {
5265
5393
  this._currentRunOwnerId = ownerParam;
5266
5394
  }
5267
- const agent = this.parsedAgent;
5395
+ let agent = this.parsedAgent;
5268
5396
  const runId = `run_${randomUUID3()}`;
5269
5397
  const start = now();
5270
5398
  const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
@@ -5275,11 +5403,11 @@ var AgentHarness = class _AgentHarness {
5275
5403
  const messages = [...input.messages ?? []];
5276
5404
  const inputMessageCount = messages.length;
5277
5405
  const events = [];
5278
- const systemPrompt = renderAgentPrompt(agent, {
5406
+ const renderCurrentAgentPrompt = () => renderAgentPrompt(this.parsedAgent, {
5279
5407
  parameters: input.parameters,
5280
5408
  runtime: {
5281
5409
  runId,
5282
- agentId: agent.frontmatter.id ?? agent.frontmatter.name,
5410
+ agentId: this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name,
5283
5411
  environment: this.environment,
5284
5412
  workingDir: this.workingDir
5285
5413
  }
@@ -5307,9 +5435,6 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
5307
5435
 
5308
5436
  ### Tabs and resources
5309
5437
  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.` : "";
5310
- const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
5311
-
5312
- ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentContext}${browserContext}`;
5313
5438
  const mainMemory = await memoryPromise;
5314
5439
  const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
5315
5440
  ...[truncated]` : mainMemory?.content;
@@ -5317,7 +5442,12 @@ ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentCont
5317
5442
  ## Persistent Memory
5318
5443
 
5319
5444
  ${boundedMainMemory.trim()}` : "";
5320
- const integrityPrompt = `${promptWithSkills}${memoryContext}
5445
+ const buildSystemPrompt = () => {
5446
+ const agentPrompt = renderCurrentAgentPrompt();
5447
+ const promptWithSkills = this.skillContextWindow ? `${agentPrompt}${developmentContext}
5448
+
5449
+ ${this.skillContextWindow}${browserContext}` : `${agentPrompt}${developmentContext}${browserContext}`;
5450
+ return `${promptWithSkills}${memoryContext}
5321
5451
 
5322
5452
  ## Execution Integrity
5323
5453
 
@@ -5325,6 +5455,10 @@ ${boundedMainMemory.trim()}` : "";
5325
5455
  - Do not fabricate "Tool Used" or "Tool Result" logs as plain text.
5326
5456
  - Never output faux execution transcripts, markdown tool logs, or "Tool Used/Result" sections.
5327
5457
  - If no suitable tool is available, explicitly say that and ask for guidance.`;
5458
+ };
5459
+ let integrityPrompt = buildSystemPrompt();
5460
+ let lastPromptFingerprint = `${this.agentFileFingerprint}
5461
+ ${this.skillFingerprint}`;
5328
5462
  const pushEvent = (event) => {
5329
5463
  events.push(event);
5330
5464
  return event;
@@ -5747,8 +5881,8 @@ ${textContent}` };
5747
5881
  let timer;
5748
5882
  nextPart = await Promise.race([
5749
5883
  fullStreamIterator.next(),
5750
- new Promise((resolve10) => {
5751
- timer = setTimeout(() => resolve10(null), timeout);
5884
+ new Promise((resolve11) => {
5885
+ timer = setTimeout(() => resolve11(null), timeout);
5752
5886
  })
5753
5887
  ]);
5754
5888
  clearTimeout(timer);
@@ -6056,6 +6190,19 @@ ${textContent}` };
6056
6190
  content: JSON.stringify(toolResultsForModel),
6057
6191
  metadata: toolMsgMeta
6058
6192
  });
6193
+ if (this.environment === "development") {
6194
+ const agentChanged = await this.refreshAgentIfChanged();
6195
+ const skillsChanged = await this.refreshSkillsIfChanged(true);
6196
+ if (agentChanged || skillsChanged) {
6197
+ agent = this.parsedAgent;
6198
+ const currentFingerprint = `${this.agentFileFingerprint}
6199
+ ${this.skillFingerprint}`;
6200
+ if (currentFingerprint !== lastPromptFingerprint) {
6201
+ integrityPrompt = buildSystemPrompt();
6202
+ lastPromptFingerprint = currentFingerprint;
6203
+ }
6204
+ }
6205
+ }
6059
6206
  yield pushEvent({
6060
6207
  type: "step:completed",
6061
6208
  step,
@@ -6244,8 +6391,8 @@ var LatitudeCapture = class {
6244
6391
 
6245
6392
  // src/state.ts
6246
6393
  import { randomUUID as randomUUID4 } from "crypto";
6247
- import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
6248
- import { dirname as dirname4, resolve as resolve9 } from "path";
6394
+ import { mkdir as mkdir4, readFile as readFile8, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
6395
+ import { dirname as dirname4, resolve as resolve10 } from "path";
6249
6396
  var DEFAULT_OWNER = "local-owner";
6250
6397
  var LOCAL_STATE_FILE = "state.json";
6251
6398
  var CONVERSATIONS_DIRECTORY = "conversations";
@@ -6438,8 +6585,8 @@ var FileConversationStore = class {
6438
6585
  agentId: this.agentId
6439
6586
  });
6440
6587
  const agentDir = getAgentStoreDirectory(identity);
6441
- const conversationsDir = resolve9(agentDir, CONVERSATIONS_DIRECTORY);
6442
- const indexPath = resolve9(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
6588
+ const conversationsDir = resolve10(agentDir, CONVERSATIONS_DIRECTORY);
6589
+ const indexPath = resolve10(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
6443
6590
  this.paths = { conversationsDir, indexPath };
6444
6591
  return this.paths;
6445
6592
  }
@@ -6453,9 +6600,9 @@ var FileConversationStore = class {
6453
6600
  }
6454
6601
  async readConversationFile(fileName) {
6455
6602
  const { conversationsDir } = await this.resolvePaths();
6456
- const filePath = resolve9(conversationsDir, fileName);
6603
+ const filePath = resolve10(conversationsDir, fileName);
6457
6604
  try {
6458
- const raw = await readFile7(filePath, "utf8");
6605
+ const raw = await readFile8(filePath, "utf8");
6459
6606
  return JSON.parse(raw);
6460
6607
  } catch {
6461
6608
  return void 0;
@@ -6501,7 +6648,7 @@ var FileConversationStore = class {
6501
6648
  this.loaded = true;
6502
6649
  const { indexPath } = await this.resolvePaths();
6503
6650
  try {
6504
- const raw = await readFile7(indexPath, "utf8");
6651
+ const raw = await readFile8(indexPath, "utf8");
6505
6652
  const parsed = JSON.parse(raw);
6506
6653
  for (const conversation of parsed.conversations ?? []) {
6507
6654
  this.conversations.set(conversation.conversationId, conversation);
@@ -6524,7 +6671,7 @@ var FileConversationStore = class {
6524
6671
  const { conversationsDir } = await this.resolvePaths();
6525
6672
  const existing = this.conversations.get(conversation.conversationId);
6526
6673
  const fileName = existing?.fileName ?? this.resolveConversationFileName(conversation);
6527
- const filePath = resolve9(conversationsDir, fileName);
6674
+ const filePath = resolve10(conversationsDir, fileName);
6528
6675
  this.writing = this.writing.then(async () => {
6529
6676
  await writeJsonAtomic2(filePath, conversation);
6530
6677
  this.conversations.set(conversation.conversationId, {
@@ -6622,7 +6769,7 @@ var FileConversationStore = class {
6622
6769
  if (removed) {
6623
6770
  this.writing = this.writing.then(async () => {
6624
6771
  if (existing) {
6625
- await rm3(resolve9(conversationsDir, existing.fileName), { force: true });
6772
+ await rm3(resolve10(conversationsDir, existing.fileName), { force: true });
6626
6773
  }
6627
6774
  await this.writeIndex();
6628
6775
  });
@@ -6652,7 +6799,7 @@ var FileStateStore = class {
6652
6799
  workingDir: this.workingDir,
6653
6800
  agentId: this.agentId
6654
6801
  });
6655
- this.filePath = resolve9(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
6802
+ this.filePath = resolve10(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
6656
6803
  }
6657
6804
  isExpired(state) {
6658
6805
  return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
@@ -6664,7 +6811,7 @@ var FileStateStore = class {
6664
6811
  }
6665
6812
  this.loaded = true;
6666
6813
  try {
6667
- const raw = await readFile7(this.filePath, "utf8");
6814
+ const raw = await readFile8(this.filePath, "utf8");
6668
6815
  const parsed = JSON.parse(raw);
6669
6816
  for (const state of parsed.states ?? []) {
6670
6817
  this.states.set(state.runId, state);
@@ -7412,6 +7559,7 @@ export {
7412
7559
  createDefaultTools,
7413
7560
  createDeleteDirectoryTool,
7414
7561
  createDeleteTool,
7562
+ createEditTool,
7415
7563
  createMemoryStore,
7416
7564
  createMemoryTools,
7417
7565
  createModelProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/config.ts CHANGED
@@ -39,6 +39,7 @@ export type BuiltInToolToggles = {
39
39
  list_directory?: boolean;
40
40
  read_file?: boolean;
41
41
  write_file?: boolean;
42
+ edit_file?: boolean;
42
43
  delete_file?: boolean;
43
44
  delete_directory?: boolean;
44
45
  };