Skip to content

Centralize tool result error handling in format with error_line helper#948

Draft
LasmarKhalifa wants to merge 1 commit into
mainfrom
06-15/tool-result-error-handling
Draft

Centralize tool result error handling in format with error_line helper#948
LasmarKhalifa wants to merge 1 commit into
mainfrom
06-15/tool-result-error-handling

Conversation

@LasmarKhalifa

Copy link
Copy Markdown
Contributor

No description provided.

@LasmarKhalifa LasmarKhalifa force-pushed the 06-15/tool-result-error-handling branch 3 times, most recently from d674c9e to 9a40624 Compare June 15, 2026 18:06

@juniper-shopify juniper-shopify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice foundation PR! The error-handling centralization is clean — pulling the error path into an early-return at the top of format means every downstream formatter only needs to think about the success case. Makes the rest of your stack cleaner.

Two small things to address:

  1. The error_line doc comment could use a couple of examples (see inline comment).
  2. A test for error_line when content is nil would round out the edge-case coverage.

And lets switch to the xml parser we had talked about

# Renders "<TOOL> ERROR <message>" with any <tool_use_error> wrapper stripped.
#
#: () -> String
def error_line

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment here is good — it explains what the method does. Since this is a foundation helper that every tool in the stack will rely on for error formatting, it would be helpful to expand it a bit:

# Renders "<TOOL> ERROR <message>" with any <tool_use_error> wrapper stripped.
#
# Reads the instance's +content+ and +tool_name+ to produce a single-line
# error summary. Error messages are intentionally NOT truncated so the full
# diagnostic is preserved for debugging.
#
# Examples:
#   BASH ERROR File has not been read yet.
#   UNKNOWN ERROR command not found
#
#: () -> String
def error_line

The reason is that someone reading a downstream formatter for the first time will look at error_line to understand the contract — having examples makes that a 5-second task instead of a 30-second one.

assert_match(/error details/, output)
end

test "error_line strips the tool_use_error wrapper and upcases the tool name" do

@juniper-shopify juniper-shopify Jun 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice coverage here. One edge case to add: what happens when content is nil? The implementation handles it via content.to_s, but an explicit test documents that expectation:

test "error_line handles nil content gracefully" do
  tool_use_message = Claude::Messages::ToolUseMessage.new(
    type: :tool_use,
    hash: { name: "read", input: {} },
  )
  tool_result = Claude::ToolResult.new(
    tool_use: tool_use_message,
    content: nil,
    is_error: true,
  )

  output = tool_result.send(:error_line)

  assert_equal "READ ERROR", output
end

Something to think about: when content is nil, this produces "READ ERROR " (with a trailing space) because the string interpolation gives "READ ERROR #{""}". If you'd prefer "READ ERROR" with no trailing space, a .strip or .squeeze(" ") at the end would do it — but it's a very minor cosmetic point.

#
#: () -> String
def error_line
message = content.to_s.gsub(%r{</?tool_use_error>}, "").strip

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we had previously discussed, let's parse this with an xml parser

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.

2 participants