| import type { AgentMessage } from "@mariozechner/pi-agent-core"; |
|
|
| type AnthropicContentBlock = { |
| type: "text" | "toolUse" | "toolResult"; |
| text?: string; |
| id?: string; |
| name?: string; |
| toolUseId?: string; |
| }; |
|
|
| |
| |
| |
| |
| |
| function stripDanglingAnthropicToolUses(messages: AgentMessage[]): AgentMessage[] { |
| const result: AgentMessage[] = []; |
|
|
| for (let i = 0; i < messages.length; i++) { |
| const msg = messages[i]; |
| if (!msg || typeof msg !== "object") { |
| result.push(msg); |
| continue; |
| } |
|
|
| const msgRole = (msg as { role?: unknown }).role as string | undefined; |
| if (msgRole !== "assistant") { |
| result.push(msg); |
| continue; |
| } |
|
|
| const assistantMsg = msg as { |
| content?: AnthropicContentBlock[]; |
| }; |
|
|
| |
| const nextMsg = messages[i + 1]; |
| const nextMsgRole = |
| nextMsg && typeof nextMsg === "object" |
| ? ((nextMsg as { role?: unknown }).role as string | undefined) |
| : undefined; |
|
|
| |
| if (nextMsgRole !== "user") { |
| result.push(msg); |
| continue; |
| } |
|
|
| |
| const nextUserMsg = nextMsg as { |
| content?: AnthropicContentBlock[]; |
| }; |
| const validToolUseIds = new Set<string>(); |
| if (Array.isArray(nextUserMsg.content)) { |
| for (const block of nextUserMsg.content) { |
| if (block && block.type === "toolResult" && block.toolUseId) { |
| validToolUseIds.add(block.toolUseId); |
| } |
| } |
| } |
|
|
| |
| const originalContent = Array.isArray(assistantMsg.content) ? assistantMsg.content : []; |
| const filteredContent = originalContent.filter((block) => { |
| if (!block) { |
| return false; |
| } |
| if (block.type !== "toolUse") { |
| return true; |
| } |
| |
| return validToolUseIds.has(block.id || ""); |
| }); |
|
|
| |
| if (originalContent.length > 0 && filteredContent.length === 0) { |
| result.push({ |
| ...assistantMsg, |
| content: [{ type: "text", text: "[tool calls omitted]" }], |
| } as AgentMessage); |
| } else { |
| result.push({ |
| ...assistantMsg, |
| content: filteredContent, |
| } as AgentMessage); |
| } |
| } |
|
|
| return result; |
| } |
|
|
| function validateTurnsWithConsecutiveMerge<TRole extends "assistant" | "user">(params: { |
| messages: AgentMessage[]; |
| role: TRole; |
| merge: ( |
| previous: Extract<AgentMessage, { role: TRole }>, |
| current: Extract<AgentMessage, { role: TRole }>, |
| ) => Extract<AgentMessage, { role: TRole }>; |
| }): AgentMessage[] { |
| const { messages, role, merge } = params; |
| if (!Array.isArray(messages) || messages.length === 0) { |
| return messages; |
| } |
|
|
| const result: AgentMessage[] = []; |
| let lastRole: string | undefined; |
|
|
| for (const msg of messages) { |
| if (!msg || typeof msg !== "object") { |
| result.push(msg); |
| continue; |
| } |
|
|
| const msgRole = (msg as { role?: unknown }).role as string | undefined; |
| if (!msgRole) { |
| result.push(msg); |
| continue; |
| } |
|
|
| if (msgRole === lastRole && lastRole === role) { |
| const lastMsg = result[result.length - 1]; |
| const currentMsg = msg as Extract<AgentMessage, { role: TRole }>; |
|
|
| if (lastMsg && typeof lastMsg === "object") { |
| const lastTyped = lastMsg as Extract<AgentMessage, { role: TRole }>; |
| result[result.length - 1] = merge(lastTyped, currentMsg); |
| continue; |
| } |
| } |
|
|
| result.push(msg); |
| lastRole = msgRole; |
| } |
|
|
| return result; |
| } |
|
|
| function mergeConsecutiveAssistantTurns( |
| previous: Extract<AgentMessage, { role: "assistant" }>, |
| current: Extract<AgentMessage, { role: "assistant" }>, |
| ): Extract<AgentMessage, { role: "assistant" }> { |
| const mergedContent = [ |
| ...(Array.isArray(previous.content) ? previous.content : []), |
| ...(Array.isArray(current.content) ? current.content : []), |
| ]; |
| return { |
| ...previous, |
| content: mergedContent, |
| ...(current.usage && { usage: current.usage }), |
| ...(current.stopReason && { stopReason: current.stopReason }), |
| ...(current.errorMessage && { |
| errorMessage: current.errorMessage, |
| }), |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function validateGeminiTurns(messages: AgentMessage[]): AgentMessage[] { |
| return validateTurnsWithConsecutiveMerge({ |
| messages, |
| role: "assistant", |
| merge: mergeConsecutiveAssistantTurns, |
| }); |
| } |
|
|
| export function mergeConsecutiveUserTurns( |
| previous: Extract<AgentMessage, { role: "user" }>, |
| current: Extract<AgentMessage, { role: "user" }>, |
| ): Extract<AgentMessage, { role: "user" }> { |
| const mergedContent = [ |
| ...(Array.isArray(previous.content) ? previous.content : []), |
| ...(Array.isArray(current.content) ? current.content : []), |
| ]; |
|
|
| return { |
| ...current, |
| content: mergedContent, |
| timestamp: current.timestamp ?? previous.timestamp, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function validateAnthropicTurns(messages: AgentMessage[]): AgentMessage[] { |
| |
| const stripped = stripDanglingAnthropicToolUses(messages); |
|
|
| return validateTurnsWithConsecutiveMerge({ |
| messages: stripped, |
| role: "user", |
| merge: mergeConsecutiveUserTurns, |
| }); |
| } |
|
|