@takara-ai/miru-code 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/docs/self-hosted-sagemaker.md +116 -0
- package/package.json +7 -2
- package/src/benchmark/compare.ts +22 -5
- package/src/benchmark/locate-compare.ts +8 -6
- package/src/benchmark/summary.ts +10 -1
- package/src/benchmark/types.ts +11 -1
- package/src/cli.ts +18 -19
- package/src/credentials.ts +77 -7
- package/src/embeddings/openai.ts +194 -84
- package/src/embeddings/sagemaker.ts +268 -0
- package/src/embeddings/validate.ts +2 -1
- package/src/help.ts +38 -4
- package/src/index/bm25.ts +31 -21
- package/src/index/create.ts +114 -44
- package/src/index/incremental.ts +4 -11
- package/src/index/sparse.ts +14 -0
- package/src/installer/search-policy.ts +4 -1
- package/src/literal.ts +1 -1
- package/src/mcp/format-text.ts +67 -0
- package/src/mcp/index-cache.ts +5 -0
- package/src/mcp/server.ts +28 -28
- package/src/prompt.ts +13 -0
- package/src/setup.ts +194 -14
package/src/help.ts
CHANGED
|
@@ -67,6 +67,27 @@ export function printEnvHelp(): void {
|
|
|
67
67
|
writeStdout(" MIRU_TOKENIZER_JSON");
|
|
68
68
|
writeStdout(" Path to tokenizer.json (default: <package>/tokenizer/tokenizer.json)");
|
|
69
69
|
writeStdout("");
|
|
70
|
+
section("Self-hosted (AWS SageMaker)");
|
|
71
|
+
writeStdout(" MIRU_SAGEMAKER_ENDPOINT_ARN");
|
|
72
|
+
writeStdout(" arn:aws:sagemaker:<region>:<account-id>:endpoint/<name> — set this to");
|
|
73
|
+
writeStdout(" bypass Takara entirely and embed via your own SageMaker endpoint.");
|
|
74
|
+
writeStdout(" MIRU_SAGEMAKER_ENDPOINT_NAME / MIRU_SAGEMAKER_REGION");
|
|
75
|
+
writeStdout(" Alternative to the ARN when you'd rather name the endpoint + region");
|
|
76
|
+
writeStdout(" directly (falls back to AWS_REGION / AWS_DEFAULT_REGION).");
|
|
77
|
+
writeStdout(" MIRU_SAGEMAKER_NORMALIZE / MIRU_SAGEMAKER_TRUNCATE");
|
|
78
|
+
writeStdout(" Default: true");
|
|
79
|
+
writeStdout(" MIRU_SAGEMAKER_TRUNCATION_DIRECTION");
|
|
80
|
+
writeStdout(' "Left" | "Right" (default: Right)');
|
|
81
|
+
writeStdout(" MIRU_SAGEMAKER_PROMPT_NAME");
|
|
82
|
+
writeStdout(" Optional prompt_name passed to the endpoint");
|
|
83
|
+
writeStdout(" AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN / AWS_PROFILE");
|
|
84
|
+
writeStdout(" Standard AWS credential resolution — nothing Miru-specific to set");
|
|
85
|
+
writeStdout("");
|
|
86
|
+
hint("Enterprise self-hosted setup: docs/self-hosted-sagemaker.md");
|
|
87
|
+
hint("Admin runbook: bun run sagemaker:create-invoke-user -- --endpoint-arn <arn>");
|
|
88
|
+
hint("Then: miru setup --sagemaker --arn <arn> --profile <name>");
|
|
89
|
+
hint("Setup confirms auth, saves SageMaker config, and removes any stored Takara API key.");
|
|
90
|
+
writeStdout("");
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
export function printFullHelp(): void {
|
|
@@ -126,13 +147,25 @@ export function printCommandHelp(command: string): void {
|
|
|
126
147
|
writeStdout("");
|
|
127
148
|
return;
|
|
128
149
|
case "setup":
|
|
129
|
-
commandHeader("setup", "Store and validate
|
|
150
|
+
commandHeader("setup", "Store and validate Takara or self-hosted SageMaker credentials.");
|
|
130
151
|
section("Usage");
|
|
131
152
|
writeStdout(" miru setup [--key TOKEN] [--force] [--clear]");
|
|
132
|
-
|
|
153
|
+
writeStdout(" miru setup --sagemaker --arn ENDPOINT_ARN --profile NAME");
|
|
154
|
+
section("Options (Takara)");
|
|
133
155
|
writeStdout(" --key, -k TOKEN Non-interactive key entry");
|
|
134
156
|
writeStdout(" --force Replace an existing stored key");
|
|
135
157
|
writeStdout(" --clear Remove stored credentials");
|
|
158
|
+
section("Options (SageMaker)");
|
|
159
|
+
writeStdout(" --sagemaker Switch setup to self-hosted SageMaker mode");
|
|
160
|
+
writeStdout(" --arn ARN Endpoint ARN (implies --sagemaker)");
|
|
161
|
+
writeStdout(" --profile NAME AWS profile to inherit credentials from");
|
|
162
|
+
writeStdout("");
|
|
163
|
+
hint("Enterprise guide: docs/self-hosted-sagemaker.md (Marketplace + invoke-user runbook).");
|
|
164
|
+
hint("Takara and SageMaker are mutually exclusive — setup for one removes the other.");
|
|
165
|
+
hint("SageMaker setup invokes the endpoint once (auth + embedding check), then saves.");
|
|
166
|
+
hint("Miru only inherits AWS credentials — it never creates IAM users or writes ~/.aws.");
|
|
167
|
+
hint("Admin runbook (repo): bun run sagemaker:create-invoke-user -- --endpoint-arn <arn>");
|
|
168
|
+
hint("Then: miru setup --sagemaker --arn <arn> --profile miru");
|
|
136
169
|
writeStdout("");
|
|
137
170
|
return;
|
|
138
171
|
case "install":
|
|
@@ -165,8 +198,9 @@ export function printCommandHelp(command: string): void {
|
|
|
165
198
|
writeStdout("Override path with MIRU_BENCHMARK_HISTORY_PATH. Append-only JSONL of savings.");
|
|
166
199
|
writeStdout("Repo paths in the report are plaintext — use `clear` on shared machines.");
|
|
167
200
|
writeStdout("");
|
|
168
|
-
writeStdout("Savings compare Miru
|
|
169
|
-
writeStdout("(rg search + read of the top matched file),
|
|
201
|
+
writeStdout("Savings compare Miru MCP response tokens (search text + expand text)");
|
|
202
|
+
writeStdout("to a Grep baseline (rg search + read of the top matched file),");
|
|
203
|
+
writeStdout("not bare chunk content and not the agent's full tool chain.");
|
|
170
204
|
writeStdout("Local paths only — git URL repos skip comparison with benchmark_skipped.");
|
|
171
205
|
writeStdout("");
|
|
172
206
|
return;
|
package/src/index/bm25.ts
CHANGED
|
@@ -47,37 +47,46 @@ export class BM25Index {
|
|
|
47
47
|
private postings = new Map<string, PostingsList>();
|
|
48
48
|
private avgDocLength = 0;
|
|
49
49
|
private numDocs = 0;
|
|
50
|
+
private totalLen = 0;
|
|
51
|
+
|
|
52
|
+
/** Append one tokenized document. Safe to call incrementally while indexing. */
|
|
53
|
+
addDocument(doc: string[]): number {
|
|
54
|
+
const docIndex = this.numDocs;
|
|
55
|
+
this.numDocs += 1;
|
|
56
|
+
this.docLengths.push(doc.length);
|
|
57
|
+
this.totalLen += doc.length;
|
|
58
|
+
this.avgDocLength = this.totalLen / this.numDocs;
|
|
59
|
+
|
|
60
|
+
const tf = new Map<string, number>();
|
|
61
|
+
for (const term of doc) {
|
|
62
|
+
tf.set(term, (tf.get(term) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
for (const [term, count] of tf) {
|
|
65
|
+
this.docFreq.set(term, (this.docFreq.get(term) ?? 0) + 1);
|
|
66
|
+
let list = this.postings.get(term);
|
|
67
|
+
if (!list) {
|
|
68
|
+
list = [];
|
|
69
|
+
this.postings.set(term, list);
|
|
70
|
+
}
|
|
71
|
+
list.push([docIndex, count]);
|
|
72
|
+
}
|
|
73
|
+
return docIndex;
|
|
74
|
+
}
|
|
50
75
|
|
|
51
76
|
index(tokenizedDocs: string[][]): void {
|
|
52
|
-
this.numDocs =
|
|
77
|
+
this.numDocs = 0;
|
|
53
78
|
this.docFreq.clear();
|
|
54
79
|
this.docLengths = [];
|
|
55
80
|
this.postings.clear();
|
|
81
|
+
this.totalLen = 0;
|
|
82
|
+
this.avgDocLength = 0;
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
for (let docIndex = 0; docIndex < tokenizedDocs.length; docIndex++) {
|
|
59
|
-
const doc = tokenizedDocs[docIndex];
|
|
84
|
+
for (const doc of tokenizedDocs) {
|
|
60
85
|
if (!doc) {
|
|
61
86
|
continue;
|
|
62
87
|
}
|
|
63
|
-
this.
|
|
64
|
-
totalLen += doc.length;
|
|
65
|
-
|
|
66
|
-
const tf = new Map<string, number>();
|
|
67
|
-
for (const term of doc) {
|
|
68
|
-
tf.set(term, (tf.get(term) ?? 0) + 1);
|
|
69
|
-
}
|
|
70
|
-
for (const [term, count] of tf) {
|
|
71
|
-
this.docFreq.set(term, (this.docFreq.get(term) ?? 0) + 1);
|
|
72
|
-
let list = this.postings.get(term);
|
|
73
|
-
if (!list) {
|
|
74
|
-
list = [];
|
|
75
|
-
this.postings.set(term, list);
|
|
76
|
-
}
|
|
77
|
-
list.push([docIndex, count]);
|
|
78
|
-
}
|
|
88
|
+
this.addDocument(doc);
|
|
79
89
|
}
|
|
80
|
-
this.avgDocLength = this.numDocs > 0 ? totalLen / this.numDocs : 0;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
private queryTerms(queryTokens: string[]): { term: string; idf: number }[] {
|
|
@@ -222,6 +231,7 @@ export class BM25Index {
|
|
|
222
231
|
idx.docLengths = data.docLengths;
|
|
223
232
|
idx.avgDocLength = data.avgDocLength;
|
|
224
233
|
idx.numDocs = data.numDocs;
|
|
234
|
+
idx.totalLen = data.avgDocLength * data.numDocs;
|
|
225
235
|
|
|
226
236
|
for (const [term, list] of Object.entries(data.postings)) {
|
|
227
237
|
idx.postings.set(term, list);
|
package/src/index/create.ts
CHANGED
|
@@ -2,24 +2,33 @@ import { relative, resolve } from "node:path";
|
|
|
2
2
|
import { chunkSource } from "../chunking/chunking.ts";
|
|
3
3
|
import { ensureParserInit } from "../chunking/grammars.ts";
|
|
4
4
|
import { resolveWorkerConcurrency } from "../concurrency.ts";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
appendEmbeddingWindowJobs,
|
|
7
|
+
assignEmbeddingWindowVectors,
|
|
8
|
+
type EmbeddingBackend,
|
|
9
|
+
type EmbeddingWindowJob,
|
|
10
|
+
embedTextsWithBackend,
|
|
11
|
+
poolEmbeddingWindowBuckets,
|
|
12
|
+
resolveEmbeddingBatchSize,
|
|
13
|
+
resolveMaxEmbedChars,
|
|
14
|
+
} from "../embeddings/openai.ts";
|
|
6
15
|
import { envOptionalInt } from "../env.ts";
|
|
7
16
|
import { searchImprovementsEnabled } from "../ranking/features.ts";
|
|
8
|
-
import { tokenize } from "../tokens.ts";
|
|
9
17
|
import { type Chunk, type ContentType, defaultContentTypes } from "../types.ts";
|
|
10
18
|
import { BM25Index } from "./bm25.ts";
|
|
11
19
|
import { loadRootEntryChunks } from "./entry-chunks.ts";
|
|
12
20
|
import { walkFiles } from "./file-walker.ts";
|
|
13
21
|
import { detectLanguage, getExtensions, getFileStatus, readFileText } from "./files.ts";
|
|
14
22
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
15
|
-
import {
|
|
23
|
+
import { addChunkToBm25 } from "./sparse.ts";
|
|
16
24
|
import { buildSemanticIndex } from "./vector-storage.ts";
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
const DEFAULT_PIPELINE_EMBED_INFLIGHT =
|
|
26
|
+
/** Keep enough embed HTTP calls in flight to saturate the serverless endpoint. */
|
|
27
|
+
const DEFAULT_PIPELINE_EMBED_INFLIGHT = 16;
|
|
20
28
|
|
|
29
|
+
/** One knob: same as embedding API batch size unless explicitly overridden. */
|
|
21
30
|
function resolvePipelineEmbedBatch(): number {
|
|
22
|
-
return envOptionalInt(["MIRU_PIPELINE_EMBED_BATCH"], 1) ??
|
|
31
|
+
return envOptionalInt(["MIRU_PIPELINE_EMBED_BATCH"], 1) ?? resolveEmbeddingBatchSize();
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
function resolvePipelineEmbedInflight(): number {
|
|
@@ -40,6 +49,7 @@ export async function createIndexFromPath(
|
|
|
40
49
|
const started = performance.now();
|
|
41
50
|
let fileProcessMs = 0;
|
|
42
51
|
let embedBackpressureWaitMs = 0;
|
|
52
|
+
let bm25BuildMs = 0;
|
|
43
53
|
let fileEnumerated = 0;
|
|
44
54
|
let fileTasks = 0;
|
|
45
55
|
let emptyFileTasks = 0;
|
|
@@ -57,14 +67,16 @@ export async function createIndexFromPath(
|
|
|
57
67
|
const fileConcurrency = resolveWorkerConcurrency();
|
|
58
68
|
const embedBatchSize = resolvePipelineEmbedBatch();
|
|
59
69
|
const maxEmbedInflight = resolvePipelineEmbedInflight();
|
|
70
|
+
const maxEmbedChars = resolveMaxEmbedChars();
|
|
60
71
|
const maxIndexFiles = resolveMaxIndexFiles();
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const embedInFlight = new Set<Promise<
|
|
65
|
-
const embedPromises: Promise<
|
|
66
|
-
|
|
67
|
-
let
|
|
72
|
+
const emittedChunks: Chunk[] = [];
|
|
73
|
+
const windowVectorsByChunk: Float32Array[][] = [];
|
|
74
|
+
const pendingWindows: EmbeddingWindowJob[] = [];
|
|
75
|
+
const embedInFlight = new Set<Promise<void>>();
|
|
76
|
+
const embedPromises: Promise<void>[] = [];
|
|
77
|
+
const bm25 = new BM25Index();
|
|
78
|
+
let emittedApiBatches = 0;
|
|
79
|
+
let bm25Cursor = 0;
|
|
68
80
|
|
|
69
81
|
const processFile = async (filePath: string): Promise<Chunk[]> => {
|
|
70
82
|
const t = performance.now();
|
|
@@ -86,35 +98,64 @@ export async function createIndexFromPath(
|
|
|
86
98
|
}
|
|
87
99
|
};
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
/** Burn idle time tokenizing + building BM25 while embeds are in flight. */
|
|
102
|
+
const progressBm25 = (budgetMs = 8): void => {
|
|
103
|
+
const t0 = performance.now();
|
|
104
|
+
while (bm25Cursor < emittedChunks.length && performance.now() - t0 < budgetMs) {
|
|
105
|
+
const chunk = emittedChunks[bm25Cursor];
|
|
106
|
+
if (!chunk) {
|
|
107
|
+
break;
|
|
95
108
|
}
|
|
96
|
-
|
|
109
|
+
const tDoc = performance.now();
|
|
110
|
+
addChunkToBm25(bm25, chunk);
|
|
111
|
+
bm25BuildMs += performance.now() - tDoc;
|
|
112
|
+
bm25Cursor++;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const enqueueChunks = (chunks: Chunk[]): void => {
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
const chunkIndex = emittedChunks.length;
|
|
119
|
+
emittedChunks.push(chunk);
|
|
120
|
+
windowVectorsByChunk.push([]);
|
|
121
|
+
appendEmbeddingWindowJobs(pendingWindows, chunkIndex, chunk.content, maxEmbedChars);
|
|
97
122
|
}
|
|
98
123
|
};
|
|
99
124
|
|
|
100
125
|
const maybeScheduleEmbed = async (force = false): Promise<void> => {
|
|
101
|
-
while (
|
|
102
|
-
pendingEmbedChunks.length > 0 &&
|
|
103
|
-
(force || pendingEmbedChunks.length >= embedBatchSize)
|
|
104
|
-
) {
|
|
126
|
+
while (pendingWindows.length > 0 && (force || pendingWindows.length >= embedBatchSize)) {
|
|
105
127
|
if (embedInFlight.size >= maxEmbedInflight) {
|
|
106
128
|
const waitStart = performance.now();
|
|
107
|
-
|
|
129
|
+
// Use backpressure stalls to advance BM25 on the main thread.
|
|
130
|
+
while (embedInFlight.size >= maxEmbedInflight) {
|
|
131
|
+
progressBm25(10);
|
|
132
|
+
await Promise.race([Promise.race(embedInFlight), Bun.sleep(0)]);
|
|
133
|
+
}
|
|
108
134
|
embedBackpressureWaitMs += performance.now() - waitStart;
|
|
109
135
|
continue;
|
|
110
136
|
}
|
|
111
137
|
|
|
112
|
-
const take =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
138
|
+
const take = Math.min(embedBatchSize, pendingWindows.length);
|
|
139
|
+
if (!force && take < embedBatchSize) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const batch = pendingWindows.splice(0, take);
|
|
144
|
+
emittedApiBatches++;
|
|
145
|
+
const promise = (async () => {
|
|
146
|
+
const vectors = await embedTextsWithBackend(
|
|
147
|
+
embeddings,
|
|
148
|
+
batch.map((job) => job.text),
|
|
149
|
+
);
|
|
150
|
+
if (vectors.length !== batch.length) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Embedding API returned ${vectors.length} vectors for ${batch.length} inputs`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
assignEmbeddingWindowVectors(batch, vectors, windowVectorsByChunk);
|
|
156
|
+
// After each batch returns, spend a little time on BM25.
|
|
157
|
+
progressBm25(5);
|
|
158
|
+
})();
|
|
118
159
|
embedPromises.push(promise);
|
|
119
160
|
embedInFlight.add(promise);
|
|
120
161
|
promise.finally(() => {
|
|
@@ -125,11 +166,13 @@ export async function createIndexFromPath(
|
|
|
125
166
|
|
|
126
167
|
const runningFiles = new Set<Promise<void>>();
|
|
127
168
|
|
|
128
|
-
const startFileTask = (filePath: string
|
|
169
|
+
const startFileTask = (filePath: string): void => {
|
|
129
170
|
const task = (async () => {
|
|
130
171
|
const chunks = await processFile(filePath);
|
|
131
|
-
|
|
132
|
-
|
|
172
|
+
// Enqueue as soon as the file is ready (no sequential barrier) so the
|
|
173
|
+
// embed pipeline fills immediately under concurrency.
|
|
174
|
+
enqueueChunks(chunks);
|
|
175
|
+
progressBm25(3);
|
|
133
176
|
await maybeScheduleEmbed(false);
|
|
134
177
|
})().finally(() => {
|
|
135
178
|
runningFiles.delete(task);
|
|
@@ -143,50 +186,62 @@ export async function createIndexFromPath(
|
|
|
143
186
|
throw new Error(`Index file budget exceeded: max ${maxIndexFiles} files per operation`);
|
|
144
187
|
}
|
|
145
188
|
while (runningFiles.size >= fileConcurrency) {
|
|
189
|
+
progressBm25(5);
|
|
146
190
|
await Promise.race(runningFiles);
|
|
147
191
|
}
|
|
148
192
|
fileTasks++;
|
|
149
|
-
startFileTask(filePath
|
|
193
|
+
startFileTask(filePath);
|
|
150
194
|
}
|
|
151
195
|
|
|
152
196
|
while (runningFiles.size > 0) {
|
|
197
|
+
progressBm25(5);
|
|
153
198
|
await Promise.race(runningFiles);
|
|
154
199
|
}
|
|
155
200
|
|
|
156
201
|
if (searchImprovementsEnabled()) {
|
|
157
202
|
const entryChunks = await loadRootEntryChunks(resolved, displayRoot ? root : undefined);
|
|
158
203
|
if (entryChunks.length > 0) {
|
|
159
|
-
|
|
160
|
-
flushReadySequentialChunks();
|
|
204
|
+
enqueueChunks(entryChunks);
|
|
161
205
|
}
|
|
162
206
|
}
|
|
163
207
|
|
|
164
|
-
|
|
208
|
+
const embedsDrainedAt = performance.now();
|
|
165
209
|
await maybeScheduleEmbed(true);
|
|
166
210
|
await Promise.all(embedPromises);
|
|
211
|
+
// Finish any BM25 docs not yet processed during idle gaps.
|
|
212
|
+
while (bm25Cursor < emittedChunks.length) {
|
|
213
|
+
progressBm25(50);
|
|
214
|
+
}
|
|
215
|
+
const embedsDoneAt = performance.now();
|
|
167
216
|
|
|
168
|
-
const chunks =
|
|
217
|
+
const chunks = emittedChunks;
|
|
169
218
|
|
|
170
219
|
if (chunks.length === 0) {
|
|
171
220
|
throw new Error(`No supported files found under ${path}.`);
|
|
172
221
|
}
|
|
173
222
|
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
223
|
+
const poolStarted = performance.now();
|
|
224
|
+
const vectors = poolEmbeddingWindowBuckets(windowVectorsByChunk);
|
|
225
|
+
const poolMs = performance.now() - poolStarted;
|
|
226
|
+
|
|
227
|
+
const semanticStarted = performance.now();
|
|
177
228
|
const semantic = buildSemanticIndex(vectors);
|
|
229
|
+
const semanticMs = performance.now() - semanticStarted;
|
|
178
230
|
|
|
179
231
|
if (profile) {
|
|
180
232
|
const finished = performance.now();
|
|
233
|
+
const wall = finished - started;
|
|
181
234
|
const embedStats =
|
|
182
235
|
"getStats" in embeddings && typeof embeddings.getStats === "function"
|
|
183
236
|
? embeddings.getStats()
|
|
184
237
|
: null;
|
|
238
|
+
const pipelineWallMs = embedsDrainedAt - started;
|
|
239
|
+
const embedDrainMs = embedsDoneAt - embedsDrainedAt;
|
|
185
240
|
console.error(
|
|
186
241
|
JSON.stringify({
|
|
187
242
|
profile: "index_build",
|
|
188
243
|
path: resolved,
|
|
189
|
-
elapsed_ms:
|
|
244
|
+
elapsed_ms: wall,
|
|
190
245
|
file_enumerated: fileEnumerated,
|
|
191
246
|
file_tasks: fileTasks,
|
|
192
247
|
empty_or_skipped_files: emptyFileTasks,
|
|
@@ -197,11 +252,26 @@ export async function createIndexFromPath(
|
|
|
197
252
|
file_concurrency: fileConcurrency,
|
|
198
253
|
embed_batch_size: embedBatchSize,
|
|
199
254
|
embed_inflight: maxEmbedInflight,
|
|
200
|
-
emitted_batches:
|
|
255
|
+
emitted_batches: emittedApiBatches,
|
|
256
|
+
max_embed_chars: maxEmbedChars,
|
|
201
257
|
},
|
|
202
258
|
stage_ms: {
|
|
203
259
|
file_process_total: fileProcessMs,
|
|
204
260
|
embed_backpressure_wait: embedBackpressureWaitMs,
|
|
261
|
+
bm25_build_cpu: bm25BuildMs,
|
|
262
|
+
wall_pipeline: pipelineWallMs,
|
|
263
|
+
wall_embed_drain: embedDrainMs,
|
|
264
|
+
wall_pool_windows: poolMs,
|
|
265
|
+
wall_bm25: 0,
|
|
266
|
+
wall_semantic: semanticMs,
|
|
267
|
+
wall_post: poolMs + semanticMs,
|
|
268
|
+
},
|
|
269
|
+
wall_pct: {
|
|
270
|
+
pipeline_overlap: +((100 * pipelineWallMs) / wall).toFixed(1),
|
|
271
|
+
embed_drain: +((100 * embedDrainMs) / wall).toFixed(1),
|
|
272
|
+
pool_windows: +((100 * poolMs) / wall).toFixed(1),
|
|
273
|
+
bm25: 0,
|
|
274
|
+
semantic: +((100 * semanticMs) / wall).toFixed(1),
|
|
205
275
|
},
|
|
206
276
|
embedding_transport: embedStats,
|
|
207
277
|
}),
|
package/src/index/incremental.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
2
|
import { chunkSource } from "../chunking/chunking.ts";
|
|
3
3
|
import type { EmbeddingBackend } from "../embeddings/openai.ts";
|
|
4
|
-
import { tokenize } from "../tokens.ts";
|
|
5
4
|
import type { Chunk, ContentType } from "../types.ts";
|
|
6
|
-
import { BM25Index } from "./bm25.ts";
|
|
5
|
+
import type { BM25Index } from "./bm25.ts";
|
|
7
6
|
import { detectLanguage, getExtensions, getFileStatus, readFileText } from "./files.ts";
|
|
8
7
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
9
|
-
import {
|
|
8
|
+
import { buildBm25FromChunks } from "./sparse.ts";
|
|
10
9
|
import { buildSemanticIndex } from "./vector-storage.ts";
|
|
11
10
|
import { vectorAt } from "./vectors.ts";
|
|
12
11
|
|
|
@@ -64,7 +63,7 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
64
63
|
if (targets.size === 0) {
|
|
65
64
|
return {
|
|
66
65
|
chunks: options.chunks,
|
|
67
|
-
bm25:
|
|
66
|
+
bm25: buildBm25FromChunks(options.chunks),
|
|
68
67
|
semantic: options.semanticIndex,
|
|
69
68
|
};
|
|
70
69
|
}
|
|
@@ -108,17 +107,11 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
108
107
|
|
|
109
108
|
return {
|
|
110
109
|
chunks,
|
|
111
|
-
bm25:
|
|
110
|
+
bm25: buildBm25FromChunks(chunks),
|
|
112
111
|
semantic: buildSemanticIndex(vectors),
|
|
113
112
|
};
|
|
114
113
|
}
|
|
115
114
|
|
|
116
|
-
function rebuildBm25(chunks: Chunk[]): BM25Index {
|
|
117
|
-
const bm25 = new BM25Index();
|
|
118
|
-
bm25.index(chunks.map((c) => tokenize(enrichForBm25(c))));
|
|
119
|
-
return bm25;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
115
|
/** Map an absolute changed path to a repo-relative path for chunk keys. */
|
|
123
116
|
export function relativePathFromRoot(root: string, absoluteOrRelative: string): string {
|
|
124
117
|
const resolvedRoot = resolve(root);
|
package/src/index/sparse.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { tokenize } from "../tokens.ts";
|
|
1
2
|
import type { Chunk } from "../types.ts";
|
|
3
|
+
import { BM25Index } from "./bm25.ts";
|
|
2
4
|
|
|
3
5
|
export function selectorToMask(
|
|
4
6
|
selector: readonly number[] | null | undefined,
|
|
@@ -23,3 +25,15 @@ export function enrichForBm25(chunk: Chunk): string {
|
|
|
23
25
|
const dirText = dirParts.slice(-3).join(" ");
|
|
24
26
|
return `${chunk.content} ${stem} ${stem} ${dirText}`;
|
|
25
27
|
}
|
|
28
|
+
|
|
29
|
+
export function addChunkToBm25(bm25: BM25Index, chunk: Chunk): void {
|
|
30
|
+
bm25.addDocument(tokenize(enrichForBm25(chunk)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildBm25FromChunks(chunks: readonly Chunk[]): BM25Index {
|
|
34
|
+
const bm25 = new BM25Index();
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
addChunkToBm25(bm25, chunk);
|
|
37
|
+
}
|
|
38
|
+
return bm25;
|
|
39
|
+
}
|
|
@@ -127,7 +127,10 @@ export const MCP_SERVER_INSTRUCTIONS =
|
|
|
127
127
|
|
|
128
128
|
export const MCP_BENCHMARK_SERVER_INSTRUCTIONS =
|
|
129
129
|
`${MCP_SERVER_INSTRUCTIONS} ` +
|
|
130
|
-
"BENCHMARK MODE: each `search` and `locate`
|
|
130
|
+
"BENCHMARK MODE: each `search` and `locate` ends with a compact JSON line " +
|
|
131
|
+
'`{"benchmark":{save_pct,miru_tok,grep_tok,saved_tok,rank1[,search_tok,miru_only]}}`. ' +
|
|
132
|
+
"Results stay plain text. For search, `miru_tok` is search MCP text + one expand (workflow vs grep+read); " +
|
|
133
|
+
"`search_tok` is this search body alone. " +
|
|
131
134
|
"Call `read_benchmark` for cumulative totals across both. Do not narrate benchmark stats unless the user asks. " +
|
|
132
135
|
"To leave benchmark mode, tell the user to run `miru benchmark off` and restart the agent.";
|
|
133
136
|
|
package/src/literal.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExpandResults } from "../utils.ts";
|
|
2
|
+
|
|
3
|
+
/** Plain-text rendering of `formatResults()` output — same info as the JSON shape, no scaffolding. */
|
|
4
|
+
export function formatResultsText(payload: {
|
|
5
|
+
query: string;
|
|
6
|
+
results: Record<string, unknown>[];
|
|
7
|
+
}): string {
|
|
8
|
+
if (payload.results.length === 0) {
|
|
9
|
+
return "No results found.";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const blocks = payload.results.map((result) => {
|
|
13
|
+
const chunk = result.chunk as Record<string, unknown>;
|
|
14
|
+
const startLine = chunk.truncated ? chunk.full_start_line : chunk.start_line;
|
|
15
|
+
const endLine = chunk.truncated ? chunk.full_end_line : chunk.end_line;
|
|
16
|
+
const truncatedNote = chunk.truncated ? " [truncated: call expand for full chunk]" : "";
|
|
17
|
+
const header = `${chunk.file_path}:${startLine}-${endLine}${truncatedNote}`;
|
|
18
|
+
return `${header}\n${chunk.content}`;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return blocks.join("\n\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Plain-text rendering of `formatLiteralLocate()` output. */
|
|
25
|
+
export function formatLiteralLocateText(payload: Record<string, unknown>): string {
|
|
26
|
+
const label =
|
|
27
|
+
(payload.literals as string[] | undefined)?.join(" | ") ?? (payload.literal as string);
|
|
28
|
+
const n = payload.n as number;
|
|
29
|
+
const files = payload.files as number;
|
|
30
|
+
const truncatedNote = payload.truncated ? " (truncated — pass a narrower include/exclude)" : "";
|
|
31
|
+
const header = `"${label}": ${n} match${n === 1 ? "" : "es"} across ${files} file${files === 1 ? "" : "s"}${truncatedNote}`;
|
|
32
|
+
|
|
33
|
+
const hits = payload.hits as
|
|
34
|
+
| Array<{ f: string; l: number; t?: string; ctx?: string[]; ctx_l?: number }>
|
|
35
|
+
| undefined;
|
|
36
|
+
if (!hits) {
|
|
37
|
+
return header;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const lines = [header, ""];
|
|
41
|
+
for (const hit of hits) {
|
|
42
|
+
lines.push(hit.t !== undefined ? `${hit.f}:${hit.l}: ${hit.t}` : `${hit.f}:${hit.l}`);
|
|
43
|
+
if (hit.ctx != null && hit.ctx_l !== undefined) {
|
|
44
|
+
const ctxStart = hit.ctx_l;
|
|
45
|
+
for (let i = 0; i < hit.ctx.length; i++) {
|
|
46
|
+
lines.push(` ${ctxStart + i}: ${hit.ctx[i]}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Plain-text rendering of `formatExpandResults()` output. */
|
|
54
|
+
export function formatExpandResultsText(payload: ExpandResults): string {
|
|
55
|
+
const anchorRange = payload.anchor
|
|
56
|
+
? `${payload.anchor.start_line}-${payload.anchor.end_line}`
|
|
57
|
+
: null;
|
|
58
|
+
const header = `${payload.file_path} — ${payload.chunk_count} chunk${payload.chunk_count === 1 ? "" : "s"} around line ${payload.line}`;
|
|
59
|
+
|
|
60
|
+
const blocks = payload.chunks.map((chunk) => {
|
|
61
|
+
const range = `${chunk.start_line}-${chunk.end_line}`;
|
|
62
|
+
const isAnchor = anchorRange === range;
|
|
63
|
+
return `${range}${isAnchor ? " (anchor)" : ""}\n${chunk.content}`;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return `${header}\n\n${blocks.join("\n\n")}`;
|
|
67
|
+
}
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -65,6 +65,11 @@ export function shouldIgnoreWatchPath(relativePath: string | null | undefined):
|
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Session cache of MiruIndex instances. Keeps them fresh by reconciling
|
|
70
|
+
* mtimes on load (`checkAndQueueStaleFiles`) and applying incremental
|
|
71
|
+
* updates from fs.watch (`flushFileUpdates` / `applyFileChanges`).
|
|
72
|
+
*/
|
|
68
73
|
export class IndexCache {
|
|
69
74
|
private readonly content: ContentType[];
|
|
70
75
|
private readonly defaultRef: string | null;
|