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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions