@poncho-ai/harness 0.59.15 → 0.59.16

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.59.15 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.59.16 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,9 +8,9 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
+ ESM dist/index.js 571.49 KB
11
12
  ESM dist/isolate-F2PPSUL6.js 53.82 KB
12
- ESM dist/index.js 570.16 KB
13
- ESM ⚡️ Build success in 242ms
13
+ ESM ⚡️ Build success in 239ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 6639ms
15
+ DTS ⚡️ Build success in 6987ms
16
16
  DTS dist/index.d.ts 103.12 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.59.16
4
+
5
+ ### Patch Changes
6
+
7
+ - [`96bbf55`](https://github.com/cesr/poncho-ai/commit/96bbf5532d85de1ae0957f770758bf26b9c1e50b) Thanks [@cesr](https://github.com/cesr)! - Tighten the oversized-tool-result spill read-back path (follow-up to the
8
+ spill guard):
9
+ - The spill handle now exposes `toolResultId` / `toolCallId` explicitly, so
10
+ the model passes the right id to `get_tool_result_by_id` instead of guessing
11
+ from the path stem.
12
+ - `get_tool_result_by_id` now redirects when it hits a spill envelope: it
13
+ returns `{ spilled: true, path, totalChars }` pointing at the VFS file rather
14
+ than silently paging the ~6k-char envelope (which read as "I've got
15
+ everything"). The file is the source of truth; read it with bash.
16
+ - The handle's `note` is now format-aware: JSONL spills keep the line tools
17
+ (`sed`/`grep`/`jq` per line), but pretty-JSON spills steer to byte-offset
18
+ reads and `jq -r` to unescape — `wc -l`/`grep` mislead on JSON whose string
19
+ fields carry escaped newlines on one line.
20
+
3
21
  ## 0.59.15
4
22
 
5
23
  ### Patch Changes
package/dist/index.js CHANGED
@@ -5676,19 +5676,24 @@ var formatSpillPayload = (output) => {
5676
5676
  return { content: JSON.stringify(output ?? null, null, 2), format: "json" };
5677
5677
  };
5678
5678
  var buildSpillHandle = (opts) => {
5679
- const { toolName, path, format, serialized, records } = opts;
5679
+ const { toolName, toolCallId, path, format, serialized, records } = opts;
5680
5680
  const approxTokens = Math.ceil(serialized.length / 4);
5681
- const isArray = format === "jsonl";
5681
+ const bytesHint = `\`wc -c ${path}\` (size), \`head -c 4000 ${path}\`, \`tail -c +<N> ${path} | head -c 4000\` (read from byte N)`;
5682
+ const note = format === "jsonl" ? `Result too large to return inline (~${approxTokens.toLocaleString()} tokens, ${records} records, one JSON object per line). Read it with bash: ${bytesHint}, \`sed -n '1,5p' ${path}\`, \`grep -i <term> ${path}\`, or \`jq -c 'select(...)' ${path}\` per line. Do NOT cat/read_file the whole file (it re-overflows). Or re-run the tool with a narrower request.` : `Result too large to return inline (~${approxTokens.toLocaleString()} tokens, JSON). Its string fields are escaped onto single lines, so \`wc -l\`/\`grep\` MISLEAD \u2014 read by byte offset (${bytesHint}) or unescape first with \`jq -r '.stdout // .' ${path}\` (then pipe to wc -l / grep). Do NOT cat/read_file the whole file (it re-overflows). Or re-run the tool with a narrower request.`;
5682
5683
  return {
5683
5684
  __toolResultSpilled: true,
5684
5685
  tool: toolName,
5686
+ // Expose the id explicitly so the model passes the right value if it
5687
+ // reaches for get_tool_result_by_id (the path stem is NOT the id).
5688
+ toolResultId: toolCallId,
5689
+ toolCallId,
5685
5690
  path,
5686
5691
  format,
5687
5692
  ...records !== void 0 ? { records } : {},
5688
5693
  totalChars: serialized.length,
5689
5694
  approxTokens,
5690
5695
  preview: serialized.slice(0, SPILLED_PREVIEW_CHARS),
5691
- note: `This result was too large to return inline (~${approxTokens.toLocaleString()} tokens) and was saved to ${path}. Inspect it with bash \u2014 e.g. \`wc -l ${path}\`, \`head -c 4000 ${path}\`, ` + (isArray ? `\`sed -n '1,5p' ${path}\`, \`grep -i <term> ${path}\`, or \`jq\` per line` : `\`grep -i <term> ${path}\``) + `. Do NOT cat or read_file the whole file (it re-overflows). Or re-run the tool with a narrower request (fewer items / a filter / metadata only).`
5696
+ note
5692
5697
  };
5693
5698
  };
5694
5699
  var buildInlineTruncation = (toolName, serialized) => {
@@ -9773,6 +9778,21 @@ var AgentHarness = class _AgentHarness {
9773
9778
  error: `No archived tool result found for id "${toolResultId}" in this conversation.`
9774
9779
  };
9775
9780
  }
9781
+ try {
9782
+ const env = JSON.parse(record.payload);
9783
+ if (env && env.__toolResultSpilled === true && typeof env.path === "string") {
9784
+ return {
9785
+ toolResultId: record.toolResultId,
9786
+ toolName: record.toolName,
9787
+ toolCallId: record.toolCallId,
9788
+ spilled: true,
9789
+ path: env.path,
9790
+ totalChars: typeof env.totalChars === "number" ? env.totalChars : void 0,
9791
+ note: `This result was spilled to ${env.path} \u2014 the archive only holds a preview. Read the full payload there with bash (head -c / tail -c +<N> | head -c, or jq), NOT via get_tool_result_by_id.`
9792
+ };
9793
+ }
9794
+ } catch {
9795
+ }
9776
9796
  const offset = Math.max(0, Number(input.offset) || 0);
9777
9797
  const limit = Math.min(Math.max(Number(input.limit) || 6e3, 1), 2e4);
9778
9798
  const end = Math.min(record.payload.length, offset + limit);
@@ -9900,7 +9920,7 @@ var AgentHarness = class _AgentHarness {
9900
9920
  await vfs.mkdir(policy.dir, { recursive: true });
9901
9921
  await vfs.writeText(path, content);
9902
9922
  await this.pruneSpillDir(vfs, policy.dir, policy.keepLast);
9903
- return buildSpillHandle({ toolName, path, format, serialized, records });
9923
+ return buildSpillHandle({ toolName, toolCallId, path, format, serialized, records });
9904
9924
  } catch {
9905
9925
  }
9906
9926
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.59.15",
3
+ "version": "0.59.16",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/harness.ts CHANGED
@@ -1068,6 +1068,30 @@ export class AgentHarness {
1068
1068
  error: `No archived tool result found for id "${toolResultId}" in this conversation.`,
1069
1069
  };
1070
1070
  }
1071
+ // Spill envelopes: the archived payload is the small handle, not the
1072
+ // data — the real payload lives in a VFS file. Paging the envelope here
1073
+ // would silently return ~6k chars and `hasMore:false`, reading as "I've
1074
+ // got everything". Redirect to the file (read it with bash) instead of
1075
+ // building a second read path the agent already has via bash/jq.
1076
+ try {
1077
+ const env = JSON.parse(record.payload) as Record<string, unknown>;
1078
+ if (env && env.__toolResultSpilled === true && typeof env.path === "string") {
1079
+ return {
1080
+ toolResultId: record.toolResultId,
1081
+ toolName: record.toolName,
1082
+ toolCallId: record.toolCallId,
1083
+ spilled: true,
1084
+ path: env.path,
1085
+ totalChars: typeof env.totalChars === "number" ? env.totalChars : undefined,
1086
+ note:
1087
+ `This result was spilled to ${env.path} — the archive only holds a preview. ` +
1088
+ `Read the full payload there with bash (head -c / tail -c +<N> | head -c, or ` +
1089
+ `jq), NOT via get_tool_result_by_id.`,
1090
+ };
1091
+ }
1092
+ } catch {
1093
+ // Not JSON / not an envelope — fall through to normal paging.
1094
+ }
1071
1095
  const offset = Math.max(0, Number(input.offset) || 0);
1072
1096
  const limit = Math.min(Math.max(Number(input.limit) || 6000, 1), 20_000);
1073
1097
  const end = Math.min(record.payload.length, offset + limit);
@@ -1219,7 +1243,7 @@ export class AgentHarness {
1219
1243
  await vfs.mkdir(policy.dir, { recursive: true });
1220
1244
  await vfs.writeText(path, content);
1221
1245
  await this.pruneSpillDir(vfs, policy.dir, policy.keepLast);
1222
- return buildSpillHandle({ toolName, path, format, serialized, records });
1246
+ return buildSpillHandle({ toolName, toolCallId, path, format, serialized, records });
1223
1247
  } catch {
1224
1248
  // Fall through to inline truncation below.
1225
1249
  }
@@ -80,32 +80,49 @@ export const formatSpillPayload = (
80
80
  * a file: a small preview plus the path and bash-readback instructions. */
81
81
  export const buildSpillHandle = (opts: {
82
82
  toolName: string;
83
+ toolCallId: string;
83
84
  path: string;
84
85
  format: "jsonl" | "json";
85
86
  serialized: string;
86
87
  records?: number;
87
88
  }): Record<string, unknown> => {
88
- const { toolName, path, format, serialized, records } = opts;
89
+ const { toolName, toolCallId, path, format, serialized, records } = opts;
89
90
  const approxTokens = Math.ceil(serialized.length / 4);
90
- const isArray = format === "jsonl";
91
+ // Byte-offset reads are correct for BOTH formats; line tools (`wc -l`,
92
+ // `grep`, `sed -n`) only behave for JSONL (one record per line). For pretty
93
+ // JSON the payload is a JSON document whose string fields (e.g. a command's
94
+ // stdout) carry escaped `\n` on a single line, so `wc -l`/`grep` mislead —
95
+ // unescape with `jq -r` first. Tailor the hint to the format so the model
96
+ // doesn't act on wrong line/match counts.
97
+ const bytesHint =
98
+ `\`wc -c ${path}\` (size), \`head -c 4000 ${path}\`, ` +
99
+ `\`tail -c +<N> ${path} | head -c 4000\` (read from byte N)`;
100
+ const note =
101
+ format === "jsonl"
102
+ ? `Result too large to return inline (~${approxTokens.toLocaleString()} tokens, ` +
103
+ `${records} records, one JSON object per line). Read it with bash: ${bytesHint}, ` +
104
+ `\`sed -n '1,5p' ${path}\`, \`grep -i <term> ${path}\`, or \`jq -c 'select(...)' ${path}\` ` +
105
+ `per line. Do NOT cat/read_file the whole file (it re-overflows). Or re-run the tool ` +
106
+ `with a narrower request.`
107
+ : `Result too large to return inline (~${approxTokens.toLocaleString()} tokens, JSON). ` +
108
+ `Its string fields are escaped onto single lines, so \`wc -l\`/\`grep\` MISLEAD — ` +
109
+ `read by byte offset (${bytesHint}) or unescape first with ` +
110
+ `\`jq -r '.stdout // .' ${path}\` (then pipe to wc -l / grep). Do NOT cat/read_file the ` +
111
+ `whole file (it re-overflows). Or re-run the tool with a narrower request.`;
91
112
  return {
92
113
  __toolResultSpilled: true,
93
114
  tool: toolName,
115
+ // Expose the id explicitly so the model passes the right value if it
116
+ // reaches for get_tool_result_by_id (the path stem is NOT the id).
117
+ toolResultId: toolCallId,
118
+ toolCallId,
94
119
  path,
95
120
  format,
96
121
  ...(records !== undefined ? { records } : {}),
97
122
  totalChars: serialized.length,
98
123
  approxTokens,
99
124
  preview: serialized.slice(0, SPILLED_PREVIEW_CHARS),
100
- note:
101
- `This result was too large to return inline (~${approxTokens.toLocaleString()} ` +
102
- `tokens) and was saved to ${path}. Inspect it with bash — e.g. ` +
103
- `\`wc -l ${path}\`, \`head -c 4000 ${path}\`, ` +
104
- (isArray
105
- ? `\`sed -n '1,5p' ${path}\`, \`grep -i <term> ${path}\`, or \`jq\` per line`
106
- : `\`grep -i <term> ${path}\``) +
107
- `. Do NOT cat or read_file the whole file (it re-overflows). Or re-run the ` +
108
- `tool with a narrower request (fewer items / a filter / metadata only).`,
125
+ note,
109
126
  };
110
127
  };
111
128
 
@@ -81,11 +81,12 @@ describe("formatSpillPayload", () => {
81
81
  });
82
82
 
83
83
  describe("buildSpillHandle", () => {
84
- it("returns a handle with path, format, records, preview, and bash instructions", () => {
84
+ it("returns a handle with path, format, records, preview, the call id, and bash instructions", () => {
85
85
  const serialized = "x".repeat(SPILLED_PREVIEW_CHARS + 50_000);
86
86
  const h = buildSpillHandle({
87
87
  toolName: "mcp_gmail_GMAIL_FETCH_EMAILS",
88
- path: "/tmp/tool-results/mcp_gmail_GMAIL_FETCH_EMAILS_tool_1.jsonl",
88
+ toolCallId: "toolu_01Ctij",
89
+ path: "/tmp/tool-results/mcp_gmail_GMAIL_FETCH_EMAILS_toolu_01Ctij.jsonl",
89
90
  format: "jsonl",
90
91
  serialized,
91
92
  records: 312,
@@ -94,20 +95,31 @@ describe("buildSpillHandle", () => {
94
95
  expect(h.records).toBe(312);
95
96
  expect(h.totalChars).toBe(serialized.length);
96
97
  expect((h.preview as string).length).toBe(SPILLED_PREVIEW_CHARS);
98
+ // Issue 1: the call id is exposed explicitly (not just embedded in the path).
99
+ expect(h.toolCallId).toBe("toolu_01Ctij");
100
+ expect(h.toolResultId).toBe("toolu_01Ctij");
97
101
  expect(String(h.note)).toContain("/tmp/tool-results/");
98
- expect(String(h.note)).toContain("jq"); // array → jsonl hint
99
102
  expect(String(h.note)).toContain("Do NOT cat");
103
+ // JSONL: line tools are valid (one record per line).
104
+ expect(String(h.note)).toContain("sed -n");
100
105
  });
101
106
 
102
- it("omits the array-only jq hint and records for json format", () => {
107
+ it("json format steers AWAY from line tools and toward byte-offset / jq (Issue 3)", () => {
103
108
  const h = buildSpillHandle({
104
- toolName: "t",
105
- path: "/tmp/tool-results/t_x.json",
109
+ toolName: "run_code",
110
+ toolCallId: "toolu_2",
111
+ path: "/tmp/tool-results/run_code_toolu_2.json",
106
112
  format: "json",
107
113
  serialized: "y".repeat(10),
108
114
  });
109
115
  expect(h.records).toBeUndefined();
110
- expect(String(h.note)).not.toContain("jq");
116
+ const note = String(h.note);
117
+ // It must warn that wc -l / grep mislead, and point at byte-offset + jq -r.
118
+ expect(note).toContain("MISLEAD");
119
+ expect(note).toContain("tail -c +");
120
+ expect(note).toContain("jq -r");
121
+ // It must NOT suggest sed-by-line for escaped-JSON.
122
+ expect(note).not.toContain("sed -n");
111
123
  });
112
124
  });
113
125