mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:11:25 +00:00
refactor(runtime): consolidate followup, gateway, and provider dedupe paths
This commit is contained in:
@@ -60,8 +60,8 @@ describe("injectHistoryImagesIntoMessages", () => {
|
||||
});
|
||||
|
||||
describe("resolvePromptBuildHookResult", () => {
|
||||
it("reuses precomputed legacy before_agent_start result without invoking hook again", async () => {
|
||||
const hookRunner = {
|
||||
function createLegacyOnlyHookRunner() {
|
||||
return {
|
||||
hasHooks: vi.fn(
|
||||
(hookName: "before_prompt_build" | "before_agent_start") =>
|
||||
hookName === "before_agent_start",
|
||||
@@ -69,6 +69,10 @@ describe("resolvePromptBuildHookResult", () => {
|
||||
runBeforePromptBuild: vi.fn(async () => undefined),
|
||||
runBeforeAgentStart: vi.fn(async () => ({ prependContext: "from-hook" })),
|
||||
};
|
||||
}
|
||||
|
||||
it("reuses precomputed legacy before_agent_start result without invoking hook again", async () => {
|
||||
const hookRunner = createLegacyOnlyHookRunner();
|
||||
const result = await resolvePromptBuildHookResult({
|
||||
prompt: "hello",
|
||||
messages: [],
|
||||
@@ -85,14 +89,7 @@ describe("resolvePromptBuildHookResult", () => {
|
||||
});
|
||||
|
||||
it("calls legacy hook when precomputed result is absent", async () => {
|
||||
const hookRunner = {
|
||||
hasHooks: vi.fn(
|
||||
(hookName: "before_prompt_build" | "before_agent_start") =>
|
||||
hookName === "before_agent_start",
|
||||
),
|
||||
runBeforePromptBuild: vi.fn(async () => undefined),
|
||||
runBeforeAgentStart: vi.fn(async () => ({ prependContext: "from-hook" })),
|
||||
};
|
||||
const hookRunner = createLegacyOnlyHookRunner();
|
||||
const messages = [{ role: "user", content: "ctx" }];
|
||||
const result = await resolvePromptBuildHookResult({
|
||||
prompt: "hello",
|
||||
|
||||
@@ -2,9 +2,15 @@ import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatBillingErrorMessage } from "../../pi-embedded-helpers.js";
|
||||
import { makeAssistantMessageFixture } from "../../test-helpers/assistant-message-fixtures.js";
|
||||
import { buildEmbeddedRunPayloads } from "./payloads.js";
|
||||
import {
|
||||
buildPayloads,
|
||||
expectSinglePayloadText,
|
||||
expectSingleToolErrorPayload,
|
||||
} from "./payloads.test-helpers.js";
|
||||
|
||||
describe("buildEmbeddedRunPayloads", () => {
|
||||
const OVERLOADED_FALLBACK_TEXT =
|
||||
"The AI service is temporarily overloaded. Please try again in a moment.";
|
||||
const errorJson =
|
||||
'{"type":"error","error":{"details":null,"type":"overloaded_error","message":"Overloaded"},"request_id":"req_011CX7DwS7tSvggaNHmefwWg"}';
|
||||
const errorJsonPretty = `{
|
||||
@@ -22,31 +28,25 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
content: [{ type: "text", text: errorJson }],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
type BuildPayloadParams = Parameters<typeof buildEmbeddedRunPayloads>[0];
|
||||
const buildPayloads = (overrides: Partial<BuildPayloadParams> = {}) =>
|
||||
buildEmbeddedRunPayloads({
|
||||
assistantTexts: [],
|
||||
toolMetas: [],
|
||||
lastAssistant: undefined,
|
||||
sessionKey: "session:telegram",
|
||||
inlineToolResultsAllowed: false,
|
||||
verboseLevel: "off",
|
||||
reasoningLevel: "off",
|
||||
toolResultFormat: "plain",
|
||||
...overrides,
|
||||
const makeStoppedAssistant = () =>
|
||||
makeAssistant({
|
||||
stopReason: "stop",
|
||||
errorMessage: undefined,
|
||||
content: [],
|
||||
});
|
||||
|
||||
const expectOverloadedFallback = (payloads: ReturnType<typeof buildPayloads>) => {
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe(OVERLOADED_FALLBACK_TEXT);
|
||||
};
|
||||
|
||||
it("suppresses raw API error JSON when the assistant errored", () => {
|
||||
const payloads = buildPayloads({
|
||||
assistantTexts: [errorJson],
|
||||
lastAssistant: makeAssistant({}),
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe(
|
||||
"The AI service is temporarily overloaded. Please try again in a moment.",
|
||||
);
|
||||
expectOverloadedFallback(payloads);
|
||||
expect(payloads[0]?.isError).toBe(true);
|
||||
expect(payloads.some((payload) => payload.text === errorJson)).toBe(false);
|
||||
});
|
||||
@@ -59,10 +59,7 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
verboseLevel: "on",
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe(
|
||||
"The AI service is temporarily overloaded. Please try again in a moment.",
|
||||
);
|
||||
expectOverloadedFallback(payloads);
|
||||
expect(payloads.some((payload) => payload.text === errorJsonPretty)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -71,10 +68,7 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
lastAssistant: makeAssistant({ content: [{ type: "text", text: errorJsonPretty }] }),
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe(
|
||||
"The AI service is temporarily overloaded. Please try again in a moment.",
|
||||
);
|
||||
expectOverloadedFallback(payloads);
|
||||
expect(payloads.some((payload) => payload.text?.includes("request_id"))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -108,15 +102,10 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
it("does not suppress error-shaped JSON when the assistant did not error", () => {
|
||||
const payloads = buildPayloads({
|
||||
assistantTexts: [errorJsonPretty],
|
||||
lastAssistant: makeAssistant({
|
||||
stopReason: "stop",
|
||||
errorMessage: undefined,
|
||||
content: [],
|
||||
}),
|
||||
lastAssistant: makeStoppedAssistant(),
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe(errorJsonPretty.trim());
|
||||
expectSinglePayloadText(payloads, errorJsonPretty.trim());
|
||||
});
|
||||
|
||||
it("adds a fallback error when a tool fails and no assistant output exists", () => {
|
||||
@@ -133,31 +122,21 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
it("does not add tool error fallback when assistant output exists", () => {
|
||||
const payloads = buildPayloads({
|
||||
assistantTexts: ["All good"],
|
||||
lastAssistant: makeAssistant({
|
||||
stopReason: "stop",
|
||||
errorMessage: undefined,
|
||||
content: [],
|
||||
}),
|
||||
lastAssistant: makeStoppedAssistant(),
|
||||
lastToolError: { toolName: "browser", error: "tab not found" },
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe("All good");
|
||||
expectSinglePayloadText(payloads, "All good");
|
||||
});
|
||||
|
||||
it("adds completion fallback when tools run successfully without final assistant text", () => {
|
||||
const payloads = buildPayloads({
|
||||
toolMetas: [{ toolName: "write", meta: "/tmp/out.md" }],
|
||||
lastAssistant: makeAssistant({
|
||||
stopReason: "stop",
|
||||
errorMessage: undefined,
|
||||
content: [],
|
||||
}),
|
||||
lastAssistant: makeStoppedAssistant(),
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expectSinglePayloadText(payloads, "✅ Done.");
|
||||
expect(payloads[0]?.isError).toBeUndefined();
|
||||
expect(payloads[0]?.text).toBe("✅ Done.");
|
||||
});
|
||||
|
||||
it("does not add completion fallback when the run still has a tool error", () => {
|
||||
@@ -171,11 +150,7 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
|
||||
it("does not add completion fallback when no tools ran", () => {
|
||||
const payloads = buildPayloads({
|
||||
lastAssistant: makeAssistant({
|
||||
stopReason: "stop",
|
||||
errorMessage: undefined,
|
||||
content: [],
|
||||
}),
|
||||
lastAssistant: makeStoppedAssistant(),
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(0);
|
||||
@@ -199,10 +174,10 @@ describe("buildEmbeddedRunPayloads", () => {
|
||||
verboseLevel: "on",
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.isError).toBe(true);
|
||||
expect(payloads[0]?.text).toContain("Exec");
|
||||
expect(payloads[0]?.text).toContain("code 1");
|
||||
expectSingleToolErrorPayload(payloads, {
|
||||
title: "Exec",
|
||||
detail: "code 1",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not add tool error fallback when assistant text exists after tool calls", () => {
|
||||
|
||||
41
src/agents/pi-embedded-runner/run/payloads.test-helpers.ts
Normal file
41
src/agents/pi-embedded-runner/run/payloads.test-helpers.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { expect } from "vitest";
|
||||
import { buildEmbeddedRunPayloads } from "./payloads.js";
|
||||
|
||||
export type BuildPayloadParams = Parameters<typeof buildEmbeddedRunPayloads>[0];
|
||||
type RunPayloads = ReturnType<typeof buildEmbeddedRunPayloads>;
|
||||
|
||||
export function buildPayloads(overrides: Partial<BuildPayloadParams> = {}) {
|
||||
return buildEmbeddedRunPayloads({
|
||||
assistantTexts: [],
|
||||
toolMetas: [],
|
||||
lastAssistant: undefined,
|
||||
sessionKey: "session:telegram",
|
||||
inlineToolResultsAllowed: false,
|
||||
verboseLevel: "off",
|
||||
reasoningLevel: "off",
|
||||
toolResultFormat: "plain",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
export function expectSinglePayloadText(
|
||||
payloads: RunPayloads,
|
||||
text: string,
|
||||
expectedError?: boolean,
|
||||
): void {
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.text).toBe(text);
|
||||
if (typeof expectedError === "boolean") {
|
||||
expect(payloads[0]?.isError).toBe(expectedError);
|
||||
}
|
||||
}
|
||||
|
||||
export function expectSingleToolErrorPayload(
|
||||
payloads: RunPayloads,
|
||||
params: { title: string; detail: string },
|
||||
): void {
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.isError).toBe(true);
|
||||
expect(payloads[0]?.text).toContain(params.title);
|
||||
expect(payloads[0]?.text).toContain(params.detail);
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildEmbeddedRunPayloads } from "./payloads.js";
|
||||
|
||||
type BuildPayloadParams = Parameters<typeof buildEmbeddedRunPayloads>[0];
|
||||
|
||||
function buildPayloads(overrides: Partial<BuildPayloadParams> = {}) {
|
||||
return buildEmbeddedRunPayloads({
|
||||
assistantTexts: [],
|
||||
toolMetas: [],
|
||||
lastAssistant: undefined,
|
||||
sessionKey: "session:telegram",
|
||||
inlineToolResultsAllowed: false,
|
||||
verboseLevel: "off",
|
||||
reasoningLevel: "off",
|
||||
toolResultFormat: "plain",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
import { buildPayloads, expectSingleToolErrorPayload } from "./payloads.test-helpers.js";
|
||||
|
||||
describe("buildEmbeddedRunPayloads tool-error warnings", () => {
|
||||
it("suppresses exec tool errors when verbose mode is off", () => {
|
||||
@@ -33,10 +17,10 @@ describe("buildEmbeddedRunPayloads tool-error warnings", () => {
|
||||
verboseLevel: "on",
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.isError).toBe(true);
|
||||
expect(payloads[0]?.text).toContain("Exec");
|
||||
expect(payloads[0]?.text).toContain("command failed");
|
||||
expectSingleToolErrorPayload(payloads, {
|
||||
title: "Exec",
|
||||
detail: "command failed",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps non-exec mutating tool failures visible", () => {
|
||||
|
||||
@@ -96,22 +96,26 @@ function hasImageBlocks(content: ReadonlyArray<TextContent | ImageContent>): boo
|
||||
return false;
|
||||
}
|
||||
|
||||
function estimateTextAndImageChars(content: ReadonlyArray<TextContent | ImageContent>): number {
|
||||
let chars = 0;
|
||||
for (const block of content) {
|
||||
if (block.type === "text") {
|
||||
chars += block.text.length;
|
||||
}
|
||||
if (block.type === "image") {
|
||||
chars += IMAGE_CHAR_ESTIMATE;
|
||||
}
|
||||
}
|
||||
return chars;
|
||||
}
|
||||
|
||||
function estimateMessageChars(message: AgentMessage): number {
|
||||
if (message.role === "user") {
|
||||
const content = message.content;
|
||||
if (typeof content === "string") {
|
||||
return content.length;
|
||||
}
|
||||
let chars = 0;
|
||||
for (const b of content) {
|
||||
if (b.type === "text") {
|
||||
chars += b.text.length;
|
||||
}
|
||||
if (b.type === "image") {
|
||||
chars += IMAGE_CHAR_ESTIMATE;
|
||||
}
|
||||
}
|
||||
return chars;
|
||||
return estimateTextAndImageChars(content);
|
||||
}
|
||||
|
||||
if (message.role === "assistant") {
|
||||
@@ -135,16 +139,7 @@ function estimateMessageChars(message: AgentMessage): number {
|
||||
}
|
||||
|
||||
if (message.role === "toolResult") {
|
||||
let chars = 0;
|
||||
for (const b of message.content) {
|
||||
if (b.type === "text") {
|
||||
chars += b.text.length;
|
||||
}
|
||||
if (b.type === "image") {
|
||||
chars += IMAGE_CHAR_ESTIMATE;
|
||||
}
|
||||
}
|
||||
return chars;
|
||||
return estimateTextAndImageChars(message.content);
|
||||
}
|
||||
|
||||
return 256;
|
||||
|
||||
Reference in New Issue
Block a user