Spec review request — SDK/shared/ingest: tool results with isError: true must count as errors (status: error, new error_category: tool_error).
Why now: on the Commerce for Agents project 55 tool_call events carried output_content.isError = true while the dashboard showed a 0 % error rate and get_errors was empty — every metric derived from status is blind to the errors the model actually sees. Note the release order in the spec (shared + ingest before the SDK), and the second finding that thrown error messages are stored without PII stripping.
Tool-Result Errors Count as Errors
Status: Proposed (2026-08-26) · Packages: sdk, shared, ingest, docs · Version: @yavio/sdk 0.4.0 (behavioural change in metrics)
Problem
A tool that answers the model with { isError: true, content: [{ type: "text", text: "This offer reference has expired…" }] } reports an error — that is exactly what the MCP CallToolResult.isError flag is for (tool execution errors are reported inside the result, not as protocol errors). The proxy only knows one kind of error: a thrown exception.
src/server/proxy.ts (wrapToolCallback):
| Path |
Today |
| handler returns (any result) |
status: "success", isError survives only inside output_content |
| handler throws |
status: "error", error_category: "unknown", error_message from the exception |
Consequences, measured on project Commerce for Agents (2026-08-24 → 26): 55 tool_call events with output_content.isError = true, all status = success; dashboard error rate 0 %, get_errors empty. Every dashboard number derived from status (lib/queries/overview.ts, tool-detail.ts, errors.ts, MCP get_errors) is blind to the errors the model actually sees.
Decision
A result with isError === true is an error. It gets its own category so "the handler told the model something went wrong" (stale reference, invalid postal code, missing field) can be told apart from "the handler crashed".
| Field |
Result-level error (isError === true) |
Thrown (unchanged) |
| status |
error |
error |
| error_category |
tool_error (new) |
unknown |
| error_message |
first content[] item of type text, trimmed, PII-stripped, clamped to 500 chars; absent if no text |
exception message, PII-stripped, clamped to 500 chars |
| latency_ms, is_retry |
as today |
as today |
| input_keys / input_types / input_values / client meta |
captured as on the success path (same captureInput gate) |
as today (captureInputOnError) |
| output_content |
captured (the model saw it; the text explains the error) |
— |
| intent_signals |
captured |
captured |
| widget token injection |
still performed — the result is returned to the client unchanged |
— |
Only the literal boolean true counts. "true", 1 or a missing flag are not errors (MCP types isError as boolean).
error_message is stripped through stripPii() on both paths — today the thrown message is stored raw (events.ts buildToolCallEvent passes data.errorMessage through untouched). A thrown Error("customer max@example.com not found") currently lands verbatim.
Not configurable. A flag to keep the old behaviour would only preserve a wrong number.
Changes
| Package |
Change |
| shared |
packages/shared/src/events.ts: error_category enum + "tool_error". |
| ingest |
No code change; deploy after the shared bump and before the SDK release — the validator (schema-validator.ts, IngestEvent.safeParse) rejects unknown categories per event (YAVIO-2100), so an SDK shipped first would lose every tool-error event. |
| sdk |
proxy.ts: status/error_category/error_message from the result on the success path (helper toolResultError(result)); events.ts: errorCategory union + "tool_error", error_message PII-stripped and clamped; README "Captured data" section. |
| dashboard |
No query change (all use status). tool-detail.ts and errors.ts render error_category raw, so tool_error appears without a label map change. Consider a tooltip: tool_error = the handler returned isError: true. |
| docs / specs |
.specs/metrics/events.md tool_call table: error_category values + tool_error; error_message note "also from isError results". Docs site: SDK captured-data page. |
Compatibility
- Error rates rise for every customer on upgrade — this is the correction of an under-count, not a regression. Release note must say so ("error rate now includes tool-reported errors").
- Alerts or thresholds customers built on
status will fire more. Minor version bump (0.4.0), changelog entry.
- Old SDKs keep sending
success; nothing on the server side breaks.
Tests
src/__tests__/proxy.test.ts
- Result
{ isError: true, content: [{type:"text", text:"boom"}] } → status: error, error_category: tool_error, error_message: "boom", inputs and output captured, widget token still injected (non-serverOnly).
- Result with
isError: true and no text content → error without error_message.
isError: false, isError: "true", no flag → success.
- Message clamp at 500 chars;
max@example.com in the text → [EMAIL_REDACTED] (both paths).
- Thrown error message with an email → stripped (regression for the raw pass-through).
packages/shared schema test: tool_error accepted. Ingest integration: a batch with error_category: tool_error is accepted (207 without rejections).
Open Questions
- Should
tool_error results count toward is_retry differently? Today a repeat call of the same tool is a retry regardless of status — keep.
- Category refinement (e.g. mapping
-32602 validation text to validation) — not now; the SDK cannot see JSON-RPC errors raised by the MCP SDK before the handler.
Spec file: .specs/sdk/tool-result-errors.md (drafted 2026-08-26 from findings on the Commerce for Agents project; not yet committed). Please review the decisions, then implement per the spec or comment here.
Spec review request — SDK/shared/ingest: tool results with
isError: truemust count as errors (status: error, newerror_category: tool_error).Why now: on the Commerce for Agents project 55 tool_call events carried
output_content.isError = truewhile the dashboard showed a 0 % error rate andget_errorswas empty — every metric derived fromstatusis blind to the errors the model actually sees. Note the release order in the spec (shared + ingest before the SDK), and the second finding that thrown error messages are stored without PII stripping.Tool-Result Errors Count as Errors
Status: Proposed (2026-08-26) · Packages: sdk, shared, ingest, docs · Version: @yavio/sdk 0.4.0 (behavioural change in metrics)
Problem
A tool that answers the model with
{ isError: true, content: [{ type: "text", text: "This offer reference has expired…" }] }reports an error — that is exactly what the MCPCallToolResult.isErrorflag is for (tool execution errors are reported inside the result, not as protocol errors). The proxy only knows one kind of error: a thrown exception.src/server/proxy.ts(wrapToolCallback):status: "success",isErrorsurvives only insideoutput_contentstatus: "error",error_category: "unknown",error_messagefrom the exceptionConsequences, measured on project Commerce for Agents (2026-08-24 → 26): 55 tool_call events with
output_content.isError = true, allstatus = success; dashboard error rate 0 %,get_errorsempty. Every dashboard number derived fromstatus(lib/queries/overview.ts,tool-detail.ts,errors.ts, MCPget_errors) is blind to the errors the model actually sees.Decision
A result with
isError === trueis an error. It gets its own category so "the handler told the model something went wrong" (stale reference, invalid postal code, missing field) can be told apart from "the handler crashed".isError === true)errorerrortool_error(new)unknowncontent[]item of typetext, trimmed, PII-stripped, clamped to 500 chars; absent if no textcaptureInputgate)captureInputOnError)Only the literal boolean
truecounts."true",1or a missing flag are not errors (MCP typesisErroras boolean).error_messageis stripped throughstripPii()on both paths — today the thrown message is stored raw (events.tsbuildToolCallEventpassesdata.errorMessagethrough untouched). A thrownError("customer max@example.com not found")currently lands verbatim.Not configurable. A flag to keep the old behaviour would only preserve a wrong number.
Changes
packages/shared/src/events.ts:error_categoryenum +"tool_error".schema-validator.ts,IngestEvent.safeParse) rejects unknown categories per event (YAVIO-2100), so an SDK shipped first would lose every tool-error event.proxy.ts:status/error_category/error_messagefrom the result on the success path (helpertoolResultError(result));events.ts:errorCategoryunion +"tool_error",error_messagePII-stripped and clamped; README "Captured data" section.status).tool-detail.tsanderrors.tsrendererror_categoryraw, sotool_errorappears without a label map change. Consider a tooltip: tool_error = the handler returned isError: true..specs/metrics/events.mdtool_call table:error_categoryvalues +tool_error;error_messagenote "also fromisErrorresults". Docs site: SDK captured-data page.Compatibility
statuswill fire more. Minor version bump (0.4.0), changelog entry.success; nothing on the server side breaks.Tests
src/__tests__/proxy.test.ts{ isError: true, content: [{type:"text", text:"boom"}] }→status: error,error_category: tool_error,error_message: "boom", inputs and output captured, widget token still injected (non-serverOnly).isError: trueand no text content → error withouterror_message.isError: false,isError: "true", no flag →success.max@example.comin the text →[EMAIL_REDACTED](both paths).packages/sharedschema test:tool_erroraccepted. Ingest integration: a batch witherror_category: tool_erroris accepted (207 without rejections).Open Questions
tool_errorresults count towardis_retrydifferently? Today a repeat call of the same tool is a retry regardless of status — keep.-32602validation text tovalidation) — not now; the SDK cannot see JSON-RPC errors raised by the MCP SDK before the handler.Spec file:
.specs/sdk/tool-result-errors.md(drafted 2026-08-26 from findings on the Commerce for Agents project; not yet committed). Please review the decisions, then implement per the spec or comment here.