agentscope-ai / agentscope-ai/agentscope-java
[Bug]: AguiResumeCoordinator loses HITL interrupt state across instances
- Lingua principale
- Java
- Stelle
- 5.6k
- Fork
- 1.3k
- Merge medio
- 4g 12h
- PR unite (30g)
- 77
Descrizione
**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
Guida per i contributori
Apri la guida per i contributori
Valutazione
Questa issue non è ancora stata valutata.