feat(agent): 接入 enableTaskList + per-session interrupt(RC3-core;compaction/plan-mode/subagent/sandbox 推迟 GA)

Task 7.1:RC3 可达的两项增强能力接入。

enableTaskList:新增 liteflow.agent.task-list.enabled(默认 false),
ReactAgentFactory.build 在 enabled 时调 builder.enableTaskList(true)
(RC3 build() 内部注册 todo_write 工具 + TaskReminderMiddleware)。
新增 TaskListConfig + AgentConfig.taskList(对齐 SkillsConfig 风格)。

interrupt:ReActAgentComponent 新增 public interrupt(conversationId, agentKey)
(及便捷重载 interrupt(conversationId)),委托 ReactAgentFactory.getOrCreate(...).interrupt(cid, akey)
(RC3 per-session 中断,userId=conversationId/sessionId=agentKey 映射)。
返回 void,与底层 ReActAgent.interrupt(String,String) 一致。

探针 R-enh(直读 RC3 源码):确认 enableTaskList(boolean)/enableTaskList() 在 Builder 上、
7 个 interrupt 重载均返回 void、TodoTools 工具名 todo_write(full-list-replace 语义)。

测试:InterruptTest(4,单元级:不抛 + 参数透传;in-flight 集成验证留 Task 9)
+ EnableTaskListTest(3,断言 toolkit 含/不含 todo_write)。
mvn -Dtest=InterruptTest,EnableTaskListTest,StreamingBridgeTest,ProcessIntegrationTest → 9 PASS。

GA-deferred(不建 no-op 占位,Task 8.2 guide 文档化):
compaction / 两层 memory / plan-mode / subagent / sandbox / self-learning skill。

Spring Boot starter 元数据补 task-list.enabled 条目(boot2 + boot4)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
everywhere.z 2026-06-20 11:57:35 +08:00
parent 199d8b8fc0
commit f5a6a0925d
9 changed files with 454 additions and 0 deletions

View File

@ -660,3 +660,92 @@ RC3 `buildAgentStream` 已经 `.contextWrite(c -> c.put(RUNTIME_CONTEXT_KEY, con
即 reactor Context 方案在流式路径下生效。`ChatUsageMiddlewareTest`ThreadLocal 回退路径)
保持不变、继续 PASS。
---
## R-enhenableTaskList + interrupt 精确签名Task 7.1 探针确认)
**来源:** 对 `agentscope-2.0.0-RC3-sources.jar``io/agentscope/core/ReActAgent.java`
Builder `enableTaskList` 37243755、`build()` 段 42604264、`interrupt(...)` 659728
`io/agentscope/core/tool/builtin/TodoTools.java`(注解式 `@Tool(name="todo_write")`
第 9298 行全量源码通读JDK 21直接读 RC3 源码,比反射更权威)。结论用于 Task 7.1。
### (a) `enableTaskList`
`ReActAgent.Builder` 两个重载(实测):
```java
public Builder enableTaskList(); // = enableTaskList(true)
public Builder enableTaskList(boolean enabled); // 仅置 this.taskListEnabled 字段,返回 this
```
`build()`42604264在 toolkit 构造后按 `taskListEnabled` 触发:
```java
if (taskListEnabled) {
configureTodoTools(agentToolkit);
}
```
`configureTodoTools(Toolkit)`42174219
```java
private void configureTodoTools(Toolkit agentToolkit) {
agentToolkit.registerTool(new io.agentscope.core.tool.builtin.TodoTools());
}
```
**`TodoTools`**`io.agentscope.core.tool.builtin.TodoTools`,注解式 `@Tool`/`@ToolParam`
- 工具名固定 **`todo_write`**`@Tool(name = "todo_write")`TodoTools.java:9298
- **full-list-replace 语义**:模型每次传完整任务列表,覆盖 AgentState 的 tasksContext见类级 javadoc 第 35 行)。
- 此外 `build()``taskListEnabled` 时会装一个 `io.agentscope.core.middleware.TaskReminderMiddleware`
在每个 reasoning step 前把当前任务列表注入提示词(见 enableTaskList(boolean) javadoc 37353742
**对 Task 7.1 的指导(已采用):**
- `TaskListConfig.enabled=true``ReactAgentFactory.build``builder.enableTaskList(true)`
无需手工 `toolkit.registerTool(new TodoTools())`——builder.build() 内部按字段自动注册。
- 断言可在工厂测试里检查 `agent.getToolkit()` 含名为 `todo_write` 的工具(`Toolkit.getTools()`/
等价查询方法,反射读注册表),或断言 builder 字段——但 builder 是 fluent 返回 this
字段私有;最干净是 agent 构建后查 toolkit 含 `todo_write`
### (b) `interrupt`
`ReActAgent` 上的 `interrupt` 重载(实测全量签名 + 返回类型):
```java
public void interrupt(); // @Deprecated
public void interrupt(io.agentscope.core.message.Msg msg); // @Deprecated
public void interrupt(io.agentscope.core.interruption.InterruptSource src); // @Deprecated
public void interrupt(io.agentscope.core.agent.RuntimeContext ctx);
public void interrupt(RuntimeContext ctx, Msg msg);
public void interrupt(String userId, String sessionId);
public void interrupt(String userId, String sessionId, Msg msg);
protected Mono<Msg> handleInterrupt(InterruptContext, Msg...); // 内部派发
```
**关键:所有 public `interrupt` 重载返回 `void`**(非 Mono/Flux/boolean。它做的是
"对 in-flight call 发中断信号"——通过 `getAgentState(userId, sessionId).interruptControl()
.trigger(InterruptSource.USER, msg)` 写一个 trigger正在跑的 `call()` 在 reactor 链里
检查到该 trigger 后以 INTERRUPTED 终止。**调用方不需要、也拿不到返回值**。
`interrupt(String userId, String sessionId)`712714实现
```java
public void interrupt(String userId, String sessionId) {
interrupt(userId, sessionId, null);
}
```
最终720723
```java
public void interrupt(String userId, String sessionId, Msg msg) {
getAgentState(userId, sessionId).interruptControl().trigger(InterruptSource.USER, msg);
}
```
**对 Task 7.1 的指导(已采用):**
- 标识映射沿用 spec §4.1`userId = conversationId`、`sessionId = agentKey`。
`ReActAgentComponent.interrupt(conversationId, agentKey)`
`ReactAgentFactory.getOrCreate(this, agentConfig()).interrupt(conversationId, agentKey)`
- component 的 interrupt 方法签名返回 **`void`**与底层一致brief 字面"返回类型正确"在此即 void
- **不**为 in-flight call 的 "返回 Msg 带 INTERRUPTED 标记"做断言——那需要 reactor 时序配合
(要先在另一线程跑 call().block()、在中途调 interrupt、再断言 block() 抛/返回特定值)。
按 brief 允许的降级路径InterruptTest 断言"调用 `interrupt(cid, akey)` 不抛异常"单元级,
并额外反射核对 component.interrupt 把参数正确映射到底层 `interrupt(String, String)`
(即 build 一个真实 agent、确认其 `getAgentState(cid, akey)` 路径可寻址、interrupt 调用
不抛 NPE/IllegalState。集成级 in-flight 中断验证留 Task 9。

View File

@ -32,6 +32,14 @@ public class AgentConfig {
/** Skills configuration for loading agent-scope SkillBox entries from SKILL.md repositories. */
private SkillsConfig skills = new SkillsConfig();
/**
* 内置任务列表todo能力配置控制是否给 agent 注册 {@code todo_write} 工具 +
* {@code TaskReminderMiddleware}v2 RC3 {@code builder.enableTaskList(true)}
*
* @since 2.16.0
*/
private TaskListConfig taskList = new TaskListConfig();
/** OpenAI 头等平台凭证({@code liteflow.agent.openai.*}),由 {@code OpenAISpec} 解析使用。 */
private PlatformCredential openai = new PlatformCredential();
@ -104,6 +112,19 @@ public class AgentConfig {
this.skills = skills;
}
/**
* 返回内置任务列表todo能力配置永非 null默认 enabled=false
*
* @since 2.16.0
*/
public TaskListConfig getTaskList() {
return taskList;
}
public void setTaskList(TaskListConfig taskList) {
this.taskList = taskList;
}
public PlatformCredential getOpenai() {
return openai;
}

View File

@ -0,0 +1,33 @@
package com.yomahub.liteflow.property.agent;
/**
* Agent 内置任务列表todo能力配置对应配置段 {@code liteflow.agent.task-list.*}
*
* <p>启用后 {@code ReactAgentFactory.build} 会调 v2
* {@code ReActAgent.Builder.enableTaskList(true)}使构建出的 agent 注册内置
* {@code todo_write} 工具full-list-replace 语义操作 AgentState tasksContext
* 并自动安装 {@code TaskReminderMiddleware}每个 reasoning step 前把当前任务列表
* 注入提示词详见 findings R-enh (a)
*
* <p>默认关闭保持现有 agent 行为不变
*
* @since 2.16.0
*/
public class TaskListConfig {
/**
* 是否启用内置任务列表能力
*
* <p>默认关闭开启后 agent toolkit 会多一个 {@code todo_write} 工具
* 并在每个推理步前重新展示当前任务列表
*/
private boolean enabled = false;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@ -256,6 +256,46 @@ public abstract class ReActAgentComponent extends NodeComponent {
ctx().getSlot().setResponseData(reply.getTextContent());
}
/* ===== 外部中断入口RC3-coreTask 7.1 ===== */
/**
* 中断指定 {@code (conversationId, agentKey)} 会话的 in-flight agent call
*
* <p>底层委派给 {@link ReactAgentFactory#getOrCreate} 取回的按组件类缓存的
* {@link ReActAgent} 单例的 {@code interrupt(String userId, String sessionId)}
* 标识映射沿用 spec §4.1{@code userId = conversationId}{@code sessionId = agentKey}
* RC3 实现会 {@code getAgentState(userId, sessionId).interruptControl().trigger(USER, null)}
* 正在跑的 {@code call()} reactor 链里检查到该 trigger 后以 INTERRUPTED 终止
* findings R-enh (b)
*
* <p><b>典型场景</b>同一 (conversationId, agentKey) call 正在另一线程执行
* 调用方从外部HTTP 取消超时回调等经本方法发中断信号返回类型 {@code void}
* 与底层一致调用方不需要也拿不到返回值中断结果经原 call Mono 传播
*
* <p><b>本方法对 in-flight call 的集成级验证留 Task 9</b>需可阻塞 mock 模型 +
* reactor 时序配合单元级"不抛异常 + 参数正确透传" {@code InterruptTest}
*
* @param conversationId 对话/业务维度标识映射为 v2 {@code userId}非空
* @param agentKey 组件维度标识映射为 v2 {@code sessionId}非空
* @since 2.16.0
*/
public void interrupt(String conversationId, String agentKey) {
ReactAgentFactory.getOrCreate(this, agentConfig()).interrupt(conversationId, agentKey);
}
/**
* 中断<b>本组件自身 agentKey</b>{@link #agentKey()}指定 conversationId
* in-flight call等价于 {@link #interrupt(String, String) interrupt(conversationId, agentKey())}
*
* <p>用于"只关心本 agent、不想手动传 agentKey"的便捷场景
*
* @param conversationId 对话/业务维度标识非空
* @since 2.16.0
*/
public void interrupt(String conversationId) {
interrupt(conversationId, agentKey());
}
/* ===== 框架 final 执行体 ===== */
/**

View File

@ -175,6 +175,13 @@ public final class ReactAgentFactory {
AgentSkillRepository skillRepo = SkillRepositoryResolver.configure(
builder, cmp.enableSkills(), cmp.skills(), name, cfg);
// TaskListConfig.enabled v2 builder.enableTaskList(true)findings R-enh (a)
// 开启后 ReActAgent.build() 内部注册内置 todo_write 工具 + TaskReminderMiddleware
// 无需手工 toolkit.registerTool默认 false不影响既有 agent
if (cfg.getTaskList() != null && cfg.getTaskList().isEnabled()) {
builder.enableTaskList(true);
}
// Task 5.1v2 middleware替代 1.0 三个 Hook
// 顺序Logging最外层包裹整个 reasoning/acting ChatUsageonModelCall 累加
// SkillTrackingonActing 跟踪 load_skill之后追加业务侧 cmp.middlewares()

View File

@ -503,6 +503,13 @@
"sourceType": "com.yomahub.liteflow.property.agent.SkillsConfig",
"defaultValue": true
},
{
"name": "liteflow.agent.task-list.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable the built-in todo_write tool + TaskReminderMiddleware (maps to v2 ReActAgent.Builder.enableTaskList(true)).",
"sourceType": "com.yomahub.liteflow.property.agent.TaskListConfig",
"defaultValue": false
},
{
"name": "liteflow.agent.openai-compatible",
"type": "java.util.Map<java.lang.String,com.yomahub.liteflow.property.agent.PlatformCredential>",

View File

@ -503,6 +503,13 @@
"sourceType": "com.yomahub.liteflow.property.agent.SkillsConfig",
"defaultValue": true
},
{
"name": "liteflow.agent.task-list.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable the built-in todo_write tool + TaskReminderMiddleware (maps to v2 ReActAgent.Builder.enableTaskList(true)).",
"sourceType": "com.yomahub.liteflow.property.agent.TaskListConfig",
"defaultValue": false
},
{
"name": "liteflow.agent.openai-compatible",
"type": "java.util.Map<java.lang.String,com.yomahub.liteflow.property.agent.PlatformCredential>",

View File

@ -0,0 +1,93 @@
package com.yomahub.liteflow.test.agent.v2;
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
import com.yomahub.liteflow.agent.component.ReactAgentFactory;
import com.yomahub.liteflow.agent.model.ModelSpec;
import com.yomahub.liteflow.property.agent.AgentConfig;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.model.Model;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Task 7.1 单元测试验证 {@code liteflow.agent.task-list.enabled=true}
* {@link ReactAgentFactory#build} {@code builder.enableTaskList(true)}
* 使构建出的 agent toolkit 含内置 {@code todo_write} 工具findings R-enh (a)
* 默认 false 时不含
*
* <p>断言路径{@code agent.getToolkit().getToolNames()} {@code "todo_write"}
* 这是 RC3 {@code ReActAgent.build()} {@code taskListEnabled} 时经
* {@code configureTodoTools(toolkit)} 注册 {@code new TodoTools()} 的可观测结果
* TodoTools {@code @Tool(name="todo_write")}
*/
class EnableTaskListTest {
static class TaskListCmp extends ReActAgentComponent {
@Override
@SuppressWarnings("rawtypes")
protected ModelSpec model() {
return new ModelSpec() {
@Override
public Model resolve(AgentConfig c) {
return HarnessFixture.stubModel();
}
};
}
@Override
protected String systemPrompt() {
return "x";
}
@Override
protected String userPrompt() {
return "y";
}
}
@BeforeEach
void reset() {
ReactAgentFactory.resetForTesting();
}
@AfterEach
void cleanup() {
ReactAgentFactory.resetForTesting();
}
@Test
void taskListEnabled_registersTodoWriteTool() {
AgentConfig cfg = HarnessFixture.minimalConfig();
cfg.getTaskList().setEnabled(true);
ReActAgent agent = ReactAgentFactory.getOrCreate(new TaskListCmp(), cfg);
assertTrue(agent.getToolkit().getToolNames().contains("todo_write"),
"task-list.enabled=true must register the built-in todo_write tool");
}
@Test
void taskListDisabledByDefault_noTodoWriteTool() {
AgentConfig cfg = HarnessFixture.minimalConfig();
// 默认 false不显式设置
assertFalse(cfg.getTaskList().isEnabled(),
"task-list.enabled must default to false");
ReActAgent agent = ReactAgentFactory.getOrCreate(new TaskListCmp(), cfg);
assertFalse(agent.getToolkit().getToolNames().contains("todo_write"),
"task-list default off must NOT register todo_write tool");
}
@Test
void taskListExplicitlyDisabled_noTodoWriteTool() {
AgentConfig cfg = HarnessFixture.minimalConfig();
cfg.getTaskList().setEnabled(false);
ReActAgent agent = ReactAgentFactory.getOrCreate(new TaskListCmp(), cfg);
assertFalse(agent.getToolkit().getToolNames().contains("todo_write"),
"task-list.enabled=false must NOT register todo_write tool");
}
}

View File

@ -0,0 +1,157 @@
package com.yomahub.liteflow.test.agent.v2;
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
import com.yomahub.liteflow.agent.component.ReactAgentFactory;
import com.yomahub.liteflow.agent.model.ModelSpec;
import com.yomahub.liteflow.property.agent.AgentConfig;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.model.Model;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Task 7.1 单元测试验证 {@link ReActAgentComponent#interrupt(String, String)}
* 把参数正确委托到底层 {@link ReActAgent#interrupt(String, String)}RC3 per-session 中断
* 且调用不抛异常
*
* <p><b>降级说明brief Step 2 允许</b>真正"in-flight call 被中断后以 INTERRUPTED 结束"
* 需要配合 reactor 时序先在另一线程 {@code call().block()}中途调 interrupt再断言
* {@code block()} 返回带 INTERRUPTED 标记的 Msg那需要可阻塞的 mock 模型 + 精确的
* reactor 调度控制属集成级验证<b> Task 9</b>本类做单元级断言
* <ol>
* <li>component.interrupt 方法存在且返回 {@code void}与底层一致</li>
* <li>对一个已构建的 agent {@code interrupt(cid, akey)} <b>不抛</b>即底层
* {@code getAgentState(cid, akey).interruptControl().trigger(...)} 路径寻址成功
* trigger 不异常</li>
* <li>反射核对component.interrupt 的参数被原样透传给 agent
* {@code interrupt(String, String)}通过 Mockito spy agent 拦截该调用验证</li>
* </ol>
*/
class InterruptTest {
/** 最小可构建组件(同 ReactAgentFactoryTest 的 StubCmp 风格)。 */
static class InterruptCmp extends ReActAgentComponent {
@Override
@SuppressWarnings("rawtypes")
protected ModelSpec model() {
return new ModelSpec() {
@Override
public Model resolve(AgentConfig c) {
return HarnessFixture.stubModel();
}
};
}
@Override
protected String systemPrompt() {
return "x";
}
@Override
protected String userPrompt() {
return "y";
}
}
@BeforeEach
void reset() {
ReactAgentFactory.resetForTesting();
}
@AfterEach
void cleanup() {
ReactAgentFactory.resetForTesting();
}
/**
* 断言 1{@code interrupt(String, String)} 方法存在且返回类型为 {@code void}
* 与底层 {@link ReActAgent#interrupt(String, String)} 一致findings R-enh (b)
*/
@Test
void interruptMethodSignature_isVoidTwoStrings() throws NoSuchMethodException {
Method m = ReActAgentComponent.class.getMethod("interrupt", String.class, String.class);
assertEquals(void.class, m.getReturnType(),
"component.interrupt(String,String) must be void (matches ReActAgent.interrupt)");
}
/**
* 断言 2对一个已构建的 agent {@code component.interrupt(cid, akey)} 不抛异常
* agent {@link ReactAgentFactory#getOrCreate} 构建并缓存底层
* {@code interrupt(cid, akey)} {@code getAgentState(cid, akey).interruptControl()
* .trigger(USER, null)}RC3 实测对不存在的 session 也会 lazy 建一个 AgentState
* 并返回其 interruptControl故不会 NPE
*/
@Test
void interruptOnBuiltAgent_doesNotThrow() {
AgentConfig cfg = HarnessFixture.minimalConfig();
InterruptCmp cmp = new InterruptCmp();
// 触发 agent 构建并缓存 process() 第一句等价
ReActAgent agent = ReactAgentFactory.getOrCreate(cmp, cfg);
assertNotNull(agent, "agent must be built & cached");
// 调用中断不抛即通过cid/akey 任意非空字符串即可单元级无真实 in-flight call
assertDoesNotThrow(() -> cmp.interrupt("conv-1", "agent-key-1"),
"interrupt(conversationId, agentKey) must not throw on a built agent");
}
/**
* 断言 3反射核对委托链component.interrupt(cid, akey) 把参数原样透传给底层
* {@code agent.interrupt(String, String)}通过 Mockito spy 替换 factory 缓存里的
* agent 实例拦截 interrupt(String, String) 调用验证入参与返回路径
*
* <p>这是 brief 要求的"反射核对"确保标识映射正确{@code conversationIduserId}
* {@code agentKeysessionId} 原样透传无颠倒/拼接
*/
@Test
void interruptDelegatesArguments_toAgentInterruptStringString() {
AgentConfig cfg = HarnessFixture.minimalConfig();
InterruptCmp cmp = new InterruptCmp();
ReActAgent realAgent = ReactAgentFactory.getOrCreate(cmp, cfg);
// spy 包装真实 agent仅拦截 interrupt(String,String)
ReActAgent spied = org.mockito.Mockito.spy(realAgent);
// 替换 factory 缓存里的实例用反射改 ConcurrentHashMap valuekey cmp.getClass()
swapCachedAgent(cmp.getClass(), spied);
String cid = "conversation-xyz";
String akey = "agent-key-xyz";
cmp.interrupt(cid, akey);
org.mockito.Mockito.verify(spied).interrupt(cid, akey);
}
/**
* 断言 4返回类型为 void编译期已保证运行期反射再确认一次brief 字面
* "返回类型正确"已在断言 1 覆盖此处不重复留作占位说明
*/
@Test
void interruptReturnsVoid() throws NoSuchMethodException {
Method m = ReActAgentComponent.class.getMethod("interrupt", String.class, String.class);
assertTrue(m.getReturnType() == void.class,
"brief requires return type 'correct' = void (matches underlying)");
assertSame(void.class, m.getReturnType());
}
/** 用反射把 ReactAgentFactory.CACHE 里 key 对应的 value 替换成 spy仅测试用。 */
@SuppressWarnings("unchecked")
private static void swapCachedAgent(Class<?> cmpClass, ReActAgent spy) {
try {
java.lang.reflect.Field f = ReactAgentFactory.class.getDeclaredField("CACHE");
f.setAccessible(true);
java.util.concurrent.ConcurrentHashMap<Class<?>, ReActAgent> cache =
(java.util.concurrent.ConcurrentHashMap<Class<?>, ReActAgent>) f.get(null);
// put 覆盖旧值computeIfAbsent 不会重建 key 已存在
cache.put(cmpClass, spy);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("Failed to swap cached agent for test", e);
}
}
}