mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(agent): refresh context previews without MCP hot-path scan
This commit is contained in:
parent
04209c3f7f
commit
50c6eb92f9
@ -236,15 +236,13 @@ public class AgentService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cached agent instance whenever one of its workspace files
|
||||
* changes. The system prompt (which embeds MEMORY.md / PROFILE.md / structured
|
||||
* memory) is baked into the cached instance at build time, so memory edits made
|
||||
* via tools, consolidation, or cleanup would otherwise stay invisible until an
|
||||
* agent config change or restart. Rebuilding on the next turn picks them up.
|
||||
* Invalidate the cached agent instance only for shared workspace files that
|
||||
* are baked into the system prompt. Owner-scoped PERSONAL memory rows are
|
||||
* injected per turn, so updating them must not force a cold agent rebuild.
|
||||
*/
|
||||
@org.springframework.context.event.EventListener
|
||||
public void onWorkspaceFileChanged(vip.mate.workspace.document.event.WorkspaceFileChangedEvent event) {
|
||||
if (event.agentId() != null) {
|
||||
if (event.agentId() != null && event.affectsSystemPrompt()) {
|
||||
agentInstances.remove(event.agentId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@ -155,6 +156,66 @@ public class ToolRegistry {
|
||||
return AgentToolSet.fromCallbacks(toolBeans, callbacks, nameByBean::get);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve aliases for currently enabled built-in {@code @Tool} beans without
|
||||
* touching {@link ToolCallbackProvider}s. This is intentionally narrower than
|
||||
* {@link #getEnabledToolSet()}: disclosure-tier snapshots only need to bridge
|
||||
* {@code mate_tool.name}/{@code bean_name} onto built-in function names, and
|
||||
* calling providers here would synchronously enumerate MCP tools on the chat
|
||||
* hot path.
|
||||
*/
|
||||
public Set<String> enabledToolBeanFunctionNamesFor(Set<String> aliases) {
|
||||
if (aliases == null || aliases.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
Map<String, Set<String>> index = enabledToolBeanFunctionNameIndex();
|
||||
LinkedHashSet<String> out = new LinkedHashSet<>();
|
||||
for (String alias : aliases) {
|
||||
Set<String> hits = index.get(alias);
|
||||
if (hits != null) {
|
||||
out.addAll(hits);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {@code alias -> @Tool function names} for enabled built-in tool beans.
|
||||
* The aliases mirror {@link AgentToolSet}: function name, Spring bean name,
|
||||
* and Java simple class name. Provider/MCP callbacks are deliberately absent.
|
||||
*/
|
||||
public Map<String, Set<String>> enabledToolBeanFunctionNameIndex() {
|
||||
LinkedHashMap<String, Object> beansByName = getEnabledToolBeansByName();
|
||||
Map<String, Set<String>> index = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : beansByName.entrySet()) {
|
||||
String beanName = entry.getKey();
|
||||
Object bean = entry.getValue();
|
||||
ToolCallback[] callbacks = ToolCallbacks.from(bean);
|
||||
LinkedHashSet<String> functionNames = new LinkedHashSet<>();
|
||||
for (ToolCallback cb : callbacks) {
|
||||
if (cb != null && cb.getToolDefinition() != null) {
|
||||
functionNames.add(cb.getToolDefinition().name());
|
||||
}
|
||||
}
|
||||
if (functionNames.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
putAlias(index, beanName, functionNames);
|
||||
putAlias(index, bean.getClass().getSimpleName(), functionNames);
|
||||
for (String functionName : functionNames) {
|
||||
putAlias(index, functionName, Set.of(functionName));
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static void putAlias(Map<String, Set<String>> index, String alias, Set<String> functionNames) {
|
||||
if (alias == null || alias.isBlank() || functionNames == null || functionNames.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
index.computeIfAbsent(alias, ignored -> new LinkedHashSet<>()).addAll(functionNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统一的 AgentToolSet(包含 @Tool Bean + ToolCallbackProvider)
|
||||
* <p>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
package vip.mate.tool.disclosure;
|
||||
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
@ -9,10 +12,9 @@ import vip.mate.agent.AgentToolSet;
|
||||
import vip.mate.agent.context.TokenEstimator;
|
||||
import vip.mate.tool.ToolRegistry;
|
||||
import vip.mate.tool.mcp.model.McpServerEntity;
|
||||
import vip.mate.tool.mcp.runtime.McpHashCollisionDetector;
|
||||
import vip.mate.tool.mcp.service.McpServerService;
|
||||
import vip.mate.tool.model.AvailableToolDTO;
|
||||
import vip.mate.tool.model.ToolEntity;
|
||||
import vip.mate.tool.service.AvailableToolService;
|
||||
import vip.mate.tool.service.ToolService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -54,7 +56,6 @@ public class DefaultToolDisclosureService implements ToolDisclosureService {
|
||||
|
||||
private final ToolService toolService;
|
||||
private final McpServerService mcpServerService;
|
||||
private final AvailableToolService availableToolService;
|
||||
private final ToolRegistry toolRegistry;
|
||||
private final ToolUsageRecencyTracker usageRecencyTracker;
|
||||
|
||||
@ -277,15 +278,15 @@ public class DefaultToolDisclosureService implements ToolDisclosureService {
|
||||
private Snapshot buildSnapshot() {
|
||||
// resolveTier() queries by the runtime function name (cb.getToolDefinition().name()),
|
||||
// but mate_tool stores the Java class name (e.g. "ImageGenerateTool") and bean name
|
||||
// (e.g. "imageGenerateTool"). Bridge both onto the function name(s) via the global
|
||||
// tool set's alias index so a persisted tier actually reaches the runtime split.
|
||||
// (e.g. "imageGenerateTool"). Bridge both onto the function name(s) via a built-in
|
||||
// alias index so a persisted tier actually reaches the runtime split. Do not call
|
||||
// ToolRegistry.getEnabledToolSet() here: it synchronously enumerates MCP providers.
|
||||
Map<String, DisclosureTier> builtinTierByName = new LinkedHashMap<>();
|
||||
AgentToolSet globalSet = null;
|
||||
Map<String, Set<String>> builtinFunctionIndex = Map.of();
|
||||
try {
|
||||
globalSet = toolRegistry.getEnabledToolSet();
|
||||
builtinFunctionIndex = toolRegistry.enabledToolBeanFunctionNameIndex();
|
||||
} catch (Exception e) {
|
||||
log.warn("ToolDisclosureService: global tool set unavailable, tier name bridge disabled: {}",
|
||||
e.getMessage());
|
||||
log.warn("ToolDisclosureService: built-in tier name bridge unavailable: {}", e.getMessage());
|
||||
}
|
||||
try {
|
||||
for (ToolEntity t : toolService.listTools()) {
|
||||
@ -296,13 +297,17 @@ public class DefaultToolDisclosureService implements ToolDisclosureService {
|
||||
// Key by the raw stored name too — harmless, and covers rows that already
|
||||
// store a function name.
|
||||
builtinTierByName.put(t.getName(), tier);
|
||||
if (globalSet != null) {
|
||||
Set<String> aliases = new LinkedHashSet<>();
|
||||
aliases.add(t.getName());
|
||||
if (t.getBeanName() != null && !t.getBeanName().isBlank()) {
|
||||
aliases.add(t.getBeanName());
|
||||
Set<String> aliases = new LinkedHashSet<>();
|
||||
aliases.add(t.getName());
|
||||
if (t.getBeanName() != null && !t.getBeanName().isBlank()) {
|
||||
aliases.add(t.getBeanName());
|
||||
}
|
||||
for (String alias : aliases) {
|
||||
Set<String> functionNames = builtinFunctionIndex.get(alias);
|
||||
if (functionNames == null) {
|
||||
continue;
|
||||
}
|
||||
for (String functionName : globalSet.functionNamesFor(aliases)) {
|
||||
for (String functionName : functionNames) {
|
||||
builtinTierByName.put(functionName, tier);
|
||||
}
|
||||
}
|
||||
@ -314,9 +319,13 @@ public class DefaultToolDisclosureService implements ToolDisclosureService {
|
||||
|
||||
Map<String, Long> mcpToolToServerId = new LinkedHashMap<>();
|
||||
try {
|
||||
for (AvailableToolDTO d : availableToolService.listAvailable()) {
|
||||
if ("mcp".equals(d.getSource()) && d.getName() != null && d.getProviderId() != null) {
|
||||
mcpToolToServerId.put(d.getName(), d.getProviderId());
|
||||
for (McpServerEntity server : mcpServerService.listEnabled()) {
|
||||
if (server == null || server.getId() == null || server.getToolsCacheJson() == null
|
||||
|| server.getToolsCacheJson().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
for (String toolName : cachedMcpToolNames(server)) {
|
||||
mcpToolToServerId.put(toolName, server.getId());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -346,6 +355,33 @@ public class DefaultToolDisclosureService implements ToolDisclosureService {
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private List<String> cachedMcpToolNames(McpServerEntity server) {
|
||||
try {
|
||||
JSONArray arr = JSONUtil.parseArray(server.getToolsCacheJson());
|
||||
List<String> rawNames = new ArrayList<>(arr.size());
|
||||
for (Object o : arr) {
|
||||
if (!(o instanceof JSONObject jo)) {
|
||||
continue;
|
||||
}
|
||||
String name = jo.getStr("name");
|
||||
if (name != null && !name.isBlank()) {
|
||||
rawNames.add(name);
|
||||
}
|
||||
}
|
||||
if (rawNames.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return McpHashCollisionDetector.classify(server.getId(), rawNames).stream()
|
||||
.filter(McpHashCollisionDetector.Decision::bindable)
|
||||
.map(McpHashCollisionDetector.Decision::prefixedName)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
log.debug("ToolDisclosureService: failed to parse MCP tools cache for server {}: {}",
|
||||
server.getId(), e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private record Snapshot(Map<String, DisclosureTier> builtinTierByName,
|
||||
Map<String, Long> mcpToolToServerId,
|
||||
Map<Long, DisclosureTier> serverTierById,
|
||||
|
||||
@ -92,7 +92,7 @@ public class WorkspaceFileService {
|
||||
existing.setContent(content);
|
||||
existing.setFileSize(size);
|
||||
fileMapper.updateById(existing);
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename, true));
|
||||
return existing;
|
||||
}
|
||||
WorkspaceFileEntity entity = new WorkspaceFileEntity();
|
||||
@ -110,7 +110,7 @@ public class WorkspaceFileService {
|
||||
entity.setOwnerKey(SHARED_OWNER_KEY);
|
||||
WorkspaceFileEntity saved = insertOrUpdateOnConflict(
|
||||
entity, () -> getFile(agentId, filename), content, size);
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename, true));
|
||||
return saved;
|
||||
}
|
||||
|
||||
@ -255,7 +255,7 @@ public class WorkspaceFileService {
|
||||
existing.setContent(content);
|
||||
existing.setFileSize(size);
|
||||
fileMapper.updateById(existing);
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename, false));
|
||||
return existing;
|
||||
}
|
||||
WorkspaceFileEntity entity = new WorkspaceFileEntity();
|
||||
@ -269,7 +269,7 @@ public class WorkspaceFileService {
|
||||
entity.setScope(MemoryScope.PERSONAL);
|
||||
WorkspaceFileEntity saved = insertOrUpdateOnConflict(
|
||||
entity, () -> getMemoryFile(agentId, filename, ownerKey), content, size);
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename, false));
|
||||
return saved;
|
||||
}
|
||||
|
||||
@ -288,7 +288,7 @@ public class WorkspaceFileService {
|
||||
.eq(WorkspaceFileEntity::getAgentId, agentId)
|
||||
.eq(WorkspaceFileEntity::getFilename, filename)
|
||||
.in(WorkspaceFileEntity::getScope, MemoryScope.TEAM, MemoryScope.GLOBAL));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename, true));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,7 +306,7 @@ public class WorkspaceFileService {
|
||||
.eq(WorkspaceFileEntity::getFilename, filename)
|
||||
.eq(WorkspaceFileEntity::getScope, MemoryScope.PERSONAL)
|
||||
.eq(WorkspaceFileEntity::getOwnerKey, ownerKey));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename));
|
||||
eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,14 +3,17 @@ package vip.mate.workspace.document.event;
|
||||
/**
|
||||
* Published whenever an agent's workspace file is created, updated, or deleted.
|
||||
* <p>
|
||||
* Workspace files (AGENTS.md, SOUL.md, PROFILE.md, MEMORY.md, structured/*.md)
|
||||
* are baked into the agent's system prompt when its runtime instance is built.
|
||||
* Listeners use this to invalidate the cached agent instance so memory edits
|
||||
* (tool writes, consolidation, cleanup) take effect on the next turn instead of
|
||||
* only after an agent config change or restart.
|
||||
* Shared workspace files (AGENTS.md, SOUL.md, PROFILE.md, MEMORY.md, ...)
|
||||
* are baked into the cached agent system prompt. Owner-scoped PERSONAL memory
|
||||
* rows are injected per turn instead, so they should not evict the agent cache.
|
||||
*
|
||||
* @param agentId the affected agent
|
||||
* @param filename the workspace file that changed
|
||||
* @param agentId the affected agent
|
||||
* @param filename the workspace file that changed
|
||||
* @param affectsSystemPrompt whether cached agent instances must be rebuilt
|
||||
*/
|
||||
public record WorkspaceFileChangedEvent(Long agentId, String filename) {
|
||||
public record WorkspaceFileChangedEvent(Long agentId, String filename, boolean affectsSystemPrompt) {
|
||||
|
||||
public WorkspaceFileChangedEvent(Long agentId, String filename) {
|
||||
this(agentId, filename, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,10 +10,9 @@ import vip.mate.agent.AgentToolSet;
|
||||
import vip.mate.agent.context.TokenEstimator;
|
||||
import vip.mate.tool.ToolRegistry;
|
||||
import vip.mate.tool.mcp.model.McpServerEntity;
|
||||
import vip.mate.tool.mcp.runtime.McpToolNameResolver;
|
||||
import vip.mate.tool.mcp.service.McpServerService;
|
||||
import vip.mate.tool.model.AvailableToolDTO;
|
||||
import vip.mate.tool.model.ToolEntity;
|
||||
import vip.mate.tool.service.AvailableToolService;
|
||||
import vip.mate.tool.service.ToolService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -47,15 +46,14 @@ class ToolDisclosureServiceTest {
|
||||
public String image_generate() { return ""; }
|
||||
}
|
||||
|
||||
/** Global tool set the bridge resolves DB class/bean names against. */
|
||||
private static AgentToolSet globalSet() {
|
||||
Object t1 = new Tools();
|
||||
Object t2 = new ImageGenerateTool();
|
||||
List<org.springframework.ai.tool.ToolCallback> cbs = new ArrayList<>();
|
||||
cbs.addAll(List.of(ToolCallbacks.from(t1)));
|
||||
cbs.addAll(List.of(ToolCallbacks.from(t2)));
|
||||
Map<Object, String> beanNames = Map.of(t1, "tools", t2, "imageGenerateTool");
|
||||
return AgentToolSet.fromCallbacks(List.of(t1, t2), cbs, beanNames::get);
|
||||
private static Map<String, Set<String>> globalFunctionIndex() {
|
||||
return Map.of(
|
||||
"Tools", Set.of("image_generate", "my_core_tool"),
|
||||
"tools", Set.of("image_generate", "my_core_tool"),
|
||||
"ImageGenerateTool", Set.of("image_generate"),
|
||||
"imageGenerateTool", Set.of("image_generate"),
|
||||
"image_generate", Set.of("image_generate"),
|
||||
"my_core_tool", Set.of("my_core_tool"));
|
||||
}
|
||||
|
||||
private static ToolEntity toolRow(String name, String type, String tier) {
|
||||
@ -71,31 +69,26 @@ class ToolDisclosureServiceTest {
|
||||
s.setId(id);
|
||||
s.setName(name);
|
||||
s.setDisclosureTier(tier);
|
||||
s.setToolsCacheJson("[{\"name\":\"create_issue\",\"description\":\"create issue\"}]");
|
||||
return s;
|
||||
}
|
||||
|
||||
private static AvailableToolDTO mcpDto(String name, Long serverId) {
|
||||
return AvailableToolDTO.builder().source("mcp").providerId(serverId).name(name).build();
|
||||
}
|
||||
|
||||
private DefaultToolDisclosureService service(List<ToolEntity> tools,
|
||||
List<McpServerEntity> servers,
|
||||
List<AvailableToolDTO> available) {
|
||||
List<McpServerEntity> servers) {
|
||||
ToolService ts = mock(ToolService.class);
|
||||
McpServerService ms = mock(McpServerService.class);
|
||||
AvailableToolService as = mock(AvailableToolService.class);
|
||||
ToolRegistry tr = mock(ToolRegistry.class);
|
||||
lenient().when(ts.listTools()).thenReturn(tools);
|
||||
lenient().when(ms.listEnabled()).thenReturn(servers);
|
||||
lenient().when(ms.listAll()).thenReturn(servers);
|
||||
lenient().when(as.listAvailable()).thenReturn(available);
|
||||
lenient().when(tr.getEnabledToolSet()).thenReturn(globalSet());
|
||||
return new DefaultToolDisclosureService(ts, ms, as, tr, new ToolUsageRecencyTracker());
|
||||
lenient().when(tr.enabledToolBeanFunctionNameIndex()).thenReturn(globalFunctionIndex());
|
||||
return new DefaultToolDisclosureService(ts, ms, tr, new ToolUsageRecencyTracker());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skill and progressive bridge meta-tools are always core")
|
||||
void metaToolsAlwaysCore() {
|
||||
var svc = service(List.of(toolRow("enable_tool", "builtin", "extension")), List.of(), List.of());
|
||||
var svc = service(List.of(toolRow("enable_tool", "builtin", "extension")), List.of());
|
||||
assertEquals(DisclosureTier.CORE, svc.resolveTierByName("enable_tool"));
|
||||
assertEquals(DisclosureTier.CORE, svc.resolveTierByName("load_skill"));
|
||||
assertEquals(DisclosureTier.CORE, svc.resolveTierByName("tool_search"));
|
||||
@ -106,7 +99,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("generative tools default to extension even without a DB row")
|
||||
void generativeDefaultsExtension() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
assertEquals(DisclosureTier.EXTENSION, svc.resolveTierByName("image_generate"));
|
||||
assertEquals(DisclosureTier.EXTENSION, svc.resolveTierByName("browser_use"));
|
||||
}
|
||||
@ -114,14 +107,14 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("unknown tools default to core (conservative)")
|
||||
void unknownDefaultsCore() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
assertEquals(DisclosureTier.CORE, svc.resolveTierByName("memory_recall"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("mate_tool.disclosure_tier overrides the code default")
|
||||
void dbRowOverrides() {
|
||||
var svc = service(List.of(toolRow("my_core_tool", "builtin", "extension")), List.of(), List.of());
|
||||
var svc = service(List.of(toolRow("my_core_tool", "builtin", "extension")), List.of());
|
||||
assertEquals(DisclosureTier.EXTENSION, svc.resolveTierByName("my_core_tool"));
|
||||
}
|
||||
|
||||
@ -129,25 +122,24 @@ class ToolDisclosureServiceTest {
|
||||
@DisplayName("DB tier stored by Java class name bridges to the runtime function name")
|
||||
void dbTierBridgesClassNameToFunctionName() {
|
||||
// mate_tool.name = class name; resolveTier is queried by function name.
|
||||
var hidden = service(List.of(toolRow("ImageGenerateTool", "builtin", "extension")), List.of(), List.of());
|
||||
var hidden = service(List.of(toolRow("ImageGenerateTool", "builtin", "extension")), List.of());
|
||||
assertEquals(DisclosureTier.EXTENSION, hidden.resolveTierByName("image_generate"));
|
||||
|
||||
// Admin un-hides it by setting the row to core; the DB value must win over
|
||||
// the code-level extension default.
|
||||
var unhidden = service(List.of(toolRow("ImageGenerateTool", "builtin", "core")), List.of(), List.of());
|
||||
var unhidden = service(List.of(toolRow("ImageGenerateTool", "builtin", "core")), List.of());
|
||||
assertEquals(DisclosureTier.CORE, unhidden.resolveTierByName("image_generate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MCP tool tier follows its owning server")
|
||||
void mcpFollowsServer() {
|
||||
var extSvc = service(List.of(), List.of(server(7L, "github", "extension")),
|
||||
List.of(mcpDto("mcp_github_create_issue", 7L)));
|
||||
assertEquals(DisclosureTier.EXTENSION, extSvc.resolveTierByName("mcp_github_create_issue"));
|
||||
String toolName = McpToolNameResolver.prefixedName(7L, "create_issue");
|
||||
var extSvc = service(List.of(), List.of(server(7L, "github", "extension")));
|
||||
assertEquals(DisclosureTier.EXTENSION, extSvc.resolveTierByName(toolName));
|
||||
|
||||
var coreSvc = service(List.of(), List.of(server(7L, "github", "core")),
|
||||
List.of(mcpDto("mcp_github_create_issue", 7L)));
|
||||
assertEquals(DisclosureTier.CORE, coreSvc.resolveTierByName("mcp_github_create_issue"));
|
||||
var coreSvc = service(List.of(), List.of(server(7L, "github", "core")));
|
||||
assertEquals(DisclosureTier.CORE, coreSvc.resolveTierByName(toolName));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -155,16 +147,16 @@ class ToolDisclosureServiceTest {
|
||||
void mcpDefaultsExtensionWhenServerTierUnset() {
|
||||
// Move 5: MCP tools default to EXTENSION so they don't flood the
|
||||
// CORE tool list. Pre-Move-4 this returned CORE.
|
||||
var svc = service(List.of(), List.of(server(7L, "github", null)),
|
||||
List.of(mcpDto("mcp_github_create_issue", 7L)));
|
||||
assertEquals(DisclosureTier.EXTENSION, svc.resolveTierByName("mcp_github_create_issue"),
|
||||
String toolName = McpToolNameResolver.prefixedName(7L, "create_issue");
|
||||
var svc = service(List.of(), List.of(server(7L, "github", null)));
|
||||
assertEquals(DisclosureTier.EXTENSION, svc.resolveTierByName(toolName),
|
||||
"Move 5: MCP tools with no explicit tier must default to EXTENSION");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("split partitions into active (core + enabled) and the full extension catalog")
|
||||
void splitPartitions() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
AgentToolSet set = AgentToolSet.fromCallbacks(List.of(new Tools()),
|
||||
List.of(ToolCallbacks.from(new Tools())));
|
||||
|
||||
@ -181,7 +173,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("legacy mode advertises everything and renders no catalog")
|
||||
void legacyMode() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
ReflectionTestUtils.setField(svc, "disclosureMode", "legacy");
|
||||
AgentToolSet set = AgentToolSet.fromCallbacks(List.of(new Tools()),
|
||||
List.of(ToolCallbacks.from(new Tools())));
|
||||
@ -196,7 +188,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("renderExtensionCatalog lists extension tools under a heading")
|
||||
void rendersCatalog() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
AgentToolSet set = AgentToolSet.fromCallbacks(List.of(new Tools()),
|
||||
List.of(ToolCallbacks.from(new Tools())));
|
||||
String catalog = svc.renderExtensionCatalog(set, 8192);
|
||||
@ -232,7 +224,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("no demotion when the core schemas fit the budget, or when budget is absent")
|
||||
void noDemotionWhenBudgetFits() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
AgentToolSet set = manyCoreSet();
|
||||
assertTrue(svc.computeAutoDemotions(set, Integer.MAX_VALUE).isEmpty());
|
||||
assertTrue(svc.computeAutoDemotions(set, null).isEmpty());
|
||||
@ -242,7 +234,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("tiny budget demotes every demotable tool, alphabetical when nothing was ever used")
|
||||
void tinyBudgetDemotesAll() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
var demoted = svc.computeAutoDemotions(manyCoreSet(), 1);
|
||||
assertEquals(Set.of("tool_a", "tool_b", "tool_c"), demoted);
|
||||
}
|
||||
@ -250,7 +242,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("budget one tool short demotes exactly the first never-used candidate")
|
||||
void partialDemotionTakesFirstCandidate() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
AgentToolSet set = manyCoreSet();
|
||||
int coreTokens = TokenEstimator.estimateToolsTokens(svc.split(set, Set.of()).activeCallbacks());
|
||||
var demoted = svc.computeAutoDemotions(set, coreTokens - 1);
|
||||
@ -264,13 +256,12 @@ class ToolDisclosureServiceTest {
|
||||
tracker.recordUse("tool_a");
|
||||
ToolService ts = mock(ToolService.class);
|
||||
McpServerService ms = mock(McpServerService.class);
|
||||
AvailableToolService as = mock(AvailableToolService.class);
|
||||
ToolRegistry tr = mock(ToolRegistry.class);
|
||||
lenient().when(ts.listTools()).thenReturn(List.of());
|
||||
lenient().when(ms.listEnabled()).thenReturn(List.of());
|
||||
lenient().when(ms.listAll()).thenReturn(List.of());
|
||||
lenient().when(as.listAvailable()).thenReturn(List.of());
|
||||
lenient().when(tr.getEnabledToolSet()).thenReturn(globalSet());
|
||||
var svc = new DefaultToolDisclosureService(ts, ms, as, tr, tracker);
|
||||
lenient().when(tr.enabledToolBeanFunctionNameIndex()).thenReturn(globalFunctionIndex());
|
||||
var svc = new DefaultToolDisclosureService(ts, ms, tr, tracker);
|
||||
|
||||
AgentToolSet set = manyCoreSet();
|
||||
int coreTokens = TokenEstimator.estimateToolsTokens(svc.split(set, Set.of()).activeCallbacks());
|
||||
@ -283,7 +274,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("hard schema ceiling may demote explicit core rows")
|
||||
void explicitCoreStillFitsHardCeiling() {
|
||||
var svc = service(List.of(toolRow("tool_a", "builtin", "core")), List.of(), List.of());
|
||||
var svc = service(List.of(toolRow("tool_a", "builtin", "core")), List.of());
|
||||
var demoted = svc.computeAutoDemotions(manyCoreSet(), 1);
|
||||
assertEquals(Set.of("tool_a", "tool_b", "tool_c"), demoted);
|
||||
}
|
||||
@ -291,7 +282,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("auto-demoted tools behave as extension in split and can be enabled back")
|
||||
void splitHonorsAutoDemotions() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
AgentToolSet set = manyCoreSet();
|
||||
|
||||
var split = svc.split(set, Set.of(), Set.of("tool_b"));
|
||||
@ -305,7 +296,7 @@ class ToolDisclosureServiceTest {
|
||||
@Test
|
||||
@DisplayName("catalog rendering lists auto-demoted tools for discoverability")
|
||||
void catalogListsAutoDemoted() {
|
||||
var svc = service(List.of(), List.of(), List.of());
|
||||
var svc = service(List.of(), List.of());
|
||||
String catalog = svc.renderExtensionCatalog(manyCoreSet(), 8192, Set.of("tool_b"));
|
||||
assertTrue(catalog.contains("tool_b"));
|
||||
assertTrue(catalog.contains("tool_call"));
|
||||
|
||||
@ -68,6 +68,22 @@ class WorkspaceMemorySearchTest {
|
||||
org.mockito.Mockito.verify(eventPublisher).publishEvent(captor.capture());
|
||||
assertThat(captor.getValue().agentId()).isEqualTo(1000000001L);
|
||||
assertThat(captor.getValue().filename()).isEqualTo("MEMORY.md");
|
||||
assertThat(captor.getValue().affectsSystemPrompt()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("saveMemoryFile publishes a non-system-prompt change event")
|
||||
void saveMemoryFilePublishesNonInvalidatingChangeEvent() {
|
||||
when(fileMapper.selectOne(any(), org.mockito.ArgumentMatchers.anyBoolean())).thenReturn(null);
|
||||
|
||||
service.saveMemoryFile(1000000001L, "memory/2026-08-14.md", "## 今日\n- 临时笔记", "web:admin");
|
||||
|
||||
ArgumentCaptor<vip.mate.workspace.document.event.WorkspaceFileChangedEvent> captor =
|
||||
ArgumentCaptor.forClass(vip.mate.workspace.document.event.WorkspaceFileChangedEvent.class);
|
||||
org.mockito.Mockito.verify(eventPublisher).publishEvent(captor.capture());
|
||||
assertThat(captor.getValue().agentId()).isEqualTo(1000000001L);
|
||||
assertThat(captor.getValue().filename()).isEqualTo("memory/2026-08-14.md");
|
||||
assertThat(captor.getValue().affectsSystemPrompt()).isFalse();
|
||||
}
|
||||
|
||||
// ---------- tokenize ----------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user