Skip to content

Conversation

@vinitkadam03
Copy link
Contributor

@vinitkadam03 vinitkadam03 commented Jan 15, 2026

Description

🐛 Fix: Handle PrismException during tool execution to allow LLM self-correction

Problem

When a PrismException occurs during tool execution (e.g., tool not found, multiple tools with same name), the exception was thrown and broke the LLM conversation flow. This prevented the model from receiving feedback about the error and self-correcting.

Example scenario:

  1. LLM calls a tool with a typo in the name: "get_wether" instead of "get_weather"
  2. resolveTool() throws PrismException::toolNotFound()
  3. Exception propagates up and breaks the entire flow
  4. User sees an error instead of the LLM correcting itself

Solution

Adopted the error handling approach from Anthropic's streaming handler: catch PrismException during tool execution and convert it to a failed ToolResult with the error message. This allows the LLM to:

  • Receive the error as tool output
  • Understand what went wrong
  • Self-correct on the next turn
// Before: Exception breaks the flow
throw PrismException::toolNotFound($name);

// After: Error returned as tool result, LLM can self-correct
catch (PrismException $e) {
    $toolResult = new ToolResult(
        toolCallId: $toolCall->id,
        toolName: $toolCall->name,
        args: $toolCall->arguments(),
        result: $e->getMessage(),  // "Tool 'get_wether' not found"
    );
    
    yield new ToolResultEvent(
        toolResult: $toolResult,
        success: false,
        error: $e->getMessage()
    );
}

Refactoring

While implementing this fix, refactored the duplicated tool execution logic into a centralized callToolsAndYieldEvents() method in the CallsTools trait:

  • Before: Each of the 8 stream handlers had its own copy of tool execution + event generation.

  • After: Single yield from $this->callToolsAndYieldEvents(...) call in each handler

Gemini Stream Handler Fix

The Gemini Stream handler had inconsistent behavior, it wrapped string tool results in an array (['result' => $output]) while the Gemini Text/Structured handlers did not. After reviewing the Gemini API function calling documentation and the codebase, confirmed that MessageMap.php already wraps the tool result in a content key and json_encode()s tool result when building the API request, so the additional array wrapping in the Stream handler was redundant. The Gemini Stream handler now uses callToolsAndYieldEvents() like all other providers, making tool result formatting consistent across all Gemini handlers.

Note for @sixlive: Please review this Gemini change to confirm the array wrapping removal is correct. All Gemini tests pass & one was updated which checked the tool result was wrapped in result key, but would appreciate a second look.

Changes

File Change
src/Concerns/CallsTools.php Added callToolsAndYieldEvents() with PrismException handling; callTools() now uses it internally
src/Providers/*/Handlers/Stream.php Replaced duplicated logic with single yield from call
tests/Concerns/CallsToolsTest.php Added comprehensive test coverage

Tests Added

Added tests/Concerns/CallsToolsTest.php with 9 tests covering:

  • ✅ Basic tool execution (streaming and non-streaming)
  • ✅ Artifact event generation
  • PrismException handling (tool not found, multiple tools found)
  • ✅ Resilience — continues processing after individual tool failures
  • ✅ Non-PrismException errors are re-thrown
  • ✅ Incremental result collection via reference parameter
  • ✅ Edge cases (empty tool calls)

Breaking Changes

None. This is a backward-compatible change:

  • callTools() method signature unchanged — existing Text/Structured handlers work without modification
  • Non-PrismException errors still re-thrown as before
  • All existing tests pass

Result

  • LLM self-correction enabled: PrismException errors returned as tool results instead of thrown
  • Consistent behavior: all handlers now handle tool errors the same way
  • duplicate tool calling code removed: duplicated code consolidated into single method
  • Full test coverage: new test file for CallsTools tests
  • No breaking changes: drop-in replacement for existing behavior

@vinitkadam03 vinitkadam03 force-pushed the fix/tool-exception-handling branch 4 times, most recently from 784c25a to 145b29e Compare January 15, 2026 19:15
@vinitkadam03 vinitkadam03 marked this pull request as draft January 15, 2026 19:27
@vinitkadam03 vinitkadam03 force-pushed the fix/tool-exception-handling branch from 145b29e to 455e9b6 Compare January 15, 2026 19:43
@vinitkadam03 vinitkadam03 marked this pull request as ready for review January 15, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant