fix(ai): filter empty error assistant messages in transformMessages

When 429/500 errors occur during tool execution, empty assistant messages
with stopReason='error' get persisted. These break the tool_use -> tool_result
chain for Claude/Gemini APIs.

Added centralized filtering in transformMessages to skip assistant messages
with empty content and no tool calls. Provider-level filters remain for
defense-in-depth.
This commit is contained in:
Mario Zechner 2026-01-16 22:34:58 +01:00
parent d2f9ab110c
commit fbb74bb29e
11 changed files with 125 additions and 8 deletions

View file

@ -1740,13 +1740,17 @@ export class AgentSession {
): Promise<BashResult> {
this._bashAbortController = new AbortController();
// Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
const prefix = this.settingsManager.getShellCommandPrefix();
const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
try {
const result = options?.operations
? await executeBashWithOperations(command, process.cwd(), options.operations, {
? await executeBashWithOperations(resolvedCommand, process.cwd(), options.operations, {
onChunk,
signal: this._bashAbortController.signal,
})
: await executeBashCommand(command, {
: await executeBashCommand(resolvedCommand, {
onChunk,
signal: this._bashAbortController.signal,
});

View file

@ -438,8 +438,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
time("discoverContextFiles");
const autoResizeImages = settingsManager.getImageAutoResize();
const shellCommandPrefix = settingsManager.getShellCommandPrefix();
// Create ALL built-in tools for the registry (extensions can enable any of them)
const allBuiltInToolsMap = createAllTools(cwd, { read: { autoResizeImages } });
const allBuiltInToolsMap = createAllTools(cwd, {
read: { autoResizeImages },
bash: { commandPrefix: shellCommandPrefix },
});
// Determine initially active built-in tools (default: read, bash, edit, write)
const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"];
const initialActiveToolNames: ToolName[] = options.tools

View file

@ -61,6 +61,7 @@ export interface Settings {
hideThinkingBlock?: boolean;
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
quietStartup?: boolean;
shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support)
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
extensions?: string[]; // Array of extension file paths
skills?: SkillsSettings;
@ -356,6 +357,15 @@ export class SettingsManager {
this.save();
}
getShellCommandPrefix(): string | undefined {
return this.settings.shellCommandPrefix;
}
setShellCommandPrefix(prefix: string | undefined): void {
this.globalSettings.shellCommandPrefix = prefix;
this.save();
}
getCollapseChangelog(): boolean {
return this.settings.collapseChangelog ?? false;
}

View file

@ -135,10 +135,13 @@ const defaultBashOperations: BashOperations = {
export interface BashToolOptions {
/** Custom operations for command execution. Default: local shell */
operations?: BashOperations;
/** Command prefix prepended to every command (e.g., "shopt -s expand_aliases" for alias support) */
commandPrefix?: string;
}
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
const ops = options?.operations ?? defaultBashOperations;
const commandPrefix = options?.commandPrefix;
return {
name: "bash",
@ -151,6 +154,9 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
signal?: AbortSignal,
onUpdate?,
) => {
// Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
return new Promise((resolve, reject) => {
// We'll stream to a temp file if output gets large
let tempFilePath: string | undefined;
@ -206,7 +212,7 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
}
};
ops.exec(command, cwd, { onData: handleData, signal, timeout })
ops.exec(resolvedCommand, cwd, { onData: handleData, signal, timeout })
.then(({ exitCode }) => {
// Close temp file stream
if (tempFileStream) {

View file

@ -1,4 +1,10 @@
export { type BashOperations, type BashToolDetails, type BashToolOptions, bashTool, createBashTool } from "./bash.js";
export {
type BashOperations,
type BashToolDetails,
type BashToolOptions,
bashTool,
createBashTool,
} from "./bash.js";
export { createEditTool, type EditOperations, type EditToolDetails, type EditToolOptions, editTool } from "./edit.js";
export { createFindTool, type FindOperations, type FindToolDetails, type FindToolOptions, findTool } from "./find.js";
export { createGrepTool, type GrepOperations, type GrepToolDetails, type GrepToolOptions, grepTool } from "./grep.js";
@ -23,7 +29,7 @@ export {
export { createWriteTool, type WriteOperations, type WriteToolOptions, writeTool } from "./write.js";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { bashTool, createBashTool } from "./bash.js";
import { type BashToolOptions, bashTool, createBashTool } from "./bash.js";
import { createEditTool, editTool } from "./edit.js";
import { createFindTool, findTool } from "./find.js";
import { createGrepTool, grepTool } from "./grep.js";
@ -56,13 +62,20 @@ export type ToolName = keyof typeof allTools;
export interface ToolsOptions {
/** Options for the read tool */
read?: ReadToolOptions;
/** Options for the bash tool */
bash?: BashToolOptions;
}
/**
* Create coding tools configured for a specific working directory.
*/
export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] {
return [createReadTool(cwd, options?.read), createBashTool(cwd), createEditTool(cwd), createWriteTool(cwd)];
return [
createReadTool(cwd, options?.read),
createBashTool(cwd, options?.bash),
createEditTool(cwd),
createWriteTool(cwd),
];
}
/**
@ -78,7 +91,7 @@ export function createReadOnlyTools(cwd: string, options?: ToolsOptions): Tool[]
export function createAllTools(cwd: string, options?: ToolsOptions): Record<ToolName, Tool> {
return {
read: createReadTool(cwd, options?.read),
bash: createBashTool(cwd),
bash: createBashTool(cwd, options?.bash),
edit: createEditTool(cwd),
write: createWriteTool(cwd),
grep: createGrepTool(cwd),