Summary
#282, #299, and #300 look like three separate asks for more granular errors, but they're the same underlying gap: the execution boundary between a tool's execute() and the caller (executeTool()) discards information the tool already produced, and replaces it with a generic UnknownError. The fix isn't three different error codes, it's one change: preserve what the tool actually knows before flattening it into a failure.
The three cases
In all three, the tool did its job (declined on purpose, finished with a partial result, finished with a full result) and the boundary reports a fault regardless. The spec's own completion steps flag this same gap four separate times with the identical note: Issue: Support more granular errors than "UnknownError", once per failure case, never generalized.
Why one fix instead of three
Treating these as independent granularity requests means the group has to agree on three separate vocabularies (refusal codes, abort-result shapes, unregistration semantics), and every future case, timeout, network loss inside a tool, anything else a tool might reasonably want to report, becomes a fourth ad hoc carve-out. A single rule, whatever value or reason the tool actually produced before the boundary is reached should reach the caller, not be replaced, closes all three at once and doesn't need a new special case for the next one.
What this doesn't propose
Not a specific envelope shape. Two independent implementations (see below) already converged on roughly the same rough structure (an outcome + a reason + whether retrying makes sense) without knowing about each other, which suggests real convergence pressure, but picking one shape and standardizing it is a separate, harder discussion than just agreeing the information shouldn't be thrown away in the first place. This issue is scoped to the narrower claim: whatever the tool has when it fulfills, rejects, or gets cut off should be preserved and reach the caller in some form, structured or not. What callers are guaranteed to receive can be a follow-up.
Real-world motivation
Two unrelated implementations hit this from different angles before finding each other in #282:
- Consequence (built for the WebMCP Challenge): fields on a form declare who's allowed to fill them. An agent calling
answer_question on a human_only field needs to decline clearly, distinct from a validation failure, and today that refusal has to be encoded as a success-shaped text response, because throwing loses the reason at the boundary.
- Redline (github.com/minjikim89/redline, docs/pattern.md): a review tool built on WebMCP, independently landed on returning
{ ok, error: { code, problems, retrySafe } } as an ordinary result rather than throwing, for the same reason, a refusal isn't a fault, and the caller needs to know whether retrying is meaningful.
Neither of us knew the other existed until this thread. That's the strongest evidence this is a recurring need rather than either app's idiosyncrasy.
Open questions for the group
Happy to help narrow this further or split it into a concrete PR against the completion steps once there's rough agreement on direction.
Summary
#282, #299, and #300 look like three separate asks for more granular errors, but they're the same underlying gap: the execution boundary between a tool's
execute()and the caller (executeTool()) discards information the tool already produced, and replaces it with a genericUnknownError. The fix isn't three different error codes, it's one change: preserve what the tool actually knows before flattening it into a failure.The three cases
UnknownErrorand can't tell "retry differently" from "ask a human."{ applied, remaining }-shaped partial result, say). That value exists, and is thrown away anyway.UnknownErroreven though the execution completed and the effects are real and on screen.In all three, the tool did its job (declined on purpose, finished with a partial result, finished with a full result) and the boundary reports a fault regardless. The spec's own completion steps flag this same gap four separate times with the identical note: Issue: Support more granular errors than "UnknownError", once per failure case, never generalized.
Why one fix instead of three
Treating these as independent granularity requests means the group has to agree on three separate vocabularies (refusal codes, abort-result shapes, unregistration semantics), and every future case, timeout, network loss inside a tool, anything else a tool might reasonably want to report, becomes a fourth ad hoc carve-out. A single rule, whatever value or reason the tool actually produced before the boundary is reached should reach the caller, not be replaced, closes all three at once and doesn't need a new special case for the next one.
What this doesn't propose
Not a specific envelope shape. Two independent implementations (see below) already converged on roughly the same rough structure (an outcome + a reason + whether retrying makes sense) without knowing about each other, which suggests real convergence pressure, but picking one shape and standardizing it is a separate, harder discussion than just agreeing the information shouldn't be thrown away in the first place. This issue is scoped to the narrower claim: whatever the tool has when it fulfills, rejects, or gets cut off should be preserved and reach the caller in some form, structured or not. What callers are guaranteed to receive can be a follow-up.
Real-world motivation
Two unrelated implementations hit this from different angles before finding each other in #282:
answer_questionon ahuman_onlyfield needs to decline clearly, distinct from a validation failure, and today that refusal has to be encoded as a success-shaped text response, because throwing loses the reason at the boundary.{ ok, error: { code, problems, retrySafe } }as an ordinary result rather than throwing, for the same reason, a refusal isn't a fault, and the caller needs to know whether retrying is meaningful.Neither of us knew the other existed until this thread. That's the strongest evidence this is a recurring need rather than either app's idiosyncrasy.
Open questions for the group
rinstead of discarding it) as a self-contained fix, or does it need a shape/type discussion first?anypayload too little to build reliable caller behavior on?Happy to help narrow this further or split it into a concrete PR against the completion steps once there's rough agreement on direction.