pi-doc-injector 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/index.ts +46 -25
- package/injector.ts +14 -8
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
* Automatically injects relevant project documentation into the LLM context
|
|
5
5
|
* by monitoring streaming output for keyword matches.
|
|
6
6
|
*
|
|
7
|
+
* ## Injection Model: CustomMessage (NOT system prompt)
|
|
8
|
+
*
|
|
9
|
+
* On match, the extension returns a `message` field from `before_agent_start`
|
|
10
|
+
* (a `CustomMessage` with `customType: "doc-injector"`). Pi appends this to the
|
|
11
|
+
* session and sends it to the LLM as part of the conversation — the system
|
|
12
|
+
* prompt is NEVER mutated.
|
|
13
|
+
*
|
|
14
|
+
* Why a message and not the system prompt:
|
|
15
|
+
* - The system prompt is the highest-value Anthropic prompt-cache slot. Each
|
|
16
|
+
* unique system prompt text breaks the cache (5-min TTL by default).
|
|
17
|
+
* Appending per-turn doc content there would invalidate the cache on every
|
|
18
|
+
* first injection.
|
|
19
|
+
* - A `message` only adds to the conversation prefix, leaving the system
|
|
20
|
+
* prompt cache warm across turns.
|
|
21
|
+
*
|
|
7
22
|
* ## Streaming Model
|
|
8
23
|
*
|
|
9
24
|
* This extension relies on Pi's streaming event contract:
|
|
@@ -13,7 +28,7 @@
|
|
|
13
28
|
* - `message_end`: Fires once when the assistant's response is complete.
|
|
14
29
|
* The extension finalizes matches and notifies the user.
|
|
15
30
|
* - `before_agent_start`: Fires before the next agent turn. The extension
|
|
16
|
-
*
|
|
31
|
+
* returns a `message` carrying the matched docs and marks them as injected.
|
|
17
32
|
*
|
|
18
33
|
* ## Injection Lifecycle
|
|
19
34
|
*
|
|
@@ -22,28 +37,23 @@
|
|
|
22
37
|
* session, once a doc is injected, it won't be re-injected unless the user
|
|
23
38
|
* manually runs `/doc-inject reset`.
|
|
24
39
|
*
|
|
25
|
-
* ##
|
|
40
|
+
* ## Double-Injection Prevention
|
|
41
|
+
*
|
|
42
|
+
* Two independent guards make duplicate injection impossible in a session:
|
|
26
43
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
44
|
+
* 1. **Matcher-level guard**: `buildMatcher()` calls `getNonInjectedEntries()`,
|
|
45
|
+
* so already-injected docs are excluded from the candidate set. The
|
|
46
|
+
* `pendingMatches` map is only populated from the matcher's output, so
|
|
47
|
+
* once a doc is injected, the next `input` event cannot re-match it.
|
|
30
48
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* *fresh* base prompt to every extension handler.
|
|
37
|
-
* 4. Extension handlers can return a modified `systemPrompt` for the current
|
|
38
|
-
* turn. Pi uses the modified prompt **only for this turn**.
|
|
39
|
-
* 5. When no extension modifies the prompt, pi explicitly resets to
|
|
40
|
-
* `this._baseSystemPrompt` (comment in source: "Ensure we're using the
|
|
41
|
-
* base prompt (in case previous turn had modifications)").
|
|
49
|
+
* 2. **Mark guard**: `markInjected()` is called inside `before_agent_start`
|
|
50
|
+
* AFTER the build step but BEFORE the return value is processed. This
|
|
51
|
+
* means the flag flips synchronously with the LLM call — even if the
|
|
52
|
+
* session is reloaded mid-turn, the next `buildMatcher()` won't see the
|
|
53
|
+
* doc as a candidate.
|
|
42
54
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* The `injected` flag alone is sufficient to prevent re-injection — no
|
|
46
|
-
* marker-based stripping or deduplication is needed.
|
|
55
|
+
* The two guards are redundant by design: if matcher exclusion ever fails
|
|
56
|
+
* (e.g. a race), the mark step still prevents the doc from being sent twice.
|
|
47
57
|
*
|
|
48
58
|
* ## Race Condition Note
|
|
49
59
|
*
|
|
@@ -58,7 +68,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
58
68
|
import { resolve } from "node:path";
|
|
59
69
|
import { loadCache, saveCache } from "./cache";
|
|
60
70
|
import { loadConfig } from "./config";
|
|
61
|
-
import {
|
|
71
|
+
import { buildInjectionContent, notifyInjection } from "./injector";
|
|
62
72
|
import { buildKeywordGenPrompt } from "./keyword-llm";
|
|
63
73
|
import { extractText, KeywordMatcher } from "./matcher";
|
|
64
74
|
import { DocRegistry } from "./registry";
|
|
@@ -308,7 +318,11 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
308
318
|
textBuffer = "";
|
|
309
319
|
});
|
|
310
320
|
|
|
311
|
-
// ---- Event: before_agent_start (inject
|
|
321
|
+
// ---- Event: before_agent_start (inject as CustomMessage) ----
|
|
322
|
+
// Returns a `message` (CustomMessage with customType: "doc-injector") rather
|
|
323
|
+
// than mutating `systemPrompt`. The system prompt stays byte-identical across
|
|
324
|
+
// turns, preserving the prompt cache. The CustomMessage is appended to the
|
|
325
|
+
// session and sent to the LLM as part of the conversation.
|
|
312
326
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
313
327
|
// P5.4b — Guard: skip injection during LLM keyword generation
|
|
314
328
|
if (keywordGenInFlight) return;
|
|
@@ -335,9 +349,12 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
335
349
|
return;
|
|
336
350
|
}
|
|
337
351
|
|
|
338
|
-
const
|
|
352
|
+
const content = buildInjectionContent(matchedEntries, pendingMatches);
|
|
339
353
|
|
|
340
|
-
// Mark as injected only after confirming injection will happen
|
|
354
|
+
// Mark as injected only after confirming injection will happen.
|
|
355
|
+
// This is the second half of the double-injection guard: even if the
|
|
356
|
+
// matcher ever produced a duplicate match, markInjected prevents a
|
|
357
|
+
// second send.
|
|
341
358
|
registry.markInjected(matchedEntries.map((e) => e.filePath));
|
|
342
359
|
|
|
343
360
|
// Notify user about injection (moved here from message_end so it fires
|
|
@@ -348,7 +365,11 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
348
365
|
pendingMatches.clear();
|
|
349
366
|
|
|
350
367
|
return {
|
|
351
|
-
|
|
368
|
+
message: {
|
|
369
|
+
customType: "doc-injector",
|
|
370
|
+
content,
|
|
371
|
+
display: true,
|
|
372
|
+
},
|
|
352
373
|
};
|
|
353
374
|
});
|
|
354
375
|
|
package/injector.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context Injector — formats matched docs into
|
|
3
|
-
*
|
|
2
|
+
* Context Injector — formats matched docs into a content string suitable for
|
|
3
|
+
* injection as a `CustomMessage` (returned from `before_agent_start`) and
|
|
4
|
+
* sends TUI notifications.
|
|
5
|
+
*
|
|
6
|
+
* The produced content is delivered to the LLM as a `CustomMessage` rather
|
|
7
|
+
* than appended to the system prompt. This keeps the system prompt
|
|
8
|
+
* byte-identical across turns so the provider's prompt cache stays warm.
|
|
4
9
|
*/
|
|
5
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
10
|
import type { DocEntry } from "./types";
|
|
7
11
|
|
|
8
12
|
/**
|
|
@@ -14,7 +18,7 @@ export interface NotifyCapability {
|
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
|
-
* Sanitize keywords for safe
|
|
21
|
+
* Sanitize keywords for safe display in the injection content.
|
|
18
22
|
*
|
|
19
23
|
* - Strips \n and \r (replaces with space) to prevent prompt injection
|
|
20
24
|
* - Caps each keyword at 100 characters
|
|
@@ -29,11 +33,13 @@ function sanitizeKeywords(keywords: string[]): string[] {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
|
-
* Build
|
|
36
|
+
* Build the content string for a `CustomMessage` injection from matched
|
|
37
|
+
* documents. This is the payload that gets returned in
|
|
38
|
+
* `before_agent_start`'s `message.content` and sent to the LLM.
|
|
33
39
|
*/
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
export function buildInjectionContent(
|
|
41
|
+
entries: DocEntry[],
|
|
42
|
+
matchedKeywords: Map<string, string[]>,
|
|
37
43
|
): string {
|
|
38
44
|
if (entries.length === 0) return "";
|
|
39
45
|
|