Repository navigation
fix(core): stop fabricating tool results for suspended external tool calls - #3435
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
xzxiaoshan
approved these changes
Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2798
Problem
When an external tool call is suspended, the runtime emitted a synthetic
ToolResultBlockwhose
outputcontained[Awaiting external execution](or theToolSuspendExceptionreason) with state
RUNNING, then streamed the fullToolResultStartEvent→ToolResultTextDeltaEvent→ToolResultEndEventsequence beforeRequireExternalExecutionEvent.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
outputwithout checkingisSuspended()presents itas a successful tool result — first-party examples:
SessionEventMapperpersisted it asAGENT_TOOL_RESULT, and the Aistio observer published it as aTOOL_RESULTsession event.AG-UI already had to special-case
ToolResultEndEvent(RUNNING)and drop the buffered text.Changes
Data model (
agentscope-core)ToolResultState.SUSPENDED.ToolResultBlock.suspended()now produces an emptyoutput,state = SUSPENDED, andcarries the suspend reason (when the exception supplied one) in new
METADATA_SUSPEND_REASONmetadata, readable viagetSuspendReason(). No placeholdertext is written anywhere.
METADATA_SUSPENDEDas the legacy wire encoding.isSuspended()nowrecognizes both the canonical
SUSPENDEDstate and the legacy metadata marker, andsuspended()keeps writing the legacy marker for one transition window so older readersstill recognize blocks produced by this version.
Event stream (
ReActAgent)runToolBatchno longer emitsToolResultTextDeltaEvent/ToolResultEndEventforsuspended results.
RequireExternalExecutionEventis the single signal, matching thepermission-HITL path.
determineToolResultStatemaps suspended results toSUSPENDEDinstead ofRUNNING.AG-UI adapter
ToolResultEventConvertertreatsSUSPENDED(and legacyRUNNING) end events assuspension and discards buffered content instead of emitting
TOOL_CALL_RESULT.adapter-owned default) instead of reading
ToolResultBlock.output.Docs
SUSPENDEDadded to theToolResultEndEventstatelist;
RequireExternalExecutionEventnow documents that suspended calls produce noToolResult*events.Compatibility
RUNNINGstate)are still recognized by
isSuspended()and by the AG-UI adapter.ToolResultEndEvent(RUNNING)or placeholder deltas, andgetOutput()on a suspendedblock now returns an empty list.
Testing
agentscope-core: full suite passes (2,524 tests; the only errors are pre-existingWindows symlink-privilege failures in
DangerousPathBypassTest, unrelated to thischange). New coverage pins the suspended block shape (empty output,
SUSPENDED, reasonmetadata), the absence of result events for suspended calls, and dual-read recognition
of the canonical and legacy encodings.
a legacy-wire-shape regression test.
agentscope-harness: full suite passes;service-dataplaneSessionEventMappertestspass.
spotless:checkclean on both modified modules.