@actalk/inkos-core 1.4.1 → 1.5.0-canary.47.1

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 (262) hide show
  1. package/dist/agent/agent-session.d.ts +15 -0
  2. package/dist/agent/agent-session.d.ts.map +1 -1
  3. package/dist/agent/agent-session.js +256 -26
  4. package/dist/agent/agent-session.js.map +1 -1
  5. package/dist/agent/agent-system-prompt.d.ts +8 -1
  6. package/dist/agent/agent-system-prompt.d.ts.map +1 -1
  7. package/dist/agent/agent-system-prompt.js +382 -176
  8. package/dist/agent/agent-system-prompt.js.map +1 -1
  9. package/dist/agent/agent-tools.d.ts +156 -19
  10. package/dist/agent/agent-tools.d.ts.map +1 -1
  11. package/dist/agent/agent-tools.js +980 -46
  12. package/dist/agent/agent-tools.js.map +1 -1
  13. package/dist/agent/context-transform.d.ts +4 -1
  14. package/dist/agent/context-transform.d.ts.map +1 -1
  15. package/dist/agent/context-transform.js +104 -9
  16. package/dist/agent/context-transform.js.map +1 -1
  17. package/dist/agent/index.d.ts +1 -1
  18. package/dist/agent/index.d.ts.map +1 -1
  19. package/dist/agent/index.js +1 -1
  20. package/dist/agent/index.js.map +1 -1
  21. package/dist/agents/architect.d.ts +11 -1
  22. package/dist/agents/architect.d.ts.map +1 -1
  23. package/dist/agents/architect.js +242 -114
  24. package/dist/agents/architect.js.map +1 -1
  25. package/dist/agents/chapter-analyzer.js +1 -1
  26. package/dist/agents/chapter-analyzer.js.map +1 -1
  27. package/dist/agents/composer.d.ts +36 -0
  28. package/dist/agents/composer.d.ts.map +1 -1
  29. package/dist/agents/composer.js +503 -20
  30. package/dist/agents/composer.js.map +1 -1
  31. package/dist/agents/continuity.d.ts +3 -0
  32. package/dist/agents/continuity.d.ts.map +1 -1
  33. package/dist/agents/continuity.js +28 -14
  34. package/dist/agents/continuity.js.map +1 -1
  35. package/dist/agents/en-prompt-sections.d.ts.map +1 -1
  36. package/dist/agents/en-prompt-sections.js +15 -1
  37. package/dist/agents/en-prompt-sections.js.map +1 -1
  38. package/dist/agents/fanfic-canon-importer.d.ts +1 -0
  39. package/dist/agents/fanfic-canon-importer.d.ts.map +1 -1
  40. package/dist/agents/fanfic-canon-importer.js +53 -6
  41. package/dist/agents/fanfic-canon-importer.js.map +1 -1
  42. package/dist/agents/foundation-reviewer.d.ts +1 -0
  43. package/dist/agents/foundation-reviewer.d.ts.map +1 -1
  44. package/dist/agents/foundation-reviewer.js +17 -12
  45. package/dist/agents/foundation-reviewer.js.map +1 -1
  46. package/dist/agents/length-normalizer.d.ts +1 -0
  47. package/dist/agents/length-normalizer.d.ts.map +1 -1
  48. package/dist/agents/length-normalizer.js +16 -3
  49. package/dist/agents/length-normalizer.js.map +1 -1
  50. package/dist/agents/planner-prompts.d.ts +7 -7
  51. package/dist/agents/planner-prompts.d.ts.map +1 -1
  52. package/dist/agents/planner-prompts.js +29 -29
  53. package/dist/agents/planner-prompts.js.map +1 -1
  54. package/dist/agents/planner.d.ts +6 -5
  55. package/dist/agents/planner.d.ts.map +1 -1
  56. package/dist/agents/planner.js +90 -6
  57. package/dist/agents/planner.js.map +1 -1
  58. package/dist/agents/post-write-validator.d.ts.map +1 -1
  59. package/dist/agents/post-write-validator.js +49 -0
  60. package/dist/agents/post-write-validator.js.map +1 -1
  61. package/dist/agents/reviser.js +10 -0
  62. package/dist/agents/reviser.js.map +1 -1
  63. package/dist/agents/rules-reader.d.ts +6 -14
  64. package/dist/agents/rules-reader.d.ts.map +1 -1
  65. package/dist/agents/rules-reader.js +15 -28
  66. package/dist/agents/rules-reader.js.map +1 -1
  67. package/dist/agents/short-fiction.d.ts +4 -0
  68. package/dist/agents/short-fiction.d.ts.map +1 -1
  69. package/dist/agents/short-fiction.js +51 -8
  70. package/dist/agents/short-fiction.js.map +1 -1
  71. package/dist/agents/state-validator.d.ts +0 -2
  72. package/dist/agents/state-validator.d.ts.map +1 -1
  73. package/dist/agents/state-validator.js +4 -16
  74. package/dist/agents/state-validator.js.map +1 -1
  75. package/dist/agents/style-analyzer.d.ts +1 -1
  76. package/dist/agents/style-analyzer.d.ts.map +1 -1
  77. package/dist/agents/style-analyzer.js +34 -17
  78. package/dist/agents/style-analyzer.js.map +1 -1
  79. package/dist/agents/writer-prompts.d.ts.map +1 -1
  80. package/dist/agents/writer-prompts.js +160 -12
  81. package/dist/agents/writer-prompts.js.map +1 -1
  82. package/dist/agents/writer.d.ts.map +1 -1
  83. package/dist/agents/writer.js +31 -9
  84. package/dist/agents/writer.js.map +1 -1
  85. package/dist/index.d.ts +18 -7
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +17 -7
  88. package/dist/index.js.map +1 -1
  89. package/dist/interaction/action-envelope.d.ts +261 -0
  90. package/dist/interaction/action-envelope.d.ts.map +1 -0
  91. package/dist/interaction/action-envelope.js +102 -0
  92. package/dist/interaction/action-envelope.js.map +1 -0
  93. package/dist/interaction/book-session-store.d.ts +6 -2
  94. package/dist/interaction/book-session-store.d.ts.map +1 -1
  95. package/dist/interaction/book-session-store.js +21 -3
  96. package/dist/interaction/book-session-store.js.map +1 -1
  97. package/dist/interaction/edit-controller.d.ts +5 -0
  98. package/dist/interaction/edit-controller.d.ts.map +1 -1
  99. package/dist/interaction/edit-controller.js +123 -26
  100. package/dist/interaction/edit-controller.js.map +1 -1
  101. package/dist/interaction/events.d.ts +4 -4
  102. package/dist/interaction/intents.d.ts +9 -6
  103. package/dist/interaction/intents.d.ts.map +1 -1
  104. package/dist/interaction/intents.js +2 -1
  105. package/dist/interaction/intents.js.map +1 -1
  106. package/dist/interaction/project-control.d.ts +3 -43
  107. package/dist/interaction/project-control.d.ts.map +1 -1
  108. package/dist/interaction/project-control.js +1 -53
  109. package/dist/interaction/project-control.js.map +1 -1
  110. package/dist/interaction/project-tools.d.ts +1 -1
  111. package/dist/interaction/project-tools.d.ts.map +1 -1
  112. package/dist/interaction/project-tools.js +41 -185
  113. package/dist/interaction/project-tools.js.map +1 -1
  114. package/dist/interaction/runtime.d.ts +1 -1
  115. package/dist/interaction/runtime.d.ts.map +1 -1
  116. package/dist/interaction/runtime.js +49 -75
  117. package/dist/interaction/runtime.js.map +1 -1
  118. package/dist/interaction/session-transcript-legacy.d.ts.map +1 -1
  119. package/dist/interaction/session-transcript-legacy.js +2 -0
  120. package/dist/interaction/session-transcript-legacy.js.map +1 -1
  121. package/dist/interaction/session-transcript-restore.d.ts +4 -3
  122. package/dist/interaction/session-transcript-restore.d.ts.map +1 -1
  123. package/dist/interaction/session-transcript-restore.js +234 -34
  124. package/dist/interaction/session-transcript-restore.js.map +1 -1
  125. package/dist/interaction/session-transcript-schema.d.ts +45 -12
  126. package/dist/interaction/session-transcript-schema.d.ts.map +1 -1
  127. package/dist/interaction/session-transcript-schema.js +6 -0
  128. package/dist/interaction/session-transcript-schema.js.map +1 -1
  129. package/dist/interaction/session-transcript.d.ts +8 -1
  130. package/dist/interaction/session-transcript.d.ts.map +1 -1
  131. package/dist/interaction/session-transcript.js +13 -1
  132. package/dist/interaction/session-transcript.js.map +1 -1
  133. package/dist/interaction/session.d.ts +78 -66
  134. package/dist/interaction/session.d.ts.map +1 -1
  135. package/dist/interaction/session.js +10 -2
  136. package/dist/interaction/session.js.map +1 -1
  137. package/dist/llm/provider.d.ts +32 -34
  138. package/dist/llm/provider.d.ts.map +1 -1
  139. package/dist/llm/provider.js +144 -127
  140. package/dist/llm/provider.js.map +1 -1
  141. package/dist/models/book-rules.d.ts +6 -4
  142. package/dist/models/book-rules.d.ts.map +1 -1
  143. package/dist/models/book-rules.js +187 -8
  144. package/dist/models/book-rules.js.map +1 -1
  145. package/dist/models/context-compression.d.ts +13 -0
  146. package/dist/models/context-compression.d.ts.map +1 -0
  147. package/dist/models/context-compression.js +2 -0
  148. package/dist/models/context-compression.js.map +1 -0
  149. package/dist/models/input-governance.d.ts +53 -12
  150. package/dist/models/input-governance.d.ts.map +1 -1
  151. package/dist/models/input-governance.js +16 -0
  152. package/dist/models/input-governance.js.map +1 -1
  153. package/dist/models/play.d.ts +530 -0
  154. package/dist/models/play.d.ts.map +1 -0
  155. package/dist/models/play.js +318 -0
  156. package/dist/models/play.js.map +1 -0
  157. package/dist/models/project.d.ts +8 -0
  158. package/dist/models/project.d.ts.map +1 -1
  159. package/dist/models/project.js +1 -0
  160. package/dist/models/project.js.map +1 -1
  161. package/dist/pipeline/chapter-review-cycle.d.ts.map +1 -1
  162. package/dist/pipeline/chapter-review-cycle.js +29 -3
  163. package/dist/pipeline/chapter-review-cycle.js.map +1 -1
  164. package/dist/pipeline/persisted-governed-plan.d.ts.map +1 -1
  165. package/dist/pipeline/persisted-governed-plan.js +98 -49
  166. package/dist/pipeline/persisted-governed-plan.js.map +1 -1
  167. package/dist/pipeline/runner.d.ts +31 -0
  168. package/dist/pipeline/runner.d.ts.map +1 -1
  169. package/dist/pipeline/runner.js +212 -68
  170. package/dist/pipeline/runner.js.map +1 -1
  171. package/dist/pipeline/short-fiction-runner.d.ts +14 -0
  172. package/dist/pipeline/short-fiction-runner.d.ts.map +1 -1
  173. package/dist/pipeline/short-fiction-runner.js +242 -94
  174. package/dist/pipeline/short-fiction-runner.js.map +1 -1
  175. package/dist/play/play-agents.d.ts +71 -0
  176. package/dist/play/play-agents.d.ts.map +1 -0
  177. package/dist/play/play-agents.js +511 -0
  178. package/dist/play/play-agents.js.map +1 -0
  179. package/dist/play/play-db-factory.d.ts +9 -0
  180. package/dist/play/play-db-factory.d.ts.map +1 -0
  181. package/dist/play/play-db-factory.js +18 -0
  182. package/dist/play/play-db-factory.js.map +1 -0
  183. package/dist/play/play-db.d.ts +22 -0
  184. package/dist/play/play-db.d.ts.map +1 -0
  185. package/dist/play/play-db.js +248 -0
  186. package/dist/play/play-db.js.map +1 -0
  187. package/dist/play/play-file-db.d.ts +32 -0
  188. package/dist/play/play-file-db.d.ts.map +1 -0
  189. package/dist/play/play-file-db.js +156 -0
  190. package/dist/play/play-file-db.js.map +1 -0
  191. package/dist/play/play-image.d.ts +58 -0
  192. package/dist/play/play-image.d.ts.map +1 -0
  193. package/dist/play/play-image.js +142 -0
  194. package/dist/play/play-image.js.map +1 -0
  195. package/dist/play/play-reducer.d.ts +31 -0
  196. package/dist/play/play-reducer.d.ts.map +1 -0
  197. package/dist/play/play-reducer.js +261 -0
  198. package/dist/play/play-reducer.js.map +1 -0
  199. package/dist/play/play-runner.d.ts +102 -0
  200. package/dist/play/play-runner.d.ts.map +1 -0
  201. package/dist/play/play-runner.js +465 -0
  202. package/dist/play/play-runner.js.map +1 -0
  203. package/dist/play/play-store.d.ts +112 -0
  204. package/dist/play/play-store.d.ts.map +1 -0
  205. package/dist/play/play-store.js +311 -0
  206. package/dist/play/play-store.js.map +1 -0
  207. package/dist/prompts/short-fiction.d.ts +5 -0
  208. package/dist/prompts/short-fiction.d.ts.map +1 -1
  209. package/dist/prompts/short-fiction.js +46 -22
  210. package/dist/prompts/short-fiction.js.map +1 -1
  211. package/dist/state/state-bootstrap.d.ts.map +1 -1
  212. package/dist/state/state-bootstrap.js +12 -25
  213. package/dist/state/state-bootstrap.js.map +1 -1
  214. package/dist/state/state-reducer.js +31 -22
  215. package/dist/state/state-reducer.js.map +1 -1
  216. package/dist/utils/book-eval.d.ts +35 -0
  217. package/dist/utils/book-eval.d.ts.map +1 -0
  218. package/dist/utils/book-eval.js +116 -0
  219. package/dist/utils/book-eval.js.map +1 -0
  220. package/dist/utils/chapter-memo-parser.d.ts +10 -7
  221. package/dist/utils/chapter-memo-parser.d.ts.map +1 -1
  222. package/dist/utils/chapter-memo-parser.js +86 -43
  223. package/dist/utils/chapter-memo-parser.js.map +1 -1
  224. package/dist/utils/context-assembly.d.ts +2 -0
  225. package/dist/utils/context-assembly.d.ts.map +1 -1
  226. package/dist/utils/context-assembly.js +38 -1
  227. package/dist/utils/context-assembly.js.map +1 -1
  228. package/dist/utils/hook-health.d.ts.map +1 -1
  229. package/dist/utils/hook-health.js +5 -2
  230. package/dist/utils/hook-health.js.map +1 -1
  231. package/dist/utils/hook-ledger-validator.d.ts +1 -1
  232. package/dist/utils/hook-ledger-validator.d.ts.map +1 -1
  233. package/dist/utils/hook-ledger-validator.js +5 -5
  234. package/dist/utils/hook-ledger-validator.js.map +1 -1
  235. package/dist/utils/hook-lifecycle.d.ts +1 -0
  236. package/dist/utils/hook-lifecycle.d.ts.map +1 -1
  237. package/dist/utils/hook-lifecycle.js +10 -3
  238. package/dist/utils/hook-lifecycle.js.map +1 -1
  239. package/dist/utils/language.d.ts +10 -0
  240. package/dist/utils/language.d.ts.map +1 -0
  241. package/dist/utils/language.js +18 -0
  242. package/dist/utils/language.js.map +1 -0
  243. package/dist/utils/length-metrics.d.ts +3 -0
  244. package/dist/utils/length-metrics.d.ts.map +1 -1
  245. package/dist/utils/length-metrics.js +8 -0
  246. package/dist/utils/length-metrics.js.map +1 -1
  247. package/dist/utils/memory-retrieval.d.ts.map +1 -1
  248. package/dist/utils/memory-retrieval.js +19 -15
  249. package/dist/utils/memory-retrieval.js.map +1 -1
  250. package/dist/utils/outline-paths.d.ts +12 -0
  251. package/dist/utils/outline-paths.d.ts.map +1 -1
  252. package/dist/utils/outline-paths.js +68 -0
  253. package/dist/utils/outline-paths.js.map +1 -1
  254. package/package.json +1 -1
  255. package/dist/interaction/nl-router.d.ts +0 -8
  256. package/dist/interaction/nl-router.d.ts.map +0 -1
  257. package/dist/interaction/nl-router.js +0 -218
  258. package/dist/interaction/nl-router.js.map +0 -1
  259. package/dist/pipeline/agent.d.ts +0 -15
  260. package/dist/pipeline/agent.d.ts.map +0 -1
  261. package/dist/pipeline/agent.js +0 -597
  262. package/dist/pipeline/agent.js.map +0 -1
@@ -3,23 +3,35 @@ import { dirname, join } from "node:path";
3
3
  import { BaseAgent } from "./base.js";
4
4
  import { ContextPackageSchema, } from "../models/input-governance.js";
5
5
  import { parseChapterSummariesMarkdown, retrieveMemorySelection, } from "../utils/memory-retrieval.js";
6
- import { buildGovernedRuleStack, buildGovernedTrace, } from "../utils/context-assembly.js";
6
+ import { buildGovernedRuleStack, buildGovernedTrace, isProtectedContextSource, } from "../utils/context-assembly.js";
7
7
  import { writeGovernedRuntimeArtifacts } from "../utils/runtime-writer.js";
8
+ import { estimateTextTokens } from "../llm/provider.js";
8
9
  export async function composeGovernedChapter(input) {
9
10
  const storyDir = join(input.bookDir, "story");
10
11
  const runtimeDir = join(storyDir, "runtime");
11
12
  await mkdir(runtimeDir, { recursive: true });
12
- const selectedContext = await collectSelectedContext(storyDir, input.plan, input.book.language ?? "zh");
13
- const contextPackage = ContextPackageSchema.parse({
13
+ const selectedContext = await collectSelectedContext(storyDir, input.plan, input.book.language ?? "zh", input.outlineSectionSelector);
14
+ const initialContextPackage = ContextPackageSchema.parse({
14
15
  chapter: input.chapterNumber,
15
16
  selectedContext,
16
17
  });
18
+ const budgeted = await applyContextBudgetIfNeeded({
19
+ contextPackage: initialContextPackage,
20
+ chapterNumber: input.chapterNumber,
21
+ goal: input.plan.intent.goal,
22
+ language: input.book.language ?? "zh",
23
+ contextBudget: input.contextBudget,
24
+ compiler: input.compressibleContextCompiler,
25
+ onContextCompression: input.onContextCompression,
26
+ });
27
+ const contextPackage = budgeted.contextPackage;
17
28
  const ruleStack = buildGovernedRuleStack(input.plan, input.chapterNumber);
18
29
  const trace = buildGovernedTrace({
19
30
  chapterNumber: input.chapterNumber,
20
31
  plan: input.plan,
21
32
  contextPackage,
22
33
  composerInputs: [input.plan.runtimePath],
34
+ notes: budgeted.notes,
23
35
  });
24
36
  const { contextPath, ruleStackPath, tracePath, } = await writeGovernedRuntimeArtifacts({
25
37
  runtimeDir,
@@ -37,15 +49,279 @@ export async function composeGovernedChapter(input) {
37
49
  tracePath,
38
50
  };
39
51
  }
52
+ async function applyContextBudgetIfNeeded(params) {
53
+ const budget = params.contextBudget;
54
+ if (!budget || budget.contextWindowTokens <= 0) {
55
+ return { contextPackage: params.contextPackage, notes: [] };
56
+ }
57
+ const availableInputTokens = budget.contextWindowTokens - Math.max(0, budget.reservedOutputTokens);
58
+ const selectedContext = params.contextPackage.selectedContext;
59
+ const totalTokens = estimateSelectedContextTokens(selectedContext);
60
+ if (totalTokens <= availableInputTokens) {
61
+ return { contextPackage: params.contextPackage, notes: [] };
62
+ }
63
+ const protectedEntries = selectedContext.filter((entry) => isProtectedContextSource(entry.source));
64
+ const compressibleEntries = selectedContext.filter((entry) => !isProtectedContextSource(entry.source));
65
+ const protectedTokens = estimateSelectedContextTokens(protectedEntries);
66
+ if (protectedTokens > availableInputTokens) {
67
+ params.onContextCompression?.({
68
+ category: "story_context",
69
+ phase: "error",
70
+ message: "Protected context exceeds available input budget.",
71
+ protectedTokens,
72
+ compressibleTokens: totalTokens - protectedTokens,
73
+ budgetTokens: availableInputTokens,
74
+ sources: protectedEntries.map((entry) => entry.source),
75
+ });
76
+ throw new Error(`Protected context exceeds available input budget (${protectedTokens}/${availableInputTokens} tokens). ` +
77
+ "InkOS will not compress protected author intent, current focus, hard state, or active hook evidence.");
78
+ }
79
+ if (compressibleEntries.length === 0) {
80
+ return { contextPackage: params.contextPackage, notes: ["context-over-budget-no-compressible-entries"] };
81
+ }
82
+ if (!params.compiler) {
83
+ params.onContextCompression?.({
84
+ category: "story_context",
85
+ phase: "error",
86
+ message: "Context exceeds available input budget but no compiler was provided.",
87
+ protectedTokens,
88
+ compressibleTokens: estimateSelectedContextTokens(compressibleEntries),
89
+ budgetTokens: availableInputTokens,
90
+ sources: compressibleEntries.map((entry) => entry.source),
91
+ });
92
+ throw new Error(`Context exceeds available input budget (${totalTokens}/${availableInputTokens} tokens), ` +
93
+ "but no compressible context compiler was provided.");
94
+ }
95
+ const compileBudget = Math.max(1, availableInputTokens - protectedTokens);
96
+ const compressibleTokens = estimateSelectedContextTokens(compressibleEntries);
97
+ params.onContextCompression?.({
98
+ category: "story_context",
99
+ phase: "start",
100
+ protectedTokens,
101
+ compressibleTokens,
102
+ budgetTokens: compileBudget,
103
+ sources: compressibleEntries.map((entry) => entry.source),
104
+ });
105
+ let compiled;
106
+ try {
107
+ compiled = (await params.compiler({
108
+ chapterNumber: params.chapterNumber,
109
+ goal: params.goal,
110
+ language: params.language,
111
+ maxInputTokens: compileBudget,
112
+ protectedEntries,
113
+ compressibleEntries,
114
+ })).trim();
115
+ }
116
+ catch (error) {
117
+ params.onContextCompression?.({
118
+ category: "story_context",
119
+ phase: "error",
120
+ message: error instanceof Error ? error.message : String(error),
121
+ protectedTokens,
122
+ compressibleTokens,
123
+ budgetTokens: compileBudget,
124
+ sources: compressibleEntries.map((entry) => entry.source),
125
+ });
126
+ throw error;
127
+ }
128
+ if (!compiled) {
129
+ params.onContextCompression?.({
130
+ category: "story_context",
131
+ phase: "error",
132
+ message: "Compressible context compiler returned empty output.",
133
+ protectedTokens,
134
+ compressibleTokens,
135
+ budgetTokens: compileBudget,
136
+ sources: compressibleEntries.map((entry) => entry.source),
137
+ });
138
+ throw new Error("Compressible context compiler returned empty output.");
139
+ }
140
+ params.onContextCompression?.({
141
+ category: "story_context",
142
+ phase: "end",
143
+ protectedTokens,
144
+ compressibleTokens,
145
+ budgetTokens: compileBudget,
146
+ sources: compressibleEntries.map((entry) => entry.source),
147
+ });
148
+ return {
149
+ contextPackage: ContextPackageSchema.parse({
150
+ chapter: params.contextPackage.chapter,
151
+ selectedContext: [
152
+ ...protectedEntries,
153
+ {
154
+ source: "runtime/compiled-compressible-context",
155
+ reason: "Semantic compilation of lower-priority context after protected context exceeded the input budget.",
156
+ excerpt: compiled,
157
+ },
158
+ ],
159
+ }),
160
+ notes: ["compiled-compressible-context"],
161
+ };
162
+ }
163
+ function estimateSelectedContextTokens(entries) {
164
+ return entries.reduce((total, entry) => (total + estimateTextTokens([entry.source, entry.reason, entry.excerpt].filter(Boolean).join("\n"))), 0);
165
+ }
166
+ function renderContextEntries(entries) {
167
+ return entries.map((entry) => [
168
+ `### ${entry.source}`,
169
+ `Reason: ${entry.reason}`,
170
+ entry.excerpt ? entry.excerpt : "(no excerpt)",
171
+ ].join("\n")).join("\n\n");
172
+ }
173
+ function parseSelectedSources(raw) {
174
+ const trimmed = raw.trim()
175
+ .replace(/^```(?:json)?\s*/i, "")
176
+ .replace(/```\s*$/i, "")
177
+ .trim();
178
+ const parse = (value) => JSON.parse(value);
179
+ let parsed;
180
+ try {
181
+ parsed = parse(trimmed);
182
+ }
183
+ catch {
184
+ const start = trimmed.indexOf("{");
185
+ const end = trimmed.lastIndexOf("}");
186
+ if (start < 0 || end <= start)
187
+ return [];
188
+ try {
189
+ parsed = parse(trimmed.slice(start, end + 1));
190
+ }
191
+ catch {
192
+ return [];
193
+ }
194
+ }
195
+ if (!parsed || typeof parsed !== "object")
196
+ return [];
197
+ const values = parsed.selectedSources;
198
+ if (!Array.isArray(values))
199
+ return [];
200
+ return values.filter((value) => typeof value === "string" && value.trim().length > 0);
201
+ }
40
202
  export class ComposerAgent extends BaseAgent {
41
203
  get name() {
42
204
  return "composer";
43
205
  }
44
206
  async composeChapter(input) {
45
- return composeGovernedChapter(input);
207
+ const contextBudget = input.contextBudget ?? contextBudgetFromClient(this.ctx.client);
208
+ return composeGovernedChapter({
209
+ ...input,
210
+ contextBudget,
211
+ compressibleContextCompiler: input.compressibleContextCompiler
212
+ ?? (contextBudget ? (request) => this.compileCompressibleContext(request) : undefined),
213
+ outlineSectionSelector: input.outlineSectionSelector ?? ((request) => this.selectOutlineSections(request)),
214
+ });
215
+ }
216
+ async selectOutlineSections(request) {
217
+ if (request.candidates.length <= 1) {
218
+ return request.candidates.map((candidate) => candidate.source);
219
+ }
220
+ const isEn = request.language === "en";
221
+ const candidates = request.candidates.map((candidate, index) => [
222
+ `#${index + 1} ${candidate.source}`,
223
+ `heading: ${candidate.heading}`,
224
+ candidate.excerpt,
225
+ ].join("\n")).join("\n\n");
226
+ const system = isEn
227
+ ? [
228
+ "You are InkOS's semantic outline-section selector.",
229
+ "Select only the outline sections needed for the current chapter. Prefer semantic relevance over keyword overlap.",
230
+ "Return strict JSON only: {\"selectedSources\":[\"...\"]}. Use exact source ids from the candidates. If uncertain, include the safest relevant anchors rather than inventing ids.",
231
+ ].join("\n")
232
+ : [
233
+ "你是 InkOS 的语义大纲选段器。",
234
+ "只选择当前章节真正需要的大纲段落。按语义相关性判断,不要按关键词重合机械选择。",
235
+ "只返回严格 JSON:{\"selectedSources\":[\"...\"]}。必须使用候选里的精确 source id;不确定时选最安全的相关锚点,不要编造 id。",
236
+ ].join("\n");
237
+ const user = isEn
238
+ ? [
239
+ `File: ${request.fileName}`,
240
+ `Chapter: ${request.chapterNumber}`,
241
+ `Goal: ${request.goal}`,
242
+ `Outline node: ${request.outlineNode}`,
243
+ "",
244
+ "Candidates:",
245
+ candidates,
246
+ ].join("\n")
247
+ : [
248
+ `文件:${request.fileName}`,
249
+ `章节:第${request.chapterNumber}章`,
250
+ `目标:${request.goal}`,
251
+ `大纲节点:${request.outlineNode}`,
252
+ "",
253
+ "候选段落:",
254
+ candidates,
255
+ ].join("\n");
256
+ const response = await this.chat([
257
+ { role: "system", content: system },
258
+ { role: "user", content: user },
259
+ ], {
260
+ temperature: 0.1,
261
+ maxTokens: 1024,
262
+ });
263
+ const allowed = new Set(request.candidates.map((candidate) => candidate.source));
264
+ return parseSelectedSources(response.content).filter((source) => allowed.has(source));
46
265
  }
266
+ async compileCompressibleContext(request) {
267
+ const isEn = request.language === "en";
268
+ const protectedBlock = renderContextEntries(request.protectedEntries);
269
+ const compressibleBlock = renderContextEntries(request.compressibleEntries);
270
+ const system = isEn
271
+ ? [
272
+ "You are InkOS's semantic context compiler.",
273
+ "Only compile the COMPRESSIBLE CONTEXT. The PROTECTED CONTEXT is binding reference material and must not be rewritten, summarized as a substitute, or weakened.",
274
+ "Output concise Markdown with source pointers. Preserve names, unresolved promises, evidence, timing, and constraints that may affect the next chapter. Drop low-relevance noise.",
275
+ ].join("\n")
276
+ : [
277
+ "你是 InkOS 的语义上下文编译器。",
278
+ "只能编译【可压缩上下文】。【受保护上下文】是绑定参照,不得改写、不得替代总结、不得削弱。",
279
+ "输出简洁 Markdown,保留来源指针。保留会影响下一章的人名、未兑现承诺、证据、时间点和约束,丢弃低相关噪声。",
280
+ ].join("\n");
281
+ const user = isEn
282
+ ? [
283
+ `Chapter: ${request.chapterNumber}`,
284
+ `Goal: ${request.goal}`,
285
+ `Target budget for compiled context: <= ${request.maxInputTokens} estimated input tokens`,
286
+ "",
287
+ "## Protected Context (reference only, do not compile)",
288
+ protectedBlock || "(none)",
289
+ "",
290
+ "## Compressible Context (compile this)",
291
+ compressibleBlock || "(none)",
292
+ ].join("\n")
293
+ : [
294
+ `章节:第${request.chapterNumber}章`,
295
+ `目标:${request.goal}`,
296
+ `压缩后目标预算:不超过 ${request.maxInputTokens} 估算输入 tokens`,
297
+ "",
298
+ "## 受保护上下文(只作为参照,不要编译它)",
299
+ protectedBlock || "(无)",
300
+ "",
301
+ "## 可压缩上下文(只编译这一部分)",
302
+ compressibleBlock || "(无)",
303
+ ].join("\n");
304
+ const response = await this.chat([
305
+ { role: "system", content: system },
306
+ { role: "user", content: user },
307
+ ], {
308
+ temperature: 0.2,
309
+ maxTokens: Math.min(8192, Math.max(512, request.maxInputTokens)),
310
+ });
311
+ return response.content.trim();
312
+ }
313
+ }
314
+ export function contextBudgetFromClient(client) {
315
+ const contextWindowTokens = client._piModel?.contextWindow;
316
+ if (!Number.isFinite(contextWindowTokens) || !contextWindowTokens || contextWindowTokens <= 0) {
317
+ return undefined;
318
+ }
319
+ return {
320
+ contextWindowTokens,
321
+ reservedOutputTokens: Math.max(0, client.defaults.maxTokens),
322
+ };
47
323
  }
48
- async function collectSelectedContext(storyDir, plan, language) {
324
+ async function collectSelectedContext(storyDir, plan, language, outlineSectionSelector) {
49
325
  const retrievalHints = deriveRetrievalHints(plan);
50
326
  const memoBodyExcerpt = plan.memo.body.trim();
51
327
  const chapterMemoEntry = memoBodyExcerpt.length > 0
@@ -65,10 +341,15 @@ async function collectSelectedContext(storyDir, plan, language) {
65
341
  }];
66
342
  const entries = await Promise.all([
67
343
  maybeContextSource(storyDir, "current_focus.md", "Current task focus for this chapter."),
344
+ maybeContextSource(storyDir, "author_intent.md", "User's long-term authorial intent and direction — binding, overrides model defaults."),
68
345
  maybeContextSource(storyDir, "audit_drift.md", "Carry forward audit drift guidance from the previous chapter without polluting hard state facts."),
69
- maybeContextSource(storyDir, "current_state.md", "Preserve hard state facts referenced by the active chapter brief or hard constraints.", retrievalHints),
70
- maybeContextSource(storyDir, "outline/story_frame.md", "Preserve canon constraints referenced by the active chapter brief or hard constraints.", retrievalHints),
71
- maybeContextSource(storyDir, "outline/volume_map.md", "Anchor the default planning node for this chapter.", plan.intent.outlineNode ? [plan.intent.outlineNode] : []),
346
+ maybeContextSource(storyDir, "current_state.md", "Preserve hard state facts referenced by the active chapter brief or hard constraints."),
347
+ ]);
348
+ const outlineEntries = [
349
+ ...await maybeOutlineSectionSources(storyDir, "outline/story_frame.md", "Preserve canon constraints referenced by the active chapter brief or hard constraints.", plan, "story-frame", language, outlineSectionSelector),
350
+ ...await maybeOutlineSectionSources(storyDir, "outline/volume_map.md", "Anchor the default planning node for this chapter.", plan, "volume-map", language, outlineSectionSelector),
351
+ ];
352
+ const canonEntries = await Promise.all([
72
353
  maybeContextSource(storyDir, "parent_canon.md", "Preserve parent canon constraints for governed continuation or fanfic writing."),
73
354
  maybeContextSource(storyDir, "fanfic_canon.md", "Preserve extracted fanfic canon constraints for governed writing."),
74
355
  ]);
@@ -108,6 +389,8 @@ async function collectSelectedContext(storyDir, plan, language) {
108
389
  return [
109
390
  ...chapterMemoEntry,
110
391
  ...entries.filter((entry) => entry !== null),
392
+ ...outlineEntries,
393
+ ...canonEntries.filter((entry) => entry !== null),
111
394
  ...trailEntries,
112
395
  ...hookDebtEntries,
113
396
  ...factEntries,
@@ -242,7 +525,7 @@ async function buildHookDebtEntries(storyDir, plan, activeHooks, language) {
242
525
  }];
243
526
  });
244
527
  }
245
- async function maybeContextSource(storyDir, fileName, reason, preferredExcerpts = []) {
528
+ async function maybeContextSource(storyDir, fileName, reason) {
246
529
  const path = join(storyDir, fileName);
247
530
  let content = await readFileOrDefault(path);
248
531
  let resolvedFileName = fileName;
@@ -264,9 +547,219 @@ async function maybeContextSource(storyDir, fileName, reason, preferredExcerpts
264
547
  return {
265
548
  source: `story/${resolvedFileName}`,
266
549
  reason,
267
- excerpt: pickExcerpt(content, preferredExcerpts),
550
+ excerpt: content.trim(),
268
551
  };
269
552
  }
553
+ async function maybeOutlineSectionSources(storyDir, fileName, reason, plan, kind, language, outlineSectionSelector) {
554
+ const path = join(storyDir, fileName);
555
+ const content = await readFileOrDefault(path);
556
+ if (!content || content === "(文件尚未创建)") {
557
+ const legacyFallback = outlineFallback(fileName);
558
+ if (!legacyFallback)
559
+ return [];
560
+ const legacyContent = await readFileOrDefault(join(storyDir, legacyFallback));
561
+ if (!legacyContent || legacyContent === "(文件尚未创建)")
562
+ return [];
563
+ return await selectOutlineSectionEntries({
564
+ fileName: legacyFallback,
565
+ content: legacyContent,
566
+ reason,
567
+ plan,
568
+ kind,
569
+ language,
570
+ outlineSectionSelector,
571
+ });
572
+ }
573
+ return await selectOutlineSectionEntries({
574
+ fileName,
575
+ content,
576
+ reason,
577
+ plan,
578
+ kind,
579
+ language,
580
+ outlineSectionSelector,
581
+ });
582
+ }
583
+ async function selectOutlineSectionEntries(params) {
584
+ const sections = splitMarkdownSections(params.content);
585
+ if (sections.length === 0) {
586
+ return [{
587
+ source: `story/${params.fileName}#document`,
588
+ reason: params.reason,
589
+ excerpt: params.content.trim(),
590
+ }];
591
+ }
592
+ const hints = deriveOutlineSelectionHints(params.plan);
593
+ const selected = sections.filter((section) => params.kind === "story-frame"
594
+ ? isRelevantStoryFrameSection(section, hints)
595
+ : isRelevantVolumeMapSection(section, hints, params.plan.intent.chapter));
596
+ const finalSections = selected.length > 0 ? selected : fallbackOutlineSections(sections, params.kind, params.plan.intent.chapter);
597
+ const candidates = sections.map((section) => ({
598
+ source: `story/${params.fileName}#${slugifyAnchor(section.heading)}`,
599
+ heading: section.heading,
600
+ excerpt: section.raw.trim(),
601
+ }));
602
+ if (params.outlineSectionSelector) {
603
+ try {
604
+ const selectedSources = await params.outlineSectionSelector({
605
+ fileName: params.fileName,
606
+ kind: params.kind,
607
+ chapterNumber: params.plan.intent.chapter,
608
+ goal: params.plan.intent.goal,
609
+ outlineNode: params.plan.intent.outlineNode ?? "",
610
+ language: params.language,
611
+ candidates,
612
+ });
613
+ const selectedSourceSet = new Set(selectedSources);
614
+ const llmSections = sections.filter((section) => selectedSourceSet.has(`story/${params.fileName}#${slugifyAnchor(section.heading)}`));
615
+ if (llmSections.length > 0) {
616
+ return dedupeBySource(llmSections.map((section) => ({
617
+ source: `story/${params.fileName}#${slugifyAnchor(section.heading)}`,
618
+ reason: params.reason,
619
+ excerpt: section.raw.trim(),
620
+ })));
621
+ }
622
+ }
623
+ catch {
624
+ // Semantic section selection is quality guidance, not a hard dependency.
625
+ // If the provider flakes or returns malformed JSON, keep the deterministic
626
+ // fallback so chapter production does not stall.
627
+ }
628
+ }
629
+ return dedupeBySource(finalSections.map((section) => ({
630
+ source: `story/${params.fileName}#${slugifyAnchor(section.heading)}`,
631
+ reason: params.reason,
632
+ excerpt: section.raw.trim(),
633
+ })));
634
+ }
635
+ function splitMarkdownSections(content) {
636
+ const sections = [];
637
+ let current = null;
638
+ for (const line of content.split(/\r?\n/)) {
639
+ const headingMatch = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
640
+ if (headingMatch) {
641
+ if (current && current.lines.some((entry) => entry.trim().length > 0)) {
642
+ sections.push(current);
643
+ }
644
+ current = {
645
+ heading: headingMatch[2].trim(),
646
+ lines: [line],
647
+ };
648
+ continue;
649
+ }
650
+ if (current) {
651
+ current.lines.push(line);
652
+ }
653
+ }
654
+ if (current && current.lines.some((entry) => entry.trim().length > 0)) {
655
+ sections.push(current);
656
+ }
657
+ return sections
658
+ .map((section) => ({
659
+ heading: section.heading,
660
+ raw: section.lines.join("\n").trim(),
661
+ }))
662
+ .filter((section) => section.raw.length > 0);
663
+ }
664
+ function deriveOutlineSelectionHints(plan) {
665
+ return [
666
+ plan.intent.goal,
667
+ plan.intent.outlineNode,
668
+ plan.intent.arcContext,
669
+ ...plan.intent.mustKeep,
670
+ ...plan.intent.mustAvoid,
671
+ ...plan.intent.styleEmphasis,
672
+ plan.memo.goal,
673
+ plan.memo.body,
674
+ ...plan.memo.threadRefs,
675
+ ].filter((value) => Boolean(value && value.trim()));
676
+ }
677
+ function isRelevantStoryFrameSection(section, hints) {
678
+ const heading = normalizeForMatch(section.heading);
679
+ const sectionText = normalizeForMatch(section.raw);
680
+ const hardHeadingSignals = [
681
+ "世界观",
682
+ "底色",
683
+ "铁律",
684
+ "规则",
685
+ "核心冲突",
686
+ "终局",
687
+ "world",
688
+ "tonal",
689
+ "rule",
690
+ "core conflict",
691
+ "endgame",
692
+ ];
693
+ if (hardHeadingSignals.some((signal) => heading.includes(normalizeForMatch(signal)))) {
694
+ return true;
695
+ }
696
+ return matchesOutlineHints(sectionText, hints);
697
+ }
698
+ function isRelevantVolumeMapSection(section, hints, chapterNumber) {
699
+ const heading = normalizeForMatch(section.heading);
700
+ if (headingMentionsChapter(heading, chapterNumber)) {
701
+ return true;
702
+ }
703
+ return matchesOutlineHints(normalizeForMatch(section.raw), hints);
704
+ }
705
+ function matchesOutlineHints(sectionText, hints) {
706
+ for (const hint of hints) {
707
+ const terms = extractMatchTerms(hint);
708
+ if (terms.length === 0)
709
+ continue;
710
+ const hits = terms.filter((term) => sectionText.includes(term));
711
+ if (hits.length >= Math.min(2, terms.length)) {
712
+ return true;
713
+ }
714
+ }
715
+ return false;
716
+ }
717
+ function fallbackOutlineSections(sections, kind, chapterNumber) {
718
+ if (kind === "volume-map") {
719
+ const chapterHit = sections.find((section) => headingMentionsChapter(normalizeForMatch(section.heading), chapterNumber));
720
+ if (chapterHit)
721
+ return [chapterHit];
722
+ }
723
+ return sections.slice(0, 1);
724
+ }
725
+ function extractMatchTerms(value) {
726
+ const normalized = normalizeForMatch(value);
727
+ const terms = new Set();
728
+ for (const term of normalized.match(/[a-z0-9]{3,}/g) ?? []) {
729
+ terms.add(term);
730
+ }
731
+ for (const term of normalized.match(/[\u4e00-\u9fff]{2,}/g) ?? []) {
732
+ terms.add(term);
733
+ }
734
+ return [...terms].filter((term) => term.length >= 2);
735
+ }
736
+ function normalizeForMatch(value) {
737
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
738
+ }
739
+ function headingMentionsChapter(normalizedHeading, chapterNumber) {
740
+ return normalizedHeading.includes(`chapter ${chapterNumber}`)
741
+ || normalizedHeading.includes(`chapter${chapterNumber}`)
742
+ || normalizedHeading.includes(`ch.${chapterNumber}`)
743
+ || normalizedHeading.includes(`ch${chapterNumber}`)
744
+ || normalizedHeading.includes(`第${chapterNumber}章`);
745
+ }
746
+ function slugifyAnchor(value) {
747
+ return value
748
+ .trim()
749
+ .toLowerCase()
750
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
751
+ .replace(/^-+|-+$/g, "")
752
+ || "section";
753
+ }
754
+ function dedupeBySource(entries) {
755
+ const seen = new Set();
756
+ return entries.filter((entry) => {
757
+ if (seen.has(entry.source))
758
+ return false;
759
+ seen.add(entry.source);
760
+ return true;
761
+ });
762
+ }
270
763
  function outlineFallback(fileName) {
271
764
  if (fileName === "outline/story_frame.md")
272
765
  return "story_bible.md";
@@ -274,16 +767,6 @@ function outlineFallback(fileName) {
274
767
  return "volume_outline.md";
275
768
  return null;
276
769
  }
277
- function pickExcerpt(content, preferredExcerpts) {
278
- for (const preferred of preferredExcerpts) {
279
- if (preferred && content.includes(preferred))
280
- return preferred;
281
- }
282
- return content
283
- .split("\n")
284
- .map((line) => line.trim())
285
- .find((line) => line.length > 0 && !line.startsWith("#"));
286
- }
287
770
  function toFactAnchor(predicate) {
288
771
  return predicate
289
772
  .trim()