Просмотр исходного кода

refactor(prompt): compact governed writer control surface

Ma 7 месяцев назад
Родитель
Сommit
4248e1db56
2 измененных файлов с 234 добавлено и 103 удалено
  1. 13 7
      packages/core/src/__tests__/writer.test.ts
  2. 221 96
      packages/core/src/agents/writer.ts

+ 13 - 7
packages/core/src/__tests__/writer.test.ts

@@ -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");

+ 221 - 96
packages/core/src/agents/writer.ts

@@ -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}
+      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`;
+    }
 
-### Selected Context
-${selectedContext || "- none"}
+    return `\n## 结算焦点
+${goal ? `目标:${goal}\n` : ""}${this.renderCompactList("结算时守住", mustKeep)}${this.renderCompactList("伏笔结算提示", hookFocus)}${this.renderCompactList("连续性锚点", continuityAnchors)}${this.renderCompactList("局部覆盖", overrides)}\n`;
+  }
 
-### Rule Stack
-- Hard guardrails: ${ruleStack.sections.hard.join(", ") || "(none)"}
-- Soft constraints: ${ruleStack.sections.soft.join(", ") || "(none)"}
-- Diagnostic rules: ${ruleStack.sections.diagnostic.join(", ") || "(none)"}
+  private readIntentScalar(chapterIntent: string, heading: string): string | undefined {
+    const section = this.extractMarkdownSection(chapterIntent, heading);
+    if (!section) {
+      return undefined;
+    }
+
+    return section
+      .split("\n")
+      .map((line) => line.trim())
+      .find((line) => line.length > 0 && !line.startsWith("-") && line.toLowerCase() !== "(not found)");
+  }
 
-### Active Overrides
-${overrides}\n`;
+  private readIntentList(chapterIntent: string, heading: string): string[] {
+    const section = this.extractMarkdownSection(chapterIntent, heading);
+    if (!section) {
+      return [];
     }
 
-    return `\n## 本章控制输入
-${chapterIntent}
+    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");
+  }
 
-### 已选上下文
-${selectedContext || "- 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;
+  }
 
-### 规则栈
-- 硬护栏:${ruleStack.sections.hard.join("、") || "(无)"}
-- 软约束:${ruleStack.sections.soft.join("、") || "(无)"}
-- 诊断规则:${ruleStack.sections.diagnostic.join("、") || "(无)"}
+  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);
+    }
 
-### 当前覆盖
-${overrides}\n`;
+    return result;
   }
 
   private buildLengthRequirementBlock(lengthSpec: LengthSpec, language: "zh" | "en"): string {