agentscope-ai / agentscope-ai/agentscope-java

[Bug]:AgentScope HarnessAgent interrupt() API 设计缺陷

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

# 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` 反编译字节码 + 源码逻辑分析

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.