refactor(channels): dedupe hook and monitor execution paths

This commit is contained in:
Peter Steinberger
2026-02-22 21:18:53 +00:00
parent 06b0a60bef
commit 2081b3a3c4
19 changed files with 347 additions and 213 deletions

View File

@@ -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;

View 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: "" }]);
});
});

View File

@@ -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;
}

View File

@@ -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 () => {