@actalk/inkos-core 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/agents/architect.d.ts +6 -1
  2. package/dist/agents/architect.d.ts.map +1 -1
  3. package/dist/agents/architect.js +362 -83
  4. package/dist/agents/architect.js.map +1 -1
  5. package/dist/agents/chapter-analyzer.d.ts +6 -0
  6. package/dist/agents/chapter-analyzer.d.ts.map +1 -1
  7. package/dist/agents/chapter-analyzer.js +220 -17
  8. package/dist/agents/chapter-analyzer.js.map +1 -1
  9. package/dist/agents/composer.d.ts +28 -0
  10. package/dist/agents/composer.d.ts.map +1 -0
  11. package/dist/agents/composer.js +154 -0
  12. package/dist/agents/composer.js.map +1 -0
  13. package/dist/agents/consolidator.d.ts.map +1 -1
  14. package/dist/agents/consolidator.js +17 -8
  15. package/dist/agents/consolidator.js.map +1 -1
  16. package/dist/agents/continuity.d.ts +10 -0
  17. package/dist/agents/continuity.d.ts.map +1 -1
  18. package/dist/agents/continuity.js +312 -133
  19. package/dist/agents/continuity.js.map +1 -1
  20. package/dist/agents/en-prompt-sections.d.ts.map +1 -1
  21. package/dist/agents/en-prompt-sections.js +1 -0
  22. package/dist/agents/en-prompt-sections.js.map +1 -1
  23. package/dist/agents/length-normalizer.d.ts +32 -0
  24. package/dist/agents/length-normalizer.d.ts.map +1 -0
  25. package/dist/agents/length-normalizer.js +156 -0
  26. package/dist/agents/length-normalizer.js.map +1 -0
  27. package/dist/agents/planner.d.ts +42 -0
  28. package/dist/agents/planner.d.ts.map +1 -0
  29. package/dist/agents/planner.js +382 -0
  30. package/dist/agents/planner.js.map +1 -0
  31. package/dist/agents/post-write-validator.d.ts +6 -1
  32. package/dist/agents/post-write-validator.d.ts.map +1 -1
  33. package/dist/agents/post-write-validator.js +88 -2
  34. package/dist/agents/post-write-validator.js.map +1 -1
  35. package/dist/agents/reviser.d.ts +10 -1
  36. package/dist/agents/reviser.d.ts.map +1 -1
  37. package/dist/agents/reviser.js +151 -36
  38. package/dist/agents/reviser.js.map +1 -1
  39. package/dist/agents/rules-reader.d.ts +1 -0
  40. package/dist/agents/rules-reader.d.ts.map +1 -1
  41. package/dist/agents/rules-reader.js +13 -0
  42. package/dist/agents/rules-reader.js.map +1 -1
  43. package/dist/agents/settler-delta-parser.d.ts +7 -0
  44. package/dist/agents/settler-delta-parser.d.ts.map +1 -0
  45. package/dist/agents/settler-delta-parser.js +35 -0
  46. package/dist/agents/settler-delta-parser.js.map +1 -0
  47. package/dist/agents/settler-prompts.d.ts +2 -0
  48. package/dist/agents/settler-prompts.d.ts.map +1 -1
  49. package/dist/agents/settler-prompts.js +77 -63
  50. package/dist/agents/settler-prompts.js.map +1 -1
  51. package/dist/agents/writer-parser.d.ts +3 -2
  52. package/dist/agents/writer-parser.d.ts.map +1 -1
  53. package/dist/agents/writer-parser.js +44 -13
  54. package/dist/agents/writer-parser.js.map +1 -1
  55. package/dist/agents/writer-prompts.d.ts +2 -1
  56. package/dist/agents/writer-prompts.d.ts.map +1 -1
  57. package/dist/agents/writer-prompts.js +65 -21
  58. package/dist/agents/writer-prompts.js.map +1 -1
  59. package/dist/agents/writer.d.ts +28 -1
  60. package/dist/agents/writer.d.ts.map +1 -1
  61. package/dist/agents/writer.js +426 -67
  62. package/dist/agents/writer.js.map +1 -1
  63. package/dist/index.d.ts +18 -3
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +17 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/llm/provider.d.ts +1 -0
  68. package/dist/llm/provider.d.ts.map +1 -1
  69. package/dist/llm/provider.js +19 -6
  70. package/dist/llm/provider.js.map +1 -1
  71. package/dist/models/chapter.d.ts +71 -0
  72. package/dist/models/chapter.d.ts.map +1 -1
  73. package/dist/models/chapter.js +3 -0
  74. package/dist/models/chapter.js.map +1 -1
  75. package/dist/models/input-governance.d.ts +351 -0
  76. package/dist/models/input-governance.d.ts.map +1 -0
  77. package/dist/models/input-governance.js +78 -0
  78. package/dist/models/input-governance.js.map +1 -0
  79. package/dist/models/length-governance.d.ts +93 -0
  80. package/dist/models/length-governance.d.ts.map +1 -0
  81. package/dist/models/length-governance.js +34 -0
  82. package/dist/models/length-governance.js.map +1 -0
  83. package/dist/models/project.d.ts +5 -0
  84. package/dist/models/project.d.ts.map +1 -1
  85. package/dist/models/project.js +2 -0
  86. package/dist/models/project.js.map +1 -1
  87. package/dist/models/runtime-state.d.ts +521 -0
  88. package/dist/models/runtime-state.d.ts.map +1 -0
  89. package/dist/models/runtime-state.js +78 -0
  90. package/dist/models/runtime-state.js.map +1 -0
  91. package/dist/pipeline/agent.d.ts +2 -1
  92. package/dist/pipeline/agent.d.ts.map +1 -1
  93. package/dist/pipeline/agent.js +90 -5
  94. package/dist/pipeline/agent.js.map +1 -1
  95. package/dist/pipeline/runner.d.ts +65 -1
  96. package/dist/pipeline/runner.d.ts.map +1 -1
  97. package/dist/pipeline/runner.js +1029 -73
  98. package/dist/pipeline/runner.js.map +1 -1
  99. package/dist/state/manager.d.ts +14 -0
  100. package/dist/state/manager.d.ts.map +1 -1
  101. package/dist/state/manager.js +114 -0
  102. package/dist/state/manager.js.map +1 -1
  103. package/dist/state/memory-db.d.ts +15 -0
  104. package/dist/state/memory-db.d.ts.map +1 -1
  105. package/dist/state/memory-db.js +119 -10
  106. package/dist/state/memory-db.js.map +1 -1
  107. package/dist/state/runtime-state-store.d.ts +23 -0
  108. package/dist/state/runtime-state-store.d.ts.map +1 -0
  109. package/dist/state/runtime-state-store.js +100 -0
  110. package/dist/state/runtime-state-store.js.map +1 -0
  111. package/dist/state/state-bootstrap.d.ts +19 -0
  112. package/dist/state/state-bootstrap.d.ts.map +1 -0
  113. package/dist/state/state-bootstrap.js +394 -0
  114. package/dist/state/state-bootstrap.js.map +1 -0
  115. package/dist/state/state-projections.d.ts +5 -0
  116. package/dist/state/state-projections.d.ts.map +1 -0
  117. package/dist/state/state-projections.js +164 -0
  118. package/dist/state/state-projections.js.map +1 -0
  119. package/dist/state/state-reducer.d.ts +12 -0
  120. package/dist/state/state-reducer.d.ts.map +1 -0
  121. package/dist/state/state-reducer.js +146 -0
  122. package/dist/state/state-reducer.js.map +1 -0
  123. package/dist/state/state-validator.d.ts +12 -0
  124. package/dist/state/state-validator.d.ts.map +1 -0
  125. package/dist/state/state-validator.js +56 -0
  126. package/dist/state/state-validator.js.map +1 -0
  127. package/dist/utils/chapter-splitter.d.ts +2 -0
  128. package/dist/utils/chapter-splitter.d.ts.map +1 -1
  129. package/dist/utils/chapter-splitter.js +22 -4
  130. package/dist/utils/chapter-splitter.js.map +1 -1
  131. package/dist/utils/config-loader.d.ts +3 -1
  132. package/dist/utils/config-loader.d.ts.map +1 -1
  133. package/dist/utils/config-loader.js +14 -3
  134. package/dist/utils/config-loader.js.map +1 -1
  135. package/dist/utils/context-filter.js +1 -1
  136. package/dist/utils/context-filter.js.map +1 -1
  137. package/dist/utils/governed-context.d.ts +7 -0
  138. package/dist/utils/governed-context.d.ts.map +1 -0
  139. package/dist/utils/governed-context.js +22 -0
  140. package/dist/utils/governed-context.js.map +1 -0
  141. package/dist/utils/governed-working-set.d.ts +18 -0
  142. package/dist/utils/governed-working-set.d.ts.map +1 -0
  143. package/dist/utils/governed-working-set.js +295 -0
  144. package/dist/utils/governed-working-set.js.map +1 -0
  145. package/dist/utils/hook-governance.d.ts +26 -0
  146. package/dist/utils/hook-governance.d.ts.map +1 -0
  147. package/dist/utils/hook-governance.js +128 -0
  148. package/dist/utils/hook-governance.js.map +1 -0
  149. package/dist/utils/hook-health.d.ts +14 -0
  150. package/dist/utils/hook-health.d.ts.map +1 -0
  151. package/dist/utils/hook-health.js +68 -0
  152. package/dist/utils/hook-health.js.map +1 -0
  153. package/dist/utils/length-metrics.d.ts +10 -0
  154. package/dist/utils/length-metrics.d.ts.map +1 -0
  155. package/dist/utils/length-metrics.js +85 -0
  156. package/dist/utils/length-metrics.js.map +1 -0
  157. package/dist/utils/long-span-fatigue.d.ts +28 -0
  158. package/dist/utils/long-span-fatigue.d.ts.map +1 -0
  159. package/dist/utils/long-span-fatigue.js +359 -0
  160. package/dist/utils/long-span-fatigue.js.map +1 -0
  161. package/dist/utils/memory-retrieval.d.ts +39 -0
  162. package/dist/utils/memory-retrieval.d.ts.map +1 -0
  163. package/dist/utils/memory-retrieval.js +574 -0
  164. package/dist/utils/memory-retrieval.js.map +1 -0
  165. package/dist/utils/spot-fix-patches.d.ts +14 -0
  166. package/dist/utils/spot-fix-patches.d.ts.map +1 -0
  167. package/dist/utils/spot-fix-patches.js +75 -0
  168. package/dist/utils/spot-fix-patches.js.map +1 -0
  169. package/package.json +1 -1
@@ -2,19 +2,36 @@ import { BaseAgent } from "./base.js";
2
2
  import { buildWriterSystemPrompt } from "./writer-prompts.js";
3
3
  import { buildSettlerSystemPrompt, buildSettlerUserPrompt } from "./settler-prompts.js";
4
4
  import { buildObserverSystemPrompt, buildObserverUserPrompt } from "./observer-prompts.js";
5
+ import { parseSettlerDeltaOutput } from "./settler-delta-parser.js";
5
6
  import { parseSettlementOutput } from "./settler-parser.js";
6
7
  import { readGenreProfile, readBookRules } from "./rules-reader.js";
7
- import { validatePostWrite } from "./post-write-validator.js";
8
+ import { validatePostWrite, detectCrossChapterRepetition } from "./post-write-validator.js";
8
9
  import { analyzeAITells } from "./ai-tells.js";
10
+ import { buildLengthSpec } from "../utils/length-metrics.js";
9
11
  import { filterHooks, filterSummaries, filterSubplots, filterEmotionalArcs, filterCharacterMatrix } from "../utils/context-filter.js";
12
+ import { buildGovernedMemoryEvidenceBlocks } from "../utils/governed-context.js";
13
+ import { buildGovernedCharacterMatrixWorkingSet, buildGovernedHookWorkingSet, mergeCharacterMatrixMarkdown, mergeTableMarkdownByKey, } from "../utils/governed-working-set.js";
10
14
  import { extractPOVFromOutline, filterMatrixByPOV, filterHooksByPOV } from "../utils/pov-filter.js";
11
15
  import { parseCreativeOutput } from "./writer-parser.js";
16
+ import { buildRuntimeStateArtifacts, saveRuntimeStateSnapshot } from "../state/runtime-state-store.js";
17
+ import { parsePendingHooksMarkdown } from "../utils/memory-retrieval.js";
18
+ import { analyzeHookHealth } from "../utils/hook-health.js";
19
+ import { buildEnglishVarianceBrief } from "../utils/long-span-fatigue.js";
12
20
  import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
13
21
  import { join } from "node:path";
14
22
  export class WriterAgent extends BaseAgent {
15
23
  get name() {
16
24
  return "writer";
17
25
  }
26
+ localize(language, messages) {
27
+ return language === "en" ? messages.en : messages.zh;
28
+ }
29
+ logInfo(language, messages) {
30
+ this.ctx.logger?.info(this.localize(language, messages));
31
+ }
32
+ logWarn(language, messages) {
33
+ this.ctx.logger?.warn(this.localize(language, messages));
34
+ }
18
35
  async writeChapter(input) {
19
36
  const { book, bookDir, chapterNumber } = input;
20
37
  const [storyBible, volumeOutline, styleGuide, currentState, ledger, hooks, chapterSummaries, subplotBoard, emotionalArcs, characterMatrix, styleProfileRaw, parentCanon, fanficCanonRaw,] = await Promise.all([
@@ -45,6 +62,18 @@ export class WriterAgent extends BaseAgent {
45
62
  const relevantSummaries = this.findRelevantSummaries(chapterSummaries, volumeOutline, chapterNumber);
46
63
  const hasParentCanon = parentCanon !== "(文件尚未创建)";
47
64
  const hasFanficCanon = fanficCanonRaw !== "(文件尚未创建)";
65
+ const resolvedLanguage = book.language ?? genreProfile.language;
66
+ const targetWords = input.lengthSpec?.target ?? input.wordCountOverride ?? book.chapterWordCount;
67
+ const resolvedLengthSpec = input.lengthSpec ?? buildLengthSpec(targetWords, resolvedLanguage);
68
+ const governedMemoryBlocks = input.contextPackage
69
+ ? buildGovernedMemoryEvidenceBlocks(input.contextPackage)
70
+ : undefined;
71
+ const englishVarianceBrief = resolvedLanguage === "en"
72
+ ? await buildEnglishVarianceBrief({
73
+ bookDir,
74
+ chapterNumber,
75
+ })
76
+ : null;
48
77
  // Build fanfic context if fanfic_canon.md exists
49
78
  const fanficContext = hasFanficCanon && bookRules?.fanficMode
50
79
  ? {
@@ -54,54 +83,95 @@ export class WriterAgent extends BaseAgent {
54
83
  }
55
84
  : undefined;
56
85
  // ── Phase 1: Creative writing (temperature 0.7) ──
57
- const resolvedLanguage = book.language ?? genreProfile.language;
58
- const creativeSystemPrompt = buildWriterSystemPrompt(book, genreProfile, bookRules, bookRulesBody, genreBody, styleGuide, styleFingerprint, chapterNumber, "creative", fanficContext, resolvedLanguage);
59
- // Smart context filtering: inject only relevant parts of truth files
60
- const filteredHooks = filterHooks(hooks);
61
- const filteredSummaries = filterSummaries(chapterSummaries, chapterNumber);
62
- const filteredSubplots = filterSubplots(subplotBoard);
63
- const filteredArcs = filterEmotionalArcs(emotionalArcs, chapterNumber);
64
- const filteredMatrix = filterCharacterMatrix(characterMatrix, volumeOutline, bookRules?.protagonist?.name);
65
- // POV-aware filtering: limit context to what the POV character knows
66
- const povCharacter = extractPOVFromOutline(volumeOutline, chapterNumber);
67
- const povFilteredMatrix = povCharacter
68
- ? filterMatrixByPOV(filteredMatrix, povCharacter)
69
- : filteredMatrix;
70
- const povFilteredHooks = povCharacter
71
- ? filterHooksByPOV(filteredHooks, povCharacter, chapterSummaries)
72
- : filteredHooks;
73
- const creativeUserPrompt = this.buildUserPrompt({
74
- chapterNumber,
75
- storyBible,
76
- volumeOutline,
77
- currentState,
78
- ledger: genreProfile.numericalSystem ? ledger : "",
79
- hooks: povFilteredHooks,
80
- recentChapters,
81
- wordCount: input.wordCountOverride ?? book.chapterWordCount,
82
- externalContext: input.externalContext,
83
- chapterSummaries: filteredSummaries,
84
- subplotBoard: filteredSubplots,
85
- emotionalArcs: filteredArcs,
86
- characterMatrix: povFilteredMatrix,
87
- dialogueFingerprints,
88
- relevantSummaries,
89
- parentCanon: hasParentCanon ? parentCanon : undefined,
90
- language: book.language ?? genreProfile.language,
91
- });
86
+ const creativeSystemPrompt = buildWriterSystemPrompt(book, genreProfile, bookRules, bookRulesBody, genreBody, styleGuide, styleFingerprint, chapterNumber, "creative", fanficContext, resolvedLanguage, input.chapterIntent ? "governed" : "legacy", resolvedLengthSpec);
87
+ const creativeUserPrompt = input.chapterIntent && input.contextPackage && input.ruleStack
88
+ ? this.buildGovernedUserPrompt({
89
+ chapterNumber,
90
+ chapterIntent: input.chapterIntent,
91
+ contextPackage: input.contextPackage,
92
+ ruleStack: input.ruleStack,
93
+ trace: input.trace,
94
+ lengthSpec: resolvedLengthSpec,
95
+ language: book.language ?? genreProfile.language,
96
+ varianceBrief: englishVarianceBrief?.text,
97
+ })
98
+ : (() => {
99
+ // Smart context filtering: inject only relevant parts of truth files
100
+ const filteredHooks = filterHooks(hooks);
101
+ const filteredSummaries = filterSummaries(chapterSummaries, chapterNumber);
102
+ const filteredSubplots = filterSubplots(subplotBoard);
103
+ const filteredArcs = filterEmotionalArcs(emotionalArcs, chapterNumber);
104
+ const filteredMatrix = filterCharacterMatrix(characterMatrix, volumeOutline, bookRules?.protagonist?.name);
105
+ // POV-aware filtering: limit context to what the POV character knows
106
+ const povCharacter = extractPOVFromOutline(volumeOutline, chapterNumber);
107
+ const povFilteredMatrix = povCharacter
108
+ ? filterMatrixByPOV(filteredMatrix, povCharacter)
109
+ : filteredMatrix;
110
+ const povFilteredHooks = povCharacter
111
+ ? filterHooksByPOV(filteredHooks, povCharacter, chapterSummaries)
112
+ : filteredHooks;
113
+ return this.buildUserPrompt({
114
+ chapterNumber,
115
+ storyBible,
116
+ volumeOutline,
117
+ currentState,
118
+ ledger: genreProfile.numericalSystem ? ledger : "",
119
+ hooks: povFilteredHooks,
120
+ recentChapters,
121
+ lengthSpec: resolvedLengthSpec,
122
+ externalContext: input.externalContext,
123
+ chapterSummaries: filteredSummaries,
124
+ subplotBoard: filteredSubplots,
125
+ emotionalArcs: filteredArcs,
126
+ characterMatrix: povFilteredMatrix,
127
+ dialogueFingerprints,
128
+ relevantSummaries,
129
+ parentCanon: hasParentCanon ? parentCanon : undefined,
130
+ language: book.language ?? genreProfile.language,
131
+ });
132
+ })();
92
133
  const creativeTemperature = input.temperatureOverride ?? 0.7;
93
- this.ctx.logger?.info(`Phase 1: creative writing for chapter ${chapterNumber}`);
134
+ this.logInfo(resolvedLanguage, {
135
+ zh: `阶段 1:创作正文(第${chapterNumber}章)`,
136
+ en: `Phase 1: creative writing for chapter ${chapterNumber}`,
137
+ });
94
138
  // Scale maxTokens to chapter word count (Chinese ≈ 1.5 tokens/char)
95
- const targetWords = input.wordCountOverride ?? book.chapterWordCount;
96
139
  const creativeMaxTokens = Math.max(8192, Math.ceil(targetWords * 2));
97
140
  const creativeResponse = await this.chat([
98
141
  { role: "system", content: creativeSystemPrompt },
99
142
  { role: "user", content: creativeUserPrompt },
100
143
  ], { maxTokens: creativeMaxTokens, temperature: creativeTemperature });
101
144
  const creativeUsage = creativeResponse.usage;
102
- const creative = parseCreativeOutput(chapterNumber, creativeResponse.content);
145
+ const creative = parseCreativeOutput(chapterNumber, creativeResponse.content, resolvedLengthSpec.countingMode);
103
146
  // ── Phase 2: State settlement (temperature 0.3) ──
104
- this.ctx.logger?.info(`Phase 2: state settlement for chapter ${chapterNumber} (${creative.wordCount} chars)`);
147
+ this.logInfo(resolvedLanguage, {
148
+ zh: `阶段 2:状态结算(第${chapterNumber}章,${creative.wordCount}字)`,
149
+ en: `Phase 2: state settlement for chapter ${chapterNumber} (${creative.wordCount} words)`,
150
+ });
151
+ const isGovernedSettlement = Boolean(input.chapterIntent && input.contextPackage && input.ruleStack);
152
+ const filteredHooksForSettlement = isGovernedSettlement && input.contextPackage
153
+ ? buildGovernedHookWorkingSet({
154
+ hooksMarkdown: hooks,
155
+ contextPackage: input.contextPackage,
156
+ chapterIntent: input.chapterIntent,
157
+ chapterNumber,
158
+ language: resolvedLanguage,
159
+ })
160
+ : hooks;
161
+ const filteredSubplotsForSettlement = isGovernedSettlement
162
+ ? filterSubplots(subplotBoard)
163
+ : subplotBoard;
164
+ const filteredArcsForSettlement = isGovernedSettlement
165
+ ? filterEmotionalArcs(emotionalArcs, chapterNumber)
166
+ : emotionalArcs;
167
+ const filteredMatrixForSettlement = isGovernedSettlement
168
+ ? buildGovernedCharacterMatrixWorkingSet({
169
+ matrixMarkdown: characterMatrix,
170
+ chapterIntent: input.chapterIntent ?? volumeOutline,
171
+ contextPackage: input.contextPackage,
172
+ protagonistName: bookRules?.protagonist?.name,
173
+ })
174
+ : characterMatrix;
105
175
  const settleResult = await this.settle({
106
176
  book,
107
177
  genreProfile,
@@ -111,32 +181,78 @@ export class WriterAgent extends BaseAgent {
111
181
  content: creative.content,
112
182
  currentState,
113
183
  ledger: genreProfile.numericalSystem ? ledger : "",
114
- hooks,
115
- chapterSummaries,
116
- subplotBoard,
117
- emotionalArcs,
118
- characterMatrix,
184
+ hooks: filteredHooksForSettlement,
185
+ chapterSummaries: input.contextPackage ? filterSummaries(chapterSummaries, chapterNumber) : chapterSummaries,
186
+ subplotBoard: filteredSubplotsForSettlement,
187
+ emotionalArcs: filteredArcsForSettlement,
188
+ characterMatrix: filteredMatrixForSettlement,
119
189
  volumeOutline,
190
+ selectedEvidenceBlock: governedMemoryBlocks
191
+ ? [
192
+ governedMemoryBlocks.hooksBlock,
193
+ governedMemoryBlocks.summariesBlock,
194
+ governedMemoryBlocks.volumeSummariesBlock,
195
+ ]
196
+ .filter(Boolean)
197
+ .join("\n")
198
+ : undefined,
199
+ chapterIntent: input.chapterIntent,
200
+ contextPackage: input.contextPackage,
201
+ ruleStack: input.ruleStack,
202
+ originalHooks: hooks,
203
+ originalSubplots: subplotBoard,
204
+ originalEmotionalArcs: emotionalArcs,
205
+ originalCharacterMatrix: characterMatrix,
120
206
  });
121
207
  const settlement = settleResult.settlement;
122
208
  const settleUsage = settleResult.usage;
209
+ const runtimeStateArtifacts = await this.buildRuntimeStateArtifactsIfPresent(bookDir, settlement.runtimeStateDelta, resolvedLanguage);
210
+ const priorHookIds = new Set(parsePendingHooksMarkdown(hooks).map((hook) => hook.hookId));
211
+ const hookHealthIssues = settlement.runtimeStateDelta
212
+ && (runtimeStateArtifacts?.snapshot ?? settlement.runtimeStateSnapshot)
213
+ ? analyzeHookHealth({
214
+ language: resolvedLanguage,
215
+ chapterNumber,
216
+ hooks: (runtimeStateArtifacts?.snapshot ?? settlement.runtimeStateSnapshot).hooks.hooks,
217
+ delta: settlement.runtimeStateDelta,
218
+ existingHookIds: [...priorHookIds],
219
+ })
220
+ : [];
123
221
  // ── Post-write validation (regex + rule-based, zero LLM cost) ──
124
- const ruleViolations = validatePostWrite(creative.content, genreProfile, bookRules);
222
+ const ruleViolations = [
223
+ ...validatePostWrite(creative.content, genreProfile, bookRules, resolvedLanguage),
224
+ ...detectCrossChapterRepetition(creative.content, fingerprintChapters, resolvedLanguage),
225
+ ];
125
226
  const aiTellIssues = analyzeAITells(creative.content).issues;
126
227
  const postWriteErrors = ruleViolations.filter(v => v.severity === "error");
127
228
  const postWriteWarnings = ruleViolations.filter(v => v.severity === "warning");
128
229
  if (ruleViolations.length > 0) {
129
- this.ctx.logger?.warn(`Post-write: ${postWriteErrors.length} errors, ${postWriteWarnings.length} warnings in chapter ${chapterNumber}`);
230
+ this.logWarn(resolvedLanguage, {
231
+ zh: `后写校验:第${chapterNumber}章 ${postWriteErrors.length} 个错误,${postWriteWarnings.length} 个警告`,
232
+ en: `Post-write: ${postWriteErrors.length} errors, ${postWriteWarnings.length} warnings in chapter ${chapterNumber}`,
233
+ });
130
234
  for (const v of ruleViolations) {
131
235
  this.ctx.logger?.warn(`[${v.severity}] ${v.rule}: ${v.description}`);
132
236
  }
133
237
  }
134
238
  if (aiTellIssues.length > 0) {
135
- this.ctx.logger?.warn(`AI-tell check: ${aiTellIssues.length} issues in chapter ${chapterNumber}`);
239
+ this.logWarn(resolvedLanguage, {
240
+ zh: `AI 味检查:第${chapterNumber}章发现 ${aiTellIssues.length} 个问题`,
241
+ en: `AI-tell check: ${aiTellIssues.length} issues in chapter ${chapterNumber}`,
242
+ });
136
243
  for (const issue of aiTellIssues) {
137
244
  this.ctx.logger?.warn(`[${issue.severity}] ${issue.category}: ${issue.description}`);
138
245
  }
139
246
  }
247
+ if (hookHealthIssues.length > 0) {
248
+ this.logWarn(resolvedLanguage, {
249
+ zh: `伏笔健康:第${chapterNumber}章发现 ${hookHealthIssues.length} 条警告`,
250
+ en: `Hook health: ${hookHealthIssues.length} warning(s) in chapter ${chapterNumber}`,
251
+ });
252
+ for (const issue of hookHealthIssues) {
253
+ this.ctx.logger?.warn(`[${issue.severity}] ${issue.category}: ${issue.description}`);
254
+ }
255
+ }
140
256
  // ── Merge into WriteChapterOutput ──
141
257
  const tokenUsage = {
142
258
  promptTokens: creativeUsage.promptTokens + settleUsage.promptTokens,
@@ -150,15 +266,21 @@ export class WriterAgent extends BaseAgent {
150
266
  wordCount: creative.wordCount,
151
267
  preWriteCheck: creative.preWriteCheck,
152
268
  postSettlement: settlement.postSettlement,
153
- updatedState: settlement.updatedState,
269
+ runtimeStateDelta: settlement.runtimeStateDelta,
270
+ runtimeStateSnapshot: runtimeStateArtifacts?.snapshot ?? settlement.runtimeStateSnapshot,
271
+ updatedState: runtimeStateArtifacts?.currentStateMarkdown ?? settlement.updatedState,
154
272
  updatedLedger: settlement.updatedLedger,
155
- updatedHooks: settlement.updatedHooks,
156
- chapterSummary: settlement.chapterSummary,
273
+ updatedHooks: runtimeStateArtifacts?.hooksMarkdown ?? settlement.updatedHooks,
274
+ chapterSummary: settlement.runtimeStateDelta
275
+ ? this.renderDeltaSummaryRow(settlement.runtimeStateDelta)
276
+ : settlement.chapterSummary,
277
+ updatedChapterSummaries: runtimeStateArtifacts?.chapterSummariesMarkdown,
157
278
  updatedSubplots: settlement.updatedSubplots,
158
279
  updatedEmotionalArcs: settlement.updatedEmotionalArcs,
159
280
  updatedCharacterMatrix: settlement.updatedCharacterMatrix,
160
281
  postWriteErrors,
161
282
  postWriteWarnings,
283
+ hookHealthIssues,
162
284
  tokenUsage,
163
285
  };
164
286
  }
@@ -167,15 +289,24 @@ export class WriterAgent extends BaseAgent {
167
289
  const resolvedLang = params.book.language ?? params.genreProfile.language;
168
290
  const observerSystem = buildObserverSystemPrompt(params.book, params.genreProfile, resolvedLang);
169
291
  const observerUser = buildObserverUserPrompt(params.chapterNumber, params.title, params.content, resolvedLang);
170
- this.ctx.logger?.info(`Phase 2a: observing facts for chapter ${params.chapterNumber}`);
292
+ this.logInfo(resolvedLang, {
293
+ zh: `阶段 2a:提取第${params.chapterNumber}章事实`,
294
+ en: `Phase 2a: observing facts for chapter ${params.chapterNumber}`,
295
+ });
171
296
  const observerResponse = await this.chat([
172
297
  { role: "system", content: observerSystem },
173
298
  { role: "user", content: observerUser },
174
299
  ], { maxTokens: 4096, temperature: 0.5 });
175
300
  const observations = observerResponse.content;
176
301
  // Phase 2b: Reflector — merge observations into truth files
177
- this.ctx.logger?.info(`Phase 2b: reflecting observations into truth files`);
302
+ this.logInfo(resolvedLang, {
303
+ zh: "阶段 2b:把观察结果回写到真相文件",
304
+ en: "Phase 2b: reflecting observations into truth files",
305
+ });
178
306
  const settlerSystem = buildSettlerSystemPrompt(params.book, params.genreProfile, params.bookRules, resolvedLang);
307
+ const governedControlBlock = params.chapterIntent && params.contextPackage && params.ruleStack
308
+ ? this.buildSettlerGovernedControlBlock(params.chapterIntent, params.contextPackage, params.ruleStack, resolvedLang)
309
+ : undefined;
179
310
  const settlerUser = buildSettlerUserPrompt({
180
311
  chapterNumber: params.chapterNumber,
181
312
  title: params.title,
@@ -189,6 +320,8 @@ export class WriterAgent extends BaseAgent {
189
320
  characterMatrix: params.characterMatrix,
190
321
  volumeOutline: params.volumeOutline,
191
322
  observations,
323
+ selectedEvidenceBlock: params.selectedEvidenceBlock,
324
+ governedControlBlock,
192
325
  });
193
326
  // Settler outputs all truth files — scale with content size
194
327
  const settlerMaxTokens = Math.max(8192, Math.ceil(params.content.length * 0.8));
@@ -196,8 +329,41 @@ export class WriterAgent extends BaseAgent {
196
329
  { role: "system", content: settlerSystem },
197
330
  { role: "user", content: settlerUser },
198
331
  ], { maxTokens: settlerMaxTokens, temperature: 0.3 });
332
+ let mergedSettlement;
333
+ try {
334
+ const deltaOutput = parseSettlerDeltaOutput(response.content);
335
+ mergedSettlement = {
336
+ postSettlement: deltaOutput.postSettlement,
337
+ runtimeStateDelta: deltaOutput.runtimeStateDelta,
338
+ updatedState: "",
339
+ updatedLedger: "",
340
+ updatedHooks: "",
341
+ chapterSummary: "",
342
+ updatedSubplots: "",
343
+ updatedEmotionalArcs: "",
344
+ updatedCharacterMatrix: "",
345
+ };
346
+ }
347
+ catch {
348
+ const settlement = parseSettlementOutput(response.content, params.genreProfile);
349
+ mergedSettlement = governedControlBlock
350
+ ? {
351
+ ...settlement,
352
+ updatedHooks: mergeTableMarkdownByKey(params.originalHooks, settlement.updatedHooks, [0]),
353
+ updatedSubplots: settlement.updatedSubplots
354
+ ? mergeTableMarkdownByKey(params.originalSubplots, settlement.updatedSubplots, [0])
355
+ : settlement.updatedSubplots,
356
+ updatedEmotionalArcs: settlement.updatedEmotionalArcs
357
+ ? mergeTableMarkdownByKey(params.originalEmotionalArcs, settlement.updatedEmotionalArcs, [0, 1])
358
+ : settlement.updatedEmotionalArcs,
359
+ updatedCharacterMatrix: settlement.updatedCharacterMatrix
360
+ ? mergeCharacterMatrixMarkdown(params.originalCharacterMatrix, settlement.updatedCharacterMatrix)
361
+ : settlement.updatedCharacterMatrix,
362
+ }
363
+ : settlement;
364
+ }
199
365
  return {
200
- settlement: parseSettlementOutput(response.content, params.genreProfile),
366
+ settlement: mergedSettlement,
201
367
  usage: response.usage,
202
368
  };
203
369
  }
@@ -215,11 +381,18 @@ export class WriterAgent extends BaseAgent {
215
381
  "",
216
382
  output.content,
217
383
  ].join("\n");
384
+ const runtimeStateArtifacts = await this.resolveRuntimeStateArtifactsForOutput(bookDir, output, language);
218
385
  const writes = [
219
386
  writeFile(join(chaptersDir, filename), chapterContent, "utf-8"),
220
- writeFile(join(storyDir, "current_state.md"), output.updatedState, "utf-8"),
221
- writeFile(join(storyDir, "pending_hooks.md"), output.updatedHooks, "utf-8"),
387
+ writeFile(join(storyDir, "current_state.md"), runtimeStateArtifacts?.currentStateMarkdown ?? output.updatedState, "utf-8"),
388
+ writeFile(join(storyDir, "pending_hooks.md"), runtimeStateArtifacts?.hooksMarkdown ?? output.updatedHooks, "utf-8"),
222
389
  ];
390
+ if (runtimeStateArtifacts?.chapterSummariesMarkdown) {
391
+ writes.push(writeFile(join(storyDir, "chapter_summaries.md"), runtimeStateArtifacts.chapterSummariesMarkdown, "utf-8"));
392
+ }
393
+ if (runtimeStateArtifacts?.snapshot ?? output.runtimeStateSnapshot) {
394
+ writes.push(saveRuntimeStateSnapshot(bookDir, runtimeStateArtifacts?.snapshot ?? output.runtimeStateSnapshot));
395
+ }
223
396
  if (numericalSystem) {
224
397
  writes.push(writeFile(join(storyDir, "particle_ledger.md"), output.updatedLedger, "utf-8"));
225
398
  }
@@ -255,6 +428,7 @@ export class WriterAgent extends BaseAgent {
255
428
  本书是番外作品。以下正典约束不可违反,角色不得引用超出其信息边界的信息。
256
429
  ${params.parentCanon}\n`
257
430
  : "";
431
+ const lengthRequirementBlock = this.buildLengthRequirementBlock(params.lengthSpec, params.language ?? "zh");
258
432
  if (params.language === "en") {
259
433
  return `Write chapter ${params.chapterNumber}.
260
434
  ${contextBlock}
@@ -279,8 +453,7 @@ ${params.volumeOutline}
279
453
  - Pacing must match the outline's chapter span: if 5 chapters are planned for an arc, do not compress into 1-2.
280
454
  - PRE_WRITE_CHECK must identify which outline node this chapter covers.
281
455
 
282
- Requirements:
283
- - Chapter body must be at least ${params.wordCount} words
456
+ ${lengthRequirementBlock}
284
457
  - Output PRE_WRITE_CHECK first, then the chapter
285
458
  - Output only PRE_WRITE_CHECK, CHAPTER_TITLE, and CHAPTER_CONTENT blocks`;
286
459
  }
@@ -307,10 +480,129 @@ ${params.volumeOutline}
307
480
  - 剧情推进速度必须与卷纲规划的章节跨度匹配:如果卷纲规划某段剧情跨5章,不得在1-2章内讲完
308
481
  - PRE_WRITE_CHECK中必须明确标注本章对应的卷纲节点
309
482
 
310
- 要求:
311
- - 正文不少于${params.wordCount}字
483
+ ${lengthRequirementBlock}
484
+ - 先输出写作自检表,再写正文
485
+ - 只需输出 PRE_WRITE_CHECK、CHAPTER_TITLE、CHAPTER_CONTENT 三个区块`;
486
+ }
487
+ buildGovernedUserPrompt(params) {
488
+ const contextSections = params.contextPackage.selectedContext
489
+ .map((entry) => [
490
+ `### ${entry.source}`,
491
+ `- reason: ${entry.reason}`,
492
+ entry.excerpt ? `- excerpt: ${entry.excerpt}` : "",
493
+ ].filter(Boolean).join("\n"))
494
+ .join("\n\n");
495
+ const overrideLines = params.ruleStack.activeOverrides.length > 0
496
+ ? params.ruleStack.activeOverrides
497
+ .map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
498
+ .join("\n")
499
+ : "- none";
500
+ const diagnosticLines = params.ruleStack.sections.diagnostic.length > 0
501
+ ? params.ruleStack.sections.diagnostic.join(", ")
502
+ : "none";
503
+ const traceNotes = params.trace && params.trace.notes.length > 0
504
+ ? params.trace.notes.map((note) => `- ${note}`).join("\n")
505
+ : "- none";
506
+ const lengthRequirementBlock = this.buildLengthRequirementBlock(params.lengthSpec, params.language ?? "zh");
507
+ const varianceBlock = params.varianceBrief
508
+ ? `\n${params.varianceBrief}\n`
509
+ : "";
510
+ if (params.language === "en") {
511
+ return `Write chapter ${params.chapterNumber}.
512
+
513
+ ## Chapter Intent
514
+ ${params.chapterIntent}
515
+
516
+ ## Selected Context
517
+ ${contextSections || "(none)"}
518
+
519
+ ## Rule Stack
520
+ - Hard: ${params.ruleStack.sections.hard.join(", ") || "(none)"}
521
+ - Soft: ${params.ruleStack.sections.soft.join(", ") || "(none)"}
522
+ - Diagnostic: ${diagnosticLines}
523
+
524
+ ## Active Overrides
525
+ ${overrideLines}
526
+
527
+ ## Trace Notes
528
+ ${traceNotes}
529
+
530
+ ${varianceBlock}
531
+ ${lengthRequirementBlock}
532
+ - Output PRE_WRITE_CHECK first, then the chapter
533
+ - Output only PRE_WRITE_CHECK, CHAPTER_TITLE, and CHAPTER_CONTENT blocks`;
534
+ }
535
+ return `请续写第${params.chapterNumber}章。
536
+
537
+ ## 本章意图
538
+ ${params.chapterIntent}
539
+
540
+ ## 已选上下文
541
+ ${contextSections || "(无)"}
542
+
543
+ ## 规则栈
544
+ - 硬护栏:${params.ruleStack.sections.hard.join("、") || "(无)"}
545
+ - 软约束:${params.ruleStack.sections.soft.join("、") || "(无)"}
546
+ - 诊断规则:${diagnosticLines}
547
+
548
+ ## 当前覆盖
549
+ ${overrideLines}
550
+
551
+ ## 追踪说明
552
+ ${traceNotes}
553
+
554
+ ${varianceBlock}
555
+ ${lengthRequirementBlock}
312
556
  - 先输出写作自检表,再写正文
313
557
  - 只需输出 PRE_WRITE_CHECK、CHAPTER_TITLE、CHAPTER_CONTENT 三个区块`;
558
+ }
559
+ buildSettlerGovernedControlBlock(chapterIntent, contextPackage, ruleStack, language) {
560
+ const selectedContext = contextPackage.selectedContext
561
+ .map((entry) => `- ${entry.source}: ${entry.reason}${entry.excerpt ? ` | ${entry.excerpt}` : ""}`)
562
+ .join("\n");
563
+ const overrides = ruleStack.activeOverrides.length > 0
564
+ ? ruleStack.activeOverrides
565
+ .map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
566
+ .join("\n")
567
+ : "- none";
568
+ if (language === "en") {
569
+ return `\n## Chapter Control Inputs
570
+ ${chapterIntent}
571
+
572
+ ### Selected Context
573
+ ${selectedContext || "- none"}
574
+
575
+ ### Rule Stack
576
+ - Hard guardrails: ${ruleStack.sections.hard.join(", ") || "(none)"}
577
+ - Soft constraints: ${ruleStack.sections.soft.join(", ") || "(none)"}
578
+ - Diagnostic rules: ${ruleStack.sections.diagnostic.join(", ") || "(none)"}
579
+
580
+ ### Active Overrides
581
+ ${overrides}\n`;
582
+ }
583
+ return `\n## 本章控制输入
584
+ ${chapterIntent}
585
+
586
+ ### 已选上下文
587
+ ${selectedContext || "- none"}
588
+
589
+ ### 规则栈
590
+ - 硬护栏:${ruleStack.sections.hard.join("、") || "(无)"}
591
+ - 软约束:${ruleStack.sections.soft.join("、") || "(无)"}
592
+ - 诊断规则:${ruleStack.sections.diagnostic.join("、") || "(无)"}
593
+
594
+ ### 当前覆盖
595
+ ${overrides}\n`;
596
+ }
597
+ buildLengthRequirementBlock(lengthSpec, language) {
598
+ if (language === "en") {
599
+ return `Requirements:
600
+ - Target length: ${lengthSpec.target} words
601
+ - Acceptable range: ${lengthSpec.softMin}-${lengthSpec.softMax} words`;
602
+ }
603
+ return `要求:
604
+ - 目标字数:${lengthSpec.target}字
605
+ - 允许区间:${lengthSpec.softMin}-${lengthSpec.softMax}字`;
314
606
  }
315
607
  async loadRecentChapters(bookDir, currentChapter, count = 1) {
316
608
  const chaptersDir = join(bookDir, "chapters");
@@ -341,12 +633,15 @@ ${params.volumeOutline}
341
633
  }
342
634
  }
343
635
  /** Save new truth files (summaries, subplots, emotional arcs, character matrix). */
344
- async saveNewTruthFiles(bookDir, output) {
636
+ async saveNewTruthFiles(bookDir, output, language = "zh") {
345
637
  const storyDir = join(bookDir, "story");
346
638
  const writes = [];
347
639
  // Append chapter summary to chapter_summaries.md
348
- if (output.chapterSummary) {
349
- writes.push(this.appendChapterSummary(storyDir, output.chapterSummary));
640
+ if (!output.runtimeStateDelta && output.updatedChapterSummaries) {
641
+ writes.push(writeFile(join(storyDir, "chapter_summaries.md"), output.updatedChapterSummaries, "utf-8"));
642
+ }
643
+ else if (!output.runtimeStateDelta && output.chapterSummary) {
644
+ writes.push(this.appendChapterSummary(storyDir, output.chapterSummary, language));
350
645
  }
351
646
  // Overwrite subplot board
352
647
  if (output.updatedSubplots) {
@@ -362,7 +657,52 @@ ${params.volumeOutline}
362
657
  }
363
658
  await Promise.all(writes);
364
659
  }
365
- async appendChapterSummary(storyDir, summary) {
660
+ renderDeltaSummaryRow(delta) {
661
+ if (!delta.chapterSummary)
662
+ return "";
663
+ const summary = delta.chapterSummary;
664
+ const row = [
665
+ summary.chapter,
666
+ summary.title,
667
+ summary.characters,
668
+ summary.events,
669
+ summary.stateChanges,
670
+ summary.hookActivity,
671
+ summary.mood,
672
+ summary.chapterType,
673
+ ].map((value) => String(value).replace(/\|/g, "\\|").trim()).join(" | ");
674
+ return `| ${row} |`;
675
+ }
676
+ async buildRuntimeStateArtifactsIfPresent(bookDir, delta, language) {
677
+ if (!delta)
678
+ return null;
679
+ return buildRuntimeStateArtifacts({
680
+ bookDir,
681
+ delta,
682
+ language,
683
+ });
684
+ }
685
+ async resolveRuntimeStateArtifactsForOutput(bookDir, output, language) {
686
+ if (!output.runtimeStateDelta)
687
+ return null;
688
+ if (output.runtimeStateSnapshot
689
+ && output.updatedChapterSummaries
690
+ && output.updatedState
691
+ && output.updatedHooks) {
692
+ return {
693
+ snapshot: output.runtimeStateSnapshot,
694
+ currentStateMarkdown: output.updatedState,
695
+ hooksMarkdown: output.updatedHooks,
696
+ chapterSummariesMarkdown: output.updatedChapterSummaries,
697
+ };
698
+ }
699
+ return buildRuntimeStateArtifacts({
700
+ bookDir,
701
+ delta: output.runtimeStateDelta,
702
+ language,
703
+ });
704
+ }
705
+ async appendChapterSummary(storyDir, summary, language) {
366
706
  const summaryPath = join(storyDir, "chapter_summaries.md");
367
707
  let existing = "";
368
708
  try {
@@ -370,15 +710,34 @@ ${params.volumeOutline}
370
710
  }
371
711
  catch {
372
712
  // File doesn't exist yet — start with header
373
- existing = "# 章节摘要\n\n| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |\n|------|------|----------|----------|----------|----------|----------|----------|\n";
713
+ existing = language === "en"
714
+ ? "# Chapter Summaries\n\n| Chapter | Title | Characters | Key Events | State Changes | Hook Activity | Mood | Chapter Type |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n"
715
+ : "# 章节摘要\n\n| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |\n|------|------|----------|----------|----------|----------|----------|----------|\n";
374
716
  }
375
717
  // Extract only the data row(s) from the summary (skip header lines)
376
718
  const dataRows = summary
377
719
  .split("\n")
378
- .filter((line) => line.startsWith("|") && !line.startsWith("| 章节") && !line.startsWith("|--"))
720
+ .filter((line) => line.startsWith("|")
721
+ && !line.startsWith("| 章节")
722
+ && !line.startsWith("| Chapter")
723
+ && !line.startsWith("|--")
724
+ && !line.startsWith("| ---"))
379
725
  .join("\n");
380
726
  if (dataRows) {
381
- await writeFile(summaryPath, `${existing.trimEnd()}\n${dataRows}\n`, "utf-8");
727
+ // Deduplicate: remove existing rows with the same chapter number before appending
728
+ const newChapterNums = new Set(dataRows.split("\n")
729
+ .map((line) => line.split("|")[1]?.trim())
730
+ .filter((ch) => ch && /^\d+$/.test(ch)));
731
+ const deduped = existing
732
+ .split("\n")
733
+ .filter((line) => {
734
+ if (!line.startsWith("|"))
735
+ return true;
736
+ const chNum = line.split("|")[1]?.trim();
737
+ return !chNum || !newChapterNums.has(chNum);
738
+ })
739
+ .join("\n");
740
+ await writeFile(summaryPath, `${deduped.trimEnd()}\n${dataRows}\n`, "utf-8");
382
741
  }
383
742
  }
384
743
  buildStyleFingerprint(styleProfileRaw) {