Skip to content

Instantly share code, notes, and snippets.

@PriNova
Created February 3, 2026 10:41
Show Gist options
  • Select an option

  • Save PriNova/a871698a580de695a5a705e90832fe1a to your computer and use it in GitHub Desktop.

Select an option

Save PriNova/a871698a580de695a5a705e90832fe1a to your computer and use it in GitHub Desktop.
pi tool call filter
/**
 * Tool Call Filter Extension
 *
 * Filters out invalid tool calls before they're sent back to the provider.
 * This addresses issues where some models construct malformed tool call blocks.
 *
 * Install: Place in ~/.pi/extensions/tool-call-filter.ts or project .pi/extensions/
 */

import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { AssistantMessage, ToolCall } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";

// ============================================================================
// Configuration (customize these filters)
// ============================================================================

/**
 * Filter function: returns true if the tool call should be kept.
 * Return false to filter it out.
 */
async function shouldKeepToolCall(
    toolName: string,
    arguments_: Record<string, unknown>,
    ctx: ExtensionContext,
): Promise<boolean> {
    // Filter out tool calls with empty/whitespace-only names
    if (!toolName || toolName.trim().length === 0) {
        ctx.ui.notify(`Filtered tool call with empty name`, "info");
        return false;
    }

    // Filter out tool calls with empty or null arguments
    if (arguments_ === null || typeof arguments_ !== "object") {
        ctx.ui.notify(`Filtered tool call with invalid arguments type`, "warning");
        return false;
    }

    // Filter out tool calls with empty arguments object (no keys)
    const keys = Object.keys(arguments_);
    if (keys.length === 0) {
        ctx.ui.notify(`Filtered tool call with empty arguments: ${toolName}`, "info");
        return false;
    }

    // Filter out tool calls where all argument values are empty/undefined/null
    const hasNonEmptyValue = keys.some((key) => {
        const value = arguments_[key];
        // Consider undefined, null, empty string, empty array, empty object as empty
        if (value === undefined || value === null) return false;
        if (typeof value === "string" && value.trim().length === 0) return false;
        if (Array.isArray(value) && value.length === 0) return false;
        if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) return false;
        return true;
    });

    if (!hasNonEmptyValue) {
        ctx.ui.notify(`Filtered tool call with all empty argument values: ${toolName}`, "info");
        return false;
    }

    return true; // Keep the tool call
}

// Simple in-memory tracker for duplicate detection (per turn)
let lastToolCalls: Array<{ name: string; args: string }> = [];

function resetDuplicateTracker() {
    lastToolCalls = [];
}

function isDuplicate(toolName: string, args: Record<string, unknown>): boolean {
    const argsString = JSON.stringify(args);
    return lastToolCalls.some((call) => call.name === toolName && call.args === argsString);
}

function recordToolCall(toolName: string, args: Record<string, unknown>) {
    lastToolCalls.push({ name: toolName, args: JSON.stringify(args) });
}

// ============================================================================
// Extension Factory
// ============================================================================

export default function (api: ExtensionAPI): Promise<void> | void {
    // Register the context event handler
    api.on("context", async (event, ctx) => {
        // Reset duplicate tracker at the start of each context processing
        resetDuplicateTracker();

        const messages = event.messages;
        const filteredMessages: AgentMessage[] = [];

        for (const message of messages) {
            // Only process assistant messages
            if (message.role !== "assistant") {
                filteredMessages.push(message);
                continue;
            }

            const assistantMsg = message as AssistantMessage;
            const filteredContent: AssistantMessage["content"] = [];

            for (const content of assistantMsg.content) {
                if (content.type === "toolCall") {
                    const toolCall = content as ToolCall;

                    // Validate the tool call
                    const isValid = await shouldKeepToolCall(
                        toolCall.name,
                        toolCall.arguments,
                        ctx,
                    );

                    if (isValid) {
                        // Check for duplicates
                        if (isDuplicate(toolCall.name, toolCall.arguments)) {
                            ctx.ui.notify(`Filtered duplicate tool call: ${toolCall.name}`, "info");
                        } else {
                            filteredContent.push(content);
                            recordToolCall(toolCall.name, toolCall.arguments);
                        }
                    }
                    // If filtered, skip adding to filteredContent
                } else {
                    // Non-tool-call content (text, thinking, etc.) - keep as-is
                    filteredContent.push(content);
                }
            }

            // Only add the message if it still has content after filtering
            if (filteredContent.length > 0) {
                filteredMessages.push({
                    ...assistantMsg,
                    content: filteredContent,
                });
            } else {
                ctx.ui.notify(`Removed assistant message with no valid content after filtering`, "info");
            }
        }

        return { messages: filteredMessages };
    });

    // Optional: Register a command to toggle filtering or adjust settings
    api.registerCommand("tool-filter-toggle", {
        description: "Toggle tool call filtering (for debugging)",
        handler: async (args, ctx) => {
            // This is a placeholder - you could add runtime configuration
            ctx.ui.notify("Tool call filter is active", "info");
        },
    });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment