bytechefhq / bytechefhq/bytechef

[bug]: AI agent: suspending tools (Request Approval, Ask User Question) don't suspend the agent or resume the LLM

Open
#5,056 2 comments 0 reactions 1 assignee Claimed by @ivicac View on GitHub
ai backend bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.