refactor: consolidated tool execution params into details object

- Changed isError property from required to optional across ToolResultMessage and tool event interfaces.
- Added SpinnerType variants with distinct animation frames per symbol preset.
- Fixed spinner animation crash when frames array is empty by adding guard clause.
- Refactored agent-loop tool execution to consolidate toolCallId, toolName, and isError into details object.
This commit is contained in:
can1357
2026-01-05 04:44:43 +01:00
parent 7f68705afa
commit 21192e2056
16 changed files with 49 additions and 75 deletions
+7 -8
View File
@@ -322,8 +322,11 @@ async function executeToolCalls(
});
let result: AgentToolResult<any>;
let isError = false;
const details: { toolCallId: string; toolName: string; isError?: boolean } = {
toolCallId: toolCall.id,
toolName: toolCall.name,
};
try {
if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
@@ -350,25 +353,21 @@ async function executeToolCalls(
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
details: {},
};
isError = true;
details.isError = true;
}
stream.push({
type: "tool_execution_end",
toolCallId: toolCall.id,
toolName: toolCall.name,
result,
isError,
...details,
});
const toolResultMessage: ToolResultMessage = {
role: "toolResult",
toolCallId: toolCall.id,
toolName: toolCall.name,
content: result.content,
details: result.details,
isError,
timestamp: Date.now(),
...details,
};
results.push(toolResultMessage);
+1 -1
View File
@@ -217,4 +217,4 @@ export type AgentEvent =
// Tool execution lifecycle
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean };
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError?: boolean };
-1
View File
@@ -455,7 +455,6 @@ describe("Agent.continue()", () => {
toolCallId: "calc-1",
toolName: "calculate",
content: [{ type: "text", text: "5 + 3 = 8" }],
isError: false,
timestamp: Date.now(),
};
+3
View File
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
### Changed
- Made `isError` field optional in `ToolResultMessage` interface, defaulting to non-error state
## [3.14.0] - 2026-01-04
-3
View File
@@ -121,7 +121,6 @@ for (const call of toolCalls) {
toolCallId: call.id,
toolName: call.name,
content: [{ type: "text", text: result }],
isError: false,
timestamp: Date.now(),
});
}
@@ -209,7 +208,6 @@ for (const block of response.content) {
toolCallId: block.id,
toolName: block.name,
content: [{ type: "text", text: JSON.stringify(result) }],
isError: false,
timestamp: Date.now(),
});
}
@@ -225,7 +223,6 @@ context.messages.push({
{ type: "text", text: "Generated chart showing temperature trends" },
{ type: "image", data: imageBuffer.toString("base64"), mimeType: "image/png" },
],
isError: false,
timestamp: Date.now(),
});
```
+1 -1
View File
@@ -138,7 +138,7 @@ export interface ToolResultMessage<TDetails = any> {
toolName: string;
content: (TextContent | ImageContent)[]; // Supports text and images
details?: TDetails;
isError: boolean;
isError?: boolean;
timestamp: number; // Unix timestamp in milliseconds
}
-4
View File
@@ -57,7 +57,6 @@ const providerContexts = {
toolCallId: "toolu_01abc123",
toolName: "get_weather",
content: [{ type: "text", text: "Weather in Tokyo: 18°C, partly cloudy" }],
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
@@ -109,7 +108,6 @@ const providerContexts = {
toolCallId: "call_gemini_123",
toolName: "get_weather",
content: [{ type: "text", text: "Weather in Berlin: 22°C, sunny" }],
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
@@ -160,7 +158,6 @@ const providerContexts = {
toolCallId: "call_abc123",
toolName: "get_weather",
content: [{ type: "text", text: "Weather in London: 15°C, rainy" }],
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
@@ -213,7 +210,6 @@ const providerContexts = {
toolCallId: "call_789_item_012", // Match the updated ID format
toolName: "get_weather",
content: [{ type: "text", text: "Weather in Sydney: 25°C, clear" }],
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
@@ -81,7 +81,6 @@ async function handleToolWithImageResult<TApi extends Api>(model: Model<TApi>, o
mimeType: "image/png",
},
],
isError: false,
timestamp: Date.now(),
};
@@ -174,7 +173,6 @@ async function handleToolWithTextAndImageResult<TApi extends Api>(model: Model<T
mimeType: "image/png",
},
],
isError: false,
timestamp: Date.now(),
};
-1
View File
@@ -313,7 +313,6 @@ async function multiTurn<TApi extends Api>(model: Model<TApi>, options?: Options
toolCallId: block.id,
toolName: block.name,
content: [{ type: "text", text: `${result}` }],
isError: false,
timestamp: Date.now(),
});
}
@@ -93,7 +93,6 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
- Special quotes: "curly" 'quotes'`,
},
],
isError: false,
timestamp: Date.now(),
};
@@ -182,7 +181,6 @@ Unanswered Comments: 2
}`,
},
],
isError: false,
timestamp: Date.now(),
};
@@ -254,7 +252,6 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
toolCallId: "test_2",
toolName: "test_tool",
content: [{ type: "text", text: `Text with unpaired surrogate: ${unpairedSurrogate} <- should be sanitized` }],
isError: false,
timestamp: Date.now(),
};
+4
View File
@@ -1,8 +1,10 @@
# Changelog
## [Unreleased]
### Added
- Added spinner type variants (status and activity) with distinct animation frames per symbol preset
- Added animated spinner for task tool progress display during subagent execution
- Added language/file type icons for read tool output with support for 35+ file types
- Added async cleanup registry for graceful session flush on SIGINT, SIGTERM, and SIGHUP signals
@@ -27,6 +29,7 @@
### Changed
- Changed `isError` property in tool result events to be optional instead of required
- Changed `SessionManager.open()` and `SessionManager.continueRecent()` to async methods for proper initialization
- Changed session file writes to use atomic rename pattern with fsync for crash-safe persistence
- Changed read tool display to show file type icons and metadata inline with path
@@ -68,6 +71,7 @@
### Fixed
- Fixed spinner animation crash when spinner frames array is empty by adding length check
- Fixed session persistence to properly await all queued writes before closing or switching sessions
- Fixed session persistence to truncate oversized content blocks before writing to prevent memory exhaustion
- Fixed extension list and inspector panel to use correct symbols for disabled and shadowed states instead of reusing unrelated status icons
@@ -59,7 +59,6 @@ export function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRu
input: params,
content: result.content,
details: result.details,
isError: false,
})) as ToolResultEventResult | undefined;
// Apply modifications if any
@@ -410,7 +410,7 @@ interface ToolResultEventBase {
/** Full content array (text and images) */
content: (TextContent | ImageContent)[];
/** Whether the tool execution was an error */
isError: boolean;
isError?: boolean;
}
/** Tool result event for bash tool */
@@ -346,7 +346,7 @@ export class ToolExecutionComponent extends Container {
private cwd: string;
private result?: {
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
isError: boolean;
isError?: boolean;
details?: any;
};
// Cached edit diff preview (computed when args arrive, before tool executes)
@@ -439,7 +439,7 @@ export class ToolExecutionComponent extends Container {
result: {
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
details?: any;
isError: boolean;
isError?: boolean;
},
isPartial = false,
): void {
@@ -456,7 +456,9 @@ export class ToolExecutionComponent extends Container {
const needsSpinner = this.isPartial && this.toolName === "task";
if (needsSpinner && !this.spinnerInterval) {
this.spinnerInterval = setInterval(() => {
this.spinnerFrame = (this.spinnerFrame + 1) % 10;
const frameCount = theme.spinnerFrames.length;
if (frameCount === 0) return;
this.spinnerFrame = (this.spinnerFrame + 1) % frameCount;
this.updateDisplay();
this.ui.requestRender();
}, 80);
@@ -127,8 +127,6 @@ export type SymbolKey =
| "md.quoteBorder"
| "md.hrChar"
| "md.bullet"
// Spinner frames (array stored as comma-separated string)
| "spinner.frames"
// Language/file type icons
| "lang.default"
| "lang.typescript"
@@ -369,8 +367,6 @@ const UNICODE_SYMBOLS: SymbolMap = {
"md.hrChar": "─",
// pick: • | alt: · ▪ ◦
"md.bullet": "•",
// Spinner frames (pulsing dot)
"spinner.frames": "·,•,●,•",
// Language icons (unicode uses code symbol prefix)
"lang.default": "❖",
"lang.typescript": "❖ ts",
@@ -610,8 +606,6 @@ const NERD_SYMBOLS: SymbolMap = {
"md.hrChar": "\u2500",
// pick:  | alt:  •
"md.bullet": "\uf111",
// Spinner frames (nerd font circles)
"spinner.frames": "󰪥,󰪤,󰪣,󰪢,󰪡,󰪠,󰪟,󰪞,󰪝,󰪜,󰪛,󰪥",
// Language icons (nerd font devicons)
"lang.default": "",
"lang.typescript": "",
@@ -757,8 +751,6 @@ const ASCII_SYMBOLS: SymbolMap = {
"md.quoteBorder": "|",
"md.hrChar": "-",
"md.bullet": "*",
// Spinner frames (ASCII)
"spinner.frames": "|,/,-,\\",
// Language icons (ASCII uses abbreviations)
"lang.default": "code",
"lang.typescript": "ts",
@@ -804,6 +796,23 @@ const SYMBOL_PRESETS: Record<SymbolPreset, SymbolMap> = {
ascii: ASCII_SYMBOLS,
};
export type SpinnerType = "status" | "activity";
const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>> = {
unicode: {
status: ["·", "•", "●", "•"],
activity: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
},
nerd: {
status: ["󰪥", "󰪤", "󰪣", "󰪢", "󰪡", "󰪠", "󰪟", "󰪞", "󰪥"],
activity: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
},
ascii: {
status: ["|", "/", "-", "\\"],
activity: ["-", "\\", "|", "/"],
},
};
// ============================================================================
// Types & Schema
// ============================================================================
@@ -1537,10 +1546,17 @@ export class Theme {
}
/**
* Get spinner frames as an array (stored as comma-separated string in symbols).
* Default spinner frames (status spinner).
*/
get spinnerFrames(): string[] {
return this.symbols["spinner.frames"].split(",");
return this.getSpinnerFrames();
}
/**
* Get spinner frames by type.
*/
getSpinnerFrames(type: SpinnerType = "status"): string[] {
return SPINNER_FRAMES[this.symbolPreset][type];
}
/**
@@ -2117,8 +2133,6 @@ export function getLanguageFromPath(filePath: string): string | undefined {
export function getSymbolTheme(): SymbolTheme {
const preset = theme.getSymbolPreset();
const spinnerFrames =
preset === "ascii" ? ["-", "\\", "|", "/"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
return {
cursor: theme.nav.cursor,
@@ -2129,7 +2143,7 @@ export function getSymbolTheme(): SymbolTheme {
table: theme.boxSharp,
quoteBorder: theme.md.quoteBorder,
hrChar: theme.md.hrChar,
spinnerFrames,
spinnerFrames: theme.getSpinnerFrames("activity"),
};
}
@@ -73,7 +73,6 @@ export const simpleHtml = {
toolCallId: "toolu_01Tu6wbnPMHtBKj9B7TMos1x",
toolName: "artifacts",
output: "Created file index.html",
isError: false,
},
{
role: "assistant",
@@ -180,7 +179,6 @@ export const longSession = {
toolCallId: "toolu_01Y3hvzepDjUWnHF8bdmgMSA",
toolName: "artifacts",
output: "Created file index.html",
isError: false,
},
{
role: "assistant",
@@ -264,7 +262,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -344,7 +341,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -428,7 +424,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -512,7 +507,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -558,7 +552,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -676,7 +669,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -723,7 +715,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -806,7 +797,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -889,7 +879,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1002,7 +991,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1085,7 +1073,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1161,7 +1148,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1208,7 +1194,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1292,7 +1277,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1339,7 +1323,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1419,7 +1402,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1536,7 +1518,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1583,7 +1564,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1625,7 +1605,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1667,7 +1646,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1714,7 +1692,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1761,7 +1738,6 @@ export const longSession = {
toolCallId: "toolu_01BtH9H2BvwxvKjLw5iHXcZC",
toolName: "artifacts",
output: "Created file news_today.md",
isError: false,
},
{
role: "assistant",
@@ -1845,7 +1821,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1892,7 +1867,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1939,7 +1913,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -1986,7 +1959,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -2033,7 +2005,6 @@ export const longSession = {
details: {
files: [],
},
isError: false,
},
{
role: "assistant",
@@ -2080,7 +2051,6 @@ export const longSession = {
toolCallId: "toolu_01J26GUEiATmMTYeMDJnmAFN",
toolName: "artifacts",
output: "Created file liveblog_ukraine.md",
isError: false,
},
{
role: "assistant",
@@ -2160,7 +2130,6 @@ export const longSession = {
toolCallId: "toolu_012E1DjRwg1wgZhD38mBNpSv",
toolName: "artifacts",
output: "Created file minimal.html",
isError: false,
},
{
role: "assistant",
@@ -2241,7 +2210,6 @@ export const longSession = {
toolName: "artifacts",
output:
"Updated file index.html\n\nExecution timed out. Partial logs:\n[log] Page loaded successfully!\n[log] Welcome to the simple HTML page",
isError: false,
},
{
role: "assistant",
@@ -2323,7 +2291,6 @@ export const longSession = {
toolName: "artifacts",
output:
"Updated file index.html\n\nExecution timed out. Partial logs:\n[log] Page loaded successfully!\n[log] Welcome to the simple HTML page\n[log] Third console log added!",
isError: false,
},
{
role: "assistant",