mirror of https://gitee.com/dromara/liteFlow
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:
parent
199d8b8fc0
commit
f5a6a0925d
|
|
@ -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<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)`(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。
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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。
|
||||
*
|
||||
* <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 执行体 ===== */
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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()。
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue