bytechefhq / bytechefhq/bytechef
[bug]: AI agent: suspending tools (Request Approval, Ask User Question) don't suspend the agent or resume the LLM
- Dominant language
- Java
- Stars
- 1k
- Forks
- 170
- Avg merge
- 11h 25m
- Merged PRs (30d)
- 115
Description
## Summary
When an AI agent invokes a tool that suspends the workflow to wait for human input — `requestApproval` (`approval` component tool cluster element) or `askUserQuestion` (`ai/agent/utils`) — the agent does **not** actually suspend, and the LLM never continues reasoning with the human's real input. This is broken for both the non-streaming agent (`AiAgentChatAction`) and the streaming agent (`AiAgentStreamChatAction`).
## Affected code
- `server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java`
- `.../action/AiAgentChatAction.java`, `.../action/AiAgentStreamChatAction.java`
- `.../facade/AiAgentToolFacade.java`
- `server/libs/modules/components/approval/src/main/java/com/bytechef/component/approval/cluster/tool/ApprovalRequestApprovalTool.java`
- `server/libs/modules/components/ai/agent/utils/.../cluster/AiAgentUtilsAskUserQuestionTool.java`
- `server/libs/platform/platform-component/platform-component-service/.../ActionDefinitionServiceImpl.java`
- `SseStreamTaskExecutionPostOutputProcessor` (in `platform-worker` and `platform-job-sync`)
## Root cause
### Non-streaming (`AiAgentChatAction`)
`perform` runs the tool-calling loop via `ChatClient.call()` + Spring AI `ToolCallAdvisor`. When a tool calls `context.suspend(...)`:
1. The tool still returns a value (a placeholder) to the loop. `ToolCallAdvisor` has no concept of suspension, so it feeds the placeholder back to the LLM, which produces a final (meaningless) response.
2. Only *after* `perform` returns does `ActionDefinitionServiceImpl.checkSuspend` observe `actionContext.getSuspend()`. By then the LLM has already consumed the placeholder and finished.
3. `AiAgentChatAction.resumePerform` is a stub — `return ResumeResponse.of(data.toMap())` — it returns the human's answer as the action output and never re-enters the LLM loop.
### `requestApproval` cannot even reach the agent context
`ApprovalRequestApprovalTool` is a `MultipleConnectionsToolFunction`, which `AiAgentToolFacade` wraps as a plain `Function` that never receives Spring AI's `ToolContext`. So it cannot read `AiAgentToolContextKey.ACTION_CONTEXT` — it suspends a detached throwaway `ActionContext` that nobody reads, and the LLM receives `null`. (`askUserQuestion` is a `ToolCallbackProviderFunction`, which *does* get the `ToolContext`, so it at least suspends the right context — but still hits the loop-timing bug above.)
### Streaming (`AiAgentStreamChatAction`)
`perform` returns an `SseEmitterHandler` immediately; the `Flux` (model call + tool execution) is consumed later, inside `SseStreamTaskExecutionPostOutputProcessor`. `checkSuspend` runs against the handler *before* the `Flux` is ever subscribed, so the suspend raised mid-stream is never observed. Suspend is completely non-functional for streaming agents.
## Steps to reproduce
1. Build an AI agent workflow with `requestApproval` (or `askUserQuestion`) attached as a tool.
2. Prompt the agent so the LLM invokes the tool.
**Expected:** the agent pauses, a human approves/responds, the agent resumes and the LLM continues reasoning with the real result.
**Actual:** the agent does not pause meaningfully. With `requestApproval` the LLM receives `null`; with `askUserQuestion` the LLM receives placeholder/empty answers. The turn finishes without real human input. In streaming, the suspend is silently dropped.
## Expected behavior
A tool calling `context.suspend()` inside an agent should suspend the agent's workflow task. On resume (human response via the resume URL), the agent should re-enter the LLM tool-calling loop with the human's answer injected as the pending tool call's result, so the LLM continues reasoning. Should work for both sync and streaming agents.
## Notes
- A design spec for the fix has been written (`docs/superpowers/specs/2026-05-20-resumable-agent-tool-calls-design.md`): a `SuspendableToolCallingManager` decorator that halts the `ToolCallAdvisor` loop at the suspend point via `ToolExecutionResult.returnDirect()`, captures the conversation into the suspend `continueParameters`, and a real `resumePerform` that re-enters the loop with the patched tool result. Streaming is handled by deferring the suspend check to after stream completion.
- Related: #5055 (suspending sub-workflow called as an agent tool — a different path, same family of suspend-vs-agent issues).
Contributor guide
Assessment
This issue has not been assessed yet.