pi-context-engine 0.4.0 → 0.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 +17 -3
- package/package.json +1 -1
- package/src/engine.ts +54 -7
- package/src/extension.ts +16 -0
- package/src/retrieval/reranker.ts +15 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ runs fixed queries. Run it yourself; nothing is cherry-picked.
|
|
|
37
37
|
|
|
38
38
|
## What you get
|
|
39
39
|
|
|
40
|
-
Ten tools the agent can call, plus
|
|
40
|
+
Ten tools the agent can call, plus four slash commands.
|
|
41
41
|
|
|
42
42
|
**Search**
|
|
43
43
|
- **`context_search`**: hybrid vector + BM25 retrieval with confidence scores.
|
|
@@ -56,7 +56,8 @@ Ten tools the agent can call, plus three slash commands.
|
|
|
56
56
|
- **`index_status`**: chunk count and measured savings.
|
|
57
57
|
- **`reindex`**: refresh a path or the whole project.
|
|
58
58
|
|
|
59
|
-
Commands: `/context-index [--full]`, `/context-status`, `/context-savings
|
|
59
|
+
Commands: `/context-index [--full]`, `/context-status`, `/context-savings`,
|
|
60
|
+
`/context-warmup`.
|
|
60
61
|
|
|
61
62
|
**Shipped guidance.** The package also injects a ~900-token guidance section
|
|
62
63
|
into the system prompt while `context_search` is active: how to formulate
|
|
@@ -106,6 +107,19 @@ unchanged repo took 237 ms.
|
|
|
106
107
|
unchanged repo), so this removes the classic cold-start failure of searching
|
|
107
108
|
an empty index, without ever blocking the UI.
|
|
108
109
|
|
|
110
|
+
### Model warmup
|
|
111
|
+
|
|
112
|
+
- **`/context-warmup`** downloads the embedding model up front, no project or
|
|
113
|
+
index needed. With `retrieval.rerank: local` it also fetches the reranker
|
|
114
|
+
model; with Ollama as the backend it forces the model pull through the same
|
|
115
|
+
command.
|
|
116
|
+
- **Why it matters**: the embedder initializes lazily, only when a run has
|
|
117
|
+
chunks to embed. An empty repo or an already up-to-date index never touches
|
|
118
|
+
the network, so on a fresh machine or an ephemeral container the download
|
|
119
|
+
lands inside the first search. Warming turns it into an explicit
|
|
120
|
+
provisioning step (a devcontainer `postCreateCommand`, a CI cache setup),
|
|
121
|
+
keeping first searches fast.
|
|
122
|
+
|
|
109
123
|
## Requirements
|
|
110
124
|
|
|
111
125
|
- **Node 22+** on macOS, Linux or Windows.
|
|
@@ -253,7 +267,7 @@ bootstrapping for non-pi editors.
|
|
|
253
267
|
|
|
254
268
|
```bash
|
|
255
269
|
npm install
|
|
256
|
-
npm run check # typecheck +
|
|
270
|
+
npm run check # typecheck + 251 unit tests
|
|
257
271
|
npm run smoke:env # tree-sitter + sqlite-vec capability check
|
|
258
272
|
npm run smoke:loader # load the extension through pi's jiti loader
|
|
259
273
|
npm run smoke:real-world # index this repo with the local embedder
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Semantic code context for pi: tree-sitter index + hybrid vector/BM25 retrieval + cross-session memory, all local. Port of code-context-engine (MIT, elara-labs) as a native pi extension.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/engine.ts
CHANGED
|
@@ -125,8 +125,19 @@ export interface SessionStats {
|
|
|
125
125
|
export interface EngineOptions {
|
|
126
126
|
projectDir: string;
|
|
127
127
|
config?: Config;
|
|
128
|
-
/** Test seam: reuse an existing backend/embedder instead of auto-detecting. */
|
|
129
|
-
runtime?: { backend: LocalBackend; embedder: Embedder };
|
|
128
|
+
/** Test seam: reuse an existing backend/embedder/reranker instead of auto-detecting. */
|
|
129
|
+
runtime?: { backend: LocalBackend; embedder: Embedder; reranker?: LocalReranker };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Issue #11 (upstream warmup): what /context-warmup resolved + cached. */
|
|
133
|
+
export interface WarmupReport {
|
|
134
|
+
backend: string;
|
|
135
|
+
model: string;
|
|
136
|
+
dimension: number;
|
|
137
|
+
/** Present only when retrieval.rerank is "local". */
|
|
138
|
+
reranker?: string;
|
|
139
|
+
rerankerReady?: boolean;
|
|
140
|
+
rerankerError?: string;
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
/** R13: cheap keyword extraction for decision tags (no LLM): distinctive
|
|
@@ -150,10 +161,13 @@ export class ContextEngine {
|
|
|
150
161
|
readonly projectName: string;
|
|
151
162
|
readonly storageBase: string;
|
|
152
163
|
config: Config;
|
|
153
|
-
private readonly injectedRuntime:
|
|
164
|
+
private readonly injectedRuntime:
|
|
165
|
+
| { backend: LocalBackend; embedder: Embedder; reranker?: LocalReranker }
|
|
166
|
+
| undefined;
|
|
154
167
|
private backendInstance: LocalBackend | null = null;
|
|
155
168
|
private embedderInstance: Embedder | null = null;
|
|
156
169
|
private retrieverInstance: HybridRetriever | null = null;
|
|
170
|
+
private rerankerInstance: LocalReranker | null = null;
|
|
157
171
|
private compressorInstance: Compressor | null = null;
|
|
158
172
|
private memoryInstance: MemoryDb | null = null;
|
|
159
173
|
private memoryFailed = false;
|
|
@@ -210,15 +224,48 @@ export class ContextEngine {
|
|
|
210
224
|
this.retrieverInstance = new HybridRetriever(this.backend, await this.getEmbedder(), {
|
|
211
225
|
expandContext: this.config.retrievalExpandContext,
|
|
212
226
|
autoElaborate: this.config.retrievalAutoElaborate,
|
|
213
|
-
reranker:
|
|
214
|
-
this.config.retrievalRerank === "local"
|
|
215
|
-
? new LocalReranker(this.config.retrievalRerankModel)
|
|
216
|
-
: null,
|
|
227
|
+
reranker: this.getReranker(),
|
|
217
228
|
});
|
|
218
229
|
}
|
|
219
230
|
return this.retrieverInstance;
|
|
220
231
|
}
|
|
221
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The opt-in local reranker, or null when reranking is off. Memoized so
|
|
235
|
+
* /context-warmup loads the SAME instance the retriever later searches
|
|
236
|
+
* with — the model stays warm across the command boundary.
|
|
237
|
+
*/
|
|
238
|
+
private getReranker(): LocalReranker | null {
|
|
239
|
+
if (this.config.retrievalRerank !== "local") return null;
|
|
240
|
+
this.rerankerInstance ??= this.injectedRuntime?.reranker ?? new LocalReranker(this.config.retrievalRerankModel);
|
|
241
|
+
return this.rerankerInstance;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Warm the model caches (issue #11, upstream elara-labs#168): resolve +
|
|
246
|
+
* download + dimension-probe the embedding backend, and — when the opt-in
|
|
247
|
+
* local reranker is configured — its cross-encoder too. Unlike a reindex
|
|
248
|
+
* on an empty or fully up-to-date repo (which never initializes the
|
|
249
|
+
* embedder and therefore never touches the network), this guarantees the
|
|
250
|
+
* models are cached, so the first search or index doesn't pay the fetch
|
|
251
|
+
* inline. Ollama pulls ride the existing ensureModel() path.
|
|
252
|
+
*/
|
|
253
|
+
async warmup(): Promise<WarmupReport> {
|
|
254
|
+
const embedder = await this.getEmbedder();
|
|
255
|
+
const report: WarmupReport = {
|
|
256
|
+
backend: embedder.backendName,
|
|
257
|
+
model: embedder.modelName,
|
|
258
|
+
dimension: embedder.dimension,
|
|
259
|
+
};
|
|
260
|
+
const reranker = this.getReranker();
|
|
261
|
+
if (reranker) {
|
|
262
|
+
report.reranker = reranker.model;
|
|
263
|
+
report.rerankerReady = await reranker.warm();
|
|
264
|
+
if (!report.rerankerReady) report.rerankerError = reranker.loadError ?? "unknown error";
|
|
265
|
+
}
|
|
266
|
+
return report;
|
|
267
|
+
}
|
|
268
|
+
|
|
222
269
|
async getCompressor(): Promise<Compressor> {
|
|
223
270
|
if (!this.compressorInstance) {
|
|
224
271
|
this.compressorInstance = new Compressor(
|
package/src/extension.ts
CHANGED
|
@@ -461,6 +461,22 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
461
461
|
},
|
|
462
462
|
});
|
|
463
463
|
|
|
464
|
+
pi.registerCommand("context-warmup", {
|
|
465
|
+
description: "Pre-download the embedding (and opt-in reranker) model so the first search doesn't fetch inline",
|
|
466
|
+
handler: async (_args, ctx) => {
|
|
467
|
+
ctx.ui.notify("Warming up embedding model…", "info");
|
|
468
|
+
const report = await getEngine(ctx.cwd).warmup();
|
|
469
|
+
const parts = [`${report.backend} ${report.model} (${report.dimension} dims)`];
|
|
470
|
+
if (report.reranker) {
|
|
471
|
+
parts.push(report.rerankerReady ? `reranker ${report.reranker} ready` : `reranker ${report.reranker} FAILED: ${report.rerankerError}`);
|
|
472
|
+
}
|
|
473
|
+
ctx.ui.notify(
|
|
474
|
+
`✓ Warmup complete: ${parts.join(" · ")}`,
|
|
475
|
+
report.reranker && !report.rerankerReady ? "warning" : "info",
|
|
476
|
+
);
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
464
480
|
/* ── Lifecycle hooks (replace upstream's curl-based hooks) ───────────── */
|
|
465
481
|
|
|
466
482
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -56,6 +56,8 @@ export class LocalReranker implements RerankerLike {
|
|
|
56
56
|
private loading: Promise<LoadedReranker | null> | null = null;
|
|
57
57
|
/** Set after a load failure; every later call short-circuits to no-op. */
|
|
58
58
|
failed = false;
|
|
59
|
+
/** Why the load failed (null before any attempt). Surfaced by /context-warmup. */
|
|
60
|
+
loadError: string | null = null;
|
|
59
61
|
|
|
60
62
|
constructor(
|
|
61
63
|
readonly model: string,
|
|
@@ -88,6 +90,17 @@ export class LocalReranker implements RerankerLike {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Force the model load now (issue #11, upstream warmup): downloads and
|
|
95
|
+
* initializes the cross-encoder without needing a query. Returns false
|
|
96
|
+
* when the load failed — the reranker then stays a silent no-op, per its
|
|
97
|
+
* degrade-never-break design; `loadError` carries the reason.
|
|
98
|
+
*/
|
|
99
|
+
async warm(): Promise<boolean> {
|
|
100
|
+
if (this.failed) return false;
|
|
101
|
+
return (await this.ensureLoaded()) !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
private ensureLoaded(): Promise<LoadedReranker | null> {
|
|
92
105
|
if (this.loaded) return Promise.resolve(this.loaded);
|
|
93
106
|
if (!this.loading) {
|
|
@@ -102,8 +115,9 @@ export class LocalReranker implements RerankerLike {
|
|
|
102
115
|
});
|
|
103
116
|
this.loaded = { tokenizer, model };
|
|
104
117
|
return this.loaded;
|
|
105
|
-
})().catch(() => {
|
|
118
|
+
})().catch((err: unknown) => {
|
|
106
119
|
this.failed = true;
|
|
120
|
+
this.loadError = err instanceof Error ? err.message : String(err);
|
|
107
121
|
return null;
|
|
108
122
|
});
|
|
109
123
|
}
|