Skip to content

TypeError: Cannot read properties of undefined (reading 'filter') in render-utils.js when tool returns custom object #1

@bandepanu

Description

@bandepanu

Environment:

Pi Version: v0.73.0

Node.js Version: v25.8.1

OS: Windows 10/11

The Problem:
When a custom tool extension returns a standard JavaScript object (e.g., { success: true, output: "text" }) instead of a raw string, the Pi Agent crashes during the rendering phase. The crash occurs in the internal render-utils.js because the UI expects a structured content array (standard for Gemini API responses) and fails to handle cases where that property is missing.

Error Trace:
TypeError: Cannot read properties of undefined (reading 'filter')
at getTextOutput (.../core/tools/render-utils.js:30:39)
at ToolExecutionComponent.getTextOutput (.../modes/interactive/components/tool-execution.js:280:16)

Secondary Issue:
The agent does not pre-validate tool names against Gemini API constraints (regex: [a-zA-Z_][a-zA-Z0-9_.:-]*). Registering a tool starting with a digit (e.g., 7z) results in a 400 Bad Request from the Google Vertex/Gemini backend, which can be difficult for users to debug without manual index counting.

While this is fixed upstream, here is a "Modular Wrapper" Workaround

Proposed Resolution:
To prevent these crashes across a large suite of tools (e.g., 60+ Windows CLI utilities), the recommended workaround is to implement a Sanitizing Template Function. This modularizes the logic, ensuring every tool registration is automatically compliant with Gemini's naming conventions and Pi's rendering expectations.

const registerCmd = async (name: string, description: string, fullPath: string) => {
// 1. API NAME SAFETY: Ensures name starts with a letter and is alphanumeric
const safeName = name.replace(/[^a-zA-Z0-9]/g, '').match(/^[a-zA-Z]/)
? name.replace(/[^a-zA-Z0-9]/g, '_')
: tool_${name.replace(/[^a-zA-Z0-9]/g, '_')};

await pi.registerTool({
name: safeName,
description,
parameters: {
type: "object",
properties: {
args: { type: "array", items: { type: "string" }, description: "CLI arguments" }
}
},
execute: async ({ args = [] }: { args?: string[] }) => {
const command = "${fullPath}" ${args.join(" ")};
try {
const { stdout, stderr } = await execAsync(command);

    // 2. DATA NORMALIZATION: Combine output and trim whitespace
    const result = (stdout + stderr).trim();
    
    // 3. THE CRASH PREVENTER: Never return an empty result
    // If the CLI tool is silent, we return a string so the UI doesn't find 'undefined'
    return result.length > 0 ? result : "Command executed (no output).";
  } catch (error: any) {
    // 4. ERROR SAFETY: Return errors as strings, not Error objects
    return `ERROR: ${error.message}\n${error.stdout || ""}${error.stderr || ""}`;
  }
}

});
};

// Application: All tools now benefit from the fix without individual boilerplate
await registerCmd("7z", "7-Zip Utility", "C:\path\to\7z.exe"); // Becomes 'cmd_7z'
await registerCmd("pip", "Python Pip", "C:\path\to\pip.exe"); // Returns safe string

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions