Skip to content

[FEATURE] Preserve tool execution failures in the agent loop #126

Description

@alectimison-maker

Is your feature request related to a problem? Please describe.

The agent loop appears to lose the real error when a registered tool throws.

executeToolCalls() catches non-abort failures, logs them, and returns no result for that tool:

async executeToolCalls(
toolCalls: PromptBasedToolNameAndParams<T>[],
taskScopeToolCalls: PromptBasedToolNameAndParamsDistributed[],
loopImages: ImageDataWithId[] = [],
taskMessageModifier: TaskMessageModifier,
eventBus: EventEmitter<AgentToolCallExecuteHooks>,
) {
toolCalls = this.deduplicateToolCalls(toolCalls)
const currentLoopToolResults: (AgentToolExecuteResult & { toolName: T })[] = []
for (const chunk of toolCalls) {
const toolName = chunk.toolName as T
const tool = this.tools[toolName]
if (tool) {
const params = chunk.params
this.log.debug('Tool call start', chunk)
const abortController = this.createAbortController()
try {
const executedResults = await tool.execute({
params,
taskScopeToolCalls,
agentStorage: this.agentStorage,
historyManager: this.historyManager,
loopImages,
abortSignal: abortController.signal,
taskMessageModifier,
hooks: eventBus,
})
for (const result of executedResults) {
currentLoopToolResults.push({ ...result, toolName })
}
this.log.debug('Tool call executed', toolName, executedResults)
}
catch (e) {
if (e instanceof AbortError) {
this.log.debug('Tool call aborted', toolName)
break
}
this.log.error('Tool call error', toolName, e)
}
taskMessageModifier.makeAllTaskDone()
}
else {
this.log.warn('Tool not found', chunk)
}
}
return currentLoopToolResults

The caller then treats an empty result list as Tool not found, even though the tool was present:

if (currentLoopToolCalls.length > 0) {
if (agentMessage.content || agentMessage.reasoning) {
// create a new group if there are some plain messages
taskMessageModifier = this.makeTaskMessageGroupProxy(abortController.signal)
}
this.log.debug('Executing tool calls', currentLoopToolCalls)
const toolExecuteResults = await this.executeToolCalls(currentLoopToolCalls, taskScopeToolCalls, loopImages, taskMessageModifier, eventBus)
this.log.debug('Tool calls executed', currentLoopToolCalls, toolExecuteResults)
const toolResults = toolExecuteResults.filter((r) => r.type === 'tool-result')
const handOffResults = toolExecuteResults.filter((r) => r.type === 'hand-off')
if (handOffResults.length > 0) {
const handoffResult = handOffResults[0]
if (handoffResult) {
// This feature is in beta, not used yet
this.log.debug('Hand-off detected', handoffResult)
const subAgent = new Agent({ tools: this.tools, agentStorage: this.agentStorage, historyManager: this.historyManager, maxIterations: this.maxIterations })
abortController.signal.addEventListener('abort', () => subAgent.stop())
loopMessages.push({ role: 'user', content: handoffResult.userPrompt })
const lastMsg = await subAgent.run(this.overrideSystemPrompt([...baseMessages, ...loopMessages], handoffResult.overrideSystemPrompt))
this.log.debug('Sub-agent finished', lastMsg)
if (lastMsg?.content) loopMessages.push(lastMsg)
}
}
else if (toolResults.length) {
loopMessages.push({ role: 'user', content: this.buildExtendedUserMessage(iteration + 1, originalUserMessageText, toolResults) })
}
else {
const errorResult = TagBuilder.fromStructured('error', { message: `Tool not found, available tools are: ${Object.keys(this.tools).join(', ')}` })
loopMessages.push({ role: 'user', content: renderPrompt`${errorResult}` })
}

That can give the model misleading recovery context and hide the actionable failure from the user. This is a source-based inference from commit 70a2acc1425749913748c92309254a8f4e69aa8a; I have not reproduced it in a running extension.

Describe the solution you'd like

Convert a non-abort tool exception into a structured error tool result associated with the original toolName. Reserve Tool not found for the branch where the requested tool is genuinely absent. Keep the existing abort behavior unchanged.

Suggested acceptance criteria:

  • A registered tool that throws produces a tool-specific error result for the next agent iteration.
  • An unknown tool still produces the available-tools guidance.
  • AbortError stops execution without being converted into a normal tool failure.
  • Unit tests cover all three paths and multiple tool calls where one fails.

Describe alternatives you've considered

  • Rethrowing the exception would terminate the whole agent loop rather than allow recovery.
  • Showing only a UI toast would still withhold the failure reason from the model.

Additional context

If maintainers agree with the desired error-result shape, I would be happy to prepare a focused PR after that guidance.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions