mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:41:24 +00:00
refactor(channels): dedupe hook and monitor execution paths
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
||||
import type { TranscriptPolicy } from "../transcript-policy.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { log } from "./logger.js";
|
||||
import { dropThinkingBlocks } from "./thinking.js";
|
||||
import { dropThinkingBlocks, isAssistantMessageWithContent } from "./thinking.js";
|
||||
import { describeUnknownError } from "./utils.js";
|
||||
|
||||
const GOOGLE_TURN_ORDERING_CUSTOM_TYPE = "google-turn-ordering-bootstrap";
|
||||
@@ -73,15 +73,11 @@ export function sanitizeAntigravityThinkingBlocks(messages: AgentMessage[]): Age
|
||||
let touched = false;
|
||||
const out: AgentMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object" || msg.role !== "assistant") {
|
||||
if (!isAssistantMessageWithContent(msg)) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
const assistant = msg;
|
||||
if (!Array.isArray(assistant.content)) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
type AssistantContentBlock = Extract<AgentMessage, { role: "assistant" }>["content"][number];
|
||||
const nextContent: AssistantContentBlock[] = [];
|
||||
let contentChanged = false;
|
||||
|
||||
60
src/agents/pi-embedded-runner/thinking.test.ts
Normal file
60
src/agents/pi-embedded-runner/thinking.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dropThinkingBlocks, isAssistantMessageWithContent } from "./thinking.js";
|
||||
|
||||
describe("isAssistantMessageWithContent", () => {
|
||||
it("accepts assistant messages with array content and rejects others", () => {
|
||||
const assistant = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
} as AgentMessage;
|
||||
const user = { role: "user", content: "hi" } as AgentMessage;
|
||||
const malformed = { role: "assistant", content: "not-array" } as unknown as AgentMessage;
|
||||
|
||||
expect(isAssistantMessageWithContent(assistant)).toBe(true);
|
||||
expect(isAssistantMessageWithContent(user)).toBe(false);
|
||||
expect(isAssistantMessageWithContent(malformed)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dropThinkingBlocks", () => {
|
||||
it("returns the original reference when no thinking blocks are present", () => {
|
||||
const messages: AgentMessage[] = [
|
||||
{ role: "user", content: "hello" } as AgentMessage,
|
||||
{ role: "assistant", content: [{ type: "text", text: "world" }] } as AgentMessage,
|
||||
];
|
||||
|
||||
const result = dropThinkingBlocks(messages);
|
||||
expect(result).toBe(messages);
|
||||
});
|
||||
|
||||
it("drops thinking blocks while preserving non-thinking assistant content", () => {
|
||||
const messages: AgentMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal" },
|
||||
{ type: "text", text: "final" },
|
||||
],
|
||||
} as unknown as AgentMessage,
|
||||
];
|
||||
|
||||
const result = dropThinkingBlocks(messages);
|
||||
const assistant = result[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||
expect(result).not.toBe(messages);
|
||||
expect(assistant.content).toEqual([{ type: "text", text: "final" }]);
|
||||
});
|
||||
|
||||
it("keeps assistant turn structure when all content blocks were thinking", () => {
|
||||
const messages: AgentMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "internal-only" }],
|
||||
} as unknown as AgentMessage,
|
||||
];
|
||||
|
||||
const result = dropThinkingBlocks(messages);
|
||||
const assistant = result[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||
expect(assistant.content).toEqual([{ type: "text", text: "" }]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
|
||||
type AssistantContentBlock = Extract<AgentMessage, { role: "assistant" }>["content"][number];
|
||||
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
||||
|
||||
export function isAssistantMessageWithContent(message: AgentMessage): message is AssistantMessage {
|
||||
return (
|
||||
!!message &&
|
||||
typeof message === "object" &&
|
||||
message.role === "assistant" &&
|
||||
Array.isArray(message.content)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip all `type: "thinking"` content blocks from assistant messages.
|
||||
@@ -16,11 +26,7 @@ export function dropThinkingBlocks(messages: AgentMessage[]): AgentMessage[] {
|
||||
let touched = false;
|
||||
const out: AgentMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object" || msg.role !== "assistant") {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(msg.content)) {
|
||||
if (!isAssistantMessageWithContent(msg)) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,18 @@ async function applyGuardToContext(
|
||||
return await agent.transformContext?.(contextForNextCall, new AbortController().signal);
|
||||
}
|
||||
|
||||
function expectCompactedToolResultsWithoutContextNotice(
|
||||
contextForNextCall: AgentMessage[],
|
||||
oldIndex: number,
|
||||
newIndex: number,
|
||||
) {
|
||||
const oldResultText = getToolResultText(contextForNextCall[oldIndex]);
|
||||
const newResultText = getToolResultText(contextForNextCall[newIndex]);
|
||||
expect(oldResultText).toBe(PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER);
|
||||
expect(newResultText).toBe(PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER);
|
||||
expect(newResultText).not.toContain(CONTEXT_LIMIT_TRUNCATION_NOTICE);
|
||||
}
|
||||
|
||||
describe("installToolResultContextGuard", () => {
|
||||
it("compacts oldest-first when total context overflows, even if each result fits individually", async () => {
|
||||
const agent = makeGuardableAgent();
|
||||
@@ -98,12 +110,7 @@ describe("installToolResultContextGuard", () => {
|
||||
const transformed = await applyGuardToContext(agent, contextForNextCall);
|
||||
|
||||
expect(transformed).toBe(contextForNextCall);
|
||||
const oldResultText = getToolResultText(contextForNextCall[1]);
|
||||
const newResultText = getToolResultText(contextForNextCall[2]);
|
||||
|
||||
expect(oldResultText).toBe(PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER);
|
||||
expect(newResultText).toBe(PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER);
|
||||
expect(newResultText).not.toContain(CONTEXT_LIMIT_TRUNCATION_NOTICE);
|
||||
expectCompactedToolResultsWithoutContextNotice(contextForNextCall, 1, 2);
|
||||
});
|
||||
|
||||
it("keeps compacting oldest-first until context is back under budget", async () => {
|
||||
@@ -187,13 +194,7 @@ describe("installToolResultContextGuard", () => {
|
||||
];
|
||||
|
||||
await agent.transformContext?.(contextForNextCall, new AbortController().signal);
|
||||
|
||||
const oldResultText = getToolResultText(contextForNextCall[1]);
|
||||
const newResultText = getToolResultText(contextForNextCall[2]);
|
||||
|
||||
expect(oldResultText).toBe(PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER);
|
||||
expect(newResultText).toBe(PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER);
|
||||
expect(newResultText).not.toContain(CONTEXT_LIMIT_TRUNCATION_NOTICE);
|
||||
expectCompactedToolResultsWithoutContextNotice(contextForNextCall, 1, 2);
|
||||
});
|
||||
|
||||
it("wraps an existing transformContext and guards the transformed output", async () => {
|
||||
|
||||
Reference in New Issue
Block a user