feat: avoid duplicate project-map hint injection by checking context
The before_agent_start handler now inspects the current session context and skips injection if a pi-project-map-hint custom message is already present in the active branch. This prevents duplicate hints on every prompt while still re-injecting after compaction or tree navigation. Also removes the unused hooks/on-prompt.ts prompt-text injector.
This commit is contained in:
+2
-3
@@ -291,8 +291,6 @@ pi-project-map/
|
|||||||
│ ├── merge.ts # Merge AST + LLM outputs
|
│ ├── merge.ts # Merge AST + LLM outputs
|
||||||
│ ├── format.ts # Dense markdown formatter
|
│ ├── format.ts # Dense markdown formatter
|
||||||
│ └── config.ts # Skill configuration (thresholds, ignore patterns)
|
│ └── config.ts # Skill configuration (thresholds, ignore patterns)
|
||||||
├── hooks/
|
|
||||||
│ └── on-prompt.ts # Injects maintenance command into prompts
|
|
||||||
└── README.md # Setup and usage for humans
|
└── README.md # Setup and usage for humans
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -303,8 +301,9 @@ pi-project-map/
|
|||||||
- `project-map:reinit [path]` — Force re-initialization of entire project or subtree.
|
- `project-map:reinit [path]` — Force re-initialization of entire project or subtree.
|
||||||
|
|
||||||
### Prompt Hook
|
### Prompt Hook
|
||||||
- On every prompt, the skill appends a lightweight instruction:
|
- On each prompt, the skill injects a lightweight custom message **only if it is not already present in the current branch of context**:
|
||||||
> "If you modify any source file, run `project-map:patch <path>` to update the analysis. If you suspect staleness, run `project-map:validate`."
|
> "If you modify any source file, run `project-map:patch <path>` to update the analysis. If you suspect staleness, run `project-map:validate`."
|
||||||
|
- The extension checks `ctx.sessionManager.buildSessionContext()` for an existing `pi-project-map-hint` custom message and skips injection when one is found. This prevents duplicate hints after steering, follow-up messages, or multi-turn conversations. The hint is automatically re-injected after compaction or `/tree` navigation removes it from the active path.
|
||||||
|
|
||||||
## 9. Risks and Tradeoffs
|
## 9. Risks and Tradeoffs
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// Pi skill prompt hook
|
|
||||||
// Injected into every prompt to remind the agent to maintain .pi-map.md files
|
|
||||||
|
|
||||||
export const MAINTENANCE_INSTRUCTION = `
|
|
||||||
If you modify any source file, run \`project-map:patch <file-path>\` to update the analysis.
|
|
||||||
If you suspect staleness, run \`project-map:validate\`.
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function injectPrompt(originalPrompt: string): string {
|
|
||||||
return `${originalPrompt}\n\n---\n${MAINTENANCE_INSTRUCTION}`;
|
|
||||||
}
|
|
||||||
+24
-2
@@ -50,6 +50,25 @@ function isDirty(content: string): boolean {
|
|||||||
return content.includes("## dirty") && !content.includes("## dirty\n-");
|
return content.includes("## dirty") && !content.includes("## dirty\n-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HINT_CUSTOM_TYPE = "pi-project-map-hint";
|
||||||
|
|
||||||
|
function hintAlreadyInContext(ctx: any): boolean {
|
||||||
|
const manager = ctx?.sessionManager;
|
||||||
|
if (!manager || typeof manager.buildSessionContext !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messages } = manager.buildSessionContext();
|
||||||
|
if (!Array.isArray(messages)) return false;
|
||||||
|
|
||||||
|
return messages.some(
|
||||||
|
(m: any) =>
|
||||||
|
m &&
|
||||||
|
m.role === "custom" &&
|
||||||
|
m.customType === HINT_CUSTOM_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
pi.registerTool({
|
pi.registerTool({
|
||||||
name: "project_map_init",
|
name: "project_map_init",
|
||||||
@@ -236,14 +255,17 @@ export default function (pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inject maintenance instructions before agent starts
|
// Inject maintenance instructions before agent starts, but only once
|
||||||
|
// within the current branch of context. Re-inject after compaction or
|
||||||
|
// tree navigation removes the hint from the active path.
|
||||||
pi.on("before_agent_start", async (_event, _ctx) => {
|
pi.on("before_agent_start", async (_event, _ctx) => {
|
||||||
const mapFiles = findPiMapFiles(_ctx.cwd);
|
const mapFiles = findPiMapFiles(_ctx.cwd);
|
||||||
if (mapFiles.length === 0) return {};
|
if (mapFiles.length === 0) return {};
|
||||||
|
if (hintAlreadyInContext(_ctx)) return {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
customType: "pi-project-map-hint",
|
customType: HINT_CUSTOM_TYPE,
|
||||||
content:
|
content:
|
||||||
"📋 Project map active: If you modify any source file, run `project_map_patch` with the file path. If you suspect staleness, run `project_map_validate`.",
|
"📋 Project map active: If you modify any source file, run `project_map_patch` with the file path. If you suspect staleness, run `project_map_validate`.",
|
||||||
display: false,
|
display: false,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ describe("pi-extension", () => {
|
|||||||
getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })),
|
getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })),
|
||||||
},
|
},
|
||||||
ui: { notify: mockNotify },
|
ui: { notify: mockNotify },
|
||||||
|
sessionManager: {
|
||||||
|
buildSessionContext: vi.fn(() => ({ messages: [] })),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockPi = {
|
const mockPi = {
|
||||||
@@ -225,5 +228,44 @@ describe("pi-extension", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not inject hint when it is already in context", async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
|
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "custom",
|
||||||
|
customType: "pi-project-map-hint",
|
||||||
|
content: "existing hint",
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handler = registeredEvents.before_agent_start;
|
||||||
|
const result = await handler(null, mockCtx);
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects hint again when previous context did not contain it", async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
|
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: "hello" },
|
||||||
|
{ role: "assistant", content: [{ type: "text", text: "hi" }] },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handler = registeredEvents.before_agent_start;
|
||||||
|
const result = await handler(null, mockCtx);
|
||||||
|
|
||||||
|
expect(result).toHaveProperty("message");
|
||||||
|
expect(result.message.customType).toBe("pi-project-map-hint");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user