agentscope-ai / agentscope-ai/agentscope-java

[Bug]: AguiResumeCoordinator loses HITL interrupt state across instances

Abierto
#2,773 1 comentario 0 reacciones 0 asignados Ver en GitHub
bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

**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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.