feat(forge): improve tool call error feedback for LLM self-correction

When tool calls fail validation, the error messages now include:
- What arguments were actually provided
- The expected parameter schema with types and required/optional indicators

This helps LLMs understand and fix their mistakes when retrying,
rather than just being told a parameter is missing.

Example improved error:
  Invalid function call for write_file: 'contents' is a required property
  You provided: {"filename": 'story.txt'}
  Expected parameters: {"filename": string (required), "contents": string (required)}

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-01-19 11:49:17 -06:00
parent cda9572acd
commit 013f728ebf
2 changed files with 94 additions and 7 deletions

View File

@@ -46,7 +46,7 @@ from .schema import (
_ModelName,
_ModelProviderSettings,
)
from .utils import validate_tool_calls
from .utils import InvalidFunctionCallError, validate_tool_calls
_T = TypeVar("_T")
_P = ParamSpec("_P")
@@ -233,8 +233,8 @@ class BaseOpenAIChatProvider(
self._logger.debug(
f"Parsing failed on response: '''{_assistant_msg}'''"
)
parse_errors_fmt = "\n\n".join(
f"{e.__class__.__name__}: {e}" for e in parse_errors
parse_errors_fmt = self._format_parse_errors(
parse_errors, tool_calls, functions
)
self._logger.warning(
f"Parsing attempt #{attempts} failed: {parse_errors_fmt}"
@@ -384,6 +384,61 @@ class BaseOpenAIChatProvider(
)
return completion, cost, prompt_tokens_used, completion_tokens_used
def _format_parse_errors(
self,
errors: list[Exception],
tool_calls: Optional[list[AssistantToolCall]],
functions: Optional[list[CompletionModelFunction]],
) -> str:
"""Format parse errors with helpful context about what was provided vs expected.
Args:
errors: List of parsing/validation errors.
tool_calls: The tool calls that were parsed (may have validation errors).
functions: List of available functions for schema lookup.
Returns:
Formatted error string with context.
"""
formatted_errors = []
for error in errors:
if isinstance(error, InvalidFunctionCallError):
# Build informative error message with context
error_parts = [str(error)]
# Show what arguments were provided
if error.arguments:
args_str = ", ".join(
f'"{k}": {repr(v)}' for k, v in error.arguments.items()
)
error_parts.append(f"\nYou provided: {{{args_str}}}")
else:
error_parts.append("\nYou provided: (no arguments)")
# Show expected schema if we have the function definition
if functions:
func = next(
(f for f in functions if f.name == error.name),
None,
)
if func and func.parameters:
params_info = []
for name, param in func.parameters.items():
req = "required" if param.required else "optional"
type_str = param.type.value if param.type else "any"
params_info.append(f'"{name}": {type_str} ({req})')
error_parts.append(
f"\nExpected parameters: {{{', '.join(params_info)}}}"
)
formatted_errors.append("".join(error_parts))
else:
# For non-tool-call errors, just use the standard format
formatted_errors.append(f"{error.__class__.__name__}: {error}")
return "\n\n".join(formatted_errors)
def _parse_assistant_tool_calls(
self, assistant_message: ChatCompletionMessage, **kwargs
) -> tuple[list[AssistantToolCall], list[Exception]]:

View File

@@ -325,7 +325,9 @@ class AnthropicProvider(BaseChatModelProvider[AnthropicModelName, AnthropicSetti
# (required if last assistant message had tool_use blocks)
tool_results = []
for tc in assistant_msg.tool_calls or []:
error_msg = self._get_tool_error_message(tc, tool_call_errors)
error_msg = self._get_tool_error_message(
tc, tool_call_errors, functions
)
tool_results.append(
{
"type": "tool_result",
@@ -520,12 +522,14 @@ class AnthropicProvider(BaseChatModelProvider[AnthropicModelName, AnthropicSetti
self,
tool_call: AssistantToolCall,
tool_call_errors: list,
functions: Optional[list[CompletionModelFunction]] = None,
) -> str:
"""Get the error message for a failed tool call.
Args:
tool_call: The tool call that failed.
tool_call_errors: List of validation errors for tool calls.
functions: List of available functions for schema lookup.
Returns:
An appropriate error message for the tool result.
@@ -538,9 +542,37 @@ class AnthropicProvider(BaseChatModelProvider[AnthropicModelName, AnthropicSetti
(err for err in tool_call_errors if err.name == tool_call.function.name),
None,
)
if matching_error:
return str(matching_error)
return "Not executed: validation failed"
if not matching_error:
return "Not executed: validation failed"
# Build informative error message with context
error_parts = [str(matching_error)]
# Show what arguments were provided
provided_args = tool_call.function.arguments
if provided_args:
args_str = ", ".join(f'"{k}": {repr(v)}' for k, v in provided_args.items())
error_parts.append(f"\nYou provided: {{{args_str}}}")
else:
error_parts.append("\nYou provided: (no arguments)")
# Show expected schema if we have the function definition
if functions:
func = next(
(f for f in functions if f.name == tool_call.function.name),
None,
)
if func and func.parameters:
params_info = []
for name, param in func.parameters.items():
req = "required" if param.required else "optional"
type_str = param.type.value if param.type else "any"
params_info.append(f'"{name}": {type_str} ({req})')
error_parts.append(
f"\nExpected parameters: {{{', '.join(params_info)}}}"
)
return "".join(error_parts)
def _parse_assistant_tool_calls(
self, assistant_message: Message