UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

79 lines • 3.75 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // 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. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { validateOptions, intOption, floatOption, textOption } from "../../vfs/search.js"; import { mountPrefixOf } from "../../utils/key_prefix.js"; import { rowsOf } from "../hierarchy/probe.js"; import { readdir } from "./readdir.js"; import { makeResolveGlob } from "../../utils/glob_walk.js"; import { detectScope } from "./scope.js"; import { enoent } from "../../errors/fs.js"; import { formatScore } from "../../utils/score.js"; import { rstripSlash, stripSlash } from "../../utils/slash.js"; import { searchMemories } from "./client.js"; function validate(query, topK, threshold) { if (query === '') throw new Error('query is required'); if (!Number.isInteger(topK) || topK <= 0) throw new Error('top-k must be positive'); if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) { throw new Error('threshold must be in [0, 1]'); } } /** * Run a semantic search in the scope and render ranked results. * * `memoryIds` is an optional result id allowlist. */ export async function searchMemoriesRendered(accessor, query, mountPrefix, topK, threshold, memoryIds) { validate(query, topK, threshold); const lines = []; for (const result of await searchMemories(accessor, query, topK, threshold)) { const id = String(result.id); if (memoryIds !== undefined && !memoryIds.has(id)) continue; const path = `${rstripSlash(mountPrefix)}/${id}.json`; const score = formatScore(result.score); const memory = typeof result.memory === 'string' ? result.memory : ''; lines.push([path, `${score === null ? path : `${path}:${score}`}\n${memory}`]); } return lines; } async function listedPaths(accessor, path) { const folder = rstripSlash(path.virtual); return rowsOf(await readdir(accessor, path)).map((row) => `${folder}/${row.name}`); } export async function searchMany(accessor, paths, query) { validateOptions(query, ['top_k', 'threshold', 'method']); const topK = intOption(query, 'top_k', accessor.config.defaultSearchLimit); const first = paths[0]; if (first === undefined) throw new Error('at least one scope is required'); const prefix = mountPrefixOf(first.virtual, first.vfsPath); const method = textOption(query, 'method', 'semantic'); const threshold = floatOption(query, 'threshold', 0); if (method !== 'semantic') throw new Error("only the 'semantic' method is supported"); validate(query.query, topK, threshold); const all = paths.some((p) => stripSlash(p.vfsPath) === ''); const targets = all ? [] : await makeResolveGlob((path) => listedPaths(accessor, path))(paths); const ids = all ? undefined : new Set(); for (const path of targets) { const match = detectScope(path); if (match.kind !== 'memory') throw enoent(path); ids?.add(match.slots.memory_id ?? ''); } return searchMemoriesRendered(accessor, query.query, prefix, topK, threshold, ids); } //# sourceMappingURL=search.js.map