agentscope-ai / agentscope-ai/agentscope-java

[Bug]: AguiResumeCoordinator loses HITL interrupt state across instances

Đang mở
#2,773 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
bug
Ngôn ngữ chính
Java
Star
5.6k
Fork
1.3k
Merge trung bình
4 ngày 12 giờ
Pull request đã merge (30 ngày)
77

Mô tả

**Describe the bug**

The AG-UI interrupt/resume contract state (`pendingInterruptsByThread`, `activeRunsByThread`) lives only in process-local `ConcurrentHashMap`s inside `AguiResumeCoordinator`. When the service is deployed with multiple replicas (e.g. 2 pods on k8s) behind a round-robin load balancer, a resume request can be routed to a replica that never saw the original interrupt. `validate()` then fails with `AGUI_INTERRUPT_CONTRACT_ERROR` (`"RunAgentInput.resume does not match any open interrupt"`), even though the client is correctly following the AG-UI resume contract.

The same state is lost on a single replica whenever the process restarts (rolling update, OOM eviction, failover).

Relevant code:

- `AguiResumeCoordinator.java:43-45` — state held in process-local `ConcurrentHashMap`s
- `AguiResumeCoordinator.java:56-61` — the failing check (`pending == null && hasResume() == true`)
- `AguiRequestProcessor.java:76` — coordinator instantiated inside the processor; no injection or extension point
- `AguiRequestProcessor.java:130-174` — `beginRun` / `trackPendingInterrupts` / `finishRun` wiring

**To Reproduce**

1. Deploy the AG-UI service with 2 replicas behind a round-robin LB (k8s Service / Nginx Ingress):

```yaml
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 2
```

1. Client sends a run request **without** `resume` → the agent issues a `tool_call` → server replies with `RUN_FINISHED` carrying an `interrupt` (`reason: "tool_call"`). The interrupt state is recorded **only on the replica that handled this request** (pod A).
2. Client sends the follow-up run request **with** `resume` (same `threadId`, same `interruptId`) → the LB routes it to the other replica (pod B).
3. Pod B has no record of the interrupt → emits `RUN_ERROR` with `code=AGUI_INTERRUPT_CONTRACT_ERROR`.

With 2 replicas and round-robin routing, roughly 50% of resume attempts fail.

**Expected behavior**

A resume request should be accepted regardless of which replica serves it. Either:

- the interrupt/resume state should be shared across replicas (e.g. a pluggable state store with a Redis backend, consistent with the existing `RedisDistributedStore` approach), or
- the limitation should be explicitly documented (e.g. "sticky sessions required for AG-UI tool-call interrupts").

**Error messages**

```json
{"type":"RUN_ERROR","threadId":"1787102227357","runId":"run-1787102359437","message":"RunAgentInput.resume does not match any open interrupt","code":"AGUI_INTERRUPT_CONTRACT_ERROR","timestamp":1787102359738}
```

**Environment (please complete the following information):**

- AgentScope-Java Version: 2.0.1
- Java Version: 17
- OS: Linux (k8s pods); reproduced from a Windows dev client

**Additional context**

- The official multi-replica guidance (`docs/v2/en/docs/others/going-to-production.md`) recommends `RedisDistributedStore` + `OssFilesystemSpec` and states "no sticky sessions required", but that applies to session/workspace state. The AG-UI resume coordinator has **no distributed backend and no SPI** to plug one in — `AguiResumeCoordinator` is a package-private `final` class with hardcoded `ConcurrentHashMap`s.
- `validate()` also requires the resume `interruptId` set to match the pending set **exactly** (`AguiResumeCoordinator.java:84-95`, missing/unknown ids are reported), so any mismatch fails hard — there is no lenient path.
- There is no TTL/cleanup for pending interrupt entries (`trackPendingInterrupts`, `AguiResumeCoordinator.java:180-198`); entries stay in memory until the next run of the same thread.

Suggested direction:

1. Extract the state access behind a small SPI (e.g. `AguiResumeStateStore`) with the in-memory implementation as the default, and provide a Redis-backed implementation in `agentscope-extensions-redis`.
2. Preserve cross-replica atomicity: `putIfAbsent`-style claim for `beginRun` (SET NX) and conditional delete for `finishRun` (compare-and-delete).
3. Either way, document the multi-replica requirement for AG-UI interr

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.