Skip to content

fix(harness): keep the runtime error message on failed provider-executed tool results - #18902

Open
gdaybrice wants to merge 3 commits into
vercel:mainfrom
gdaybrice:fix/harness-tool-error-message
Open

fix(harness): keep the runtime error message on failed provider-executed tool results#18902
gdaybrice wants to merge 3 commits into
vercel:mainfrom
gdaybrice:fix/harness-tool-error-message

Conversation

@gdaybrice

@gdaybrice gdaybrice commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Background

When a provider-executed tool fails, the harness runtime already sends the real reason across the wire — a failed shell command's stderr, an MCP server's timeout — as a tool-result event with isError: true.

translateStreamPart dropped that flag, projecting the event into a plain AI SDK tool-result part whose output happened to be the error text. Consumers saw a tool that succeeded and returned an error-shaped payload — the UI message stream emitted tool-output-available — so nothing downstream could distinguish a failed provider-executed tool from a successful one without inspecting its output.

Summary

  • Translate a tool-result carrying isError into a tool-error part, so the failure is typed as one and reaches consumers as tool-output-error. Marking it providerExecuted: true is load-bearing: toUIMessageChunk redacts non-provider-executed errors through onError, which would replace the runtime's own message with a generic string. providerMetadata and dynamic pass through unchanged, keeping the error on the same UI part as its tool call.
  • Resolve providerExecuted from the originating tool-call rather than hardcoding it. Host tools travel the same tool-result events — their calls are emitted with providerExecuted: false and their failures are submitted back to the runtime and echoed with isError — so hardcoding the flag would have marked host failures provider-executed and bypassed the onError redaction consumers rely on. Host tool failures keep their existing tool-result projection.
  • Append tool-error parts to the current step content, so a failed provider-executed tool stays visible in step.content the way its successful counterpart does. This also covers the host-tool tool-error parts the agent loop already emitted and that were being dropped.
  • Collapse the five identical arms of appendToCurrentStepContent into one fall-through group, matching how stream-text.ts accumulates the same set of part types.

End-to-End Verification

Ran a harness agent against a local sandbox and forced a provider-executed tool to fail. The client now receives that failure typed as a tool error carrying the runtime's own message. Previously the same failure arrived as a completed tool result whose output happened to hold the error text, with nothing marking the tool as having failed; and typing it as an error without providerExecuted set replaces the message with a generic placeholder, which is what the flag prevents.

The run exercised this change together with other harness changes in the same batch rather than in isolation.

Checklist

  • All commits are signed (PRs with unsigned commits cannot be merged)
  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • I have reviewed this pull request (self-review)

Future Work

  • A failed host tool still surfaces as a tool-result whose output is an error payload, rather than as a tool-error. This PR deliberately leaves that path untouched; aligning it with core would be a separate, consumer-visible change.
  • appendToCurrentStepContent keeps preliminary tool results in step content, where stream-text.ts excludes them. Pre-existing drift between the two accumulators, not addressed here.

…ted tool results

A harness `tool-result` event with `isError` was translated into a plain
AI SDK `tool-result` part whose output happened to be the error. Because
that part is not marked as a provider-executed error, `toUIMessageChunk`
never takes the provider-executed branch, and the runtime's own failure
text is replaced by the generic string returned from `onError` — the
real reason (a failed shell command, an MCP timeout) never reaches the
consumer.

Translate it into a `tool-error` part with `providerExecuted: true`
instead, carrying `providerMetadata` and `dynamic` through unchanged so
the error keeps matching the UI part opened by its tool call.

`tool-error` parts are now also appended to the current step content, so
a failed provider-executed tool stays visible in `step.content` the way
its successful counterpart does (this also covers the host-tool
`tool-error` parts the agent loop already emitted).
… call

Review follow-up to the previous commit.

`providerExecuted: true` was hardcoded on the new `tool-error` branch, but
host tools travel the same wire events: their `tool-call` is emitted with
`providerExecuted: false` and their failures are submitted back to the
runtime and echoed as a `tool-result` with `isError`. Hardcoding the flag
labelled those host failures provider-executed, which bypasses the
`onError` redaction consumers rely on. `translateStreamPart` now takes an
`isProviderExecuted` lookup, resolved in `run-prompt.ts` from the
`rawToolCallsByToolCallId` map it already maintains, so only genuine
provider-executed failures become `tool-error` parts.

Also collapse the five identical arms of `appendToCurrentStepContent`
into one fall-through group, matching how `stream-text.ts` accumulates
the same set of part types, and trim a redundant unit test.
An `execute` that only throws infers `Promise<never>`, which propagates
into the `tool()` generic and makes `inputSchema` fail to match
`FlexibleSchema<never>`. Annotate the return type, matching the existing
throwing-tool test in this file.
@felixarntz felixarntz added the ai/harness related to the harness layer label Aug 14, 2026
@ai-sdk-factory

Copy link
Copy Markdown
Contributor

Bugfix review

Outcome: changes-required

Fixes issue

Status: partially-addresses

Provider-executed failures explicitly marked true are correctly translated to tool-error, but legitimate host calls with providerExecuted omitted are incorrectly treated as provider-executed.

Concerns:

  • The provider contract defines an omitted providerExecuted flag as client-executed, and existing harness adapters such as Pi omit the flag for host tools. The lookup uses providerExecuted ?? true, misclassifying those host failures.

Side effects

Risk: medium

Failed host tools whose calls omit providerExecuted change from tool-result to provider-executed tool-error, altering downstream UI and error handling.

Concerns:

  • Known host calls with an undefined providerExecuted value should resolve false; only an entirely unknown originating call should use the provider-executed fallback.

Performance

Risk: none

The change adds a constant-time lookup in an existing per-turn map and does not materially increase memory use.

Backwards compatibility

Risk: none

The change does not modify or migrate existing persisted data formats; it only changes newly emitted stream parts.

Breaking changes

Risk: medium

No public types or exports change, but an already accepted input—host tool calls with providerExecuted omitted—now produces a different stream and UI output shape.

Concerns:

  • Such host failures are emitted as provider-executed tool-error/tool-output-error rather than retaining their existing tool-result/tool-output-available behavior.

Architecture

Risk: low

The implementation remains localized to harness stream translation and result accumulation, with no cross-package source imports or dependency-boundary violations.

Change scope

Status: minimal

The production changes, regression tests, refactoring of identical switch arms, and patch changeset are all directly related to the claimed fix.

Security

Risk: low

No new execution or parsing primitive is introduced, but misclassified host errors bypass the non-provider error redaction path.

Concerns:

  • Correctly distinguishing omitted providerExecuted from an unknown tool call is necessary to preserve downstream error-handling policy.

Testing

Status: needs-more

Tests cover explicit true and false values but omit the contractually equivalent host case where providerExecuted is undefined.

Concerns:

  • Add a runPrompt regression test for a failed host tool call with providerExecuted omitted and assert that it remains a tool-result rather than a provider-executed tool-error.

Verification

Reviewed every changed hunk and the relevant provider, harness, UI conversion, adapter, and step-accumulation code. Harness Node and Edge suites, package type checking, package build, and diff validation passed. A focused translation probe confirmed that a known host call with providerExecuted omitted is emitted as providerExecuted tool-error.

@felixarntz felixarntz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@gdaybrice Thank you for the PR!

This looks reasonable, but it needs some iteration to account for the variety of tool kinds AI SDK supports.

*/
const translateOptions = {
isProviderExecuted: (toolCallId: string): boolean =>
rawToolCallsByToolCallId.get(toolCallId)?.providerExecuted ?? true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See the agent review in #18902 (comment), the true here isn't a reasonable fallback. Certain tools, including client-side tools, may omit the providerExecuted property altogether.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants