agentscope-ai / agentscope-ai/agentscope-java

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

Abierto
#2,610 1 comentario 0 reacciones 0 asignados Ver en GitHub
area/build area/core/agent area/core/memory area/harness bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

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

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.