From a39c81367d8103e43fae086c1cb20480586390f6 Mon Sep 17 00:00:00 2001 From: shenxiaobo Date: Thu, 1 Oct 2026 23:39:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(harness):=20session=5Fsearch/session=5F?= =?UTF-8?q?history=20=E7=9A=84=E6=A8=A1=E5=9E=8B=E5=8F=AF=E6=8E=A7=20limit?= =?UTF-8?q?=20=E5=8A=A0=E7=A1=AC=E4=B8=8A=E9=99=90(100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session_search 的 maxResults 无上界——工具在 ToolResultEvictionConfig 的 DEFAULT_EXCLUDED_TOOLS 里(理由"bounded results"),无下流裁剪, 模型传 maxResults=100000 即可扫描全部 session 条目并产生无界结果, 还放大 I/O(每个 session 文件全量加载解析)。 - MAX_RESULTS_CEILING=100,sessionSearch 的 maxResults 与 sessionHistory 的 lastN(同款洞)都经 Math.min 钳制 - ToolParam 描述同步标注 capped at 100 - KeywordMatcher 随 upstream main 同步引入(matchMode 依赖) - 测试:150 条命中下 maxResults=100000 只返回 100;lastN=100000 钳到 100;默认 limit 不受影响 Fixes #3270 --- .../harness/agent/tool/SessionSearchTool.java | 22 +++- .../tool/SessionSearchToolCeilingTest.java | 113 ++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/SessionSearchTool.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/SessionSearchTool.java index 84405fbc0d..61b988a737 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/SessionSearchTool.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/SessionSearchTool.java @@ -44,6 +44,13 @@ public class SessionSearchTool { private static final Logger log = LoggerFactory.getLogger(SessionSearchTool.class); + /** + * Hard ceiling for model-supplied result limits. The tool is excluded from + * {@code ToolResultEvictionConfig} on the "bounded results" rationale, so nothing + * downstream trims an oversized result — the clamp has to happen here (#3270). + */ + private static final int MAX_RESULTS_CEILING = 100; + private final WorkspaceManager workspaceManager; public SessionSearchTool(WorkspaceManager workspaceManager) { @@ -77,7 +84,9 @@ public String sessionSearch( String agentId, @ToolParam( name = "maxResults", - description = "Maximum number of results to return (default: 10)", + description = + "Maximum number of results to return (default: 10," + + " capped at 100)", required = false) Integer maxResults, @ToolParam( @@ -93,7 +102,10 @@ public String sessionSearch( } RuntimeContext rc = runtimeContext != null ? runtimeContext : RuntimeContext.empty(); - int limit = maxResults != null && maxResults > 0 ? maxResults : 10; + int limit = + maxResults != null && maxResults > 0 + ? Math.min(maxResults, MAX_RESULTS_CEILING) + : 10; String effectiveAgentId = agentId != null && !agentId.isBlank() ? agentId : null; Predicate matcher; try { @@ -207,7 +219,9 @@ public String sessionHistory( String sessionId, @ToolParam( name = "lastN", - description = "Number of recent messages to return (default: 20)", + description = + "Number of recent messages to return (default: 20," + + " capped at 100)", required = false) Integer lastN) { if (agentId == null || agentId.isBlank() || sessionId == null || sessionId.isBlank()) { @@ -215,7 +229,7 @@ public String sessionHistory( } RuntimeContext rc = runtimeContext != null ? runtimeContext : RuntimeContext.empty(); - int limit = lastN != null && lastN > 0 ? lastN : 20; + int limit = lastN != null && lastN > 0 ? Math.min(lastN, MAX_RESULTS_CEILING) : 20; Path contextFile = workspaceManager.resolveSessionContextFile(rc, agentId, sessionId); if (!Files.isRegularFile(contextFile)) { diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java new file mode 100644 index 0000000000..b5ac21f27b --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent.tool; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +import io.agentscope.harness.agent.memory.MemoryFlushManager; +import io.agentscope.harness.agent.workspace.WorkspaceManager; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Regression tests for the model-supplied result-limit clamp (#3270): {@code session_search} + * and {@code session_history} are excluded from {@code ToolResultEvictionConfig} on the + * "bounded results" rationale, so an unbounded model-supplied limit bypasses every downstream + * trim. + */ +class SessionSearchToolCeilingTest { + + @TempDir Path workspace; + + private static Msg message(String id, MsgRole role, String text) { + return Msg.builder() + .id(id) + .role(role) + .content(TextBlock.builder().text(text).build()) + .build(); + } + + private void seedSession(int count) throws Exception { + try (WorkspaceManager workspaceManager = new WorkspaceManager(workspace)) { + MemoryFlushManager flushManager = new MemoryFlushManager(workspaceManager, null); + RuntimeContext rc = RuntimeContext.builder().sessionId("session-1").build(); + List batch = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + batch.add(message("m" + i, MsgRole.USER, "needle item " + i)); + } + flushManager.offloadMessages(rc, batch, "agent-a", "session-1"); + } + Path context = workspace.resolve("agents/agent-a/sessions/session-1.jsonl"); + assertTrue(Files.isRegularFile(context), "session context should be offloaded"); + } + + @Test + void sessionSearch_clampsModelSuppliedMaxResults() throws Exception { + seedSession(150); + SessionSearchTool tool = new SessionSearchTool(new WorkspaceManager(workspace)); + + String out = + tool.sessionSearch( + RuntimeContext.builder().sessionId("session-1").build(), + "needle", + "agent-a", + 100000); + + assertTrue(out.contains("Found 100 matches"), () -> "output was: " + out); + assertFalse(out.contains("Found 150"), () -> "output was: " + out); + } + + @Test + void sessionHistory_clampsModelSuppliedLastN() throws Exception { + seedSession(150); + SessionSearchTool tool = new SessionSearchTool(new WorkspaceManager(workspace)); + + String out = + tool.sessionHistory( + RuntimeContext.builder().sessionId("session-1").build(), + "agent-a", + "session-1", + 100000); + + // Exactly 100 message blocks, not 150: count the entry ids. + long emitted = out.split("m\\d+", -1).length - 1; + assertTrue(emitted <= 100, () -> "history must be clamped to 100, got " + emitted); + } + + @Test + void defaultLimitStillApplies() throws Exception { + seedSession(15); + SessionSearchTool tool = new SessionSearchTool(new WorkspaceManager(workspace)); + + String out = + tool.sessionSearch( + RuntimeContext.builder().sessionId("session-1").build(), + "needle", + "agent-a", + null); + + assertTrue(out.contains("Found 10 matches"), () -> "output was: " + out); + } +} From a193e3e368aba61d2dfcf2ef4eece30bdd9469fd Mon Sep 17 00:00:00 2001 From: shenxiaobo Date: Fri, 2 Oct 2026 07:07:56 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test+docs(harness):=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E8=B7=9F=E8=BF=9B=E2=80=94=E2=80=94sessionHistory=20=E6=96=AD?= =?UTF-8?q?=E8=A8=80=E6=94=B9=E4=B8=BA=E7=9C=9F=E5=AE=9E=E5=8F=AF=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=20+=20=E6=B3=A8=E9=87=8A=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vacuous 断言修复:split("m\d+") 计数永远为 0(输出不含 entry id), 钳制被删测试仍绿;改为断言头部的 showing last 100),并补 lastN=99/101 边界用例(同时覆盖 Codecov 的 2 个 partial 分支) - MAX_RESULTS_CEILING 注释修正:session_history 不在 DEFAULT_EXCLUDED_TOOLS(下游可驱逐),"nothing downstream trims" 仅对 session_search 成立;钳制仍是正确做法(源头限界优于事后驱逐, 且同时限制扫描量) --- .../tool/SessionSearchToolCeilingTest.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java index b5ac21f27b..f02c36d06a 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/SessionSearchToolCeilingTest.java @@ -91,9 +91,29 @@ void sessionHistory_clampsModelSuppliedLastN() throws Exception { "session-1", 100000); - // Exactly 100 message blocks, not 150: count the entry ids. - long emitted = out.split("m\\d+", -1).length - 1; - assertTrue(emitted <= 100, () -> "history must be clamped to 100, got " + emitted); + assertTrue(out.contains("showing last 100)"), () -> "history must be clamped: " + out); + } + + @Test + void sessionHistory_boundary_lastN99And101() throws Exception { + seedSession(150); + SessionSearchTool tool = new SessionSearchTool(new WorkspaceManager(workspace)); + + String out99 = + tool.sessionHistory( + RuntimeContext.builder().sessionId("session-1").build(), + "agent-a", + "session-1", + 99); + assertTrue(out99.contains("showing last 99)"), () -> "out99 was: " + out99); + + String out101 = + tool.sessionHistory( + RuntimeContext.builder().sessionId("session-1").build(), + "agent-a", + "session-1", + 101); + assertTrue(out101.contains("showing last 100)"), () -> "out101 was: " + out101); } @Test