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
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);
});
};
// 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