Skip to content

fix(tool): mark ToolResultBlock.error() results with state=ERROR#2186

Open
logicwu0 wants to merge 1 commit into
agentscope-ai:mainfrom
logicwu0:fix/2157-tool-error-state
Open

fix(tool): mark ToolResultBlock.error() results with state=ERROR#2186
logicwu0 wants to merge 1 commit into
agentscope-ai:mainfrom
logicwu0:fix/2157-tool-error-state

Conversation

@logicwu0

Copy link
Copy Markdown

What

ToolResultBlock.error() returned a result whose state was left unset (the
constructor defaults it to RUNNING) with an "Error: " text prefix. But
ReActAgent.determineToolResultState() only classifies a result as ERROR
when either the structured state is a non-RUNNING value, or the output text
starts with "[ERROR]". Neither held, so failed tool calls fell through to the
default SUCCESS branch — the agent never saw the failure.

This affected every error() call site: MCP timeouts, tool-not-found, empty
results, and generic execution failures.

Reproduced in the issue with an MCP tool timing out after 3 retries:

WARN  Retrying tool call 'maps_weather' (attempt 2/2) due to: Tool execution timeout after PT30S
INFO  POST_ACTING | name=maps_weather, result_len=96, state=SUCCESS   <-- should be ERROR

Fix

Set state = ToolResultState.ERROR in ToolResultBlock.error(), so failures are
classified by structured state, decoupled from the fragile text-prefix heuristic.
This fixes all error paths at once. The output text ("Error: ...") is
unchanged, so existing behavior and assertions are preserved.

Tests

  • Extended shouldReturnErrorWhenToolThrows to assert the failing-tool response
    carries state == ERROR.
  • Added errorResultCarriesErrorState as a focused regression guard.
  • Full agentscope-core suite: 2210 passed, 0 failed (9 skipped).

Closes #2157

ToolResultBlock.error() left state unset (defaulting to RUNNING) and
produced an "Error: " text prefix. determineToolResultState() only
recognizes a non-RUNNING state or an "[ERROR]" text prefix, so failed
tool calls (MCP timeouts, tool-not-found, execution errors, ...) fell
through to SUCCESS and the agent never saw the failure.

Set state=ERROR in error() so failures are classified by structured
state, independent of the output text. Fixes all error() call sites at
once; the output text is unchanged.

Closes agentscope-ai#2157
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@oss-maintainer oss-maintainer 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.

Summary

Critical bug fix — ToolResultBlock.error() was leaving state=null (defaulting to RUNNING), causing determineToolResultState() to misclassify failures as SUCCESS.

Findings

  • [Critical] ToolResultBlock.java:207 — Now explicitly sets state = ToolResultState.ERROR. This is the correct fix.
  • [Info] Test errorResultCarriesErrorState() added as regression guard. Good.

Verdict

Essential fix for tool error handling. LGTM.


Automated review by github-manager

@AgentScopeJavaBot AgentScopeJavaBot added bug Something isn't working area/core/tool Tool, skill, RAG abstractions labels Jul 14, 2026

@AgentScopeJavaBot AgentScopeJavaBot 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.

🤖 AI Review

This is a clean, well-targeted bug fix for issue #2157. The root cause was a mismatch between ToolResultBlock.error() (which produced "Error: " text prefix with state defaulting to RUNNING) and ReActAgent.determineToolResultState() (which checks for non-RUNNING state or "[ERROR]" text prefix). Neither condition matched, so all tool failures — MCP timeouts, tool-not-found, execution errors — were silently classified as SUCCESS. The fix correctly sets state = ToolResultState.ERROR in the factory method, decoupling error identification from fragile text-prefix heuristics. The change is minimal (+21/-2), backward-compatible (text output unchanged), and includes both an extended integration assertion and a focused regression test. Full test suite reportedly green (2210 passed, 0 failed).

One minor follow-up consideration (outside this PR's scope): ReActAgent.buildErrorToolResult() (line ~1753) still constructs error results via the builder without setting state, relying on the "[ERROR]" text prefix heuristic. Applying the same structured-state approach there would improve consistency and allow eventual deprecation of the text-prefix fallback.

// determineToolResultState() misclassified failures as SUCCESS.
ToolResultBlock result = ToolResultBlock.error("boom");
assertEquals(ToolResultState.ERROR, result.getState());
}

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.

[praise] Excellent regression guard with a clear historical comment explaining why the bug existed (state defaulted to RUNNING, text prefix was "Error: " not "[ERROR]"). This level of context in a test comment is exactly what future maintainers need to understand the intent without digging through git history.

* @return ToolResultBlock with error output
* @return ToolResultBlock with error output and {@code state = ERROR}
*/
public static ToolResultBlock error(String errorMessage) {

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.

[praise] Clean fix — setting the structured state field directly in the factory method is the right approach. It fixes all error call sites (MCP timeouts, tool-not-found, generic failures) in one place and removes the dependency on the brittle "[ERROR]" text-prefix heuristic in determineToolResultState().

@AgentScopeJavaBot AgentScopeJavaBot 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.

🤖 AI Review

This is a clean, well-targeted bug fix for issue #2157. The root cause was a mismatch between ToolResultBlock.error() (which produced "Error: " text prefix with state defaulting to RUNNING) and ReActAgent.determineToolResultState() (which checks for non-RUNNING state or "[ERROR]" text prefix). Neither condition matched, so all tool failures — MCP timeouts, tool-not-found, execution errors — were silently classified as SUCCESS. The fix correctly sets state = ToolResultState.ERROR in the factory method, decoupling error identification from fragile text-prefix heuristics. The change is minimal (+21/-2), backward-compatible (text output unchanged), and includes both an extended integration assertion and a focused regression test. Full test suite reportedly green (2210 passed, 0 failed).

One minor follow-up consideration (outside this PR's scope): ReActAgent.buildErrorToolResult() (line ~1753) still constructs error results via the builder without setting state, relying on the "[ERROR]" text prefix heuristic. Applying the same structured-state approach there would improve consistency and allow eventual deprecation of the text-prefix fallback.

// determineToolResultState() misclassified failures as SUCCESS.
ToolResultBlock result = ToolResultBlock.error("boom");
assertEquals(ToolResultState.ERROR, result.getState());
}

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.

[praise] Excellent regression guard with a clear historical comment explaining why the bug existed (state defaulted to RUNNING, text prefix was "Error: " not "[ERROR]"). This level of context in a test comment is exactly what future maintainers need to understand the intent without digging through git history.

* @return ToolResultBlock with error output
* @return ToolResultBlock with error output and {@code state = ERROR}
*/
public static ToolResultBlock error(String errorMessage) {

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.

[praise] Clean fix — setting the structured state field directly in the factory method is the right approach. It fixes all error call sites (MCP timeouts, tool-not-found, generic failures) in one place and removes the dependency on the brittle "[ERROR]" text-prefix heuristic in determineToolResultState().

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

Labels

area/core/tool Tool, skill, RAG abstractions bug Something isn't working

Projects

None yet

3 participants