@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +18 -0
- package/dist/index.js +24 -4
- package/package.json +1 -1
- package/src/harness.ts +25 -1
- package/src/tool-result-guard.ts +28 -11
- package/test/tool-result-guard.test.ts +19 -7
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.59.
|
|
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
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m571.49 KB[39m
|
|
11
12
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
12
|
-
[32mESM[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in 242ms
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 239ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 6987ms
|
|
16
16
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m103.12 KB[39m
|
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
|
|
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
|
|
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
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
|
}
|
package/src/tool-result-guard.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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("
|
|
107
|
+
it("json format steers AWAY from line tools and toward byte-offset / jq (Issue 3)", () => {
|
|
103
108
|
const h = buildSpillHandle({
|
|
104
|
-
toolName: "
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|