mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(gateway): bound Responses tool replay prompts
This commit is contained in:
@@ -8,6 +8,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vites
|
||||
import { createDeferred } from "../../test/helpers/promise.js";
|
||||
import { createClientToolNameConflictError } from "../agents/agent-tool-definition-adapter.js";
|
||||
import { FailoverError } from "../agents/failover-error.js";
|
||||
import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS } from "../agents/tool-result-limits.js";
|
||||
import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js";
|
||||
import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js";
|
||||
import { resetConfigRuntimeState } from "../config/config.js";
|
||||
@@ -1875,6 +1876,33 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
expect(agentCommandMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rejects structured tool output that exceeds the model-visible prompt budget", async () => {
|
||||
agentCommandMock.mockClear();
|
||||
const res = await postResponses(enabledPort, {
|
||||
model: "openclaw",
|
||||
input: [
|
||||
{
|
||||
type: "function_call_output",
|
||||
call_id: "call_oversized",
|
||||
output: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "x".repeat(DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS + 1),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const body = (await res.json()) as { error?: { message?: string; type?: string } };
|
||||
expect(res.status).toBe(400);
|
||||
expect(body.error?.type).toBe("invalid_request_error");
|
||||
expect(body.error?.message).toContain(
|
||||
`${DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS}-character live tool-result limit`,
|
||||
);
|
||||
expect(agentCommandMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "an unknown reasoning summary discriminant",
|
||||
|
||||
@@ -95,7 +95,7 @@ import {
|
||||
type ToolChoiceConstraint,
|
||||
} from "./openai-tool-choice.js";
|
||||
import { wrapUntrustedFileContent } from "./openresponses-file-content.js";
|
||||
import { buildAgentPrompt } from "./openresponses-prompt.js";
|
||||
import { buildAgentPrompt, OpenResponsesToolOutputTooLargeError } from "./openresponses-prompt.js";
|
||||
import { createAssistantOutputItem, createFunctionCallOutputItem } from "./openresponses-shape.js";
|
||||
|
||||
type OpenResponsesHttpOptions = {
|
||||
@@ -509,7 +509,18 @@ export async function handleOpenResponsesHttpRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
const prompt = buildAgentPrompt(payload.input);
|
||||
let prompt: ReturnType<typeof buildAgentPrompt>;
|
||||
try {
|
||||
prompt = buildAgentPrompt(payload.input);
|
||||
} catch (err) {
|
||||
if (err instanceof OpenResponsesToolOutputTooLargeError) {
|
||||
sendJson(res, 400, {
|
||||
error: { message: err.message, type: "invalid_request_error" },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Count URL sources request-wide, but replay media only from the current user turn.
|
||||
let images: ImageContent[] = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS } from "../agents/tool-result-limits.js";
|
||||
import { IMAGE_ONLY_USER_MESSAGE } from "./agent-prompt.js";
|
||||
import { CreateResponseBodySchema } from "./open-responses.schema.js";
|
||||
import { wrapUntrustedFileContent } from "./openresponses-file-content.js";
|
||||
@@ -88,6 +89,28 @@ describe("OpenResponses aggregate behavior", () => {
|
||||
expect(result.message).toContain("Summarize it");
|
||||
});
|
||||
|
||||
it("enforces the canonical live tool-result prompt budget", () => {
|
||||
expect(
|
||||
buildAgentPrompt([
|
||||
{
|
||||
type: "function_call_output",
|
||||
call_id: "call-max",
|
||||
output: "x".repeat(DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS),
|
||||
},
|
||||
]).message,
|
||||
).toHaveLength(DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS);
|
||||
|
||||
expect(() =>
|
||||
buildAgentPrompt([
|
||||
{
|
||||
type: "function_call_output",
|
||||
call_id: "call-oversized",
|
||||
output: "x".repeat(DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS + 1),
|
||||
},
|
||||
]),
|
||||
).toThrow(`${DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS}-character live tool-result limit`);
|
||||
});
|
||||
|
||||
it("preserves attachment-only turn placeholders", () => {
|
||||
expect(
|
||||
buildAgentPrompt([
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Prompt adapter from OpenAI Responses input items to OpenClaw agent messages.
|
||||
import { estimateToolResultTextChars } from "../agents/embedded-agent-runner/tool-result-text-budget.js";
|
||||
import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS } from "../agents/tool-result-limits.js";
|
||||
import {
|
||||
buildAgentMessageFromConversationEntries,
|
||||
type ConversationEntry,
|
||||
@@ -9,6 +11,8 @@ import type { ContentPart, ItemParam } from "./open-responses.schema.js";
|
||||
const FILE_ONLY_USER_MESSAGE = "User sent file(s) with no text.";
|
||||
type ResponseMessageItem = Extract<ItemParam, { type: "message" }>;
|
||||
|
||||
export class OpenResponsesToolOutputTooLargeError extends Error {}
|
||||
|
||||
function extractTextContent(content: string | ContentPart[]): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
@@ -101,12 +105,19 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
|
||||
});
|
||||
} else if (item.type === "function_call_output") {
|
||||
// Structured SDK tool output remains transport data, not permission to
|
||||
// fetch its file or image URLs; serialize every accepted part losslessly.
|
||||
// fetch its file or image URLs. The HTTP boundary does not yet know the
|
||||
// effective model context, so use the canonical low-context live-result cap.
|
||||
const body = typeof item.output === "string" ? item.output : JSON.stringify(item.output);
|
||||
if (estimateToolResultTextChars(body) > DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS) {
|
||||
throw new OpenResponsesToolOutputTooLargeError(
|
||||
`Function call output exceeds the ${DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS}-character live tool-result limit.`,
|
||||
);
|
||||
}
|
||||
conversationEntries.push({
|
||||
role: "tool",
|
||||
entry: {
|
||||
sender: `Tool:${item.call_id}`,
|
||||
body: typeof item.output === "string" ? item.output : JSON.stringify(item.output),
|
||||
body,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user