Skip to content

fix(core): stop fabricating tool results for suspended external tool calls - #3435

Merged
xzxiaoshan merged 2 commits into
mainfrom
fix/2798-suspended-tool-results
Oct 8, 2026
Merged

xzxiaoshan merged 2 commits into
mainfrom
fix/2798-suspended-tool-results

Conversation

@jujn

@jujn jujn commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2798

Problem

When an external tool call is suspended, the runtime emitted a synthetic ToolResultBlock
whose output contained [Awaiting external execution] (or the ToolSuspendException
reason) with state RUNNING, then streamed the full
ToolResultStartEvent → ToolResultTextDeltaEvent → ToolResultEndEvent sequence before
RequireExternalExecutionEvent.

This conflicts with the HITL contract: the tool has not executed, and the real result
arrives from the caller on resume. The placeholder lives in the same field as real tool
output, so any consumer that renders output without checking isSuspended() presents it
as a successful tool result — first-party examples: SessionEventMapper persisted it as
AGENT_TOOL_RESULT, and the Aistio observer published it as a TOOL_RESULT session event.
AG-UI already had to special-case ToolResultEndEvent(RUNNING) and drop the buffered text.

Changes

Data model (agentscope-core)

  • Add ToolResultState.SUSPENDED.
  • ToolResultBlock.suspended() now produces an empty output, state = SUSPENDED, and
    carries the suspend reason (when the exception supplied one) in new
    METADATA_SUSPEND_REASON metadata, readable via getSuspendReason(). No placeholder
    text is written anywhere.
  • Deprecate METADATA_SUSPENDED as the legacy wire encoding. isSuspended() now
    recognizes both the canonical SUSPENDED state and the legacy metadata marker, and
    suspended() keeps writing the legacy marker for one transition window so older readers
    still recognize blocks produced by this version.

Event stream (ReActAgent)

  • runToolBatch no longer emits ToolResultTextDeltaEvent / ToolResultEndEvent for
    suspended results. RequireExternalExecutionEvent is the single signal, matching the
    permission-HITL path.
  • determineToolResultState maps suspended results to SUSPENDED instead of RUNNING.

AG-UI adapter

  • ToolResultEventConverter treats SUSPENDED (and legacy RUNNING) end events as
    suspension and discards buffered content instead of emitting TOOL_CALL_RESULT.
  • The tool-call interrupt message now comes from the suspend-reason metadata (with an
    adapter-owned default) instead of reading ToolResultBlock.output.

Docs

  • v2 EN/ZH event contract updated: SUSPENDED added to the ToolResultEndEvent state
    list; RequireExternalExecutionEvent now documents that suspended calls produce no
    ToolResult* events.

Compatibility

  • Blocks serialized by older versions (legacy metadata with a defaulted RUNNING state)
    are still recognized by isSuspended() and by the AG-UI adapter.
  • Behavior changes for event-stream consumers: suspended calls no longer emit
    ToolResultEndEvent(RUNNING) or placeholder deltas, and getOutput() on a suspended
    block now returns an empty list.

Testing

  • agentscope-core: full suite passes (2,524 tests; the only errors are pre-existing
    Windows symlink-privilege failures in DangerousPathBypassTest, unrelated to this
    change). New coverage pins the suspended block shape (empty output, SUSPENDED, reason
    metadata), the absence of result events for suspended calls, and dual-read recognition
    of the canonical and legacy encodings.
  • AG-UI extension: full module suite passes, including tests for the new event shape and
    a legacy-wire-shape regression test.
  • agentscope-harness: full suite passes; service-dataplane SessionEventMapper tests
    pass.
  • spotless:check clean on both modified modules.

Copilot AI balanced review requested due to automatic review settings October 7, 2026 15:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Oct 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.95652% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...va/io/agentscope/core/message/ToolResultBlock.java 83.33% 1 Missing and 1 partial ⚠️
...e/src/main/java/io/agentscope/core/ReActAgent.java 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@xzxiaoshan
xzxiaoshan merged commit a6c9464 into main Oct 8, 2026
8 checks passed
@chickenlj
chickenlj deleted the fix/2798-suspended-tool-results branch October 9, 2026 01:50
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.

[Question]: External tool suspend still emits a synthetic tool result [Awaiting external execution]

3 participants