agentscope-ai / agentscope-ai/agentscope-java

[Bug]:ASKING 状态会被持久化到状态存储中

オープン
#3,135 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
area/core/agent area/harness bug
主要言語
Java
スター
5.6k
フォーク
1.4k
平均マージ
3日 23時間
マージ済み PR(30日)
77

説明

[Bug]: plan_exit ASKING state persists across runs when enablePendingToolRecovery + enablePlanMode + PermissionMode.BYPASS are combined

**AgentScope-Java Version**: 2.0.1

**Describe the bug**

When `HarnessAgent` is configured with `enablePlanMode(true)` + `enablePendingToolRecovery(true)` + `PermissionMode.BYPASS` + a persistent `AgentStateStore` (MySQL), a `plan_exit` tool call can remain in `ASKING` state after a run is interrupted (timeout / cancellation / exception). The ASKING state is persisted to the state store. When a **new request** reuses the same `sessionId` **without** carrying `ConfirmResult` metadata, the framework throws a hard error and the new run fails immediately.

This is a follow-up edge case to #2534 / PR #2109. The 2.0.1 fix correctly prevents pending recovery from consuming HITL approvals **when the new input carries ConfirmResult**. However, when the new request is a **normal user message** (no ConfirmResult), the leftover ASKING state from a previous run is neither auto-cleared nor gracefully handled — it blocks the new run entirely.

Agent configuration:

```java
HarnessAgent.builder()
.name("testx-" + threadId)
.enablePendingToolRecovery(true)
.permissionContext(PermissionContextState.builder().mode(PermissionMode.BYPASS).build())
.enablePlanMode(true)
.stateStore(mysqlAgentStateStore) // custom AgentStateStore backed by MySQL
.defaultSessionId(threadId)
// ... other settings
.build();
```

Reproduction flow:

```
Run A (sessionId = T1):
1. Agent starts → enters Plan Mode
2. LLM generates plan → calls plan_exit to start execution
3. plan_exit enters ASKING state (awaiting HITL confirmation)
4. Request is interrupted (timeout / user cancel / exception)
5. AgentState (including ASKING plan_exit) persisted to MySQL

Run B (sessionId = T1, NEW request, normal user message, no ConfirmResult):
1. Agent restores state from MySQL
2. Detects plan_exit in ASKING state
3. New request has NO ConfirmResult in metadata
4. Framework throws hard error — new request FAILS immediately
```

**To Reproduce**

Steps to reproduce the behavior:

1. Configure a `HarnessAgent` with `enablePlanMode(true)` + `enablePendingToolRecovery(true)` + `PermissionMode.BYPASS` + a persistent `AgentStateStore`
2. Send a request that triggers the agent to enter Plan Mode and call `plan_exit`
3. Interrupt the run (cancel / timeout) while `plan_exit` is in ASKING state
4. Send a new request with the same `sessionId` (normal user message, no `ConfirmResult`)
5. See error

**Expected behavior**

Two possible fixes:

1. `plan_exit` should not enter ASKING state under `PermissionMode.BYPASS`. Since BYPASS means "skip all permission checks", framework-internal plan tools (`plan_enter` / `plan_write` / `plan_exit`) should never require HITL confirmation.

2. When a new request arrives without `ConfirmResult` and the restored state contains ASKING tool calls from a previous run, the framework should auto-clear the orphaned ASKING state (or emit a warning event and skip it) instead of throwing a hard error.

**Error messages**

```
Agent is paused for human-in-the-loop confirmation: the following tool call(s) are in ASKING state and need your approval before the agent can continue: [plan_exit (id=call_f178664a2cf04251ae4afd19)]. This call supplied no confirmation, so it cannot proceed. To resume, send a follow-up message that carries a List under the metadata key "agentscope_confirm_results", e.g.: UserMessage.builder() .metadata(Map.of(Msg.METADATA_CONFIRM_RESULTS, List.of(new ConfirmResult(true, toolCall)))) .build(); Tip: capture the ToolUseBlocks from the RequireUserConfirmEvent emitted when the agent paused. If you did NOT expect a pending confirmation here, a previous run most likely paused on one of these tool calls and persisted that state under the same (agentId, sessionId); start a fresh session, clear the persisted state, or use an in-memory state store to begin clean.
```

**Environment**

- AgentScope-Java Version: 2.0.1
- Java Version: 17
- OS: Linux (production) / Windows (development)

**Additional context**

- Related: #2534 (人工介入与PendingToolRecovery不兼容), fixed by PR #2109 in 2.0.1. The 2.0.1 fix handles the case where the new input **carries** `ConfirmResult`. This issue covers the **different** scenario where a normal new request encounters leftover ASKING state with no `ConfirmResult` to provide.
- RC2 release notes mention "Plan Mode 优化 — 改进计划文件持久化与恢复机制,优化 plan_enter / plan_write / plan_exit 工具链的交互体验,增强 HITL 审批流程的稳定性" — this issue suggests there are still edge cases in that interaction.
- All our HarnessAgent-based agents are affected because they all use the same combination of `enablePlanMode(true)` + `enablePendingToolRecovery(true)` + `PermissionMode.BYPASS`.
- Current workaround: manually delete the persisted AgentScope state for the affected sessionId before each new run, or switch to `InMemoryAgentStateStore` (sacrificing session recovery).

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

Start with the HarnessAgent configuration and the pending tool recovery path involving plan_exit, PermissionMode.BYPASS, AgentStateStore, and session restoration. Reproduce the interrupted Run A and normal Run B flow with the same sessionId and verify that the new request no longer fails when restored state contains an orphaned ASKING call.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
java, mysql
領域
ai, backend, database
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
45/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。