mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat: 系统提示词改为单一入口统一管理
多个扩展各自整段改写系统提示词并按加载顺序接力,任一扩展定死提示词后 其余只能做字符串手术,且各上游的工具说明与组合包实际工具格局冲突。 - 新增 extensions/system-prompt.ts 作为唯一出口(加载列表最后),按 config/prompt/(preamble.md、tools.jsonc、routing.jsonc、sections.jsonc) 组合开场白、工具简介/规则与 <tool_routing> 分区;替代原封面与 tool-routing 扩展,/dump-system-prompt 输出最终提示词。 - pi-chrome、pi-ssh、pi-hermes-memory 改为写命名分区,pi-rtk-optimizer 改写 promptGuidelines,pi-permission-system 直接过滤结构化 skills, 均不再返回 systemPrompt;pi-chrome 未授权时不再每轮冻结提示词。 - 新增 prompt-guard 代理加载第三方扩展(Hashline、FFF、LSP、 context-mode):拦截直接改写提示词并按目录覆盖工具说明;context-mode 去掉与组合包路由冲突的锚点段,保留记忆与恢复快照。 - 消除冲突:bash 简介不再建议 grep/find;Hashline edit 不再称可用 grep 锚点、不再介绍已关闭的 replace_text;context-mode 读写/网页路由提示。 - 守门测试:工具目录覆盖率、禁用用法检查、context-mode 上游原文锁定; AGENTS.md 新增「系统提示词单一所有权」。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
dc4fe92e27
commit
ba164896d2
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
- Move the global config directory to `<agentDir>/my-pi/rtk-optimizer/` under the bundle-owned rebuild tree (`config.json` is still created on demand by `ensureConfigExists`; the old `extensions/pi-rtk-optimizer/` location is ignored).
|
||||
- Contribute the source-filter troubleshooting note through `systemPromptOptions.promptGuidelines` instead of rewriting the returned system prompt: `extensions/system-prompt.ts` is the bundle's single system-prompt owner, so handlers must never return `systemPrompt`. `injectGuidelineIntoPrompt` is removed.
|
||||
|
||||
## [0.9.0] - 2026-07-03
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ mock.module("@earendil-works/pi-tui", {
|
||||
});
|
||||
|
||||
const indexModule = await import("./index.ts");
|
||||
const { createBoundedNoticeTracker, shouldInjectSourceFilterTroubleshootingNote, injectGuidelineIntoPrompt } = indexModule;
|
||||
const { createBoundedNoticeTracker, shouldInjectSourceFilterTroubleshootingNote } = indexModule;
|
||||
const rtkIntegrationExtension = indexModule.default;
|
||||
const { DEFAULT_RTK_INTEGRATION_CONFIG } = await import("./types.ts");
|
||||
|
||||
@@ -206,72 +206,39 @@ runTest("source-filter note skipped when all read filtering safeguards are disab
|
||||
);
|
||||
});
|
||||
|
||||
runTest("injectGuidelineIntoPrompt inserts bullet inside Guidelines section when header is at index 0", () => {
|
||||
const guideline = "Test guideline for RTK troubleshooting.";
|
||||
await runTest("before_agent_start contributes the source-filter note via promptGuidelines", async () => {
|
||||
const config = configWith({
|
||||
readCompactionEnabled: true,
|
||||
sourceFilteringEnabled: true,
|
||||
sourceFilteringLevel: "minimal",
|
||||
smartTruncateEnabled: true,
|
||||
});
|
||||
writeFileSync(TEST_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
||||
|
||||
const prompt = [
|
||||
"Guidelines:",
|
||||
"- Use mcp for MCP discovery first",
|
||||
"- Be concise in your responses",
|
||||
"",
|
||||
"<project_context>",
|
||||
"Project-specific instructions",
|
||||
].join("\n");
|
||||
const handlers: Record<string, ExtensionHandler> = {};
|
||||
rtkIntegrationExtension({
|
||||
exec: async () => ({ code: 1, stdout: "", stderr: "missing rtk" }),
|
||||
on(eventName: string, handler: ExtensionHandler) {
|
||||
handlers[eventName] = handler;
|
||||
},
|
||||
registerCommand() {},
|
||||
} as never);
|
||||
|
||||
const result = injectGuidelineIntoPrompt(prompt, guideline);
|
||||
const beforeAgentStart = handlers.before_agent_start;
|
||||
assert.ok(beforeAgentStart);
|
||||
|
||||
assert.ok(result.includes(guideline), "guideline must be present");
|
||||
const event = {
|
||||
systemPrompt: "base prompt",
|
||||
systemPromptOptions: { promptGuidelines: [] as string[] },
|
||||
};
|
||||
const result = await beforeAgentStart(event, createNotificationContext([]));
|
||||
assert.ok(!result || !("systemPrompt" in result), "must not force the system prompt");
|
||||
assert.equal(event.systemPromptOptions.promptGuidelines.length, 1);
|
||||
assert.match(event.systemPromptOptions.promptGuidelines[0], /RTK note:/);
|
||||
|
||||
const bullet = `- ${guideline}`;
|
||||
assert.ok(result.includes(bullet), "guideline must be a bullet line");
|
||||
|
||||
const guidelinesStart = result.indexOf("Guidelines:\n");
|
||||
const bulletIndex = result.indexOf(bullet);
|
||||
const projectContextIndex = result.indexOf("\n<project_context>");
|
||||
|
||||
assert.equal(guidelinesStart, 0, "Guidelines header must be at index 0");
|
||||
assert.ok(bulletIndex > guidelinesStart, "bullet must be after Guidelines header");
|
||||
assert.ok(projectContextIndex !== -1, "project_context section must exist");
|
||||
assert.ok(bulletIndex < projectContextIndex, "bullet must be before project_context section");
|
||||
|
||||
assert.equal(injectGuidelineIntoPrompt(result, guideline), result, "should be idempotent");
|
||||
});
|
||||
|
||||
runTest("injectGuidelineIntoPrompt inserts bullet inside Guidelines section when header is mid-prompt", () => {
|
||||
const guideline = "Test guideline for RTK troubleshooting.";
|
||||
|
||||
const prompt = [
|
||||
"You are an expert coding assistant.",
|
||||
"",
|
||||
"Guidelines:",
|
||||
"- Be concise in your responses",
|
||||
"",
|
||||
"Pi documentation:",
|
||||
"- Main documentation: /path/to/readme",
|
||||
].join("\n");
|
||||
|
||||
const result = injectGuidelineIntoPrompt(prompt, guideline);
|
||||
|
||||
const bullet = `- ${guideline}`;
|
||||
assert.ok(result.includes(bullet), "guideline must be a bullet line");
|
||||
|
||||
const guidelinesStart = result.indexOf("\nGuidelines:\n");
|
||||
const bulletIndex = result.indexOf(bullet);
|
||||
const piDocsIndex = result.indexOf("\nPi documentation:");
|
||||
|
||||
assert.ok(guidelinesStart !== -1, "Guidelines header must exist");
|
||||
assert.ok(bulletIndex > guidelinesStart, "bullet must be after Guidelines header");
|
||||
assert.ok(bulletIndex < piDocsIndex, "bullet must be before Pi documentation section");
|
||||
});
|
||||
|
||||
runTest("injectGuidelineIntoPrompt falls back to appending when no Guidelines section exists", () => {
|
||||
const guideline = "Test guideline for RTK troubleshooting.";
|
||||
const prompt = "You are a coding assistant with no guidelines section.";
|
||||
|
||||
const result = injectGuidelineIntoPrompt(prompt, guideline);
|
||||
|
||||
assert.ok(result.includes(guideline), "guideline must be present");
|
||||
assert.ok(result.endsWith(guideline), "guideline must be appended at the end");
|
||||
await beforeAgentStart(event, createNotificationContext([]));
|
||||
assert.equal(event.systemPromptOptions.promptGuidelines.length, 1, "must not duplicate the note");
|
||||
writeTestConfig(false);
|
||||
});
|
||||
|
||||
await runTest("session_start refreshes RTK provenance and runtime guard skips missing rewrites", async () => {
|
||||
|
||||
@@ -32,61 +32,6 @@ function trimMessage(raw: string, maxLength = 220): string {
|
||||
const SOURCE_FILTER_TROUBLESHOOTING_NOTE =
|
||||
"RTK note: If file edits repeatedly fail because old text does not match, ask the user to manually run '/rtk' in the Pi TUI, disable 'Read compaction enabled', re-read the file, apply the edit, then ask the user to manually re-enable it in the Pi TUI.";
|
||||
|
||||
/**
|
||||
* Inject a guideline bullet into the Guidelines section of the system prompt.
|
||||
*
|
||||
* Locates the `Guidelines:` block and inserts the bullet after the last
|
||||
* existing guideline, preserving the section structure. Falls back to
|
||||
* appending at the end when the Guidelines section cannot be found.
|
||||
*/
|
||||
export function injectGuidelineIntoPrompt(systemPrompt: string, guideline: string): string {
|
||||
if (!systemPrompt || systemPrompt.includes(guideline)) {
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
const bullet = `- ${guideline}`;
|
||||
|
||||
// "Guidelines:" may appear at the very start of the prompt (index 0) or
|
||||
// after a newline. Check both cases so the header is always detected.
|
||||
let guidelinesHeaderIndex = systemPrompt.indexOf("\nGuidelines:\n");
|
||||
let headerLength = "\nGuidelines:\n".length;
|
||||
|
||||
if (guidelinesHeaderIndex === -1 && systemPrompt.startsWith("Guidelines:\n")) {
|
||||
guidelinesHeaderIndex = 0;
|
||||
headerLength = "Guidelines:\n".length;
|
||||
}
|
||||
|
||||
if (guidelinesHeaderIndex === -1) {
|
||||
return `${systemPrompt}\n\n${guideline}`;
|
||||
}
|
||||
|
||||
const linesStart = guidelinesHeaderIndex + headerLength;
|
||||
const remainder = systemPrompt.slice(linesStart);
|
||||
const lines = remainder.split("\n");
|
||||
|
||||
let consumedChars = 0;
|
||||
for (const line of lines) {
|
||||
if (line === "") {
|
||||
break;
|
||||
}
|
||||
if (/^[-*+\s]/.test(line)) {
|
||||
consumedChars += line.length + 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const insertAt = consumedChars === 0 ? linesStart : linesStart + consumedChars - 1;
|
||||
|
||||
const before = systemPrompt.slice(0, insertAt);
|
||||
const after = systemPrompt.slice(insertAt);
|
||||
|
||||
const needsNewlineBefore = before.length > 0 && !before.endsWith("\n");
|
||||
const needsNewlineAfter = after.length > 0 && !after.startsWith("\n");
|
||||
|
||||
return [before, needsNewlineBefore ? "\n" : "", bullet, needsNewlineAfter ? "\n" : "", after].join("");
|
||||
}
|
||||
|
||||
const loadOutputCompactorModule = createLazyModuleLoader<typeof import("./output-compactor.js")>("./output-compactor.js");
|
||||
|
||||
export function shouldInjectSourceFilterTroubleshootingNote(config: RtkIntegrationConfig): boolean {
|
||||
@@ -436,19 +381,18 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (event.systemPrompt.includes(SOURCE_FILTER_TROUBLESHOOTING_NOTE)) {
|
||||
// Contribute structurally: the bundle's single system-prompt owner
|
||||
// (extensions/system-prompt.ts) composes the final prompt; extensions
|
||||
// must not return `systemPrompt` themselves.
|
||||
const promptOptions = event.systemPromptOptions;
|
||||
if (!promptOptions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const updatedPrompt = injectGuidelineIntoPrompt(event.systemPrompt, SOURCE_FILTER_TROUBLESHOOTING_NOTE);
|
||||
|
||||
if (updatedPrompt === event.systemPrompt) {
|
||||
return {};
|
||||
const guidelines = (promptOptions.promptGuidelines ??= []);
|
||||
if (!guidelines.includes(SOURCE_FILTER_TROUBLESHOOTING_NOTE)) {
|
||||
guidelines.push(SOURCE_FILTER_TROUBLESHOOTING_NOTE);
|
||||
}
|
||||
|
||||
return {
|
||||
systemPrompt: updatedPrompt,
|
||||
};
|
||||
return {};
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
|
||||
+4
-1
@@ -120,7 +120,10 @@ declare module "@earendil-works/pi-coding-agent" {
|
||||
on(
|
||||
eventName: "before_agent_start",
|
||||
handler: (
|
||||
event: { systemPrompt: string },
|
||||
event: {
|
||||
systemPrompt: string;
|
||||
systemPromptOptions?: { promptGuidelines?: string[] } & Record<string, unknown>;
|
||||
},
|
||||
ctx: ExtensionContext,
|
||||
) => MaybePromise<{ systemPrompt: string } | Record<string, unknown> | void>,
|
||||
): void;
|
||||
|
||||
Reference in New Issue
Block a user