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