@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
@@ -1,4 +1,7 @@
1
1
  import { Type } from "@mariozechner/pi-ai";
2
+ import { ArchitectIncompleteFoundationError } from "../agents/architect.js";
3
+ import { defaultChapterLength } from "../utils/length-metrics.js";
4
+ import { inferLanguage } from "../utils/language.js";
2
5
  import { readFile, writeFile, readdir, stat } from "node:fs/promises";
3
6
  import { isAbsolute, join, resolve } from "node:path";
4
7
  import { StateManager } from "../state/manager.js";
@@ -8,6 +11,10 @@ import { assertSafeBookId, deriveBookIdFromTitle } from "../utils/book-id.js";
8
11
  import { safeChildPath } from "../utils/path-safety.js";
9
12
  import { normalizePlatformId, normalizePlatformOrOther } from "../models/book.js";
10
13
  import { generateShortFictionCover, runShortFictionProduction } from "../pipeline/short-fiction-runner.js";
14
+ import { createPlayDB } from "../play/play-db-factory.js";
15
+ import { PlayRunner } from "../play/play-runner.js";
16
+ import { PlayStore } from "../play/play-store.js";
17
+ import { ActionPayloadSchema, isUsablePlayInitialScene } from "../interaction/action-envelope.js";
11
18
  function textResult(text, details) {
12
19
  return { content: [{ type: "text", text }], details: details };
13
20
  }
@@ -33,8 +40,331 @@ function createDeterministicInteractionTools(pipeline, projectRoot) {
33
40
  const state = new StateManager(projectRoot);
34
41
  return createInteractionToolsFromDeps(pipeline, state);
35
42
  }
43
+ function closePlayDB(db) {
44
+ db.close?.();
45
+ }
46
+ function safePlayId(value, fallback) {
47
+ const raw = (value?.trim() || fallback).slice(0, 80);
48
+ if (!raw || raw === "." || raw === ".." || raw.includes("/") || raw.includes("\\") || raw.includes("\0")) {
49
+ throw new Error(`Invalid play id: ${JSON.stringify(value)}`);
50
+ }
51
+ return raw;
52
+ }
53
+ const SuggestedActionParam = Type.Union([
54
+ Type.String({ description: "A short clickable player action." }),
55
+ Type.Object({
56
+ label: Type.Optional(Type.String({ description: "Short clickable player action." })),
57
+ action: Type.Optional(Type.String({ description: "Concrete action text." })),
58
+ text: Type.Optional(Type.String({ description: "Concrete action text." })),
59
+ title: Type.Optional(Type.String({ description: "Short action title." })),
60
+ description: Type.Optional(Type.String({ description: "Optional action description." })),
61
+ }, { description: "A model may describe an action as an object; InkOS will normalize it to one short action string." }),
62
+ ], { description: "Suggested action as a string or small action object." });
63
+ function normalizeSuggestedActions(value) {
64
+ if (!Array.isArray(value))
65
+ return [];
66
+ const out = [];
67
+ for (const raw of value) {
68
+ const text = typeof raw === "string"
69
+ ? raw
70
+ : raw.action ?? raw.label ?? raw.text ?? raw.title ?? raw.description ?? "";
71
+ const normalized = text.replace(/\s+/g, " ").trim();
72
+ if (normalized)
73
+ out.push(normalized);
74
+ if (out.length >= 4)
75
+ break;
76
+ }
77
+ return out;
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // 1. Proposed Action Tool (propose_action)
81
+ // ---------------------------------------------------------------------------
82
+ const ProposeActionParams = Type.Object({
83
+ action: Type.Union([
84
+ Type.Literal("create_book"),
85
+ Type.Literal("short_run"),
86
+ Type.Literal("play_start"),
87
+ Type.Literal("generate_cover"),
88
+ Type.Literal("fanfic_init"),
89
+ Type.Literal("continuation_import"),
90
+ Type.Literal("spinoff_create"),
91
+ Type.Literal("style_imitation"),
92
+ ], {
93
+ description: "The production or assisted Studio workflow the user appears to want, but which needs explicit confirmation from general chat.",
94
+ }),
95
+ instruction: Type.String({
96
+ description: "The exact production instruction to run after the user confirms. It must be self-contained: include title, story direction, active target, output directory, cover visual direction, or any referenced context that would otherwise be lost when switching sessions.",
97
+ }),
98
+ title: Type.Optional(Type.String({
99
+ description: "Short user-facing title for the confirmation card.",
100
+ })),
101
+ summary: Type.Optional(Type.String({
102
+ description: "One or two sentences explaining what will happen if the user confirms.",
103
+ })),
104
+ createBook: Type.Optional(Type.Object({
105
+ title: Type.Optional(Type.String({
106
+ description: "Confirmed long-form book title.",
107
+ })),
108
+ genre: Type.Optional(Type.String({
109
+ description: "Confirmed book genre/category.",
110
+ })),
111
+ platform: Type.Optional(Type.Union([
112
+ Type.Literal("tomato"),
113
+ Type.Literal("qidian"),
114
+ Type.Literal("feilu"),
115
+ Type.Literal("other"),
116
+ ], { description: "Confirmed target platform, e.g. tomato for 番茄." })),
117
+ language: Type.Optional(Type.Union([
118
+ Type.Literal("zh"),
119
+ Type.Literal("en"),
120
+ ], { description: "Confirmed writing language." })),
121
+ targetChapters: Type.Optional(Type.Number({
122
+ description: "Confirmed total chapter count.",
123
+ })),
124
+ chapterWordCount: Type.Optional(Type.Number({
125
+ description: "Confirmed per-chapter length in the book's native unit.",
126
+ })),
127
+ }, { description: "Structured execution args for action=create_book. Put platform/length here; do not leave them only in instruction text." })),
128
+ shortRun: Type.Optional(Type.Object({
129
+ direction: Type.Optional(Type.String({
130
+ description: "Confirmed standalone short direction.",
131
+ })),
132
+ reference: Type.Optional(Type.String({
133
+ description: "Optional confirmed reference notes or constraints.",
134
+ })),
135
+ storyId: Type.Optional(Type.String({
136
+ description: "Optional confirmed output id under shorts/.",
137
+ })),
138
+ chapters: Type.Optional(Type.Number({
139
+ description: "Confirmed complete short chapter count, 12-18.",
140
+ })),
141
+ charsPerChapter: Type.Optional(Type.Number({
142
+ description: "Confirmed Chinese characters per chapter, 900-1200. Do not put total story length here.",
143
+ })),
144
+ cover: Type.Optional(Type.Boolean({
145
+ description: "Whether to attempt cover generation.",
146
+ })),
147
+ }, { description: "Structured execution args for action=short_run." })),
148
+ playStart: Type.Optional(Type.Object({
149
+ title: Type.Optional(Type.String({ description: "Confirmed interactive world title." })),
150
+ premise: Type.Optional(Type.String({ description: "Confirmed playable premise." })),
151
+ worldContract: Type.Optional(Type.String({
152
+ description: "Confirmed durable world contract in natural language: time semantics, role autonomy, object/clue/relationship rules, taboos, or other long-lived rules the user explicitly asked for. Do not invent RPG/level systems.",
153
+ })),
154
+ visualContract: Type.Optional(Type.String({
155
+ description: "Confirmed visual contract for Play illustrations in natural language. Only include user-defined visual semantics; do not invent game frames, colored tiers, UI, or stats.",
156
+ })),
157
+ mode: Type.Optional(Type.Union([
158
+ Type.Literal("open"),
159
+ Type.Literal("guided"),
160
+ ], { description: "Confirmed play mode: open for free actions, guided for suggested choices." })),
161
+ initialScene: Type.Optional(Type.String({
162
+ description: "Confirmed opening scene shown to the player after confirmation. It must be pure narrative prose, not a title/setup/rules summary, not a question prompt, and not an action/options list.",
163
+ })),
164
+ suggestedActions: Type.Optional(Type.Array(SuggestedActionParam, {
165
+ description: "Optional action springboards shown as separate UI chips. Do not include these in initialScene.",
166
+ })),
167
+ }, { description: "Structured execution args for action=play_start." })),
168
+ generateCover: Type.Optional(Type.Object({
169
+ title: Type.Optional(Type.String({ description: "Confirmed cover title." })),
170
+ intro: Type.Optional(Type.String({ description: "Confirmed synopsis/hook for the cover." })),
171
+ sellingPoints: Type.Optional(Type.String({ description: "Confirmed selling points for the cover." })),
172
+ coverPrompt: Type.Optional(Type.String({ description: "Confirmed visual direction." })),
173
+ outputDir: Type.Optional(Type.String({ description: "Confirmed output directory." })),
174
+ }, { description: "Structured execution args for action=generate_cover." })),
175
+ });
176
+ function proposedActionSessionKind(action) {
177
+ if (action === "create_book")
178
+ return "book-create";
179
+ if (action === "play_start")
180
+ return "play";
181
+ if (action === "fanfic_init" || action === "continuation_import" || action === "spinoff_create" || action === "style_imitation")
182
+ return "chat";
183
+ return "short";
184
+ }
185
+ function proposedActionTargetRoute(action) {
186
+ if (action === "fanfic_init")
187
+ return "import:fanfic";
188
+ if (action === "continuation_import")
189
+ return "import:chapters";
190
+ if (action === "spinoff_create")
191
+ return "import:spinoff";
192
+ if (action === "style_imitation")
193
+ return "import:imitation";
194
+ return undefined;
195
+ }
196
+ function proposedActionFallbackTitle(action, isZh) {
197
+ switch (action) {
198
+ case "create_book":
199
+ return isZh ? "创建长篇书籍" : "Create a long-form book";
200
+ case "short_run":
201
+ return isZh ? "生成 InkOS Short" : "Generate InkOS Short";
202
+ case "play_start":
203
+ return isZh ? "启动 InkOS Play" : "Start InkOS Play";
204
+ case "generate_cover":
205
+ return isZh ? "生成封面" : "Generate cover";
206
+ case "fanfic_init":
207
+ return isZh ? "打开同人创作" : "Open fanfiction workflow";
208
+ case "continuation_import":
209
+ return isZh ? "打开续写导入" : "Open continuation import";
210
+ case "spinoff_create":
211
+ return isZh ? "打开番外创作" : "Open side-story workflow";
212
+ case "style_imitation":
213
+ return isZh ? "打开仿写/文风分析" : "Open style imitation";
214
+ }
215
+ }
216
+ function proposedActionFallbackSummary(action, isZh) {
217
+ if (proposedActionTargetRoute(action)) {
218
+ return isZh
219
+ ? "确认后只会打开现有 Studio 工具,不会直接生成成品。"
220
+ : "After confirmation, InkOS will only open the existing Studio tool; it will not generate finished content directly.";
221
+ }
222
+ return isZh
223
+ ? "确认后会切换到对应入口并执行这条需求。"
224
+ : "After confirmation, InkOS will switch to the matching surface and run this request.";
225
+ }
226
+ function compactObject(value) {
227
+ if (!value || typeof value !== "object")
228
+ return undefined;
229
+ const out = {};
230
+ for (const [key, raw] of Object.entries(value)) {
231
+ if (typeof raw === "string") {
232
+ const text = raw.trim();
233
+ if (text)
234
+ out[key] = text;
235
+ continue;
236
+ }
237
+ if (Array.isArray(raw)) {
238
+ const items = raw.filter((item) => typeof item === "string" && item.trim().length > 0)
239
+ .map((item) => item.trim());
240
+ if (items.length > 0)
241
+ out[key] = items;
242
+ continue;
243
+ }
244
+ if (raw !== undefined && raw !== null) {
245
+ out[key] = raw;
246
+ }
247
+ }
248
+ return Object.keys(out).length > 0 ? out : undefined;
249
+ }
250
+ function compactPlayStartPayload(value) {
251
+ if (!value || typeof value !== "object")
252
+ return undefined;
253
+ const out = {};
254
+ const title = value.title?.trim();
255
+ if (title)
256
+ out.title = title;
257
+ const premise = value.premise?.trim();
258
+ if (premise)
259
+ out.premise = premise;
260
+ const worldContract = value.worldContract?.trim();
261
+ if (worldContract)
262
+ out.worldContract = worldContract;
263
+ const visualContract = value.visualContract?.trim();
264
+ if (visualContract)
265
+ out.visualContract = visualContract;
266
+ if (value.mode)
267
+ out.mode = value.mode;
268
+ const initialScene = value.initialScene?.trim();
269
+ if (isUsablePlayInitialScene(initialScene))
270
+ out.initialScene = initialScene;
271
+ const suggestedActions = normalizeSuggestedActions(value.suggestedActions);
272
+ if (suggestedActions.length > 0)
273
+ out.suggestedActions = suggestedActions;
274
+ return Object.keys(out).length > 0 ? out : undefined;
275
+ }
276
+ function proposedActionPayload(params) {
277
+ const payload = {};
278
+ if (params.action === "create_book") {
279
+ const createBook = compactObject(params.createBook);
280
+ if (createBook)
281
+ payload.createBook = createBook;
282
+ }
283
+ if (params.action === "short_run") {
284
+ const shortRun = compactObject(params.shortRun);
285
+ if (shortRun)
286
+ payload.shortRun = shortRun;
287
+ }
288
+ if (params.action === "play_start") {
289
+ const playStart = compactPlayStartPayload(params.playStart);
290
+ if (playStart)
291
+ payload.playStart = playStart;
292
+ }
293
+ if (params.action === "generate_cover") {
294
+ const generateCover = compactObject(params.generateCover);
295
+ if (generateCover)
296
+ payload.generateCover = generateCover;
297
+ }
298
+ return Object.keys(payload).length > 0 ? payload : undefined;
299
+ }
300
+ function validateProposedActionPayload(payload) {
301
+ if (!payload)
302
+ return {};
303
+ const parsed = ActionPayloadSchema.safeParse(payload);
304
+ if (parsed.success)
305
+ return { payload: parsed.data };
306
+ return { error: parsed.error.issues.map((issue) => issue.message).join("; ") };
307
+ }
308
+ function requireProposedText(value, label) {
309
+ if (typeof value === "string" && value.trim().length > 0)
310
+ return;
311
+ throw new Error(`propose_action is missing ${label}; retry with that field in the structured payload, not only in summary or instruction.`);
312
+ }
313
+ function assertExecutableProposedAction(params, payload) {
314
+ if (params.action === "create_book") {
315
+ requireProposedText(payload?.createBook?.title, "createBook.title");
316
+ return;
317
+ }
318
+ if (params.action === "play_start") {
319
+ requireProposedText(payload?.playStart?.title, "playStart.title");
320
+ requireProposedText(payload?.playStart?.premise, "playStart.premise");
321
+ requireProposedText(payload?.playStart?.initialScene, "playStart.initialScene");
322
+ return;
323
+ }
324
+ if (params.action === "generate_cover") {
325
+ requireProposedText(payload?.generateCover?.title, "generateCover.title");
326
+ }
327
+ }
328
+ export function createProposeActionTool(language = "zh", options = {}) {
329
+ return {
330
+ name: "propose_action",
331
+ description: "Ask the user to confirm a production action from general chat. " +
332
+ "Use this before creating books, generating shorts/covers, or starting play worlds when the user has not clicked a confirmation.",
333
+ label: "Confirm Action",
334
+ parameters: ProposeActionParams,
335
+ async execute(_toolCallId, params) {
336
+ const targetSessionKind = proposedActionSessionKind(params.action);
337
+ const targetRoute = proposedActionTargetRoute(params.action);
338
+ const isZh = language === "zh";
339
+ const title = params.title?.trim() || proposedActionFallbackTitle(params.action, isZh);
340
+ const summary = params.summary?.trim() || proposedActionFallbackSummary(params.action, isZh);
341
+ const proposedPayload = validateProposedActionPayload(proposedActionPayload(params));
342
+ if (proposedPayload.error) {
343
+ throw new Error(`Invalid proposed action payload: ${proposedPayload.error}`);
344
+ }
345
+ const actionPayload = proposedPayload.payload;
346
+ assertExecutableProposedAction(params, actionPayload);
347
+ return textResult([
348
+ title,
349
+ summary,
350
+ "",
351
+ `Instruction: ${params.instruction}`,
352
+ ].join("\n"), {
353
+ kind: "proposed_action",
354
+ action: params.action,
355
+ targetSessionKind,
356
+ ...(targetRoute ? { targetRoute } : {}),
357
+ sameSession: options.sameSession === true,
358
+ title,
359
+ summary,
360
+ instruction: params.instruction,
361
+ ...(actionPayload ? { actionPayload } : {}),
362
+ });
363
+ },
364
+ };
365
+ }
36
366
  // ---------------------------------------------------------------------------
37
- // 1. SubAgentTool (sub_agent)
367
+ // 2. SubAgentTool (sub_agent)
38
368
  // ---------------------------------------------------------------------------
39
369
  const SubAgentParams = Type.Object({
40
370
  agent: Type.Union([
@@ -63,7 +393,7 @@ const SubAgentParams = Type.Object({
63
393
  Type.Literal("en"),
64
394
  ], { description: "architect only: writing language. Default: zh" })),
65
395
  targetChapters: Type.Optional(Type.Number({ description: "architect only: total chapter count. Default: 200" })),
66
- chapterWordCount: Type.Optional(Type.Number({ description: "architect/writer: words per chapter. Default: 3000" })),
396
+ chapterWordCount: Type.Optional(Type.Number({ description: "architect/writer: per-chapter length in the book's native unit (zh characters / en words). Default: 3000 zh, 2000 en" })),
67
397
  revise: Type.Optional(Type.Boolean({
68
398
  description: "architect only: true 表示在当前 active book 上重新生成架构稿,而不是新建书籍。no-book creation sessions cannot revise an existing book.",
69
399
  })),
@@ -86,6 +416,27 @@ const SubAgentParams = Type.Object({
86
416
  ], { description: "exporter only: export format. Default: txt" })),
87
417
  approvedOnly: Type.Optional(Type.Boolean({ description: "exporter only: export only approved chapters. Default: false" })),
88
418
  });
419
+ const ArchitectCreateSubAgentParams = Type.Object({
420
+ agent: Type.Literal("architect"),
421
+ instruction: Type.String({ description: "Confirmed self-contained book-creation instruction for the architect." }),
422
+ bookId: Type.Optional(Type.String({
423
+ description: "Optional new book ID. Usually omit it and let InkOS derive the ID from title.",
424
+ })),
425
+ title: Type.Optional(Type.String({ description: "Confirmed book title. Required when creating a book." })),
426
+ genre: Type.Optional(Type.String({ description: "Confirmed book genre." })),
427
+ platform: Type.Optional(Type.Union([
428
+ Type.Literal("tomato"),
429
+ Type.Literal("qidian"),
430
+ Type.Literal("feilu"),
431
+ Type.Literal("other"),
432
+ ], { description: "Confirmed target platform. Default: other" })),
433
+ language: Type.Optional(Type.Union([
434
+ Type.Literal("zh"),
435
+ Type.Literal("en"),
436
+ ], { description: "Confirmed writing language. Default: zh" })),
437
+ targetChapters: Type.Optional(Type.Number({ description: "Confirmed total chapter count. Default: 200" })),
438
+ chapterWordCount: Type.Optional(Type.Number({ description: "Confirmed per-chapter length in the book's native unit. Default: 3000 zh, 2000 en" })),
439
+ });
89
440
  function prepareSubAgentArguments(args) {
90
441
  if (!args || typeof args !== "object" || Array.isArray(args)) {
91
442
  return args;
@@ -102,14 +453,16 @@ function prepareSubAgentArguments(args) {
102
453
  }
103
454
  return prepared;
104
455
  }
105
- export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
456
+ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options = {}) {
106
457
  return {
107
458
  name: "sub_agent",
108
- description: "Delegate a heavy operation to a specialised sub-agent. " +
109
- "Use agent='architect' to initialise a new book, 'writer' to write the next chapter, " +
110
- "'auditor' to audit quality, 'reviser' to revise a chapter, 'exporter' to export.",
459
+ description: options.architectCreateOnly
460
+ ? "Create a new long-form InkOS book foundation. This confirmation turn can only call agent='architect'; writing chapters happens after the session is bound to the created book."
461
+ : "Delegate a heavy operation to a specialised sub-agent. " +
462
+ "Use agent='architect' to initialise a new book, 'writer' to write the next chapter, " +
463
+ "'auditor' to audit quality, 'reviser' to revise a chapter, 'exporter' to export.",
111
464
  label: "Sub-Agent",
112
- parameters: SubAgentParams,
465
+ parameters: options.architectCreateOnly ? ArchitectCreateSubAgentParams : SubAgentParams,
113
466
  prepareArguments: prepareSubAgentArguments,
114
467
  async execute(_toolCallId, params, _signal, onUpdate) {
115
468
  const { agent, instruction, bookId, title, chapterNumber, genre, platform, language, targetChapters, chapterWordCount, revise, feedback, mode, format, approvedOnly } = params;
@@ -117,6 +470,9 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
117
470
  onUpdate?.(textResult(msg));
118
471
  };
119
472
  try {
473
+ if (options.architectCreateOnly && agent !== "architect") {
474
+ throw new Error("This confirmed book-creation turn can only run the architect. Open the created book or use the book session to write chapters.");
475
+ }
120
476
  if (!activeBookId && agent !== "architect") {
121
477
  return textResult("No active book. Only the architect agent can create a book from this session.");
122
478
  }
@@ -125,6 +481,7 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
125
481
  }
126
482
  switch (agent) {
127
483
  case "architect": {
484
+ const createBookPayload = options.actionPayload?.createBook;
128
485
  if (revise) {
129
486
  if (!activeBookId) {
130
487
  return textResult("Open the book first before revising its foundation.");
@@ -135,24 +492,28 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
135
492
  progress(`Foundation revised for "${targetBookId}".`);
136
493
  return textResult(`Book "${targetBookId}" 架构稿已按要求重写。原书的条目式架构稿已备份到 story/.backup-phase4-<时间戳>/。`);
137
494
  }
138
- const resolvedTitle = title?.trim();
495
+ const confirmedTitle = createBookPayload?.title?.trim();
496
+ const resolvedTitle = confirmedTitle || title?.trim();
139
497
  if (!resolvedTitle) {
140
498
  return textResult('Error: title is required for the architect agent.');
141
499
  }
142
- const id = bookId
143
- ? assertSafeBookId(bookId, "architect.bookId")
144
- : deriveBookIdFromTitle(resolvedTitle) || `book-${Date.now().toString(36)}`;
500
+ const id = confirmedTitle
501
+ ? deriveBookIdFromTitle(confirmedTitle) || `book-${Date.now().toString(36)}`
502
+ : bookId
503
+ ? assertSafeBookId(bookId, "architect.bookId")
504
+ : deriveBookIdFromTitle(resolvedTitle) || `book-${Date.now().toString(36)}`;
145
505
  const now = new Date().toISOString();
506
+ const resolvedLanguage = createBookPayload?.language ?? language ?? inferLanguage(instruction);
146
507
  progress(`Starting architect for book "${id}"...`);
147
508
  await pipeline.initBook({
148
509
  id,
149
510
  title: resolvedTitle,
150
- genre: genre ?? "general",
151
- platform: normalizePlatformOrOther(platform),
152
- language: (language ?? "zh"),
511
+ genre: createBookPayload?.genre ?? genre ?? "general",
512
+ platform: normalizePlatformOrOther(createBookPayload?.platform ?? platform),
513
+ language: resolvedLanguage,
153
514
  status: "outlining",
154
- targetChapters: targetChapters ?? 200,
155
- chapterWordCount: chapterWordCount ?? 3000,
515
+ targetChapters: createBookPayload?.targetChapters ?? targetChapters ?? 200,
516
+ chapterWordCount: createBookPayload?.chapterWordCount ?? chapterWordCount ?? defaultChapterLength(resolvedLanguage),
156
517
  createdAt: now,
157
518
  updatedAt: now,
158
519
  }, { externalContext: instruction });
@@ -164,14 +525,20 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
164
525
  progress(`Writing next chapter for "${targetBookId}"...`);
165
526
  const result = await pipeline.writeNextChapter(targetBookId, chapterWordCount);
166
527
  progress(`Writer finished chapter for "${targetBookId}".`);
167
- return textResult(`Chapter written for "${targetBookId}". ` +
168
- `Word count: ${result.wordCount ?? "unknown"}.`, {
528
+ const resultStatus = result.status;
529
+ const wordCount = result.wordCount ?? "unknown";
530
+ const chapterNumberResult = result.chapterNumber;
531
+ const titleResult = result.title;
532
+ const message = resultStatus && resultStatus !== "ready-for-review" && resultStatus !== "active"
533
+ ? `Chapter output for "${targetBookId}" ended with status "${resultStatus}" and needs review before it is treated as complete. Word count: ${wordCount}.`
534
+ : `Chapter written for "${targetBookId}". Word count: ${wordCount}.`;
535
+ return textResult(message, {
169
536
  kind: "chapter_written",
170
537
  bookId: targetBookId,
171
- chapterNumber: result.chapterNumber,
172
- title: result.title,
173
- wordCount: result.wordCount,
174
- status: result.status,
538
+ chapterNumber: chapterNumberResult,
539
+ title: titleResult,
540
+ wordCount,
541
+ status: resultStatus,
175
542
  });
176
543
  }
177
544
  case "auditor": {
@@ -189,9 +556,26 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
189
556
  const targetBookId = resolveToolBookId("reviser", bookId, activeBookId);
190
557
  const resolvedMode = mode ?? "spot-fix";
191
558
  progress(`Revising "${targetBookId}" chapter ${chapterNumber ?? "latest"} in ${resolvedMode} mode...`);
192
- await pipeline.reviseDraft(targetBookId, chapterNumber, resolvedMode);
559
+ const result = await pipeline.reviseDraft(targetBookId, chapterNumber, resolvedMode);
560
+ const applied = result.applied !== false;
561
+ const resultChapter = result.chapterNumber ?? chapterNumber;
562
+ const details = {
563
+ kind: "chapter_revision",
564
+ bookId: targetBookId,
565
+ chapterNumber: resultChapter,
566
+ mode: resolvedMode,
567
+ applied,
568
+ status: result.status,
569
+ wordCount: result.wordCount,
570
+ fixedIssues: result.fixedIssues,
571
+ skippedReason: result.skippedReason,
572
+ };
573
+ if (!applied) {
574
+ progress(`Revision not applied for "${targetBookId}".`);
575
+ return textResult(`Revision not applied for "${targetBookId}" chapter ${resultChapter ?? "latest"}: ${result.skippedReason ?? result.status ?? "pipeline kept the original chapter"}.`, details);
576
+ }
193
577
  progress(`Revision complete for "${targetBookId}".`);
194
- return textResult(`Revision (${resolvedMode}) complete for "${targetBookId}" chapter ${chapterNumber ?? "latest"}.`);
578
+ return textResult(`Revision (${resolvedMode}) complete for "${targetBookId}" chapter ${resultChapter ?? "latest"}.`, details);
195
579
  }
196
580
  case "exporter": {
197
581
  const targetBookId = resolveToolBookId("exporter", bookId, activeBookId);
@@ -215,6 +599,20 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot) {
215
599
  }
216
600
  }
217
601
  catch (err) {
602
+ if (agent === "architect" && err instanceof ArchitectIncompleteFoundationError) {
603
+ const missing = err.missing.join(", ");
604
+ return textResult([
605
+ err.message,
606
+ "",
607
+ `缺失 section: ${missing}`,
608
+ "我会把已生成的部分保留下来,并继续补齐缺失 section;不要重新发明一本书。",
609
+ ].join("\n"), {
610
+ kind: "architect_incomplete",
611
+ missing: [...err.missing],
612
+ partialContent: err.partialContent,
613
+ retryInstruction: `Continue repairing the architect foundation. Preserve the partial content and fill missing sections: ${missing}.`,
614
+ });
615
+ }
218
616
  console.error(`[sub_agent] "${agent}" failed:`, err);
219
617
  throw err;
220
618
  }
@@ -237,8 +635,8 @@ const ShortFictionRunParams = Type.Object({
237
635
  chapters: Type.Optional(Type.Number({
238
636
  description: "Target complete short chapter count, 12-18. Default 12.",
239
637
  })),
240
- chars: Type.Optional(Type.Number({
241
- description: "Target Chinese characters per chapter, 900-1200. Default 1000.",
638
+ charsPerChapter: Type.Optional(Type.Number({
639
+ description: "Target Chinese characters per chapter, 900-1200. Default 1000. Do not use total story length here.",
242
640
  })),
243
641
  cover: Type.Optional(Type.Boolean({
244
642
  description: "Whether to attempt cover image generation after synopsis and cover prompt. Default true; use false if the user only wants text assets.",
@@ -259,7 +657,7 @@ const ShortFictionRunParams = Type.Object({
259
657
  description: "Optional env var containing the cover API key. Default INKOS_COVER_API_KEY.",
260
658
  })),
261
659
  });
262
- export function createShortFictionRunTool(pipeline, projectRoot) {
660
+ export function createShortFictionRunTool(pipeline, projectRoot, options = {}) {
263
661
  return {
264
662
  name: "short_fiction_run",
265
663
  description: "Create a standalone short fiction project from a direction. " +
@@ -269,9 +667,10 @@ export function createShortFictionRunTool(pipeline, projectRoot) {
269
667
  parameters: ShortFictionRunParams,
270
668
  async execute(_toolCallId, params, _signal, onUpdate) {
271
669
  const progress = (message) => onUpdate?.(textResult(message));
670
+ const shortPayload = options.actionPayload?.shortRun;
272
671
  const result = await runShortFictionProduction({
273
672
  projectRoot,
274
- direction: params.direction,
673
+ direction: shortPayload?.direction ?? params.direction,
275
674
  runtimes: {
276
675
  planner: pipeline.createAgentContext("short-outline"),
277
676
  outlineReview: pipeline.createAgentContext("short-outline-review"),
@@ -280,11 +679,11 @@ export function createShortFictionRunTool(pipeline, projectRoot) {
280
679
  revise: pipeline.createAgentContext("short-revise"),
281
680
  package: pipeline.createAgentContext("short-package"),
282
681
  },
283
- ...(params.reference ? { reference: { text: params.reference } } : {}),
284
- storyId: params.storyId,
285
- chapterCount: params.chapters,
286
- charsPerChapter: params.chars,
287
- cover: params.cover,
682
+ ...((shortPayload?.reference ?? params.reference) ? { reference: { text: shortPayload?.reference ?? params.reference } } : {}),
683
+ storyId: shortPayload?.storyId ?? params.storyId,
684
+ chapterCount: shortPayload?.chapters ?? params.chapters,
685
+ charsPerChapter: shortPayload?.charsPerChapter ?? params.charsPerChapter,
686
+ cover: shortPayload?.cover ?? params.cover,
288
687
  coverBaseUrl: params.coverBaseUrl,
289
688
  coverEndpoint: params.coverEndpoint,
290
689
  coverModel: params.coverModel,
@@ -356,7 +755,7 @@ const GenerateCoverParams = Type.Object({
356
755
  description: "Optional env var containing the cover API key. Usually omit and use Studio cover config.",
357
756
  })),
358
757
  });
359
- export function createGenerateCoverTool(projectRoot) {
758
+ export function createGenerateCoverTool(projectRoot, options = {}) {
360
759
  return {
361
760
  name: "generate_cover",
362
761
  description: "Generate only a cover image and cover prompt from a title/synopsis/visual direction. " +
@@ -365,13 +764,14 @@ export function createGenerateCoverTool(projectRoot) {
365
764
  parameters: GenerateCoverParams,
366
765
  async execute(_toolCallId, params, _signal, onUpdate) {
367
766
  onUpdate?.(textResult("Generating cover image..."));
767
+ const coverPayload = options.actionPayload?.generateCover;
368
768
  const result = await generateShortFictionCover({
369
769
  projectRoot,
370
- title: params.title,
371
- intro: params.intro,
372
- sellingPoints: params.sellingPoints,
373
- coverPrompt: params.coverPrompt,
374
- outputDir: params.outputDir,
770
+ title: coverPayload?.title ?? params.title,
771
+ intro: coverPayload?.intro ?? params.intro,
772
+ sellingPoints: coverPayload?.sellingPoints ?? params.sellingPoints,
773
+ coverPrompt: coverPayload?.coverPrompt ?? params.coverPrompt,
774
+ outputDir: coverPayload?.outputDir ?? params.outputDir,
375
775
  coverBaseUrl: params.coverBaseUrl,
376
776
  coverEndpoint: params.coverEndpoint,
377
777
  coverModel: params.coverModel,
@@ -387,11 +787,527 @@ export function createGenerateCoverTool(projectRoot) {
387
787
  };
388
788
  }
389
789
  // ---------------------------------------------------------------------------
390
- // 4. Deterministic writing tools
790
+ // 4. Interactive Play tools
791
+ // ---------------------------------------------------------------------------
792
+ const PlayStartParams = Type.Object({
793
+ title: Type.String({
794
+ description: "Interactive world title. Use the user's natural direction as a short playable world title.",
795
+ }),
796
+ premise: Type.Optional(Type.String({
797
+ description: "Playable premise: player role, location, pressure, and core conflict. Keep it concise.",
798
+ })),
799
+ worldContract: Type.Optional(Type.String({
800
+ description: "Durable world contract in natural language. Preserve only user-defined long-lived rules: semantic time, role autonomy, object/clue/relationship systems, taboos, or setting laws. Leave empty when the user did not define rules; do not invent RPG/level systems.",
801
+ })),
802
+ visualContract: Type.Optional(Type.String({
803
+ description: "Visual contract for Play illustrations. Preserve only user-defined visual rules; leave empty when unspecified. Do not invent game frames, colored tiers, UI, or stats.",
804
+ })),
805
+ mode: Type.Optional(Type.Union([
806
+ Type.Literal("open"),
807
+ Type.Literal("guided"),
808
+ ], { description: "open = free actions; guided = emphasize suggested actions. Default open." })),
809
+ initialScene: Type.Optional(Type.String({
810
+ description: "Opening scene shown to the player. Write pure narrative prose for the first playable moment, not a config summary, not a question prompt, and not an action/options list.",
811
+ })),
812
+ suggestedActions: Type.Optional(Type.Array(SuggestedActionParam)),
813
+ });
814
+ export function createPlayStartTool(pipeline, projectRoot, sessionId, playMode, options = {}) {
815
+ return {
816
+ name: "play_start",
817
+ description: "Start an interactive InkOS Play world directly from chat. " +
818
+ "Use when the user asks to play, roleplay, run an open-world interactive story, or start a Tavern-like scene.",
819
+ label: "Start Play",
820
+ parameters: PlayStartParams,
821
+ async execute(_toolCallId, params, _signal, onUpdate) {
822
+ onUpdate?.(textResult("Starting interactive world..."));
823
+ const playPayload = options.actionPayload?.playStart;
824
+ const store = new PlayStore(projectRoot);
825
+ // The play world is bound 1:1 to the chat session: worldId IS the
826
+ // sessionId. This removes any "which world?" ambiguity, so two play
827
+ // sessions never advance each other's world.
828
+ const worldId = safePlayId(sessionId, sessionId);
829
+ const runId = "main";
830
+ const title = playPayload?.title ?? params.title;
831
+ const premise = playPayload?.premise ?? params.premise;
832
+ const worldContract = playPayload?.worldContract ?? params.worldContract;
833
+ const visualContract = playPayload?.visualContract ?? params.visualContract;
834
+ const initialScene = isUsablePlayInitialScene(playPayload?.initialScene)
835
+ ? playPayload?.initialScene
836
+ : params.initialScene;
837
+ const playLanguage = inferLanguage([title, premise, worldContract, visualContract, initialScene].filter(Boolean).join("\n"));
838
+ const world = await store.createWorld({
839
+ id: worldId,
840
+ title: title.trim(),
841
+ premise: premise?.trim() ?? "",
842
+ worldContract: worldContract?.trim() ?? "",
843
+ visualContract: visualContract?.trim() ?? "",
844
+ mode: playMode ?? params.mode ?? "open",
845
+ language: playLanguage,
846
+ });
847
+ await store.ensureRun(world.id, runId);
848
+ const existingTranscript = await store.readTranscript(world.id, runId);
849
+ const sceneText = (initialScene?.trim() || (world.language === "en"
850
+ ? [`You enter "${world.title}".`, world.premise || "The scene is set. Make your first move."].join("\n")
851
+ : [`你进入「${world.title}」。`, world.premise || "场景已经就位,等待你的第一个动作。"].join("\n"))).trim();
852
+ if (existingTranscript.length === 0) {
853
+ await store.writeProjection(world.id, runId, "projections/scene.md", `${sceneText}\n`);
854
+ await store.saveCurrentState(world.id, runId, {
855
+ turn: 0,
856
+ worldId: world.id,
857
+ runId,
858
+ mode: world.mode,
859
+ premise: world.premise,
860
+ worldContract: world.worldContract,
861
+ visualContract: world.visualContract,
862
+ });
863
+ await store.appendTranscriptTurn(world.id, runId, {
864
+ role: "assistant",
865
+ content: sceneText,
866
+ timestamp: Date.now(),
867
+ });
868
+ }
869
+ const suggestedActions = normalizeSuggestedActions(playPayload?.suggestedActions ?? params.suggestedActions);
870
+ let seed = null;
871
+ let graph;
872
+ if (existingTranscript.length === 0 && pipeline) {
873
+ const db = createPlayDB(store.runDir(world.id, runId));
874
+ try {
875
+ const ctx = pipeline.createAgentContext("play");
876
+ const runner = options.runnerFactory?.({
877
+ projectRoot,
878
+ worldId: world.id,
879
+ runId,
880
+ ctx,
881
+ }) ?? new PlayRunner({
882
+ projectRoot,
883
+ worldId: world.id,
884
+ runId,
885
+ ctx,
886
+ db,
887
+ });
888
+ seed = await runner.seedOpening({ sceneText, suggestedActions });
889
+ graph = db.snapshot();
890
+ }
891
+ catch {
892
+ // Opening graph seed is a HUD enhancement, not a launch precondition.
893
+ // Starting the world must stay fail-open when a model drifts.
894
+ }
895
+ finally {
896
+ closePlayDB(db);
897
+ }
898
+ }
899
+ return textResult(sceneText, {
900
+ kind: "play_world_started",
901
+ worldId: world.id,
902
+ runId,
903
+ title: world.title,
904
+ mode: world.mode,
905
+ premise: world.premise,
906
+ worldContract: world.worldContract,
907
+ visualContract: world.visualContract,
908
+ sceneText,
909
+ suggestedActions,
910
+ ...(seed ? { seedMutation: seed.mutation } : {}),
911
+ ...(graph ? { graph } : {}),
912
+ });
913
+ },
914
+ };
915
+ }
916
+ const PlayStepParams = Type.Object({
917
+ input: Type.String({
918
+ description: "The player's next free-form action or chosen option.",
919
+ }),
920
+ });
921
+ const PlayReviseParams = Type.Object({
922
+ action: Type.Union([
923
+ Type.Literal("regenerate_last"),
924
+ Type.Literal("edit_last_input"),
925
+ Type.Literal("restore_variant"),
926
+ ], {
927
+ description: "How to revise the latest play turn: regenerate the same player input, edit the previous player input, or restore a saved variant.",
928
+ }),
929
+ input: Type.Optional(Type.String({
930
+ description: "Replacement player input when action=edit_last_input.",
931
+ })),
932
+ turn: Type.Optional(Type.Number({
933
+ description: "Turn number when restoring a saved variant.",
934
+ })),
935
+ variantId: Type.Optional(Type.String({
936
+ description: "Saved variant id when action=restore_variant.",
937
+ })),
938
+ });
939
+ const PlayEntityUpdateParam = Type.Object({
940
+ id: Type.Optional(Type.String({
941
+ description: "Existing entity id to update. Use actor_player for the player persona.",
942
+ })),
943
+ label: Type.Optional(Type.String({
944
+ description: "Existing entity label to update when id is unknown.",
945
+ })),
946
+ type: Type.Optional(Type.Union([
947
+ Type.Literal("actor"),
948
+ Type.Literal("location"),
949
+ Type.Literal("item"),
950
+ Type.Literal("evidence"),
951
+ Type.Literal("clue"),
952
+ Type.Literal("claim"),
953
+ Type.Literal("proof_chain"),
954
+ Type.Literal("organization"),
955
+ Type.Literal("rule"),
956
+ Type.Literal("scene"),
957
+ Type.Literal("event"),
958
+ ], { description: "Entity type when creating a missing entity. Usually actor for character/persona edits." })),
959
+ summary: Type.Optional(Type.String({
960
+ description: "Replacement or enriched entity summary, including goals/motives/persona when relevant.",
961
+ })),
962
+ status: Type.Optional(Type.String({
963
+ description: "Natural-language current status. Do not invent numeric meters unless the user asked for them.",
964
+ })),
965
+ });
966
+ const PlayContractReplacementParam = Type.Object({
967
+ from: Type.String({
968
+ description: "Exact old wording to replace in the existing contract.",
969
+ }),
970
+ to: Type.String({
971
+ description: "New wording that should replace the old wording.",
972
+ }),
973
+ });
974
+ const PlayEditParams = Type.Object({
975
+ worldContract: Type.Optional(Type.String({
976
+ description: "Full updated world contract after applying the user's requested rule change. Use when the user edits world rules, time semantics, item semantics, role autonomy, taboos, or costs.",
977
+ })),
978
+ worldContractReplacements: Type.Optional(Type.Array(PlayContractReplacementParam, {
979
+ description: "Exact replacements for existing world-contract wording. Use when the user says to change/replace X into Y; do not append the new rule while leaving the old wording in place.",
980
+ })),
981
+ worldContractAppend: Type.Optional(Type.String({
982
+ description: "A narrow new world-contract addition. Do not use this for replacements such as 'change X to Y'; use worldContractReplacements or full worldContract instead.",
983
+ })),
984
+ visualContract: Type.Optional(Type.String({
985
+ description: "Full updated visual contract after applying the user's requested image/visual-rule change.",
986
+ })),
987
+ visualContractReplacements: Type.Optional(Type.Array(PlayContractReplacementParam, {
988
+ description: "Exact replacements for existing visual-contract wording. Use when the user says to change/replace one visual rule into another.",
989
+ })),
990
+ visualContractAppend: Type.Optional(Type.String({
991
+ description: "A narrow new visual-contract addition. Do not use this for replacements such as 'change X to Y'; use visualContractReplacements or full visualContract instead.",
992
+ })),
993
+ premise: Type.Optional(Type.String({
994
+ description: "Updated world premise only when the user explicitly changes premise/backstory. Do not rewrite premise for ordinary turns.",
995
+ })),
996
+ playerPersona: Type.Optional(Type.String({
997
+ description: "Updated player persona/identity/goals. This updates the reserved actor_player entity.",
998
+ })),
999
+ entityUpdates: Type.Optional(Type.Array(PlayEntityUpdateParam, {
1000
+ description: "Character, object, place, or rule-card updates requested by the user. Use for role goals, status, motives, taboos, or known facts.",
1001
+ })),
1002
+ note: Type.Optional(Type.String({
1003
+ description: "Short human-readable note summarizing what changed.",
1004
+ })),
1005
+ });
1006
+ export function createPlayEditTool(projectRoot, sessionId) {
1007
+ return {
1008
+ name: "play_edit",
1009
+ description: "Persistently edit the active InkOS Play world card, visual contract, player persona, or entity/role cards without advancing time or narrating a turn. " +
1010
+ "Use when the user says to change world rules, visual rules, character goals/persona/status, or long-lived play contracts.",
1011
+ label: "Edit Play World",
1012
+ parameters: PlayEditParams,
1013
+ async execute(_toolCallId, params) {
1014
+ const store = new PlayStore(projectRoot);
1015
+ const worldId = safePlayId(sessionId, sessionId);
1016
+ const runId = "main";
1017
+ const world = await store.loadWorld(worldId);
1018
+ if (!world) {
1019
+ return textResult("还没有可编辑的互动世界。先用 play_start 开一局。");
1020
+ }
1021
+ const patch = {};
1022
+ const nextWorldContract = mergeContract(world.worldContract, params.worldContract, params.worldContractReplacements, params.worldContractAppend);
1023
+ const nextVisualContract = mergeContract(world.visualContract, params.visualContract, params.visualContractReplacements, params.visualContractAppend);
1024
+ if (nextWorldContract !== world.worldContract)
1025
+ patch.worldContract = nextWorldContract;
1026
+ if (nextVisualContract !== world.visualContract)
1027
+ patch.visualContract = nextVisualContract;
1028
+ const premise = params.premise?.trim();
1029
+ if (premise && premise !== world.premise)
1030
+ patch.premise = premise;
1031
+ const updatedWorld = Object.keys(patch).length > 0
1032
+ ? await store.updateWorld(worldId, patch)
1033
+ : world;
1034
+ await store.ensureRun(worldId, runId);
1035
+ const db = createPlayDB(store.runDir(worldId, runId));
1036
+ let updatedEntities = 0;
1037
+ try {
1038
+ const playerPersona = params.playerPersona?.trim();
1039
+ if (playerPersona) {
1040
+ const existingPlayer = db.getEntity("actor_player");
1041
+ upsertPlayEditEntity(db, {
1042
+ id: "actor_player",
1043
+ type: "actor",
1044
+ label: existingPlayer?.label ?? "玩家",
1045
+ summary: playerPersona,
1046
+ status: "已更新",
1047
+ });
1048
+ updatedEntities += 1;
1049
+ }
1050
+ for (const update of params.entityUpdates ?? []) {
1051
+ if (upsertPlayEditEntity(db, update))
1052
+ updatedEntities += 1;
1053
+ }
1054
+ const graph = db.snapshot();
1055
+ const currentState = await store.loadCurrentState(worldId, runId).catch(() => ({}));
1056
+ await store.saveCurrentState(worldId, runId, {
1057
+ ...(currentState && typeof currentState === "object" ? currentState : {}),
1058
+ worldContract: updatedWorld.worldContract,
1059
+ visualContract: updatedWorld.visualContract,
1060
+ premise: updatedWorld.premise,
1061
+ graphEditedAt: new Date().toISOString(),
1062
+ });
1063
+ return textResult(params.note?.trim() || "互动世界设定已更新。", {
1064
+ kind: "play_world_updated",
1065
+ worldId,
1066
+ runId,
1067
+ world: updatedWorld,
1068
+ updatedWorldContract: nextWorldContract !== world.worldContract,
1069
+ updatedVisualContract: nextVisualContract !== world.visualContract,
1070
+ updatedPremise: Boolean(patch.premise),
1071
+ updatedEntities,
1072
+ graph,
1073
+ });
1074
+ }
1075
+ finally {
1076
+ closePlayDB(db);
1077
+ }
1078
+ },
1079
+ };
1080
+ }
1081
+ export function createPlayStepTool(pipeline, projectRoot, sessionId, options = {}) {
1082
+ return {
1083
+ name: "play_step",
1084
+ description: "Advance the current InkOS Play world by one player action. " +
1085
+ "Use after play_start when the user keeps acting in the interactive scene.",
1086
+ label: "Play Step",
1087
+ parameters: PlayStepParams,
1088
+ async execute(_toolCallId, params, _signal, onUpdate) {
1089
+ const input = params.input.trim();
1090
+ if (!input)
1091
+ return textResult("Play input is empty.");
1092
+ const store = new PlayStore(projectRoot);
1093
+ // The play world is bound to this chat session (worldId === sessionId).
1094
+ const worldId = safePlayId(sessionId, sessionId);
1095
+ const runId = "main";
1096
+ const world = await store.loadWorld(worldId);
1097
+ if (!world) {
1098
+ return textResult("还没有可推进的互动世界。先用 play_start 开一局。");
1099
+ }
1100
+ const target = { worldId, runId, world };
1101
+ onUpdate?.(textResult(`Advancing "${target.worldId}" / "${target.runId}"...`));
1102
+ const ctx = pipeline.createAgentContext("play");
1103
+ const runner = options.runnerFactory?.({
1104
+ projectRoot,
1105
+ worldId: target.worldId,
1106
+ runId: target.runId,
1107
+ ctx,
1108
+ }) ?? new PlayRunner({
1109
+ projectRoot,
1110
+ worldId: target.worldId,
1111
+ runId: target.runId,
1112
+ ctx,
1113
+ });
1114
+ let step;
1115
+ try {
1116
+ step = await runner.step(input);
1117
+ }
1118
+ catch (err) {
1119
+ // Never hand a raw tool error to the outer agent — it improvises a fake
1120
+ // "service unavailable / reload your save" message. Return a fixed, graceful
1121
+ // structured failure so the turn fails honestly and recoverably instead.
1122
+ const isZh = (target.world?.language ?? "zh") !== "en";
1123
+ return textResult(isZh
1124
+ ? "(系统刚才卡了一下,这一步没能展开。把你刚才想做的再说一遍,我就接着推进。)"
1125
+ : "(The system hiccuped and this step didn't resolve. Say what you just did again and I'll continue.)", {
1126
+ kind: "play_step_failed",
1127
+ worldId: target.worldId,
1128
+ runId: target.runId,
1129
+ error: err instanceof Error ? err.message : String(err),
1130
+ });
1131
+ }
1132
+ const db = createPlayDB(store.runDir(target.worldId, target.runId));
1133
+ let graph;
1134
+ try {
1135
+ graph = db.snapshot();
1136
+ }
1137
+ finally {
1138
+ closePlayDB(db);
1139
+ }
1140
+ const currentState = await store.loadCurrentState(target.worldId, target.runId).catch(() => null);
1141
+ return textResult(step.sceneText, {
1142
+ kind: "play_turn_advanced",
1143
+ worldId: target.worldId,
1144
+ runId: target.runId,
1145
+ title: target.world?.title,
1146
+ sceneText: step.sceneText,
1147
+ suggestedActions: step.suggestedActions,
1148
+ action: step.action,
1149
+ mutation: step.mutation,
1150
+ currentState,
1151
+ graph,
1152
+ });
1153
+ },
1154
+ };
1155
+ }
1156
+ export function createPlayReviseTool(pipeline, projectRoot, sessionId, options = {}) {
1157
+ return {
1158
+ name: "play_revise",
1159
+ description: "Regenerate, edit, or restore the latest InkOS Play turn using saved turn checkpoints. " +
1160
+ "Use when the user says to redo the previous turn, try another version, swipe, or replace their last player input.",
1161
+ label: "Revise Play Turn",
1162
+ parameters: PlayReviseParams,
1163
+ async execute(_toolCallId, params, _signal, onUpdate) {
1164
+ const store = new PlayStore(projectRoot);
1165
+ const worldId = safePlayId(sessionId, sessionId);
1166
+ const runId = "main";
1167
+ const world = await store.loadWorld(worldId);
1168
+ if (!world) {
1169
+ return textResult("还没有可重做的互动世界。先用 play_start 开一局。");
1170
+ }
1171
+ const ctx = pipeline.createAgentContext("play");
1172
+ const runner = options.runnerFactory?.({ projectRoot, worldId, runId, ctx }) ?? new PlayRunner({
1173
+ projectRoot,
1174
+ worldId,
1175
+ runId,
1176
+ ctx,
1177
+ });
1178
+ if (params.action === "restore_variant") {
1179
+ const turn = params.turn;
1180
+ const variantId = params.variantId?.trim();
1181
+ if (typeof turn !== "number" || !Number.isFinite(turn) || !variantId) {
1182
+ return textResult("恢复版本需要 turn 和 variantId。");
1183
+ }
1184
+ onUpdate?.(textResult(`Restoring play variant "${variantId}"...`));
1185
+ const restored = await runner.restoreVariant({
1186
+ turn: Math.trunc(turn),
1187
+ variantId,
1188
+ });
1189
+ return textResult(restored.sceneText || "已切换到指定互动回合版本。", {
1190
+ kind: "play_variant_restored",
1191
+ worldId,
1192
+ runId,
1193
+ title: world.title,
1194
+ turn: restored.turn,
1195
+ variantId: restored.variantId,
1196
+ sceneText: restored.sceneText,
1197
+ });
1198
+ }
1199
+ const replacement = params.action === "edit_last_input" ? params.input?.trim() : undefined;
1200
+ if (params.action === "edit_last_input" && !replacement) {
1201
+ return textResult("编辑上一条玩家动作需要提供新的 input。");
1202
+ }
1203
+ onUpdate?.(textResult(params.action === "edit_last_input" ? "Replaying edited play turn..." : "Regenerating last play turn..."));
1204
+ let replay;
1205
+ try {
1206
+ replay = await runner.regenerateLastTurn(replacement);
1207
+ }
1208
+ catch (err) {
1209
+ const isZh = (world.language ?? "zh") !== "en";
1210
+ return textResult(isZh
1211
+ ? "(上一回合暂时不能安全重做。继续输入新的动作,我会从当前状态推进。)"
1212
+ : "(The previous turn cannot be safely regenerated yet. Enter a new action and I will continue from the current state.)", {
1213
+ kind: "play_revise_failed",
1214
+ worldId,
1215
+ runId,
1216
+ error: err instanceof Error ? err.message : String(err),
1217
+ });
1218
+ }
1219
+ const db = createPlayDB(store.runDir(worldId, runId));
1220
+ let graph;
1221
+ try {
1222
+ graph = db.snapshot();
1223
+ }
1224
+ finally {
1225
+ closePlayDB(db);
1226
+ }
1227
+ const currentState = await store.loadCurrentState(worldId, runId).catch(() => null);
1228
+ return textResult(replay.sceneText, {
1229
+ kind: "play_turn_revised",
1230
+ worldId,
1231
+ runId,
1232
+ title: world.title,
1233
+ sceneText: replay.sceneText,
1234
+ suggestedActions: replay.suggestedActions,
1235
+ action: replay.action,
1236
+ mutation: replay.mutation,
1237
+ replayedInput: replay.replayedInput,
1238
+ previousVariantId: replay.previousVariantId,
1239
+ variantId: replay.variantId,
1240
+ currentState,
1241
+ graph,
1242
+ });
1243
+ },
1244
+ };
1245
+ }
1246
+ function mergeContract(existing, replacement, replacements, addition) {
1247
+ const next = replacement?.trim();
1248
+ if (next)
1249
+ return next;
1250
+ let current = existing;
1251
+ for (const patch of replacements ?? []) {
1252
+ const from = patch.from.trim();
1253
+ const to = patch.to.trim();
1254
+ if (!from || !to || !current.includes(from))
1255
+ continue;
1256
+ current = current.split(from).join(to);
1257
+ }
1258
+ const add = addition?.trim();
1259
+ if (!add)
1260
+ return current;
1261
+ if (current.includes(add))
1262
+ return current;
1263
+ return current.trim() ? `${current.trim()}\n- ${add}` : add;
1264
+ }
1265
+ function upsertPlayEditEntity(db, update) {
1266
+ const summary = update.summary?.trim();
1267
+ const status = update.status?.trim();
1268
+ const label = update.label?.trim();
1269
+ const entityId = resolvePlayEditEntityId(db, update);
1270
+ if (!entityId && !label)
1271
+ return false;
1272
+ const existing = entityId ? db.getEntity(entityId) : null;
1273
+ const id = entityId || playEditEntityId(update.type ?? "actor", label);
1274
+ db.upsertEntity({
1275
+ id,
1276
+ type: update.type ?? existing?.type ?? "actor",
1277
+ label: label || existing?.label || id,
1278
+ summary: summary ?? existing?.summary ?? "",
1279
+ status: status ?? existing?.status ?? "",
1280
+ createdEventId: existing?.createdEventId ?? "manual-edit",
1281
+ updatedEventId: "manual-edit",
1282
+ });
1283
+ return true;
1284
+ }
1285
+ function resolvePlayEditEntityId(db, update) {
1286
+ const id = update.id?.trim();
1287
+ if (id)
1288
+ return id;
1289
+ const label = update.label?.trim();
1290
+ if (!label)
1291
+ return undefined;
1292
+ const snapshot = db.snapshot();
1293
+ const match = snapshot.entities.find((entity) => entity.label === label || entity.id === label);
1294
+ return match?.id;
1295
+ }
1296
+ function playEditEntityId(type, label) {
1297
+ const ascii = label
1298
+ .trim()
1299
+ .toLowerCase()
1300
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/gu, "_")
1301
+ .replace(/^_+|_+$/g, "")
1302
+ .slice(0, 48);
1303
+ return `${type}_${ascii || Date.now().toString(36)}`;
1304
+ }
1305
+ // ---------------------------------------------------------------------------
1306
+ // 5. Deterministic writing tools
391
1307
  // ---------------------------------------------------------------------------
392
1308
  const WriteTruthFileParams = Type.Object({
393
1309
  bookId: Type.Optional(Type.String({ description: "Book ID. Omit to use the active book." })),
394
- fileName: Type.String({ description: "Truth file name under story/, e.g. story_bible.md or current_focus.md." }),
1310
+ fileName: Type.String({ description: "Truth file path under story/. Prefer outline/story_frame.md, outline/volume_map.md, roles/major/<name>.md, roles/minor/<name>.md; flat files such as current_focus.md and author_intent.md are also supported." }),
395
1311
  content: Type.String({ description: "Full replacement content for the truth file." }),
396
1312
  });
397
1313
  export function createWriteTruthFileTool(pipeline, projectRoot, activeBookId) {
@@ -455,6 +1371,27 @@ export function createPatchChapterTextTool(pipeline, projectRoot, activeBookId)
455
1371
  },
456
1372
  };
457
1373
  }
1374
+ const ReplaceChapterTextParams = Type.Object({
1375
+ bookId: Type.Optional(Type.String({ description: "Book ID. Omit to use the active book." })),
1376
+ chapterNumber: Type.Number({ description: "Chapter number to replace." }),
1377
+ fullText: Type.String({ description: "The complete replacement chapter markdown/text supplied by the user." }),
1378
+ });
1379
+ export function createReplaceChapterTextTool(pipeline, projectRoot, activeBookId) {
1380
+ const tools = createDeterministicInteractionTools(pipeline, projectRoot);
1381
+ return {
1382
+ name: "replace_chapter_text",
1383
+ description: "Replace a whole existing chapter with user-supplied full chapter text and mark it for review. " +
1384
+ "Use only when the user provides the complete replacement chapter; for model-generated rewrites use sub_agent reviser.",
1385
+ label: "Replace Chapter",
1386
+ parameters: ReplaceChapterTextParams,
1387
+ async execute(_toolCallId, params) {
1388
+ const bookId = resolveToolBookId("replace_chapter_text", params.bookId, activeBookId);
1389
+ const result = await tools.replaceChapterText(bookId, params.chapterNumber, params.fullText);
1390
+ const summary = result.__interaction?.responseText ?? `Replaced chapter ${params.chapterNumber} for "${bookId}".`;
1391
+ return textResult(summary);
1392
+ },
1393
+ };
1394
+ }
458
1395
  // ---------------------------------------------------------------------------
459
1396
  // 3. Read Tool
460
1397
  // ---------------------------------------------------------------------------
@@ -480,10 +1417,7 @@ export function createReadTool(projectRoot, options = {}) {
480
1417
  async execute(_toolCallId, params) {
481
1418
  try {
482
1419
  const filePath = resolveReadPath(booksRoot, params.path, options);
483
- let content = await readFile(filePath, "utf-8");
484
- if (content.length > 10_000) {
485
- content = content.slice(0, 10_000) + "\n\n... [truncated at 10 000 chars]";
486
- }
1420
+ const content = await readFile(filePath, "utf-8");
487
1421
  return textResult(content);
488
1422
  }
489
1423
  catch (err) {
@@ -506,7 +1440,7 @@ export function createEditTool(projectRoot) {
506
1440
  name: "edit",
507
1441
  description: "Edit a file under books/ via exact string replacement. " +
508
1442
  "old_string must appear exactly once in the file. " +
509
- "For chapter text use patch_chapter_text; for canonical truth files (story_bible/volume_outline/book_rules/current_focus) prefer write_truth_file; " +
1443
+ "For chapter text use patch_chapter_text; for canonical truth files (outline/story_frame.md, outline/volume_map.md, roles/**/*.md, current_focus.md, author_intent.md) prefer write_truth_file; " +
510
1444
  "to rewrite or polish a whole chapter call sub_agent with agent=\"reviser\".",
511
1445
  label: "Edit File",
512
1446
  parameters: EditParams,