Skip to content

[EPIC-U][LOU-U14] One structured tool-error shape on every tool failure path - #93

Merged
LinuxDevil merged 2 commits into
mainfrom
lou-u14-one-tool-error-shape
Oct 1, 2026
Merged

LinuxDevil merged 2 commits into
mainfrom
lou-u14-one-tool-error-shape

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

New exported helper toolErrorResult({ toolName, toolCallId?, error, kind?, details? }) (src/execution/toolErrors.ts) builds { error, toolName, message, kind, ...extras } with the 2,000 character message cap. Every tool failure path now uses it, and the transcript message is flagged isError everywhere. Exported: toolErrorResult, ToolErrorResult, ToolErrorKind, ToolErrorInput.

kind is one of execution | validation | not-found | rejected | not-run | mcp | sandbox. Thrown errors can declare their kind through a toolErrorKind property (McpToolError -> mcp, the sandbox guard's new SandboxRequiredError -> sandbox, ToolArgumentsValidationError -> validation).

Audit: shapes that existed before

Path Before Now
Thrown error, main loop (toolCallExecution.ts) { error: <name>, toolName, message } (LOU-U12) + kind: 'execution'
Validation (toolArgsValidation.ts) { error: 'ToolArgumentsValidationError', toolName, message, issues } + kind: 'validation' (same builder, so the cap applies)
"No tool registry" / "Tool not found", main loop outcome.result = null; the transcript got { error: '<message>' } from the fallback in pushToolResult { error: 'ToolNotFoundError', toolName, message, kind: 'not-found' }
MCP isError (mcp/result.ts) thrown McpToolError, so the thrown-error shape + kind: 'mcp' via toolErrorKind
Sandbox fail-closed (sandboxGuard.ts) plain Error, so the thrown-error shape SandboxRequiredError (same message), kind: 'sandbox'
Resume: rejected approval { error: 'Tool execution was rejected by the reviewer', note } { error: 'ToolRejectedError', toolName, message: <same text>, kind: 'rejected', note }
Resume: remaining calls "not run" { error: 'Tool call was not run: ...' } { error: 'ToolNotRunError', toolName, message: <same text>, kind: 'not-run' }
Resume: tool throws after approval { error: '<message>' } thrown-error shape with kind
Resume: tool not found threw, so resumeAfterApproval() rejected after the approval was already consumed resolves with a not-found error result flagged isError
toolBatch.ts builds no result shapes; nothing to change unchanged

Why

LOU-U12 gave thrown errors a structured result, but each other failure path built its own shape, so a model, hook, or eval check could not rely on one shape. Existing error / toolName / message fields are kept and kind is additive. error stays the error name (not the literal true) because tool.error events derive their name from it.

Breaking, with CHANGELOG migration note: not-found, rejected, not-run and post-approval-throw results used to carry the message in error; the message is now in message and error is a name. Test expectations were updated accordingly.

Acceptance criteria

  • One exported helper with the shared cap; every listed failure path uses it; isError consistent (hooks, tool-result events and tool.error names see the same thing)
  • Existing field names kept, kind added; changed shapes CHANGELOGged with migration note
  • Table-driven test (src/execution/toolErrors.test.ts): thrown error, validation, tool not found, no registry, sandbox fail-closed, MCP isError, resume after rejection, remaining "not run", resume tool not found, resume thrown error, resume sandbox; plus helper unit tests
  • docs/tools.md has a new "Errors" section (shape and kind table); api-overview examples updated; llms files regenerated

Left alone (outside this ticket's scope), still on their own shapes

  • src/execution/agentRunState.ts pushCancelledToolResult: { error: 'Tool call was cancelled ...' }, no isError (should be kind: 'not-run')
  • src/execution/subagentRuntime.ts settleSuspensions: { error: 'Sub-agent ... was not run ...' } with isError
  • The pushToolResult fallback { error: outcome.error } is now unreachable for failures built in toolCallExecution.ts.
  • AgentExecutor.ts was not touched and holds no tool-error shapes of its own.

How verified

  • npx tsc --noEmit: pass
  • npm run lint: 0 errors, 405 warnings; no new warnings in changed files
  • npm run test:types: no errors
  • npx vitest run: 145 files passed (after npm run build); npm run test:coverage: 2105 passed, 3 skipped
  • npm run build && npm run build --workspace=packages/create-loushy-agent: pass
  • npm run docs:verify-snippets -- --skip-build: all 107 snippets type-check
  • npm run docs:llms:check: pass
  • npm run fallow: no issues

🤖 Generated with Claude Code

https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR


Generated by Claude Code

claude added 2 commits October 1, 2026 15:02
…re path

Add toolErrorResult() (src/execution/toolErrors.ts) producing
{ error, toolName, message, kind } with the 2,000 character message cap.
Thrown errors, validation failures, unknown tool / no registry, rejected
approvals, "not run" results on resume, MCP isError and the sandbox
fail-closed guard all use it; the resume path no longer rejects when an
approved tool is missing from the registry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
@LinuxDevil
LinuxDevil merged commit cc20e58 into main Oct 1, 2026
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