agentscope-ai / agentscope-ai/agentscope-java

[Bug] enablePendingToolRecovery mutates the shared ReActAgent.Builder.hooks on every build(), racing under concurrent A2A requests (AIOOBE / NPE during ReActAgent construction)

Đang mở
#2,539 4 bình luận 0 reaction 0 người được giao Xem trên GitHub
area/build area/core/agent area/ext/integration area/ext/spring-boot 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ả

### Environment
- agentscope: `1.0.12`(另反编译确认 `1.1.0-RC2` 同样存在,见文末)
- Spring Boot 3 · `agentscope-extensions-a2a-server` · `a2a-java-sdk-server-common 0.3.3.Final`
- Topology: the `ReActAgent.Builder` is exposed as a shared Spring **singleton** bean to the A2A server; the client sends **concurrent** A2A streaming requests (≥2) to the same server.

### Problem

When `ReActAgent.Builder.enablePendingToolRecovery(true)` is set, `Builder.build()` executes:

```java
if (enablePendingToolRecovery) {
this.hooks.add(new PendingToolRecoveryHook()); // mutates the SHARED builder's hooks Set
}
```

In the A2A server, `ReActAgentWithBuilderRunner.buildReActAgent()` calls `build()` on the **same shared builder bean for every incoming request**. When requests arrive concurrently, multiple threads mutate `builder.hooks` (a plain, non-thread-safe `HashSet`) at the same time as `ReActAgent.` iterates it via `new ArrayList<>(builder.hooks)`. This produces two failure modes, both aborting agent construction (`AgentScopeAgentExecutor` reports "Agent execution failed"):

1. `java.util.LinkedHashMap.keysToArray` → `ArrayIndexOutOfBoundsException: Index 23 out of bounds for length 23` — concurrent modification while converting the backing map to an array.
2. `NullPointerException` in `AgentBase.sortHooks` (`Comparator.comparingInt` on a partially-initialized element copied from the racing set).

### Root cause (bytecode)

`Builder.build()` in 1.0.12 (and 1.1.0-RC2) contains:

```
if (enablePendingToolRecovery) { this.hooks.add(new PendingToolRecoveryHook()); }
```

i.e. `build()` **mutates shared builder state**, which is unsafe when the builder is reused across concurrent builds (the A2A server's design).

### Impact
- Any concurrent A2A request can fail to construct its agent, failing the whole call.
- Secondary: `PendingToolRecoveryHook` has no `equals()`/`hashCode()`, so the shared `hooks` set **grows by one instance per build** (unbounded leak + increasing per-build copy cost).

### Logs

Two servers, same millisecond, 3 concurrent `Handling streaming request` on different threads (`ctor-http-nio-8/9/10`):

```text
2026-08-03T18:03:56.603+08:00 INFO 35244 --- [reader-server] [tor-http-nio-10] i.a.c.a.s.t.j.JsonRpcTransportWrapper : Handling streaming request, returning SSE Flux
2026-08-03T18:03:56.603+08:00 INFO 35244 --- [reader-server] [ctor-http-nio-9] i.a.c.a.s.t.j.JsonRpcTransportWrapper : Handling streaming request, returning SSE Flux
2026-08-03T18:03:56.603+08:00 INFO 35244 --- [reader-server] [ctor-http-nio-8] i.a.c.a.s.t.j.JsonRpcTransportWrapper : Handling streaming request, returning SSE Flux
2026-08-03T18:03:56.604+08:00 ERROR 35244 --- [reader-server] [ool-10-thread-1] i.a.c.a.s.e.AgentScopeAgentExecutor : [c0e2c3ba-a990-4055-8a9f-d30f6cb65314] Agent execution failed

java.lang.NullPointerException: null
at java.base/java.util.Comparator.lambda$comparingInt$7b0bb60$1(Comparator.java:494) ~[na:na]
at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:360) ~[na:na]
at java.base/java.util.TimSort.sort(TimSort.java:220) ~[na:na]
at java.base/java.util.Arrays.sort(Arrays.java:1308) ~[na:na]
at java.base/java.util.concurrent.CopyOnWriteArrayList.sortRange(CopyOnWriteArrayList.java:980) ~[na:na]
at java.base/java.util.concurrent.CopyOnWriteArrayList.sort(CopyOnWriteArrayList.java:972) ~[na:na]
at io.agentscope.core.agent.AgentBase.sortHooks(AgentBase.java:529) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.agent.AgentBase.(AgentBase.java:145) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.agent.StructuredOutputCapableAgent.(StructuredOutputCapableAgent.java:97) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.ReActAgent.(ReActAgent.java:158) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.ReActAgent$Builder.build(ReActAgent.java:1594) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.a2a.server.executor.runner.ReActAgentWithBuilderRunner.buildReActAgent(ReActAgentWithBuilderRunner.java:40) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.a2a.server.executor.runner.BaseReActAgentRunner.stream(BaseReActAgentRunner.java:57) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.a2a.server.executor.AgentScopeAgentExecutor.execute(AgentScopeAgentExecutor.java:101) ~[agentscope-1.0.12.jar:na]
at io.a2a.server.requesthandlers.DefaultRequestHandler$2.run(DefaultRequestHandler.java:640) ~[a2a-java-sdk-server-common-0.3.3.Final.jar:na]
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

2026-08-03T18:03:56.579+08:00 INFO 42664 --- [researcher-server] [ctor-http-nio-8] i.a.c.a.s.t.j.JsonRpcTransportWrapper : Handling streaming request, returning SSE Flux
2026-08-03T18:03:56.579+08:00 INFO 42664 --- [researcher-server] [tor-http-nio-10] i.a.c.a.s.t.j.JsonRpcTransportWrapper : Handling streaming request, returning SSE Flux
2026-08-03T18:03:56.579+08:00 INFO 42664 --- [researcher-server] [ctor-http-nio-9] i.a.c.a.s.t.j.JsonRpcTransportWrapper : Handling streaming request, returning SSE Flux
2026-08-03T18:03:56.580+08:00 ERROR 42664 --- [researcher-server] [ool-10-thread-6] i.a.c.a.s.e.AgentScopeAgentExecutor : [4942d095-3cd5-4c91-b576-d575a0e9f51c] Agent execution failed

java.lang.ArrayIndexOutOfBoundsException: Index 23 out of bounds for length 23
at java.base/java.util.LinkedHashMap.keysToArray(LinkedHashMap.java:673) ~[na:na]
at java.base/java.util.LinkedHashMap.keysToArray(LinkedHashMap.java:661) ~[na:na]
at java.base/java.util.HashSet.toArray(HashSet.java:376) ~[na:na]
at java.base/java.util.ArrayList.(ArrayList.java:181) ~[na:na]
at io.agentscope.core.ReActAgent.(ReActAgent.java:158) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.ReActAgent$Builder.build(ReActAgent.java:1594) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.a2a.server.executor.runner.ReActAgentWithBuilderRunner.buildReActAgent(ReActAgentWithBuilderRunner.java:40) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.a2a.server.executor.runner.BaseReActAgentRunner.stream(BaseReActAgentRunner.java:57) ~[agentscope-1.0.12.jar:na]
at io.agentscope.core.a2a.server.executor.AgentScopeAgentExecutor.execute(AgentScopeAgentExecutor.java:101) ~[agentscope-1.0.12.jar:na]
at io.a2a.server.requesthandlers.DefaultRequestHandler$2.run(DefaultRequestHandler.java:640) ~[a2a-java-sdk-server-common-0.3.3.Final.jar:na]
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
```

### Expected behavior
`build()` must not mutate shared builder state. Building an agent from a shared builder concurrently should be safe (all reads, no writes to the builder's collections).

### Suggested fix
Move the `if (enablePendingToolRecovery) this.hooks.add(new PendingToolRecoveryHook())` from `Builder.build()` into `ReActAgent.`, operating on the per-instance `new ArrayList<>(builder.hooks)` copy — i.e., what the 2.0 rewrite already does (`build()` no longer touches `this.hooks`; the flag is handled at execution time).

### Version status
- `1.0.12`(latest stable 1.x):**affected** — verified by bytecode.
- `1.1.0-RC2`(latest 1.x RC):**still affected** — verified by bytecode, `build()` still does `this.hooks.add(new PendingToolRecoveryHook())`.
- `2.0.0`(full rewrite):**fixed** — `build()` no longer mutates `this.hooks`; but upgrading to 2.0 is a breaking migration, not a drop-in fix.

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.