mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
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:
parent
d2f9ab110c
commit
fbb74bb29e
11 changed files with 125 additions and 8 deletions
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
- Fixed OpenAI-compatible provider feature detection to use `model.provider` in addition to URL, allowing custom base URLs (e.g., proxies) to work correctly with provider-specific settings ([#774](https://github.com/badlogic/pi-mono/issues/774))
|
||||
- Fixed Bedrock tool call IDs to use only alphanumeric characters, avoiding API errors from invalid characters ([#781](https://github.com/badlogic/pi-mono/pull/781) by [@pjtf93](https://github.com/pjtf93))
|
||||
- Fixed empty error assistant messages (from 429/500 errors) breaking the tool_use to tool_result chain by filtering them in `transformMessages`
|
||||
|
||||
## [0.47.0] - 2026-01-16
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,14 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
|
|||
existingToolResultIds = new Set();
|
||||
}
|
||||
|
||||
// Skip empty assistant messages (no content and no tool calls)
|
||||
// This handles error responses (e.g., 429/500) that produced no content
|
||||
// All providers already filter these in convertMessages, but we do it here
|
||||
// centrally to prevent issues with the tool_use -> tool_result chain
|
||||
if (assistantMsg.content.length === 0 && toolCalls.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(msg);
|
||||
} else if (msg.role === "toolResult") {
|
||||
existingToolResultIds.add(msg.toolCallId);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Added `shellCommandPrefix` setting to prepend commands to every bash execution, enabling alias expansion in non-interactive shells (e.g., `"shellCommandPrefix": "shopt -s expand_aliases"`) ([#790](https://github.com/badlogic/pi-mono/pull/790) by [@richardgill](https://github.com/richardgill))
|
||||
- Added bash-style argument slicing for prompt templates ([#770](https://github.com/badlogic/pi-mono/pull/770) by [@airtonix](https://github.com/airtonix))
|
||||
- Extension commands can provide argument auto-completions via `getArgumentCompletions` in `pi.registerCommand()` ([#775](https://github.com/badlogic/pi-mono/pull/775) by [@ribelo](https://github.com/ribelo))
|
||||
- Bash tool now displays the timeout value in the UI when a timeout is set ([#780](https://github.com/badlogic/pi-mono/pull/780) by [@dannote](https://github.com/dannote))
|
||||
|
|
|
|||
|
|
@ -116,6 +116,17 @@ For most users, [Git for Windows](https://git-scm.com/download/win) is sufficien
|
|||
}
|
||||
```
|
||||
|
||||
**Alias expansion:** Pi runs bash in non-interactive mode (`bash -c`), which doesn't expand aliases by default. To enable your shell aliases:
|
||||
|
||||
```json
|
||||
// ~/.pi/agent/settings.json
|
||||
{
|
||||
"shellCommandPrefix": "shopt -s expand_aliases\neval \"$(grep '^alias ' ~/.zshrc)\""
|
||||
}
|
||||
```
|
||||
|
||||
Adjust the path (`~/.zshrc`, `~/.bashrc`, etc.) to match your shell config.
|
||||
|
||||
### Terminal Setup
|
||||
|
||||
Pi uses the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) for reliable modifier key detection. Most modern terminals support this protocol, but some require configuration.
|
||||
|
|
@ -742,6 +753,7 @@ Global `~/.pi/agent/settings.json` stores persistent preferences:
|
|||
"steeringMode": "one-at-a-time",
|
||||
"followUpMode": "one-at-a-time",
|
||||
"shellPath": "C:\\path\\to\\bash.exe",
|
||||
"shellCommandPrefix": "shopt -s expand_aliases",
|
||||
"hideThinkingBlock": false,
|
||||
"collapseChangelog": false,
|
||||
"compaction": {
|
||||
|
|
@ -778,6 +790,7 @@ Global `~/.pi/agent/settings.json` stores persistent preferences:
|
|||
| `steeringMode` | Steering message delivery: `all` or `one-at-a-time` | `one-at-a-time` |
|
||||
| `followUpMode` | Follow-up message delivery: `all` or `one-at-a-time` | `one-at-a-time` |
|
||||
| `shellPath` | Custom bash path (Windows) | auto-detected |
|
||||
| `shellCommandPrefix` | Command prefix for bash (e.g., `shopt -s expand_aliases` for alias support) | - |
|
||||
| `hideThinkingBlock` | Hide thinking blocks in output (Ctrl+T to toggle) | `false` |
|
||||
| `collapseChangelog` | Show condensed changelog after update | `false` |
|
||||
| `compaction.enabled` | Enable auto-compaction | `true` |
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -105,4 +105,36 @@ describe("SettingsManager", () => {
|
|||
expect(savedSettings.defaultThinkingLevel).toBe("high");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shellCommandPrefix", () => {
|
||||
it("should load shellCommandPrefix from settings", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ shellCommandPrefix: "shopt -s expand_aliases" }));
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getShellCommandPrefix()).toBe("shopt -s expand_aliases");
|
||||
});
|
||||
|
||||
it("should return undefined when shellCommandPrefix is not set", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ theme: "dark" }));
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getShellCommandPrefix()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should preserve shellCommandPrefix when saving unrelated settings", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ shellCommandPrefix: "shopt -s expand_aliases" }));
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
manager.setTheme("light");
|
||||
|
||||
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
||||
expect(savedSettings.shellCommandPrefix).toBe("shopt -s expand_aliases");
|
||||
expect(savedSettings.theme).toBe("light");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -299,6 +299,31 @@ describe("Coding Agent Tools", () => {
|
|||
|
||||
await expect(bashWithBadShell.execute("test-call-12", { command: "echo test" })).rejects.toThrow(/ENOENT/);
|
||||
});
|
||||
|
||||
it("should prepend command prefix when configured", async () => {
|
||||
const bashWithPrefix = createBashTool(testDir, {
|
||||
commandPrefix: "export TEST_VAR=hello",
|
||||
});
|
||||
|
||||
const result = await bashWithPrefix.execute("test-prefix-1", { command: "echo $TEST_VAR" });
|
||||
expect(getTextOutput(result).trim()).toBe("hello");
|
||||
});
|
||||
|
||||
it("should include output from both prefix and command", async () => {
|
||||
const bashWithPrefix = createBashTool(testDir, {
|
||||
commandPrefix: "echo prefix-output",
|
||||
});
|
||||
|
||||
const result = await bashWithPrefix.execute("test-prefix-2", { command: "echo command-output" });
|
||||
expect(getTextOutput(result).trim()).toBe("prefix-output\ncommand-output");
|
||||
});
|
||||
|
||||
it("should work without command prefix", async () => {
|
||||
const bashWithoutPrefix = createBashTool(testDir, {});
|
||||
|
||||
const result = await bashWithoutPrefix.execute("test-prefix-3", { command: "echo no-prefix" });
|
||||
expect(getTextOutput(result).trim()).toBe("no-prefix");
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep tool", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue