Project Files
src / tools / recall.ts
/**
* recall — metadata-only document search.
* Never reads file content; returns title, filename, tags, and score.
*/
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
// @ts-ignore — zod/lib re-export chain breaks with NodeNext; runtime is fine
import { z } from "zod";
import path from "node:path";
import type { IndexManager } from "../indexManager.js";
import { EmbeddingClient } from "../embeddings/embeddingClient.js";
import {
checkEmbeddingCapability,
type EmbeddingPrimerResult,
} from "../helpers/embeddingCapabilityPrimer.js";
import { formatToolMetaBlock } from "../helpers/pluginMeta.js";
// ── module-level cache ────────────────────────────────────────────────────────
let embeddingClient: EmbeddingClient | null = null;
let cachedCapabilityResult: EmbeddingPrimerResult | null = null;
let lastCapabilityCheckMs = 0;
const CAPABILITY_CHECK_INTERVAL_MS = 30_000;
export function createRecallTool(
_ctl: ToolsProviderController,
index: IndexManager
): Tool {
return tool({
name: "recall",
description: `Search the playbook — the agent's persistent Markdown knowledge base.
Returns document metadata only (title, filename, tags, score), never file contents.
Use this tool:
- At the start of complex or multi-step tasks
- Before advising on a topic you may have notes about
- When the user references something familiar but you're not certain about
- To check whether a note already exists before writing a new one
Returns:
- Ranked list of matching documents with title, filename, tags, and relevance score (0–100)
- Documents below the configured minRecallScore are excluded
Examples:
- "project planning strategies" — finds notes about planning approaches
- "API authentication" — finds any notes on auth topics
- "Abendroutine" — also finds "evening routine" when semantic search is enabled
Follow up with read to retrieve the full text of any matching document.
${formatToolMetaBlock()}`,
parameters: {
query: z.string().describe("Natural-language search query."),
tags: z
.array(z.string())
.optional()
.describe("Optional list of tags to filter by. Only documents that have ALL listed tags will be returned."),
},
implementation: async (args, ctx) => {
const config = index.getConfig();
const now = Date.now();
// Check (or refresh) embedding capability every 30 s
const needsCheck =
!cachedCapabilityResult ||
now - lastCapabilityCheckMs > CAPABILITY_CHECK_INTERVAL_MS ||
embeddingClient?.getModelName() !== config.embeddingModel;
if (needsCheck) {
ctx.status("Checking embedding model availability...");
cachedCapabilityResult = await checkEmbeddingCapability({
modelId: config.embeddingModel,
baseUrl: config.lmStudioBaseUrl,
});
lastCapabilityCheckMs = Date.now();
if (cachedCapabilityResult.ready) {
embeddingClient = new EmbeddingClient({
baseUrl: config.lmStudioBaseUrl,
model: cachedCapabilityResult.modelId,
});
index.setEmbeddingClient(embeddingClient);
} else {
embeddingClient = null;
index.setEmbeddingClient(null);
}
}
const docCount = index.getDocumentCount();
ctx.status(`Searching ${docCount} documents...`);
const results = await index.search(args.query, args.tags);
const lines: string[] = [];
if (results.length === 0) {
lines.push("No documents found matching your query.");
} else {
lines.push(
...results.map((r) => {
const filename = path.basename(r.filePath);
const tagStr = r.tags.length > 0 ? ` [${r.tags.join(", ")}]` : "";
return `- **${r.title}** (${filename})${tagStr} — score: ${r.score}`;
})
);
}
// Append capability warning when semantic search is degraded
if (cachedCapabilityResult && !cachedCapabilityResult.ready && cachedCapabilityResult.userMessage) {
lines.push("", cachedCapabilityResult.userMessage);
} else if (cachedCapabilityResult?.userMessage && cachedCapabilityResult.messageSeverity === "info") {
lines.push("", `_${cachedCapabilityResult.userMessage}_`);
}
return lines.join("\n");
},
});
}