mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): make codemode output joining linear
joinAdjacentText checked endsWith on the growing joined string, which makes V8 flatten the concatenation on every item. Collect each text run in an array, check the separator against the last non-empty part, and join once. Same fix in the durable code mode copy. closes #10678
This commit is contained in:
parent
a447ba11f3
commit
f11e50f72e
4 changed files with 57 additions and 12 deletions
|
|
@ -26,6 +26,7 @@
|
|||
- Fixed a symlinked `AGENTS.md` in a git worktree nested inside its main repo causing no project context file to load ([#10681](https://github.com/earendil-works/pi/issues/10681))
|
||||
- Fixed the code mode tool description claiming every failed tool call rejects; a tool that declares an output schema resolves with its value on failure too (e.g. `bash` with a nonzero exit)
|
||||
- Fixed runs started by `pi.sendMessage(..., { triggerTurn: true })` removing system prompt sections added in `before_agent_start` after their first tool call, which changed the prompt mid-run and broke the provider prompt cache twice per run ([#10267](https://github.com/earendil-works/pi/issues/10267))
|
||||
- Fixed codemode blocking the process for seconds to minutes after scripts with many `text()` calls; joining the output items was quadratic ([#10678](https://github.com/earendil-works/pi/issues/10678))
|
||||
|
||||
## [1.1.0] - 2026-10-07
|
||||
|
||||
|
|
|
|||
|
|
@ -285,18 +285,34 @@ function formatOutput(output: readonly CodemodeOutputItem[]): (TextContent | Ima
|
|||
return items;
|
||||
}
|
||||
|
||||
/** Join adjacent text items into one, each part starting on its own line. */
|
||||
/**
|
||||
* Join adjacent text items into one, each part starting on its own line. The separator check looks
|
||||
* at the last non-empty part, not the joined text: reading the end of a growing concatenation makes
|
||||
* V8 copy it on every item, which is quadratic (#10678).
|
||||
*/
|
||||
function joinAdjacentText(items: (TextContent | ImageContent)[]): (TextContent | ImageContent)[] {
|
||||
const joined: (TextContent | ImageContent)[] = [];
|
||||
let parts: string[] = [];
|
||||
let lastPart = "";
|
||||
const flush = () => {
|
||||
if (parts.length > 0) joined.push({ type: "text", text: parts.join("") });
|
||||
parts = [];
|
||||
lastPart = "";
|
||||
};
|
||||
for (const item of items) {
|
||||
const last = joined.at(-1);
|
||||
if (item.type === "text" && last?.type === "text") {
|
||||
const separator = last.text === "" || last.text.endsWith("\n") ? "" : "\n";
|
||||
joined[joined.length - 1] = { type: "text", text: `${last.text}${separator}${item.text}` };
|
||||
} else {
|
||||
if (item.type !== "text") {
|
||||
flush();
|
||||
joined.push(item);
|
||||
continue;
|
||||
}
|
||||
if (lastPart !== "" && !lastPart.endsWith("\n")) {
|
||||
parts.push("\n");
|
||||
lastPart = "\n";
|
||||
}
|
||||
parts.push(item.text);
|
||||
if (item.text !== "") lastPart = item.text;
|
||||
}
|
||||
flush();
|
||||
return joined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -541,6 +541,18 @@ describe("codemode options and store", () => {
|
|||
expect((small.details as unknown as CodemodeToolDetails).fullOutputPath).toBeUndefined();
|
||||
});
|
||||
|
||||
// #10678: joining the text items was quadratic, about 10 s here for the item limit.
|
||||
it("formats output at the item limit in linear time", async () => {
|
||||
const harness = await setup();
|
||||
const startedAt = performance.now();
|
||||
const result = await run(harness, 'for (let i = 0; i < 100000; i++) text("");');
|
||||
const elapsed = performance.now() - startedAt;
|
||||
rmSync((result.details as unknown as CodemodeToolDetails).fullOutputPath ?? "", { force: true });
|
||||
expect(result.isError).toBe(false);
|
||||
expect(resultText(result)).toContain("==> text 100000/100000 <==");
|
||||
expect(elapsed).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
it("resolves bash calls to structured results, also for non-zero exit codes", async () => {
|
||||
const harness = await createHarness({
|
||||
initialActiveToolNames: ["codemode", "bash"],
|
||||
|
|
|
|||
|
|
@ -60,18 +60,34 @@ function formatItems(output: readonly CodemodeOutputItem[]): Item[] {
|
|||
return items;
|
||||
}
|
||||
|
||||
/** Join adjacent text items into one, each part starting on its own line. */
|
||||
/**
|
||||
* Join adjacent text items into one, each part starting on its own line. The separator check looks at the last
|
||||
* non-empty part, not the joined text: reading the end of a growing concatenation makes V8 copy it on every item, which
|
||||
* is quadratic.
|
||||
*/
|
||||
function joinAdjacentText(items: readonly Item[]): Item[] {
|
||||
const joined: Item[] = [];
|
||||
let parts: string[] = [];
|
||||
let lastPart = "";
|
||||
const flush = () => {
|
||||
if (parts.length > 0) joined.push({ type: "text", text: parts.join("") });
|
||||
parts = [];
|
||||
lastPart = "";
|
||||
};
|
||||
for (const item of items) {
|
||||
const last = joined.at(-1);
|
||||
if (item.type === "text" && last?.type === "text") {
|
||||
const separator = last.text === "" || last.text.endsWith("\n") ? "" : "\n";
|
||||
joined[joined.length - 1] = { type: "text", text: `${last.text}${separator}${item.text}` };
|
||||
} else {
|
||||
if (item.type !== "text") {
|
||||
flush();
|
||||
joined.push(item);
|
||||
continue;
|
||||
}
|
||||
if (lastPart !== "" && !lastPart.endsWith("\n")) {
|
||||
parts.push("\n");
|
||||
lastPart = "\n";
|
||||
}
|
||||
parts.push(item.text);
|
||||
if (item.text !== "") lastPart = item.text;
|
||||
}
|
||||
flush();
|
||||
return joined;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue