agentscope-ai / agentscope-ai/agentscope-java
[Bug]:AgentScope HarnessAgent interrupt() API 设计缺陷
- Dominant language
- Java
- Stars
- 5.6k
- Forks
- 1.3k
- Avg merge
- 4d 12h
- Merged PRs (30d)
- 77
Description
# AgentScope HarnessAgent interrupt() API 设计缺陷
## 概述
`HarnessAgent` 公开的 `interrupt()` / `interrupt(Msg)` 方法无法正确中断通过 `streamEvents(List, RuntimeContext)` 启动的流式会话,因为中断始终使用 `defaultSessionId` 而非 `RuntimeContext` 中传入的真实 `sessionId`。
**影响范围**:AgentScope 2.0.0-RC5(可能影响所有 2.0.x 版本)。
---
## 问题描述
### 典型使用场景
```java
// 1. 创建 agent(通常 defaultSessionId 未显式设置,回退到 name)
HarnessAgent agent = HarnessAgent.builder()
.name("coding-assistant")
.model(model)
// .defaultSessionId(...) // ← 未设置
.build();
// 2. 启动流式会话,传入自定义 sessionId
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("conversation-123") // 真实会话 ID
.userId("user-1")
.build();
Flux events = agent.streamEvents(messages, ctx);
// 3. 尝试中断
agent.interrupt(new UserMessage("用户取消")); // ← 无法命中目标会话
```
### 根因
`interrupt()` 的调用链如下:
```
HarnessAgent.interrupt(Msg)
→ ReActAgent.interrupt(Msg)
→ ReActAgent.interrupt(null, this.defaultSessionId, msg)
→ getAgentState(null, defaultSessionId).interruptControl().trigger(...)
```
而 `ReActAgent` 构造函数中 `defaultSessionId` 的赋值逻辑为:
```java
if (builder.defaultSessionId != null && !builder.defaultSessionId.isBlank()) {
this.defaultSessionId = builder.defaultSessionId;
} else if (builder.name != null) {
this.defaultSessionId = builder.name; // ← 多数场景走这里
} else {
this.defaultSessionId = "ReActAgent";
}
```
**结果**:`interrupt()` 去中断 session `"coding-assistant"`(agent name),而真正的流跑在 session `"conversation-123"` 上——**完全打不中目标**。
---
## 证据:底层已有正确能力,但未暴露
`ReActAgent` 提供了正确的重载,能从 `RuntimeContext` 中提取 sessionId:
```java
// ReActAgent.interrupt(RuntimeContext, Msg) — 正确版本
public void interrupt(RuntimeContext rc, Msg msg) {
String userId = (rc != null) ? rc.getUserId() : null;
String sessionId = (rc != null) ? rc.getSessionId() : null;
if (sessionId == null || sessionId.isBlank()) {
sessionId = this.defaultSessionId;
}
this.getAgentState(userId, sessionId)
.interruptControl()
.trigger(InterruptSource.USER, msg);
}
```
同时,`ReActAgent` 在流执行期间会将当前的 `RuntimeContext` 写入 `activeRc` 字段(`beforeAgentExecution` 中 set,`afterAgentExecution` 中 clear),并通过 `getRuntimeContext()` 公开。
但 `HarnessAgent` **没有暴露** `interrupt(RuntimeContext, Msg)`,只暴露了:
```java
public void interrupt(); // → defaultSessionId
public void interrupt(Msg msg); // → defaultSessionId
```
---
## 对比
| 调用方式 | 使用的 sessionId | 是否命中 |
|---------|-----------------|---------|
| `agent.interrupt(msg)` | `defaultSessionId`(agent name) | ❌ 未命中 |
| `agent.getDelegate().interrupt(ctx, msg)` | `RuntimeContext` 中的真实 sessionId | ✅ 正确命中 |
调用方不得不绕过 `HarnessAgent`,直接操作内部的 `ReActAgent` 来实现正确的中断逻辑。
---
## 建议修复
### 方案 A(推荐):暴露 `interrupt(RuntimeContext, Msg)`
在 `HarnessAgent` 中添加:
```java
public void interrupt(RuntimeContext rc, Msg msg) {
this.delegate.interrupt(rc, msg);
}
```
这是最小改动,且与现有的 `streamEvents(List, RuntimeContext)` 对称。
### 方案 B:`interrupt()` 自动使用 `activeRc`
修改 `interrupt()` / `interrupt(Msg)` 实现,优先使用 `activeRc`(如果非空):
```java
public void interrupt(Msg msg) {
RuntimeContext active = this.delegate.getRuntimeContext();
if (active != null) {
this.delegate.interrupt(active, msg);
} else {
this.delegate.interrupt(msg); // fallback
}
}
```
优点是对已有调用方透明,无需改动使用代码。
---
## 当前 Workaround
调用方可在 `HarnessAgent` 修复前,通过 `getDelegate()` 绕过:
```java
HarnessAgent agent = ...;
RuntimeContext ctx = ...; // 与 streamEvents 使用同一个 ctx
// 正确的中断方式
agent.getDelegate().interrupt(ctx, new UserMessage("用户已取消操作"));
```
---
## 版本信息
- AgentScope version: `2.0.0-RC5`
- 验证方式:`javap` 反编译字节码 + 源码逻辑分析
Contributor guide
Assessment
This issue has not been assessed yet.