package vip.mate.tool.builtin;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import vip.mate.agent.AgentToolSet;
import vip.mate.agent.binding.service.AgentBindingService;
import vip.mate.agent.context.ChatOrigin;
import vip.mate.tool.ToolRegistry;
import vip.mate.tool.guard.service.ToolGuardConfigService;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Stable, small-schema bridge for progressively disclosed tools.
*
*
The catalog is rebuilt from the current agent's effective tool set on
* every call, so search/describe cannot reveal tools outside its binding.
* {@code tool_call} is intentionally not executed here: the graph executor
* unwraps it before guard/approval/audit and invokes the real callback in the
* same action round. That keeps the bridge from becoming a security bypass.
*/
@Component
@RequiredArgsConstructor
public class ProgressiveToolBridgeTool {
public static final String SEARCH = "tool_search";
public static final String DESCRIBE = "tool_describe";
public static final String CALL = "tool_call";
public static final Set BRIDGE_NAMES = Set.of(SEARCH, DESCRIBE, CALL);
/** Executor-owned, immutable callback snapshot carried through ToolContext. */
public static final String SCOPED_TOOL_CALLBACKS_CONTEXT_KEY =
"mateclaw.progressiveToolCallbacks";
private static final int DEFAULT_LIMIT = 8;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final ToolRegistry toolRegistry;
private final AgentBindingService agentBindingService;
private final ToolGuardConfigService toolGuardConfigService;
@Tool(name = SEARCH, description = """
Search the current agent's tool catalog by capability. Returns compact
names and one-line descriptions, not full schemas. If you already know
the exact tool name from the Extension Tools catalog, skip this search
and call tool_call directly.
""")
public String search(
@ToolParam(description = "Capability or keywords to search for", required = false)
String query,
@ToolParam(description = "Maximum results (default 8, maximum 20)", required = false)
Integer limit,
@Nullable ToolContext ctx) {
int safeLimit = Math.max(1, Math.min(limit == null ? DEFAULT_LIMIT : limit, 20));
List terms = terms(query);
boolean browseCatalog = query == null || query.isBlank();
List candidates = effectiveToolSet(ctx).callbacks().stream()
.filter(cb -> !BRIDGE_NAMES.contains(cb.getToolDefinition().name()))
.toList();
List matches = (browseCatalog ? rank(candidates, List.of())
: terms.isEmpty() ? List.of() : rank(candidates, terms)).stream()
.filter(st -> browseCatalog || st.score() > 0.0d)
.sorted(Comparator.comparingDouble(ScoredTool::score).reversed()
.thenComparing(st -> st.callback().getToolDefinition().name()))
.limit(safeLimit)
.toList();
List