fix(agents): preserve approved exec output stream labels (#127595)

Adopted from Microsoft's Lobster patch 0013 via
giodl73-repo/lobster-plugins-and-patches (source commit e9f84234ebdf).
This commit is contained in:
Peter Steinberger
2026-08-21 14:31:26 -07:00
committed by GitHub
parent 41acededbc
commit c18654bf01
2 changed files with 57 additions and 1 deletions
@@ -74,6 +74,33 @@ describe("formatExecApprovalContinuationSourceOutput", () => {
expect(formatted.endsWith("b")).toBe(true);
});
it.each([
{ name: "stderr", label: "stderr", stdoutUnits: 200_000, streamUnits: 100_000 },
{ name: "error", label: "error", stdoutUnits: 200_000, streamUnits: 100_000 },
{ name: "head header cut", label: "stderr", stdoutUnits: 191_907, streamUnits: 100_000 },
{ name: "tail header cut", label: "stderr", stdoutUnits: 200_000, streamUnits: 63_970 },
{ name: "already retained header", label: "stderr", stdoutUnits: 200_000, streamUnits: 63_960 },
])("preserves the retained stream label across $name", ({ label, stdoutUnits, streamUnits }) => {
const formatted = formatExecApprovalContinuationSourceOutput([
{ label: "stdout", value: "a".repeat(stdoutUnits) },
{ label, value: "b".repeat(streamUnits) },
]);
expect(formatted.length).toBeLessThanOrEqual(MAX_SOURCE_UTF16_UNITS);
expect(formatted).toContain("[stdout]\n");
expect(formatted).toContain(`${MARKER}\n[${label}]\n`);
expect(formatted.split(`[${label}]\n`)).toHaveLength(2);
expect(formatted.endsWith("b")).toBe(true);
});
it("does not mistake stream-like text in single-stream output for a stream boundary", () => {
const formatted = formatExecApprovalContinuationSourceOutput([
{ label: "stdout", value: `${"a".repeat(200_000)}\n[stderr]\n${"b".repeat(100_000)}` },
]);
expect(formatted).not.toContain(`${MARKER}\n[stderr]\n`);
});
it("uses an honest marker because capture may already have dropped output", () => {
const formatted = formatExecApprovalContinuationSourceOutput([
{ label: "stdout", value: "z".repeat(MAX_SOURCE_UTF16_UNITS + 1) },
@@ -193,6 +220,25 @@ describe("resizeExecApprovalContinuationPrompt", () => {
expect(resized.split(OUTPUT_BEGIN)).toHaveLength(2);
expect(resized.split(OUTPUT_END)).toHaveLength(2);
});
it("preserves the retained stream label when the resumed model applies its output budget", () => {
const output = formatExecApprovalContinuationSourceOutput([
{ label: "stdout", value: "a".repeat(12_000) },
{ label: "stderr", value: "b".repeat(10_000) },
]);
const built = buildExecApprovalContinuationPrompt(
`Exec finished (node=node-1 id=approval-1, code 0)\n${output}`,
);
const resized = resizeExecApprovalContinuationPrompt({
prompt: built.message,
range: built.resultRange,
maxOutputUtf16Units: 16_000,
});
expect(resized).toContain(`${MARKER}\n[stderr]\n`);
expect(resized).toContain(OUTPUT_BEGIN);
expect(resized).toContain(OUTPUT_END);
});
});
describe("exec output rendering", () => {
+11 -1
View File
@@ -74,8 +74,18 @@ function capContinuationOutput(text: string, maxUtf16Units: number): string {
return truncateUtf16Safe(TRUNCATION_MARKER, boundedMax);
}
const headBudget = Math.floor(cutBudget * HEAD_SHARE);
const head = alignHeadToLineBreak(truncateUtf16Safe(text, headBudget));
let head = alignHeadToLineBreak(truncateUtf16Safe(text, headBudget));
const tail = alignTailToLineBreak(sliceUtf16Safe(text, text.length - (cutBudget - headBudget)));
// Recapping can omit a stream header while retaining its contents; restore its
// label from the omitted span so stderr/error never masquerades as stdout.
const omittedHeaders = /^\[(?:stdout|stderr|error)\]\n/m.test(head)
? text.slice(head.length, text.length - tail.length).match(/^\[(?:stdout|stderr|error)\]\n/gm)
: null;
const tailHeader = omittedHeaders?.at(-1) ?? "";
if (tailHeader && tailHeader.length <= headBudget) {
head = alignHeadToLineBreak(truncateUtf16Safe(text, headBudget - tailHeader.length));
return `${head}\n${TRUNCATION_MARKER}\n${tailHeader}${tail}`;
}
return `${head}\n${TRUNCATION_MARKER}\n${tail}`;
}