agentscope-ai / agentscope-ai/agentscope-java

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

Aberta
#2,610 1 comentário 0 reações 0 responsáveis Ver no GitHub
area/build area/core/agent area/core/memory area/harness bug
Linguagem predominante
Java
Estrelas
5.6k
Forks
1.3k
Merge médio
4d 12h
PRs com merge (30d)
77

Descrição

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

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.