Skip to content

[Bug]:AG-UI: tool results from a resumed run are dropped because the stream context is per-run #3096

Closed

Description

@gxianglc

Version: agentscope-java 2.0.3 (agentscope-extensions-agui)

What happens

Permission-mode HITL spans two runs. Run 1 emits TOOL_CALL_START/ARGS/END and finishes with an
interrupt. After the user approves, run 2 executes the tool and produces ToolResultStartEvent →
delta events → ToolResultEndEvent.

Every AG-UI event from run 2 for that tool call is silently discarded, so the client never receives
TOOL_CALL_RESULT and the tool card stays unresolved forever.

Why

  1. AguiAgentAdapter#streamWithRuntimeContext creates a new AguiStreamContext per run (both the
    ReActAgent and HarnessAgent branches). Nothing carries over from the previous run.
  2. ReActAgent#resumeAgent() is return acting(0) — it does not replay ToolCallStart/Delta/End for
    the tool call that was paused, so the new context never records the toolCallId.
  3. AguiStreamContext#beginToolResult / appendToolResultText / appendToolResultData /
    endToolResult all guard on the private startedToolCalls set via hasStartedToolCall(...).
    The id is absent, so the whole result is dropped.

There is no public API to query or seed startedToolCalls, so an application-side converter cannot
tell "this is a cross-run result" from "this is a bogus id" either.

What the AG-UI spec expects

A resumed run should emit TOOL_CALL_RESULT for the original toolCallId without replaying
TOOL_CALL_START/ARGS/END. The client correlates by toolCallId. (@ag-ui/client's verifyEvents
does not require a preceding TOOL_CALL_START for TOOL_CALL_RESULT, so the standard shape is
accepted by the official client.)

Current workaround

A custom AgentEventConverter overriding ToolResultStartEvent that makes the context adopt the id
silently: call startToolCall(id, name) + endToolCall(id), then beginEvent() to discard the
TOOL_CALL_START/TOOL_CALL_END those two calls queued (the registry calls beginEvent() before and
drainEvents() after each event, so only the current event's output is cleared), then
beginToolResult(id). This produces the spec-compliant single TOOL_CALL_RESULT, but it leans on
internal ordering guarantees and is fragile across upgrades.

Suggested fix

Either

  • let a resumed run map tool results for tool calls belonging to a known resume interrupt (the
    interrupt id already carries replyId:toolCallId), or
  • expose a public entry point on AguiStreamContext to adopt a tool call id without emitting events,
    e.g. adoptToolCall(String toolCallId, String toolCallName).

Related: #2437 (interrupt not propagated, fixed by #2495), #2492, #2956. PR #1231 shows the previous
adapter could back-fill a missing tool-call; that capability was lost in the converter-based design.

Activity

  1. Shay-Deng commented on Sep 11, 2026

    @Shay-Deng

    Hi, I'd like to work on this issue.

    I reproduced it on current main at commit c6af781836b23b1325968585104445856c57bba3 using the existing AguiAgentAdapterV2Test fixtures:

    • Baseline: 82 tests passed.
    • With a cross-run permission-resume regression test: 84 tests, 1 expected failure. Run 1 emits the tool call and permission interrupt; run 2 emits ToolResultStartEvent / deltas / ToolResultEndEvent for the same toolCallId. Expected TOOL_CALL_RESULT, but the output is only RUN_STARTED → RUN_FINISHED.
    • The control case with an unknown toolCallId still passes and is ignored.

    I also checked related and unlinked open/closed/merged PRs, including #2890, #2955, #3013, #3046, #2922, #2908, #2874, #2473, #2495, and #2605. I did not find one that addresses this same output-conversion root cause.

    My proposed scope is narrow: allow a resumed run to adopt only a tool-call ID backed by a validated resume interrupt, emit exactly one TOOL_CALL_RESULT without replaying TOOL_CALL_START/ARGS/END, and keep unknown IDs ignored.

    Would this direction be acceptable, and is anyone already working on it?

  2. added a commit that references this issue on Sep 11, 2026
    c5db8f7
  3. hejian900625 commented on Oct 1, 2026

    @hejian900625

    Confirmed on 2.0.3 (agentscope-extensions-agui) with a scripted model — no network involved, so the repro is fully deterministic.

    Run 1 emits TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END and finishes with RUN_FINISHED whose outcome.type = "interrupt" (interrupt id is replyId:toolCallId, metadata agentscope.interruptKind = permission_confirm). Run 2 — same threadId, resume: [{status: "resolved", payload: {approved: true}}] — does execute the tool and does put ToolResultBlock(state=SUCCESS) into the second model call, but the SSE stream for run 2 contains neither TOOL_CALL_END nor TOOL_CALL_RESULT. Exactly the mechanism described in the issue: beginToolResult / endToolResult gate on hasStartedToolCall(toolCallId), and AguiStreamContext is created per run, so the resumed run has no record of a tool call that started in the previous one. The model gets the result; only the event stream (and therefore any UI rendering it) does not.

    Two things worth adding for anyone hitting this from the other direction:

    1. If you reconstruct the pending ToolUseBlock yourself when resuming, note that tool dispatch validates ToolUseBlock.getContent() (the raw JSON args string), not getInput(). A block built with only the input map fails with Parameter validation failed for tool 'x': missing required property "y" and the tool never runs — which looks identical to "the approved run dropped the result" but is a different bug, in the caller.
    2. editedArgs in the resume payload works as documented (whole replacement, and both the tool and the model's history then show the edited value), so the fix only needs to restore the event, not the argument plumbing.

    Would be great to get a release containing #3100 — v2.0.3 is still the newest tag, so the workaround in the issue body is currently the only option, and it leans on internal beginEvent() / drainEvents() ordering.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions