From 511c9390ea04dd33b3d927d4a7a09235f97625fd 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());
+ }
+
/**
* 创建每次模型调用仅返回下一条预设响应的运行时。
*