@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
79 lines • 3.75 kB
JavaScript
// ========= 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