Q22: cap every tool result and bound connector responses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 05:04:59 -04:00
1 parent 4d6c74a21e
commit 8e8b66fed4
5 files changed
+79 -5

No files matched your search

+2 -2
View File
@@ -6,7 +6,7 @@ import { executeWorkspaceTool, isWorkspaceTool, WORKSPACE_TOOLS } from "./agent-
import { canonicalMentionHandle } from "../../shared/mention-handles";
import type { AgentTurnMessage, ToolDefinition } from "./provider-tool-turn";
import { isComposioTool, executeComposioTool } from "./composio/composio-tool-bridge";
import { compactToolHistory } from "./run-context-compaction";
import { capToolOutput, compactToolHistory } from "./run-context-compaction";
import { fitContextBudget } from "./context-budget";
const MAX_ITERATIONS = 12;
@@ -114,7 +114,7 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
announceTaskAssignments: shouldAnnounceTaskAssignment(call, turn.toolCalls),
});
}
results.push({ toolCallId: call.id, toolName: call.name, output });
results.push({ toolCallId: call.id, toolName: call.name, output: capToolOutput(call.name, output) });
}
const followUp = input.pollNewMessages?.();
messages.push(followUp ? { role: "tool", results, followUp } : { role: "tool", results });
+18 -2
View File
@@ -33,6 +33,8 @@ export interface ComposioToolExecutionResult {
error: string | null;
}
const MAX_RESPONSE_BYTES = 2_000_000;
export class ComposioClient {
constructor(private readonly apiKey: string) {}
@@ -155,10 +157,24 @@ export class ComposioClient {
request.setHeader("Accept", "application/json");
if (body) request.setHeader("Content-Type", "application/json");
let responseData = "";
const chunks: Buffer[] = [];
let received = 0;
let aborted = false;
request.on("response", (response) => {
response.on("data", (chunk) => { responseData += chunk.toString(); });
response.on("data", (chunk) => {
if (aborted) return;
received += chunk.length;
if (received > MAX_RESPONSE_BYTES) {
aborted = true;
request.abort();
reject(new Error(`Composio API ${method} ${url} returned more than ${MAX_RESPONSE_BYTES} bytes; ask for fewer items or narrower fields.`));
return;
}
chunks.push(chunk);
});
response.on("end", () => {
if (aborted) return;
const responseData = Buffer.concat(chunks).toString("utf8");
if (response.statusCode === 404 && options?.tolerateNotFound) {
resolve({});
return;
@@ -3,6 +3,14 @@ import type { AgentTurnMessage } from "./provider-tool-turn";
const KEEP_RECENT_TOOL_TURNS = 3;
const ELIDE_ABOVE_CHARS = 600;
const PREVIEW_CHARS = 300;
const TOOL_RESULT_CHAR_LIMIT = 26_000;
/** One tool result never exceeds the per-result cap. */
export function capToolOutput(toolName: string, output: string): string {
if (output.length <= TOOL_RESULT_CHAR_LIMIT) return output;
console.warn(`[run-loop] ${toolName} returned ${output.length} chars; cut to ${TOOL_RESULT_CHAR_LIMIT}`);
return `${output.slice(0, TOOL_RESULT_CHAR_LIMIT)}\n\n[result of ${toolName} cut at ${TOOL_RESULT_CHAR_LIMIT} of ${output.length} characters; ask for less (filters, pagination, fewer fields)]`;
}
/** Older tool results shrink to a preview; the latest turns stay verbatim. */
export function compactToolHistory(messages: AgentTurnMessage[], keepRecent = KEEP_RECENT_TOOL_TURNS): { messages: AgentTurnMessage[]; elided: number } {
+43
View File
@@ -0,0 +1,43 @@
import { EventEmitter } from "node:events";
import { describe, expect, test, vi } from "vitest";
const chunksToSend: Buffer[] = [];
let aborted = false;
vi.mock("electron", () => ({
net: {
request: () => {
const request = new EventEmitter() as EventEmitter & { setHeader: () => void; write: () => void; end: () => void; abort: () => void };
request.setHeader = () => undefined;
request.write = () => undefined;
request.abort = () => { aborted = true; };
request.end = () => {
const response = new EventEmitter() as EventEmitter & { statusCode: number };
response.statusCode = 200;
request.emit("response", response);
for (const chunk of chunksToSend) response.emit("data", chunk);
response.emit("end");
};
return request;
},
},
}));
const { ComposioClient } = await import("../../src/main/services/composio/composio-client");
describe("composio client response limits", () => {
test("a multi-byte character split across chunks decodes intact", async () => {
const json = Buffer.from(JSON.stringify({ items: [{ slug: "GMAIL_SEND", description: "发送邮件" }] }), "utf8");
const split = json.indexOf(Buffer.from("邮", "utf8")) + 1;
chunksToSend.splice(0, chunksToSend.length, json.subarray(0, split), json.subarray(split));
const tools = await new ComposioClient("ak_test").getTools(["gmail"]);
expect(tools.map((tool) => [tool.name, tool.description])).toEqual([["GMAIL_SEND", "发送邮件"]]);
});
test("a response over the byte cap aborts and fails loudly", async () => {
aborted = false;
chunksToSend.splice(0, chunksToSend.length, Buffer.alloc(1_500_000, 32), Buffer.alloc(1_500_000, 32));
await expect(new ComposioClient("ak_test").getTools(["gmail"])).rejects.toThrow("returned more than 2000000 bytes");
expect(aborted).toBe(true);
});
});
+8 -1
View File
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { compactToolHistory } from "../../src/main/services/run-context-compaction";
import { capToolOutput, compactToolHistory } from "../../src/main/services/run-context-compaction";
import { runAgentLoop } from "../../src/main/services/agent-run-loop";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
@@ -83,4 +83,11 @@ describe("tool history compaction", () => {
expect(outputs.slice(2).every((output) => output.includes("y".repeat(3000)))).toBe(true);
connection.close();
});
test("every tool result is capped with a note that names the tool and the full size", () => {
expect(capToolOutput("composio_gmail_list", "short")).toBe("short");
const capped = capToolOutput("composio_gmail_list", "r".repeat(40_000));
expect(capped.length).toBeLessThan(26_200);
expect(capped.endsWith("[result of composio_gmail_list cut at 26000 of 40000 characters; ask for less (filters, pagination, fewer fields)]")).toBe(true);
});
});