diff --git a/docs/superpowers/specs/v2-api-findings.md b/docs/superpowers/specs/v2-api-findings.md index 972d558a1..ed16e9bf7 100644 --- a/docs/superpowers/specs/v2-api-findings.md +++ b/docs/superpowers/specs/v2-api-findings.md @@ -660,3 +660,92 @@ RC3 `buildAgentStream` 已经 `.contextWrite(c -> c.put(RUNTIME_CONTEXT_KEY, con 即 reactor Context 方案在流式路径下生效。`ChatUsageMiddlewareTest`(ThreadLocal 回退路径) 保持不变、继续 PASS。 +--- + +## R-enh:enableTaskList + interrupt 精确签名(Task 7.1 探针确认) + +**来源:** 对 `agentscope-2.0.0-RC3-sources.jar` 中 `io/agentscope/core/ReActAgent.java` +(Builder `enableTaskList` 3724–3755、`build()` 段 4260–4264、`interrupt(...)` 659–728) +与 `io/agentscope/core/tool/builtin/TodoTools.java`(注解式 `@Tool(name="todo_write")`, +第 92–98 行)全量源码通读(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()` 段(4260–4264)在 toolkit 构造后按 `taskListEnabled` 触发: +```java +if (taskListEnabled) { + configureTodoTools(agentToolkit); +} +``` +`configureTodoTools(Toolkit)`(4217–4219): +```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:92–98)。 +- **full-list-replace 语义**:模型每次传完整任务列表,覆盖 AgentState 的 tasksContext(见类级 javadoc 第 35 行)。 +- 此外 `build()` 在 `taskListEnabled` 时会装一个 `io.agentscope.core.middleware.TaskReminderMiddleware`, + 在每个 reasoning step 前把当前任务列表注入提示词(见 enableTaskList(boolean) javadoc 3735–3742)。 + +**对 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 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)`(712–714)实现: +```java +public void interrupt(String userId, String sessionId) { + interrupt(userId, sessionId, null); +} +``` +最终(720–723): +```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。 + + + diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java index fda3e5fff..15170f647 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java @@ -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; } diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/TaskListConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/TaskListConfig.java new file mode 100644 index 000000000..d307bd4bf --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/TaskListConfig.java @@ -0,0 +1,33 @@ +package com.yomahub.liteflow.property.agent; + +/** + * Agent 内置任务列表(todo)能力配置,对应配置段 {@code liteflow.agent.task-list.*}。 + * + *

启用后 {@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)。 + * + *

默认关闭,保持现有 agent 行为不变。 + * + * @since 2.16.0 + */ +public class TaskListConfig { + + /** + * 是否启用内置任务列表能力。 + * + *

默认关闭。开启后 agent 的 toolkit 会多一个 {@code todo_write} 工具, + * 并在每个推理步前重新展示当前任务列表。 + */ + private boolean enabled = false; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java index ef59c31b9..9c27ce8d7 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java @@ -256,6 +256,46 @@ public abstract class ReActAgentComponent extends NodeComponent { ctx().getSlot().setResponseData(reply.getTextContent()); } + /* ===== 外部中断入口(RC3-core,Task 7.1) ===== */ + + /** + * 中断指定 {@code (conversationId, agentKey)} 会话的 in-flight agent call。 + * + *

底层委派给 {@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))。 + * + *

典型场景:同一 (conversationId, agentKey) 的 call 正在另一线程执行, + * 调用方从外部(HTTP 取消、超时回调等)经本方法发中断信号。返回类型 {@code void}, + * 与底层一致——调用方不需要、也拿不到返回值;中断结果经原 call 的 Mono 传播。 + * + *

本方法对 in-flight call 的集成级验证留 Task 9(需可阻塞 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); + } + + /** + * 中断本组件自身 agentKey({@link #agentKey()})下、指定 conversationId 的 + * in-flight call。等价于 {@link #interrupt(String, String) interrupt(conversationId, agentKey())}。 + * + *

用于"只关心本 agent、不想手动传 agentKey"的便捷场景。 + * + * @param conversationId 对话/业务维度标识,非空 + * @since 2.16.0 + */ + public void interrupt(String conversationId) { + interrupt(conversationId, agentKey()); + } + /* ===== 框架 final 执行体 ===== */ /** diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReactAgentFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReactAgentFactory.java index 40a666cc7..876b1145f 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReactAgentFactory.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReactAgentFactory.java @@ -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.1:v2 middleware(替代 1.0 三个 Hook)。 // 顺序:Logging(最外层,包裹整个 reasoning/acting)→ ChatUsage(onModelCall 累加)→ // SkillTracking(onActing 跟踪 load_skill)。之后追加业务侧 cmp.middlewares()。 diff --git a/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json index b094c09ec..d1b0036f7 100644 --- a/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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", diff --git a/liteflow-spring-boot4-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/liteflow-spring-boot4-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json index fe9306b8d..6859ed565 100644 --- a/liteflow-spring-boot4-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/liteflow-spring-boot4-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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", diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/EnableTaskListTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/EnableTaskListTest.java new file mode 100644 index 000000000..17317f7d7 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/EnableTaskListTest.java @@ -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 时不含。 + * + *

断言路径:{@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"); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/InterruptTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/InterruptTest.java new file mode 100644 index 000000000..3c89c8d8d --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/InterruptTest.java @@ -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 中断), + * 且调用不抛异常。 + * + *

降级说明(brief Step 2 允许):真正"in-flight call 被中断后以 INTERRUPTED 结束" + * 需要配合 reactor 时序(先在另一线程 {@code call().block()}、中途调 interrupt、再断言 + * {@code block()} 返回带 INTERRUPTED 标记的 Msg)。那需要可阻塞的 mock 模型 + 精确的 + * reactor 调度控制,属集成级验证,留 Task 9。本类做单元级断言: + *

    + *
  1. component.interrupt 方法存在且返回 {@code void}(与底层一致);
  2. + *
  3. 对一个已构建的 agent,调 {@code interrupt(cid, akey)} 不抛(即底层 + * {@code getAgentState(cid, akey).interruptControl().trigger(...)} 路径寻址成功、 + * 写 trigger 不异常);
  4. + *
  5. 反射核对:component.interrupt 的参数被原样透传给 agent 的 + * {@code interrupt(String, String)}(通过 Mockito spy agent 拦截该调用验证)。
  6. + *
+ */ +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) 调用,验证入参与返回路径。 + * + *

这是 brief 要求的"反射核对"——确保标识映射正确({@code conversationId→userId}、 + * {@code agentKey→sessionId} 原样透传,无颠倒/拼接)。 + */ + @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 的 value——key 是 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, ReActAgent> cache = + (java.util.concurrent.ConcurrentHashMap, 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); + } + } +}