Skip to content

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

Description

@StevenZhang2002

AgentScope HarnessAgent interrupt() API 设计缺陷

概述

HarnessAgent 公开的 interrupt() / interrupt(Msg) 方法无法正确中断通过 streamEvents(List<Msg>, RuntimeContext) 启动的流式会话,因为中断始终使用 defaultSessionId 而非 RuntimeContext 中传入的真实 sessionId。

影响范围:AgentScope 2.0.0-RC5(可能影响所有 2.0.x 版本)。


问题描述

典型使用场景

// 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<AgentEvent> 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 的赋值逻辑为:

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:

// 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),只暴露了:

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 中添加:

public void interrupt(RuntimeContext rc, Msg msg) {
    this.delegate.interrupt(rc, msg);
}

这是最小改动,且与现有的 streamEvents(List<Msg>, RuntimeContext) 对称。

方案 B:interrupt() 自动使用 activeRc

修改 interrupt() / interrupt(Msg) 实现,优先使用 activeRc(如果非空):

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() 绕过:

HarnessAgent agent = ...;
RuntimeContext ctx = ...;  // 与 streamEvents 使用同一个 ctx

// 正确的中断方式
agent.getDelegate().interrupt(ctx, new UserMessage("用户已取消操作"));

版本信息

  • AgentScope version: 2.0.0-RC5
  • 验证方式:javap 反编译字节码 + 源码逻辑分析

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/buildBuild, CI, BOM, distributionarea/core/agentAgent runtime, pipeline, hooks, planarea/core/memoryMemory, session, statearea/harnessagentscope-harness (test/runtime support)bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions