/** * context — branch-message massaging for the advisor side-call. Strips the * executor's in-flight advisor() toolCall from the tail (orphan toolCalls are * rejected by providers) and guarantees a user-role tail (some providers reject * an assistant-prefill tail). */ import type { Message } from "@earendil-works/pi-ai"; import { ADVISOR_TOOL_NAME, MSG_ADVISOR_NUDGE } from "./messages.js"; // Strip the executor's in-flight advisor() toolCall from the tail assistant // message. That call is what invoked *us* — there is no matching toolResult // yet, and providers (Anthropic, GLM/zai, OpenAI) reject payloads with orphan // toolCalls. Name-targeted to leave any other trailing toolCalls visible. export function stripInflightAdvisorCall(messages: Message[]): Message[] { if (messages.length === 0) return messages; const last = messages[messages.length - 1]; if (last.role !== "assistant") return messages; const filtered = last.content.filter((c) => !(c.type === "toolCall" && c.name === ADVISOR_TOOL_NAME)); if (filtered.length === last.content.length) return messages; if (filtered.length === 0) return messages.slice(0, -1); return [...messages.slice(0, -1), { ...last, content: filtered }]; } // Some providers (recent Anthropic Claude models) reject payloads ending on an // assistant turn ("This model does not support assistant message prefill. The // conversation must end with a user message."). After stripInflightAdvisorCall // the tail can be assistant (e.g. the executor wrote thinking text before // calling advisor). Append a minimal user-role nudge to guarantee user-tail. export function ensureUserTailForAdvisor(messages: Message[]): Message[] { if (messages.length === 0) return messages; const last = messages[messages.length - 1]; if (last.role !== "assistant") return messages; const nudge: Message = { role: "user", content: [{ type: "text", text: MSG_ADVISOR_NUDGE }], timestamp: Date.now(), }; return [...messages, nudge]; }