Skip to content

Commit 1cdf084

Browse files
committed
fix(subagents): avoid unnecessary result reads
1 parent fd5b68f commit 1cdf084

5 files changed

Lines changed: 96 additions & 20 deletions

File tree

home/.pi/agent/extensions/subagents/bootstrap.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CleanupAggregateError, type AgentQuestion, type AgentSummary } from "./
66
import { loadProfiles, type ProfilesConfig } from "./profiles.ts";
77
import type { RunUsage } from "./run-state.ts";
88
import { SpawnAdmissionController } from "./spawn-admission.ts";
9-
import { activateForSubagentState, requiresExactResultRead } from "./tool-activation.ts";
9+
import { activateForSubagentState, activateSubagentTools, requiresExactResultRead } from "./tool-activation.ts";
1010
import { bindRegistryUi, notifyCompletion, type RegistryUiBinding } from "./ui/widget.ts";
1111

1212
export const BACKGROUND_COMPLETION_DEBOUNCE_MS = 50;
@@ -161,6 +161,7 @@ export function registerSubagentLifecycle(pi: ExtensionAPI, runtime: DefaultSuba
161161
}
162162

163163
function sendCompletions(pi: ExtensionAPI, summaries: readonly AgentSummary[]): void {
164+
if (backgroundCompletionsNeedExactRead(summaries)) activateSubagentTools(pi, ["read_agent_result"]);
164165
pi.sendMessage(
165166
{
166167
customType: "subagent-completion",
@@ -177,7 +178,24 @@ const BACKGROUND_COMPLETION_NOTICE =
177178
const BACKGROUND_RESULT_MAX_BYTES = 2 * 1024;
178179
const BACKGROUND_BATCH_MAX_BYTES = 16 * 1024;
179180

181+
export function backgroundCompletionsNeedExactRead(summaries: readonly AgentSummary[]): boolean {
182+
if (summaries.some((summary) => requiresExactResultRead(summary, BACKGROUND_RESULT_MAX_BYTES))) return true;
183+
return (
184+
summaries.some((summary) => summary.result !== undefined) &&
185+
Buffer.byteLength(formatBackgroundCompletionContent(summaries), "utf8") > BACKGROUND_BATCH_MAX_BYTES
186+
);
187+
}
188+
180189
export function formatBackgroundCompletions(summaries: readonly AgentSummary[]): string {
190+
const content = formatBackgroundCompletionContent(summaries);
191+
const exactResultGuidance = backgroundCompletionsNeedExactRead(summaries)
192+
? "\nUse read_agent_result with agent_id and generation when exact reconstruction is needed.\n"
193+
: "";
194+
const boundedContent = truncateHead(content, { maxBytes: BACKGROUND_BATCH_MAX_BYTES }).content;
195+
return `${BACKGROUND_COMPLETION_NOTICE}${exactResultGuidance}\n${boundedContent}`;
196+
}
197+
198+
function formatBackgroundCompletionContent(summaries: readonly AgentSummary[]): string {
181199
const results = summaries.map((summary) => {
182200
const output = escapeXml(
183201
truncateHead(summary.final_text || summary.error || "(no output)", {
@@ -191,13 +209,7 @@ export function formatBackgroundCompletions(summaries: readonly AgentSummary[]):
191209
: "";
192210
return `<subagent_result agent_id="${escapeXmlAttribute(summary.agent_id)}" task_name="${escapeXmlAttribute(summary.task_name)}" generation="${summary.generation}" status="${escapeXmlAttribute(summary.status)}" profile="${escapeXmlAttribute(summary.profile)}" model="${escapeXmlAttribute(summary.model)}"${timing}>\n <output>${output}</output>${usage}${resultReference}\n</subagent_result>`;
193211
});
194-
const content =
195-
results.length === 1 ? (results[0] ?? "") : `<subagent_results>\n${results.join("\n")}\n</subagent_results>`;
196-
const exactResultGuidance = summaries.some(requiresExactResultRead)
197-
? "\nUse read_agent_result with agent_id and generation when exact reconstruction is needed.\n"
198-
: "";
199-
const boundedContent = truncateHead(content, { maxBytes: BACKGROUND_BATCH_MAX_BYTES }).content;
200-
return `${BACKGROUND_COMPLETION_NOTICE}${exactResultGuidance}\n${boundedContent}`;
212+
return results.length === 1 ? (results[0] ?? "") : `<subagent_results>\n${results.join("\n")}\n</subagent_results>`;
201213
}
202214

203215
export function formatSubagentQuestion(summary: AgentSummary, question: AgentQuestion): string {

home/.pi/agent/extensions/subagents/result-store.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const RESULT_READ_DEFAULT_BYTES = 6 * 1024;
1414
export const RESULT_READ_MAX_BYTES = 6 * 1024;
1515
export const RESULT_PREVIEW_MAX_BYTES = 4 * 1024;
1616
export const RESULT_PREVIEW_MAX_LINES = 100;
17+
export const RESULT_PREVIEW_TRUNCATION_NOTICE = "\n[Result preview truncated; use read_agent_result for exact output.]";
1718

1819
const RESULT_ID_PATTERN = /^[0-9a-f]{64}$/;
1920
const ResultPageDataSchema = z.strictObject({
@@ -95,8 +96,7 @@ export interface ResultPage {
9596

9697
/** Presentation-only text; the persisted result remains available by locator. */
9798
export function resultPreview(text: string): string {
98-
const notice = "\n[Result preview truncated; use read_agent_result for exact output.]";
99-
const maxContentBytes = RESULT_PREVIEW_MAX_BYTES - Buffer.byteLength(notice, "utf8");
99+
const maxContentBytes = RESULT_PREVIEW_MAX_BYTES - Buffer.byteLength(RESULT_PREVIEW_TRUNCATION_NOTICE, "utf8");
100100
let end = 0;
101101
let bytes = 0;
102102
let lines = 1;
@@ -110,7 +110,11 @@ export function resultPreview(text: string): string {
110110
if (character === "\n") lines++;
111111
}
112112
if (end === text.length) return text;
113-
return `${text.slice(0, end)}${notice}`;
113+
return `${text.slice(0, end)}${RESULT_PREVIEW_TRUNCATION_NOTICE}`;
114+
}
115+
116+
export function isTruncatedResultPreview(text: string): boolean {
117+
return text.endsWith(RESULT_PREVIEW_TRUNCATION_NOTICE);
114118
}
115119

116120
export interface ChildRunStats {

home/.pi/agent/extensions/subagents/tests/completion.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { AgentSummary } from "../agent-types.ts";
77
import {
88
BACKGROUND_COMPLETION_DEBOUNCE_MS,
99
DefaultSubagentRuntime,
10+
backgroundCompletionsNeedExactRead,
1011
formatBackgroundCompletions,
1112
formatSubagentQuestion,
1213
isCompletionSuperseded,
@@ -79,6 +80,42 @@ test("background completions keep previews bounded and direct exact reads", () =
7980
assert.ok(Buffer.byteLength(content, "utf8") < 3 * 1024);
8081
assert.match(content, /read_agent_result/);
8182
assert.match(content, /result_ref/);
83+
assert.equal(
84+
backgroundCompletionsNeedExactRead([
85+
{
86+
...summary(1),
87+
final_text: "x".repeat(50 * 1024),
88+
result: {
89+
generation: 1,
90+
result_id: "a".repeat(64),
91+
pages: 9,
92+
complete: true,
93+
total_bytes: 50 * 1024,
94+
sha256: "b".repeat(64),
95+
source: "assistant",
96+
},
97+
},
98+
]),
99+
true,
100+
);
101+
});
102+
103+
test("complete background results do not direct exact reads", () => {
104+
const result = {
105+
...summary(1),
106+
final_text: "complete result",
107+
result: {
108+
generation: 1,
109+
result_id: "a".repeat(64),
110+
pages: 1,
111+
complete: true,
112+
total_bytes: 15,
113+
sha256: "b".repeat(64),
114+
source: "assistant" as const,
115+
},
116+
};
117+
assert.equal(backgroundCompletionsNeedExactRead([result]), false);
118+
assert.doesNotMatch(formatBackgroundCompletions([result]), /read_agent_result/);
82119
});
83120

84121
test("subagent questions serialize their routing fields as safe XML", () => {

home/.pi/agent/extensions/subagents/tests/tool-activation.test.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
requiresExactResultRead,
1010
SUBAGENT_TOOL_NAMES,
1111
} from "../tool-activation.ts";
12+
import { resultPreview } from "../result-store.ts";
1213

1314
function summary(overrides: Partial<AgentSummary> = {}): AgentSummary {
1415
return {
@@ -130,14 +131,15 @@ test("routed questions and oversized results activate their matching tool", () =
130131
"send_agent",
131132
]);
132133

134+
const oversizedText = "x".repeat(50 * 1024);
133135
const oversized = summary({
134-
final_text: "x".repeat(50 * 1024),
136+
final_text: resultPreview(oversizedText),
135137
result: {
136138
generation: 1,
137139
result_id: "a".repeat(64),
138140
pages: 1,
139141
complete: true,
140-
total_bytes: 50 * 1024,
142+
total_bytes: Buffer.byteLength(oversizedText, "utf8"),
141143
sha256: "a".repeat(64),
142144
source: "pages",
143145
},
@@ -148,7 +150,7 @@ test("routed questions and oversized results activate their matching tool", () =
148150
assert.deepEqual(result.active(), ["spawn_agent", "read_agent_result"]);
149151
});
150152

151-
test("every settled result activates exact reading, including small results", () => {
153+
test("complete small results do not activate exact reading", () => {
152154
const result = toolApi(["spawn_agent"]);
153155
activateForSubagentState(
154156
result as never,
@@ -166,5 +168,22 @@ test("every settled result activates exact reading, including small results", ()
166168
}),
167169
false,
168170
);
169-
assert.deepEqual(result.active(), ["spawn_agent", "read_agent_result"]);
171+
assert.equal(
172+
requiresExactResultRead(
173+
summary({
174+
final_text: "small result",
175+
result: {
176+
generation: 1,
177+
result_id: "a".repeat(64),
178+
pages: 1,
179+
complete: true,
180+
total_bytes: 12,
181+
sha256: "b".repeat(64),
182+
source: "assistant",
183+
},
184+
}),
185+
),
186+
false,
187+
);
188+
assert.deepEqual(result.active(), ["spawn_agent"]);
170189
});

home/.pi/agent/extensions/subagents/tool-activation.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
import type { AgentSummary } from "./agent-types.ts";
3+
import { isTruncatedResultPreview } from "./result-store.ts";
34

45
export const SUBAGENT_TOOL_NAMES = [
56
"spawn_agent",
@@ -57,9 +58,12 @@ export function activateForSubagentState(
5758
return activateSubagentTools(pi, names);
5859
}
5960

60-
/** Native tool-output bounds may still require exact retrieval of an otherwise complete result. */
61-
export function requiresExactResultRead(summary: AgentSummary): boolean {
62-
// Summaries intentionally contain only a preview, even for small results.
63-
// The persisted terminal text is always reconstructed through the reader.
64-
return summary.result !== undefined;
61+
/** The delivered terminal result is incomplete and must be reconstructed from storage. */
62+
export function requiresExactResultRead(summary: AgentSummary, maximumDisplayedBytes?: number): boolean {
63+
if (summary.result === undefined) return false;
64+
const text = summary.final_text ?? "";
65+
return (
66+
isTruncatedResultPreview(text) ||
67+
(maximumDisplayedBytes !== undefined && Buffer.byteLength(text, "utf8") > maximumDisplayedBytes)
68+
);
6569
}

0 commit comments

Comments
 (0)