From e146653de7f3612e5e3a0053590c4a5b9b9b56e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Wed, 26 Aug 2026 22:55:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=90=8E=E7=9A=84=E4=BC=9A=E8=AF=9D=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 保存模型失败与无正文取消路径中的 AgentScope 会话 - 补充失败和推理中断场景的上下文恢复测试 --- .../agentscope/AgentScopeReActRuntime.java | 9 +- .../AgentScopeStatefulRuntimeTest.java | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java index a013812..2afbbed 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java @@ -689,6 +689,7 @@ public class AgentScopeReActRuntime implements AgentRuntime { saveSession(); return Flux.just(cancelled(context)); } + saveSession(); return Flux.just(failed(context, error)); } @@ -733,7 +734,8 @@ public class AgentScopeReActRuntime implements AgentRuntime { * 将取消前已输出的助手内容补写入 AgentScope memory 并保存 session。 * *

AgentScope 的正常完成路径会自行把最终助手消息写入 memory。取消订阅时不会触发 - * 完成路径,因此这里仅在已有非空助手内容时补写一次,确保下一轮对话能拿到中断前上下文。

+ * 完成路径,因此这里仅在已有非空助手内容时补写一次,并始终保存已经进入 memory 的 + * 用户消息,确保下一轮对话能拿到中断前上下文。

* * @param finalText 当前已累计的助手文本 * @param finalMessage 当前已捕获的结构化助手消息 @@ -741,10 +743,9 @@ public class AgentScopeReActRuntime implements AgentRuntime { private void persistPartialAssistantOnCancel(StringBuilder finalText, AtomicReference finalMessage) { AgentMessage partialMessage = partialAssistantMessage(finalText, finalMessage); - if (partialMessage == null) { - return; + if (partialMessage != null) { + agent.getMemory().addMessage(messageAdapter.toMsg(partialMessage)); } - agent.getMemory().addMessage(messageAdapter.toMsg(partialMessage)); saveSession(); } diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java index 95886fe..9a34211 100644 --- a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java @@ -838,6 +838,59 @@ public class AgentScopeStatefulRuntimeTest { && "partial answer".equals(message.getTextContent()))); } + @Test + public void shouldPersistUserMessageWhenModelFails() { + InMemoryAgentSessionStore sessionStore = new InMemoryAgentSessionStore(); + AgentInitRequest request = initRequest(); + request.setSessionStore(sessionStore); + AgentScopeReActRuntime runtime = runtimeWithError(new IllegalStateException("model unavailable")); + runtime.init(request); + + List events = runtime.stream( + AgentMessage.text(AgentMessageRole.USER, "remember this request")) + .collectList() + .block(); + + Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.FAILED)); + Assert.assertTrue(sessionStore.exists("session-1")); + AgentScopeReActRuntime restoredRuntime = fakeRuntime(); + restoredRuntime.init(request); + Assert.assertTrue(restoredRuntime.getAgent().getMemory().getMessages().stream() + .anyMatch(message -> message.getRole() == MsgRole.USER + && "remember this request".equals(message.getTextContent()))); + } + + @Test + public void shouldPersistUserMessageWhenReasoningOnlyStreamIsCancelled() throws Exception { + InMemoryAgentSessionStore sessionStore = new InMemoryAgentSessionStore(); + AgentInitRequest request = initRequest(); + request.setSessionStore(sessionStore); + AgentScopeReActRuntime runtime = runtimeWithModel(List.of( + ChatResponse.builder() + .id("reasoning-only") + .content(List.of(ThinkingBlock.builder().thinking("still thinking").build())) + .build()), Duration.ofSeconds(5)); + runtime.init(request); + + CompletableFuture firstReasoning = new CompletableFuture<>(); + reactor.core.Disposable disposable = runtime.stream( + AgentMessage.text(AgentMessageRole.USER, "remember reasoning request")) + .subscribe(event -> { + if (event.getEventType() == AgentRuntimeEventType.REASONING_DELTA) { + firstReasoning.complete(event); + } + }, firstReasoning::completeExceptionally); + firstReasoning.get(3, TimeUnit.SECONDS); + + disposable.dispose(); + awaitCondition(() -> sessionStore.exists("session-1")); + AgentScopeReActRuntime restoredRuntime = fakeRuntime(); + restoredRuntime.init(request); + Assert.assertTrue(restoredRuntime.getAgent().getMemory().getMessages().stream() + .anyMatch(message -> message.getRole() == MsgRole.USER + && "remember reasoning request".equals(message.getTextContent()))); + } + @Test public void shouldNotDuplicateNormalToolEventsFromMainStream() { AgentInitRequest request = initRequest(); @@ -1636,6 +1689,37 @@ public class AgentScopeStatefulRuntimeTest { new AgentScopeMessageAdapter()); } + /** + * 创建模型调用直接失败的运行时。 + * + * @param error 模型异常 + * @return 测试运行时 + */ + private AgentScopeReActRuntime runtimeWithError(Throwable error) { + AgentScopeModelFactory modelFactory = new AgentScopeModelFactory() { + @Override + public Model create(AgentModelSpec modelSpec, + com.easyagents.agent.runtime.model.AgentGenerationOptions generationOptions) { + return new Model() { + @Override + public Flux stream(List messages, + List toolSchemas, + GenerateOptions options) { + return Flux.error(error); + } + + @Override + public String getModelName() { + return modelSpec == null ? "fake-model" : modelSpec.getModelName(); + } + }; + } + }; + return new AgentScopeReActRuntime(modelFactory, new AgentScopeToolAdapter(), + new AgentScopeKnowledgeAdapter(), new AgentScopeMemoryAdapter(), new AgentScopeSkillAdapter(), + new AgentScopeMessageAdapter()); + } + /** * 创建每次模型调用仅返回下一条预设响应的运行时。 *