feat(hooks): SessionStart 注入人格的 AGENTS.md 操作規則
新增 pl.opsBrief(slug):把該人格的 AGENTS.md 全文包成 <persona-ops> 區塊, 由 SessionStart hook 在「接續人格」與「預設人格自動載入」兩條分支各注入一次。 刻意不放進 turnContext——操作規則是低頻的「怎麼做事」,每輪重貼只是燒 context。 超過 OPS_BRIEF_MAX_CHARS(6000)會截斷並指路回原檔;檔案不存在時回空字串。 這讓「這個人格自己的工具箱」可以寫在 AGENTS.md 裡跟著人格走(AGENTS.md 本來 就在 sync 的 wiki 白名單內),不必落在機器本機的 ~/.claude/agents。 selftest 補 5 條:全文注入、指路原檔、不進 turnContext、檔案不存在回空、 超長截斷。全套 345 項通過。
This commit is contained in:
@@ -1733,6 +1733,9 @@ export function sentenceCount(text) {
|
||||
|
||||
export const MAX_SENTENCE_CHARS = 45;
|
||||
|
||||
// AGENTS.md 開機注入的上限:超過就截斷並指路回原檔,不要一份規則吃掉整個 context。
|
||||
export const OPS_BRIEF_MAX_CHARS = 6000;
|
||||
|
||||
// 講完就算了:這些開頭都是在解釋自己剛才講的話,真的在講話的人不會這樣。
|
||||
export const EXPLAINER_OPENERS = [
|
||||
"我的意思是", "我是想說", "換句話說", "換個說法", "也就是說", "換言之",
|
||||
@@ -3082,6 +3085,35 @@ export function identityBrief(slug) {
|
||||
return order.filter((k) => fields[k]).map((k) => `${k}: ${fields[k]}`).join("|");
|
||||
}
|
||||
|
||||
/**
|
||||
* SessionStart 注入的操作規則:AGENTS.md 全文。
|
||||
*
|
||||
* 只在開機注入一次(不進 turnContext)——它是低頻的「怎麼做事」,
|
||||
* 每輪重貼只是把 context 燒掉。內容太長就截斷並指路回原檔。
|
||||
*/
|
||||
export function opsBrief(slug) {
|
||||
let text;
|
||||
try {
|
||||
text = fs.readFileSync(path.join(personaDir(slug), "AGENTS.md"), "utf8").trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
if (!text) return "";
|
||||
const file = path.join(personaDir(slug), "AGENTS.md");
|
||||
let body = text;
|
||||
if (body.length > OPS_BRIEF_MAX_CHARS) {
|
||||
body = body.slice(0, OPS_BRIEF_MAX_CHARS) + `\n\n(後略;全文見 ${file})`;
|
||||
}
|
||||
return [
|
||||
"<persona-ops>",
|
||||
`以下是 \`${slug}\` 的操作規則(${file}):這輪開機注入一次,之後不再重複貼。`,
|
||||
"要重看用 `persona.mjs show --what agents --session <id>`;改了就直接編輯那個檔案。",
|
||||
"",
|
||||
body,
|
||||
"</persona-ops>",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** UserPromptSubmit 注入的人格上下文:身分 + 情緒 + 短期記憶 + 相關長期記憶 + 關係。 */
|
||||
export function turnContext(slug, sessionId, prompt = "") {
|
||||
const state = decayEmotion(loadEmotion(slug));
|
||||
|
||||
@@ -81,6 +81,31 @@ check("IDENTITY 五欄位可解析", pl.identityBrief("alpha").includes("Name: A
|
||||
check("SOUL/AGENTS/USER 都有產生",
|
||||
["SOUL.md", "AGENTS.md", "USER.md"].every((f) => fs.existsSync(path.join(pl.personaDir("alpha"), f))));
|
||||
|
||||
// 操作規則(AGENTS.md)只在開機注入一次:人格自己的工具箱寫在裡面就會跟著人格走。
|
||||
check("opsBrief 帶回 AGENTS.md 全文", (() => {
|
||||
const ops = pl.opsBrief("alpha");
|
||||
return ops.includes("<persona-ops>") && ops.includes("</persona-ops>") && ops.includes("每輪對話要做的事");
|
||||
})(), pl.opsBrief("alpha").slice(0, 200));
|
||||
check("opsBrief 指得出原檔路徑", pl.opsBrief("alpha").includes(path.join(pl.personaDir("alpha"), "AGENTS.md")));
|
||||
check("opsBrief 不進 turnContext(開機一次就好)",
|
||||
!pl.turnContext("alpha", S_HOST, "在嗎").includes("<persona-ops>"));
|
||||
check("AGENTS.md 不存在時 opsBrief 回空字串", (() => {
|
||||
const file = path.join(pl.personaDir("omega"), "AGENTS.md");
|
||||
const backup = fs.readFileSync(file, "utf8");
|
||||
fs.rmSync(file);
|
||||
const empty = pl.opsBrief("omega");
|
||||
fs.writeFileSync(file, backup);
|
||||
return empty === "";
|
||||
})());
|
||||
check("opsBrief 超長會截斷並指路", (() => {
|
||||
const file = path.join(pl.personaDir("omega"), "AGENTS.md");
|
||||
const backup = fs.readFileSync(file, "utf8");
|
||||
fs.writeFileSync(file, "劍".repeat(pl.OPS_BRIEF_MAX_CHARS + 500));
|
||||
const ops = pl.opsBrief("omega");
|
||||
fs.writeFileSync(file, backup);
|
||||
return ops.includes("(後略;全文見") && ops.length < pl.OPS_BRIEF_MAX_CHARS + 500;
|
||||
})());
|
||||
|
||||
console.log("② 人格鎖:一個人格只能被一個程序載入");
|
||||
check("建立時即取得鎖", pl.lockStatus("alpha").locked);
|
||||
check("同 session 重入成功", cli(["load", "--persona", "alpha", "--session", S_HOST]).status === 0);
|
||||
|
||||
Reference in New Issue
Block a user