mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-31 01:42:58 +08:00
refactor(prompt): compact governed writer control surface
This commit is contained in:
@@ -226,8 +226,9 @@ describe("WriterAgent", () => {
|
||||
});
|
||||
|
||||
const settlePrompt = (chatSpy.mock.calls[2]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[1]?.content ?? "";
|
||||
expect(settlePrompt).toContain("## 本章控制输入");
|
||||
expect(settlePrompt).toContain("story/chapter_summaries.md#99");
|
||||
expect(settlePrompt).toContain("## 结算焦点");
|
||||
expect(settlePrompt).not.toContain("### 已选上下文");
|
||||
expect(settlePrompt).not.toContain("## Hook Agenda");
|
||||
expect(settlePrompt).toContain("| 99 | Locked Gate |");
|
||||
expect(settlePrompt).toContain("## Hook Debt Briefs");
|
||||
expect(settlePrompt).toContain("mentor-oath | cadence: slow-burn");
|
||||
@@ -903,11 +904,12 @@ describe("WriterAgent", () => {
|
||||
});
|
||||
|
||||
const creativePrompt = (chatSpy.mock.calls[0]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[1]?.content ?? "";
|
||||
expect(creativePrompt).toContain("## Recent Title History");
|
||||
expect(creativePrompt).toContain("## Story Brief");
|
||||
expect(creativePrompt).not.toContain("## Recent Title History");
|
||||
expect(creativePrompt).toContain("Ledger in Rain");
|
||||
expect(creativePrompt).toContain("## Recent Mood / Chapter Type Trail");
|
||||
expect(creativePrompt).not.toContain("## Recent Mood / Chapter Type Trail");
|
||||
expect(creativePrompt).toContain("tight / investigation");
|
||||
expect(creativePrompt).toContain("## Canon Evidence");
|
||||
expect(creativePrompt).not.toContain("## Canon Evidence");
|
||||
expect(creativePrompt).toContain("archive fire until volume two");
|
||||
expect(creativePrompt).toContain("oath debt logic must stay intact");
|
||||
} finally {
|
||||
@@ -1055,8 +1057,12 @@ describe("WriterAgent", () => {
|
||||
expect(systemPrompt).toContain("真实 hook_id");
|
||||
expect(systemPrompt).toContain("preferred narrative response");
|
||||
expect(systemPrompt).not.toContain("follow the requested move");
|
||||
expect(creativePrompt).toContain("## Hook Pressure and Debt");
|
||||
expect(creativePrompt).not.toContain("## Explicit Hook Agenda");
|
||||
expect(creativePrompt).toContain("## Story Brief");
|
||||
expect(creativePrompt).not.toContain("## Hook Pressure and Debt");
|
||||
expect(creativePrompt).not.toContain("## Selected Context");
|
||||
expect(creativePrompt).not.toContain("### Pressure Map");
|
||||
expect(creativePrompt).not.toContain("## Pending Hooks Snapshot");
|
||||
expect(creativePrompt).not.toContain("## Chapter Summaries Snapshot");
|
||||
expect(creativePrompt).toContain("mentor-oath");
|
||||
expect(creativePrompt).toContain("ledger-fragment");
|
||||
expect(creativePrompt).toContain("stale-ledger");
|
||||
|
||||
@@ -195,7 +195,6 @@ export class WriterAgent extends BaseAgent {
|
||||
lengthSpec: resolvedLengthSpec,
|
||||
language: book.language ?? genreProfile.language,
|
||||
varianceBrief: englishVarianceBrief?.text,
|
||||
selectedEvidenceBlock: this.joinGovernedEvidenceBlocks(governedMemoryBlocks),
|
||||
})
|
||||
: (() => {
|
||||
// Smart context filtering: inject only relevant parts of truth files
|
||||
@@ -810,64 +809,23 @@ ${lengthRequirementBlock}
|
||||
readonly lengthSpec: LengthSpec;
|
||||
readonly language?: "zh" | "en";
|
||||
readonly varianceBrief?: string;
|
||||
readonly selectedEvidenceBlock?: string;
|
||||
}): string {
|
||||
const contextSections = params.contextPackage.selectedContext
|
||||
.map((entry) => [
|
||||
`### ${entry.source}`,
|
||||
`- reason: ${entry.reason}`,
|
||||
entry.excerpt ? `- excerpt: ${entry.excerpt}` : "",
|
||||
].filter(Boolean).join("\n"))
|
||||
.join("\n\n");
|
||||
|
||||
const overrideLines = params.ruleStack.activeOverrides.length > 0
|
||||
? params.ruleStack.activeOverrides
|
||||
.map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
|
||||
.join("\n")
|
||||
: "- none";
|
||||
|
||||
const diagnosticLines = params.ruleStack.sections.diagnostic.length > 0
|
||||
? params.ruleStack.sections.diagnostic.join(", ")
|
||||
: "none";
|
||||
|
||||
const traceNotes = params.trace && params.trace.notes.length > 0
|
||||
? params.trace.notes.map((note) => `- ${note}`).join("\n")
|
||||
: "- none";
|
||||
const lengthRequirementBlock = this.buildLengthRequirementBlock(params.lengthSpec, params.language ?? "zh");
|
||||
const varianceBlock = params.varianceBrief
|
||||
? `\n${params.varianceBrief}\n`
|
||||
: "";
|
||||
const selectedEvidenceBlock = params.selectedEvidenceBlock
|
||||
? `\n${params.selectedEvidenceBlock}\n`
|
||||
: "";
|
||||
const explicitHookAgenda = this.extractMarkdownSection(params.chapterIntent, "## Hook Agenda");
|
||||
const hookAgendaBlock = explicitHookAgenda
|
||||
? params.language === "en"
|
||||
? `\n## Hook Pressure and Debt\n${explicitHookAgenda}\n`
|
||||
: `\n## 伏笔压力与旧债\n${explicitHookAgenda}\n`
|
||||
: "";
|
||||
const storyBrief = this.buildGovernedStoryBrief({
|
||||
chapterIntent: params.chapterIntent,
|
||||
contextPackage: params.contextPackage,
|
||||
ruleStack: params.ruleStack,
|
||||
trace: params.trace,
|
||||
language: params.language ?? "zh",
|
||||
});
|
||||
|
||||
if (params.language === "en") {
|
||||
return `Write chapter ${params.chapterNumber}.
|
||||
|
||||
## Chapter Intent
|
||||
${params.chapterIntent}
|
||||
|
||||
## Selected Context
|
||||
${contextSections || "(none)"}
|
||||
${selectedEvidenceBlock}
|
||||
${hookAgendaBlock}
|
||||
|
||||
## Rule Stack
|
||||
- Hard: ${params.ruleStack.sections.hard.join(", ") || "(none)"}
|
||||
- Soft: ${params.ruleStack.sections.soft.join(", ") || "(none)"}
|
||||
- Diagnostic: ${diagnosticLines}
|
||||
|
||||
## Active Overrides
|
||||
${overrideLines}
|
||||
|
||||
## Trace Notes
|
||||
${traceNotes}
|
||||
${storyBrief}
|
||||
|
||||
${varianceBlock}
|
||||
${lengthRequirementBlock}
|
||||
@@ -877,24 +835,7 @@ ${lengthRequirementBlock}
|
||||
|
||||
return `请续写第${params.chapterNumber}章。
|
||||
|
||||
## 本章意图
|
||||
${params.chapterIntent}
|
||||
|
||||
## 已选上下文
|
||||
${contextSections || "(无)"}
|
||||
${selectedEvidenceBlock}
|
||||
${hookAgendaBlock}
|
||||
|
||||
## 规则栈
|
||||
- 硬护栏:${params.ruleStack.sections.hard.join("、") || "(无)"}
|
||||
- 软约束:${params.ruleStack.sections.soft.join("、") || "(无)"}
|
||||
- 诊断规则:${diagnosticLines}
|
||||
|
||||
## 当前覆盖
|
||||
${overrideLines}
|
||||
|
||||
## 追踪说明
|
||||
${traceNotes}
|
||||
${storyBrief}
|
||||
|
||||
${varianceBlock}
|
||||
${lengthRequirementBlock}
|
||||
@@ -922,6 +863,73 @@ ${lengthRequirementBlock}
|
||||
return joined || undefined;
|
||||
}
|
||||
|
||||
private buildGovernedStoryBrief(params: {
|
||||
readonly chapterIntent: string;
|
||||
readonly contextPackage: ContextPackage;
|
||||
readonly ruleStack: RuleStack;
|
||||
readonly trace?: ChapterTrace;
|
||||
readonly language: "zh" | "en";
|
||||
}): string {
|
||||
const goal = this.readIntentScalar(params.chapterIntent, "## Goal");
|
||||
const mustKeep = this.readIntentList(params.chapterIntent, "## Must Keep");
|
||||
const mustAvoid = this.readIntentList(params.chapterIntent, "## Must Avoid");
|
||||
const styleEmphasis = this.readIntentList(params.chapterIntent, "## Style Emphasis");
|
||||
const creativePressure = this.readIntentList(params.chapterIntent, "## Creative Pressure");
|
||||
const hookFocus = this.buildHookFocusLines(params.chapterIntent, params.contextPackage, params.language);
|
||||
const continuityAnchors = this.collectContextExcerpts(params.contextPackage, (entry) =>
|
||||
entry.source.startsWith("story/current_state.md")
|
||||
|| entry.source === "story/story_bible.md",
|
||||
);
|
||||
const canonGuardrails = this.collectContextExcerpts(params.contextPackage, (entry) =>
|
||||
entry.source === "story/parent_canon.md" || entry.source === "story/fanfic_canon.md",
|
||||
);
|
||||
const titleHistory = this.findContextExcerpt(
|
||||
params.contextPackage,
|
||||
"story/chapter_summaries.md#recent_titles",
|
||||
);
|
||||
const moodTrail = this.findContextExcerpt(
|
||||
params.contextPackage,
|
||||
"story/chapter_summaries.md#recent_mood_type_trail",
|
||||
);
|
||||
const overrideLines = params.ruleStack.activeOverrides
|
||||
.map((override) => `${override.reason} (${override.target})`);
|
||||
const traceNotes = params.trace?.notes.filter(Boolean) ?? [];
|
||||
|
||||
if (params.language === "en") {
|
||||
return [
|
||||
"## Story Brief",
|
||||
goal ? `Goal: ${goal}` : "",
|
||||
this.renderCompactList("Keep continuity on", mustKeep),
|
||||
this.renderCompactList("Avoid this chapter", mustAvoid),
|
||||
this.renderCompactList("Style emphasis", styleEmphasis),
|
||||
this.renderCompactList("Creative pressure", creativePressure),
|
||||
this.renderCompactList("Hook focus", hookFocus),
|
||||
this.renderCompactList("Continuity anchors", continuityAnchors),
|
||||
this.renderCompactList("Canon guardrails", canonGuardrails),
|
||||
titleHistory ? `Recent title history: ${titleHistory}` : "",
|
||||
moodTrail ? `Recent mood/type trail: ${moodTrail}` : "",
|
||||
this.renderCompactList("Local overrides", overrideLines),
|
||||
this.renderCompactList("Trace notes", traceNotes),
|
||||
].filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
return [
|
||||
"## Story Brief",
|
||||
goal ? `目标:${goal}` : "",
|
||||
this.renderCompactList("本章必须守住", mustKeep),
|
||||
this.renderCompactList("本章避免", mustAvoid),
|
||||
this.renderCompactList("文风强调", styleEmphasis),
|
||||
this.renderCompactList("创作压力", creativePressure),
|
||||
this.renderCompactList("伏笔焦点", hookFocus),
|
||||
this.renderCompactList("连续性锚点", continuityAnchors),
|
||||
this.renderCompactList("正典护栏", canonGuardrails),
|
||||
titleHistory ? `近期标题历史:${titleHistory}` : "",
|
||||
moodTrail ? `近期情绪/章节类型轨迹:${moodTrail}` : "",
|
||||
this.renderCompactList("局部覆盖", overrideLines),
|
||||
this.renderCompactList("追踪备注", traceNotes),
|
||||
].filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
private extractMarkdownSection(content: string, heading: string): string | undefined {
|
||||
const lines = content.split("\n");
|
||||
let buffer: string[] | null = null;
|
||||
@@ -951,44 +959,161 @@ ${lengthRequirementBlock}
|
||||
ruleStack: RuleStack,
|
||||
language: "zh" | "en",
|
||||
): string {
|
||||
const selectedContext = contextPackage.selectedContext
|
||||
.map((entry) => `- ${entry.source}: ${entry.reason}${entry.excerpt ? ` | ${entry.excerpt}` : ""}`)
|
||||
.join("\n");
|
||||
const overrides = ruleStack.activeOverrides.length > 0
|
||||
? ruleStack.activeOverrides
|
||||
.map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
|
||||
.join("\n")
|
||||
: "- none";
|
||||
const goal = this.readIntentScalar(chapterIntent, "## Goal");
|
||||
const mustKeep = this.readIntentList(chapterIntent, "## Must Keep");
|
||||
const hookFocus = this.buildHookFocusLines(chapterIntent, contextPackage, language);
|
||||
const continuityAnchors = this.collectContextExcerpts(contextPackage, (entry) =>
|
||||
entry.source.startsWith("story/current_state.md")
|
||||
|| entry.source === "story/story_bible.md"
|
||||
|| entry.source === "story/parent_canon.md"
|
||||
|| entry.source === "story/fanfic_canon.md",
|
||||
);
|
||||
const overrides = ruleStack.activeOverrides
|
||||
.map((override) => `${override.reason} (${override.target})`);
|
||||
|
||||
if (language === "en") {
|
||||
return `\n## Chapter Control Inputs
|
||||
${chapterIntent}
|
||||
|
||||
### Selected Context
|
||||
${selectedContext || "- none"}
|
||||
|
||||
### Rule Stack
|
||||
- Hard guardrails: ${ruleStack.sections.hard.join(", ") || "(none)"}
|
||||
- Soft constraints: ${ruleStack.sections.soft.join(", ") || "(none)"}
|
||||
- Diagnostic rules: ${ruleStack.sections.diagnostic.join(", ") || "(none)"}
|
||||
|
||||
### Active Overrides
|
||||
${overrides}\n`;
|
||||
return `\n## Settlement Focus
|
||||
${goal ? `Goal: ${goal}\n` : ""}${this.renderCompactList("Keep synced with", mustKeep)}${this.renderCompactList("Hook settlement cues", hookFocus)}${this.renderCompactList("Continuity anchors", continuityAnchors)}${this.renderCompactList("Local overrides", overrides)}\n`;
|
||||
}
|
||||
|
||||
return `\n## 本章控制输入
|
||||
${chapterIntent}
|
||||
return `\n## 结算焦点
|
||||
${goal ? `目标:${goal}\n` : ""}${this.renderCompactList("结算时守住", mustKeep)}${this.renderCompactList("伏笔结算提示", hookFocus)}${this.renderCompactList("连续性锚点", continuityAnchors)}${this.renderCompactList("局部覆盖", overrides)}\n`;
|
||||
}
|
||||
|
||||
### 已选上下文
|
||||
${selectedContext || "- none"}
|
||||
private readIntentScalar(chapterIntent: string, heading: string): string | undefined {
|
||||
const section = this.extractMarkdownSection(chapterIntent, heading);
|
||||
if (!section) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
### 规则栈
|
||||
- 硬护栏:${ruleStack.sections.hard.join("、") || "(无)"}
|
||||
- 软约束:${ruleStack.sections.soft.join("、") || "(无)"}
|
||||
- 诊断规则:${ruleStack.sections.diagnostic.join("、") || "(无)"}
|
||||
return section
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0 && !line.startsWith("-") && line.toLowerCase() !== "(not found)");
|
||||
}
|
||||
|
||||
### 当前覆盖
|
||||
${overrides}\n`;
|
||||
private readIntentList(chapterIntent: string, heading: string): string[] {
|
||||
const section = this.extractMarkdownSection(chapterIntent, heading);
|
||||
if (!section) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return section
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith("- "))
|
||||
.map((line) => line.slice(2).trim())
|
||||
.filter((line) => line.length > 0 && line.toLowerCase() !== "none");
|
||||
}
|
||||
|
||||
private buildHookFocusLines(
|
||||
chapterIntent: string,
|
||||
contextPackage: ContextPackage,
|
||||
language: "zh" | "en",
|
||||
): string[] {
|
||||
const hookDebtLines = this.collectContextExcerpts(
|
||||
contextPackage,
|
||||
(entry) => entry.source.startsWith("runtime/hook_debt#"),
|
||||
);
|
||||
const fallbackLines = [
|
||||
...this.formatAgendaLines(chapterIntent, "Must Advance", language === "en" ? "Must advance" : "必须推进"),
|
||||
...this.formatAgendaLines(chapterIntent, "Eligible Resolve", language === "en" ? "Eligible payoff" : "可兑现"),
|
||||
...this.formatAgendaLines(chapterIntent, "Stale Debt", language === "en" ? "Stale debt" : "旧债"),
|
||||
...this.formatAgendaLines(
|
||||
chapterIntent,
|
||||
"Avoid New Hook Families",
|
||||
language === "en" ? "Avoid new hook families" : "避免新增同类 hook",
|
||||
),
|
||||
];
|
||||
|
||||
return this.uniqueContextLines([
|
||||
...(hookDebtLines.length > 0 ? hookDebtLines : []),
|
||||
...fallbackLines,
|
||||
]).slice(0, 6);
|
||||
}
|
||||
|
||||
private formatAgendaLines(
|
||||
chapterIntent: string,
|
||||
subheading: string,
|
||||
label: string,
|
||||
): string[] {
|
||||
const hookAgenda = this.extractMarkdownSection(chapterIntent, "## Hook Agenda");
|
||||
if (!hookAgenda) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = hookAgenda.split("\n");
|
||||
const values: string[] = [];
|
||||
let capture = false;
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (line === `### ${subheading}`) {
|
||||
capture = true;
|
||||
continue;
|
||||
}
|
||||
if (capture && line.startsWith("### ")) {
|
||||
break;
|
||||
}
|
||||
if (capture && line.startsWith("- ")) {
|
||||
const value = line.slice(2).trim();
|
||||
if (value && value.toLowerCase() !== "none") {
|
||||
values.push(`${label}: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private collectContextExcerpts(
|
||||
contextPackage: ContextPackage,
|
||||
predicate: (entry: ContextPackage["selectedContext"][number]) => boolean,
|
||||
): string[] {
|
||||
return this.uniqueContextLines(
|
||||
contextPackage.selectedContext
|
||||
.filter(predicate)
|
||||
.map((entry) => entry.excerpt ?? entry.reason)
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
private findContextExcerpt(
|
||||
contextPackage: ContextPackage,
|
||||
source: string,
|
||||
): string | undefined {
|
||||
return contextPackage.selectedContext.find((entry) => entry.source === source)?.excerpt;
|
||||
}
|
||||
|
||||
private renderCompactList(
|
||||
heading: string,
|
||||
items: ReadonlyArray<string>,
|
||||
): string {
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${heading}:\n${items.map((item) => `- ${item}`).join("\n")}`;
|
||||
}
|
||||
|
||||
private uniqueContextLines(items: ReadonlyArray<string>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const normalized = trimmed.toLowerCase();
|
||||
if (seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(trimmed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private buildLengthRequirementBlock(lengthSpec: LengthSpec, language: "zh" | "en"): string {
|
||||
|
||||
Reference in New Issue
Block a user