runtime.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. import type { AutomationMode } from "./modes.js";
  2. import { routeInteractionRequest } from "./request-router.js";
  3. import type { InteractionRequest } from "./intents.js";
  4. import type { ExecutionState, InteractionEvent } from "./events.js";
  5. import type { PendingDecision, InteractionSession } from "./session.js";
  6. import {
  7. appendInteractionEvent,
  8. bindActiveBook,
  9. clearPendingDecision,
  10. updateAutomationMode,
  11. } from "./session.js";
  12. type ReviseMode = "local-fix" | "rewrite";
  13. export interface InteractionRuntimeTools {
  14. readonly listBooks: () => Promise<ReadonlyArray<string>>;
  15. readonly createBook?: (input: {
  16. readonly title: string;
  17. readonly genre?: string;
  18. readonly platform?: string;
  19. readonly language?: "zh" | "en";
  20. readonly chapterWordCount?: number;
  21. readonly targetChapters?: number;
  22. }) => Promise<unknown>;
  23. readonly exportBook?: (bookId: string, options: {
  24. readonly format?: "txt" | "md" | "epub";
  25. readonly approvedOnly?: boolean;
  26. readonly outputPath?: string;
  27. }) => Promise<unknown>;
  28. readonly chat?: (
  29. input: string,
  30. options: {
  31. readonly bookId?: string;
  32. readonly automationMode: AutomationMode;
  33. },
  34. ) => Promise<unknown>;
  35. readonly writeNextChapter: (bookId: string) => Promise<unknown>;
  36. readonly reviseDraft: (bookId: string, chapterNumber: number, mode: ReviseMode) => Promise<unknown>;
  37. readonly patchChapterText: (
  38. bookId: string,
  39. chapterNumber: number,
  40. targetText: string,
  41. replacementText: string,
  42. ) => Promise<unknown>;
  43. readonly renameEntity: (
  44. bookId: string,
  45. oldValue: string,
  46. newValue: string,
  47. ) => Promise<unknown>;
  48. readonly updateCurrentFocus: (bookId: string, content: string) => Promise<unknown>;
  49. readonly updateAuthorIntent: (bookId: string, content: string) => Promise<unknown>;
  50. readonly writeTruthFile: (bookId: string, fileName: string, content: string) => Promise<unknown>;
  51. }
  52. export interface InteractionRuntimeResult {
  53. readonly session: InteractionSession;
  54. readonly responseText?: string;
  55. readonly details?: Readonly<Record<string, unknown>>;
  56. }
  57. interface InteractionToolMetadata {
  58. readonly events?: ReadonlyArray<InteractionEvent>;
  59. readonly activeChapterNumber?: number;
  60. readonly currentExecution?: ExecutionState;
  61. readonly pendingDecision?: PendingDecision;
  62. readonly responseText?: string;
  63. readonly details?: Readonly<Record<string, unknown>>;
  64. }
  65. function extractToolMetadata(value: unknown): InteractionToolMetadata {
  66. const chapterNumber = typeof value === "object" && value !== null && "chapterNumber" in value
  67. && typeof (value as { chapterNumber?: unknown }).chapterNumber === "number"
  68. ? (value as { chapterNumber: number }).chapterNumber
  69. : undefined;
  70. if (!value || typeof value !== "object" || !("__interaction" in value)) {
  71. return {
  72. ...(chapterNumber !== undefined ? { activeChapterNumber: chapterNumber } : {}),
  73. };
  74. }
  75. const interaction = (value as {
  76. readonly __interaction?: InteractionToolMetadata;
  77. }).__interaction;
  78. return {
  79. ...interaction,
  80. ...(interaction?.activeChapterNumber === undefined && chapterNumber !== undefined
  81. ? { activeChapterNumber: chapterNumber }
  82. : {}),
  83. };
  84. }
  85. function buildTaskStartedState(
  86. session: InteractionSession,
  87. request: InteractionRequest,
  88. ): ExecutionState {
  89. switch (request.intent) {
  90. case "write_next":
  91. case "continue_book":
  92. return {
  93. status: "planning",
  94. bookId: request.bookId ?? session.activeBookId,
  95. chapterNumber: session.activeChapterNumber,
  96. stageLabel: "preparing chapter inputs",
  97. };
  98. case "create_book":
  99. return {
  100. status: "planning",
  101. bookId: request.bookId ?? session.activeBookId,
  102. stageLabel: "creating book foundation",
  103. };
  104. case "export_book":
  105. return {
  106. status: "persisting",
  107. bookId: request.bookId ?? session.activeBookId,
  108. chapterNumber: session.activeChapterNumber,
  109. stageLabel: "exporting book artifacts",
  110. };
  111. case "revise_chapter":
  112. case "rewrite_chapter":
  113. return {
  114. status: "repairing",
  115. bookId: request.bookId ?? session.activeBookId,
  116. chapterNumber: request.chapterNumber ?? session.activeChapterNumber,
  117. stageLabel: request.intent === "rewrite_chapter" ? "rewriting chapter" : "revising chapter",
  118. };
  119. case "update_focus":
  120. case "update_author_intent":
  121. case "edit_truth":
  122. return {
  123. status: "persisting",
  124. bookId: request.bookId ?? session.activeBookId,
  125. chapterNumber: session.activeChapterNumber,
  126. stageLabel: "applying project edit",
  127. };
  128. case "pause_book":
  129. return {
  130. status: "blocked",
  131. bookId: request.bookId ?? session.activeBookId,
  132. chapterNumber: session.activeChapterNumber,
  133. stageLabel: "paused by user",
  134. };
  135. default:
  136. return {
  137. status: "planning",
  138. bookId: request.bookId ?? session.activeBookId,
  139. chapterNumber: session.activeChapterNumber,
  140. stageLabel: `handling ${request.intent}`,
  141. };
  142. }
  143. }
  144. function shouldWaitForHuman(
  145. automationMode: AutomationMode,
  146. request: InteractionRequest,
  147. ): boolean {
  148. const contentIntent = request.intent === "write_next"
  149. || request.intent === "continue_book"
  150. || request.intent === "revise_chapter"
  151. || request.intent === "rewrite_chapter"
  152. || request.intent === "patch_chapter_text";
  153. const editIntent = request.intent === "update_focus"
  154. || request.intent === "update_author_intent"
  155. || request.intent === "edit_truth"
  156. || request.intent === "rename_entity";
  157. if (automationMode === "auto") {
  158. return false;
  159. }
  160. if (automationMode === "semi") {
  161. return contentIntent;
  162. }
  163. return contentIntent || editIntent;
  164. }
  165. function buildPendingDecision(
  166. session: InteractionSession,
  167. request: InteractionRequest,
  168. chapterNumber?: number,
  169. ): PendingDecision | undefined {
  170. if (!shouldWaitForHuman(session.automationMode, request)) {
  171. return undefined;
  172. }
  173. const bookId = request.bookId ?? session.activeBookId;
  174. if (!bookId) {
  175. return undefined;
  176. }
  177. return {
  178. kind: "review-next-step",
  179. bookId,
  180. ...(chapterNumber !== undefined ? { chapterNumber } : {}),
  181. summary: session.automationMode === "manual"
  182. ? "Execution finished. Choose the next action explicitly."
  183. : "Execution finished. Waiting for your next decision.",
  184. };
  185. }
  186. function buildWaitingExecution(
  187. session: InteractionSession,
  188. request: InteractionRequest,
  189. chapterNumber?: number,
  190. ): ExecutionState {
  191. return {
  192. status: "waiting_human",
  193. bookId: request.bookId ?? session.activeBookId,
  194. ...(chapterNumber !== undefined ? { chapterNumber } : {}),
  195. stageLabel: "waiting for your next decision",
  196. };
  197. }
  198. function appendToolEvents(
  199. session: InteractionSession,
  200. events: ReadonlyArray<InteractionEvent> | undefined,
  201. ): InteractionSession {
  202. if (!events || events.length === 0) {
  203. return session;
  204. }
  205. const baseTimestamp = Date.now();
  206. return events.reduce((nextSession, event, index) => appendInteractionEvent(nextSession, {
  207. ...event,
  208. timestamp: baseTimestamp - events.length + index,
  209. }), session);
  210. }
  211. export async function runInteractionRequest(params: {
  212. readonly session: InteractionSession;
  213. readonly request: InteractionRequest;
  214. readonly tools: InteractionRuntimeTools;
  215. }): Promise<InteractionRuntimeResult> {
  216. const request = routeInteractionRequest(params.request);
  217. let session = params.session;
  218. const addEvent = (
  219. nextSession: InteractionSession,
  220. kind: string,
  221. status: InteractionEvent["status"],
  222. detail: string,
  223. ): InteractionSession => appendInteractionEvent(nextSession, {
  224. kind,
  225. timestamp: Date.now(),
  226. status,
  227. bookId: nextSession.activeBookId,
  228. chapterNumber: nextSession.activeChapterNumber,
  229. detail,
  230. });
  231. if (request.mode) {
  232. session = updateAutomationMode(session, request.mode as AutomationMode);
  233. }
  234. session = clearPendingDecision({
  235. ...session,
  236. currentExecution: buildTaskStartedState(session, request),
  237. });
  238. session = addEvent(session, "task.started", session.currentExecution!.status, `Started ${request.intent}.`);
  239. const markCompleted = (nextSession: InteractionSession): InteractionSession => ({
  240. ...nextSession,
  241. currentExecution: {
  242. status: "completed",
  243. bookId: nextSession.activeBookId,
  244. chapterNumber: nextSession.activeChapterNumber,
  245. stageLabel: "completed",
  246. },
  247. });
  248. switch (request.intent) {
  249. case "create_book": {
  250. if (!params.tools.createBook) {
  251. throw new Error("Book creation is not implemented in the interaction runtime yet.");
  252. }
  253. if (!request.title) {
  254. throw new Error("Book creation requires a title.");
  255. }
  256. const toolResult = await params.tools.createBook({
  257. title: request.title,
  258. genre: request.genre,
  259. platform: request.platform,
  260. language: request.language,
  261. chapterWordCount: request.chapterWordCount,
  262. targetChapters: request.targetChapters,
  263. });
  264. const metadata = extractToolMetadata(toolResult);
  265. const createdBookId = typeof toolResult === "object" && toolResult !== null && "bookId" in toolResult
  266. && typeof (toolResult as { bookId?: unknown }).bookId === "string"
  267. ? (toolResult as { bookId: string }).bookId
  268. : undefined;
  269. if (!createdBookId) {
  270. throw new Error("Create-book tool did not return a book id.");
  271. }
  272. session = bindActiveBook(session, createdBookId);
  273. session = appendToolEvents(session, metadata.events);
  274. const completed = {
  275. ...markCompleted(session),
  276. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  277. };
  278. return {
  279. session: addEvent(completed, "task.completed", "completed", `Created ${createdBookId}.`),
  280. responseText: metadata.responseText ?? `Created ${createdBookId}.`,
  281. details: metadata.details,
  282. };
  283. }
  284. case "write_next":
  285. case "continue_book": {
  286. const bookId = request.bookId ?? session.activeBookId;
  287. if (!bookId) {
  288. throw new Error("No active book is bound to the interaction session.");
  289. }
  290. const toolResult = await params.tools.writeNextChapter(bookId);
  291. const metadata = extractToolMetadata(toolResult);
  292. session = bindActiveBook(session, bookId, metadata.activeChapterNumber);
  293. session = appendToolEvents(session, metadata.events);
  294. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
  295. session,
  296. request,
  297. metadata.activeChapterNumber,
  298. );
  299. const completed = pendingDecision
  300. ? {
  301. ...session,
  302. pendingDecision,
  303. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, metadata.activeChapterNumber),
  304. }
  305. : {
  306. ...markCompleted(session),
  307. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  308. };
  309. return {
  310. session: addEvent(completed, "task.completed", "completed", `Completed write_next for ${bookId}.`),
  311. responseText: metadata.responseText ?? (
  312. pendingDecision
  313. ? `Completed write_next for ${bookId}; waiting for your next decision.`
  314. : `Completed write_next for ${bookId}.`
  315. ),
  316. };
  317. }
  318. case "list_books": {
  319. const books = await params.tools.listBooks();
  320. const completed = markCompleted(session);
  321. return {
  322. session: addEvent(completed, "task.completed", "completed", `Listed ${books.length} book(s).`),
  323. responseText: books.length > 0
  324. ? `Books: ${books.join(", ")}`
  325. : "No books found in this project.",
  326. };
  327. }
  328. case "select_book": {
  329. if (!request.bookId) {
  330. throw new Error("Book selection requires a book id.");
  331. }
  332. const books = await params.tools.listBooks();
  333. if (!books.includes(request.bookId)) {
  334. throw new Error(`Book "${request.bookId}" not found in this project.`);
  335. }
  336. const completed = markCompleted(bindActiveBook(session, request.bookId));
  337. return {
  338. session: addEvent(completed, "task.completed", "completed", `Bound active book to ${request.bookId}.`),
  339. responseText: `Opened ${request.bookId}.`,
  340. };
  341. }
  342. case "revise_chapter":
  343. case "rewrite_chapter": {
  344. const bookId = request.bookId ?? session.activeBookId;
  345. if (!bookId) {
  346. throw new Error("No active book is bound to the interaction session.");
  347. }
  348. if (!request.chapterNumber) {
  349. throw new Error("Chapter number is required for chapter revision.");
  350. }
  351. const mode: ReviseMode = request.intent === "rewrite_chapter" ? "rewrite" : "local-fix";
  352. const toolResult = await params.tools.reviseDraft(bookId, request.chapterNumber, mode);
  353. const metadata = extractToolMetadata(toolResult);
  354. const chapterNumber = metadata.activeChapterNumber ?? request.chapterNumber;
  355. session = bindActiveBook(session, bookId, chapterNumber);
  356. session = appendToolEvents(session, metadata.events);
  357. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
  358. session,
  359. request,
  360. chapterNumber,
  361. );
  362. const completed = pendingDecision
  363. ? {
  364. ...session,
  365. pendingDecision,
  366. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, chapterNumber),
  367. }
  368. : {
  369. ...markCompleted(session),
  370. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  371. };
  372. return {
  373. session: addEvent(completed, "task.completed", "completed", `Completed ${request.intent} for ${bookId}.`),
  374. responseText: metadata.responseText ?? (
  375. pendingDecision
  376. ? `Completed ${request.intent} for ${bookId}; waiting for your next decision.`
  377. : `Completed ${request.intent} for ${bookId}.`
  378. ),
  379. };
  380. }
  381. case "patch_chapter_text": {
  382. const bookId = request.bookId ?? session.activeBookId;
  383. if (!bookId) {
  384. throw new Error("No active book is bound to the interaction session.");
  385. }
  386. if (!request.chapterNumber || !request.targetText || !request.replacementText) {
  387. throw new Error("Chapter patch requires chapter number, target text, and replacement text.");
  388. }
  389. const toolResult = await params.tools.patchChapterText(
  390. bookId,
  391. request.chapterNumber,
  392. request.targetText,
  393. request.replacementText,
  394. );
  395. const metadata = extractToolMetadata(toolResult);
  396. const chapterNumber = metadata.activeChapterNumber ?? request.chapterNumber;
  397. session = bindActiveBook(session, bookId, chapterNumber);
  398. session = appendToolEvents(session, metadata.events);
  399. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
  400. session,
  401. request,
  402. chapterNumber,
  403. );
  404. const completed = pendingDecision
  405. ? {
  406. ...session,
  407. pendingDecision,
  408. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, chapterNumber),
  409. }
  410. : {
  411. ...markCompleted(session),
  412. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  413. };
  414. return {
  415. session: addEvent(completed, "task.completed", "completed", `Patched chapter ${chapterNumber} for ${bookId}.`),
  416. responseText: metadata.responseText ?? (
  417. pendingDecision
  418. ? `Patched chapter ${chapterNumber} for ${bookId}; waiting for your next decision.`
  419. : `Patched chapter ${chapterNumber} for ${bookId}.`
  420. ),
  421. };
  422. }
  423. case "rename_entity": {
  424. const bookId = request.bookId ?? session.activeBookId;
  425. if (!bookId) {
  426. throw new Error("No active book is bound to the interaction session.");
  427. }
  428. if (!request.oldValue || !request.newValue) {
  429. throw new Error("Entity rename requires old and new values.");
  430. }
  431. const toolResult = await params.tools.renameEntity(bookId, request.oldValue, request.newValue);
  432. const metadata = extractToolMetadata(toolResult);
  433. session = bindActiveBook(session, bookId, metadata.activeChapterNumber);
  434. session = appendToolEvents(session, metadata.events);
  435. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
  436. session,
  437. request,
  438. metadata.activeChapterNumber,
  439. );
  440. const completed = pendingDecision
  441. ? {
  442. ...session,
  443. pendingDecision,
  444. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, metadata.activeChapterNumber),
  445. }
  446. : {
  447. ...markCompleted(session),
  448. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  449. };
  450. return {
  451. session: addEvent(completed, "task.completed", "completed", `Renamed ${request.oldValue} to ${request.newValue} in ${bookId}.`),
  452. responseText: metadata.responseText ?? (
  453. pendingDecision
  454. ? `Renamed ${request.oldValue} to ${request.newValue} in ${bookId}; waiting for your next decision.`
  455. : `Renamed ${request.oldValue} to ${request.newValue} in ${bookId}.`
  456. ),
  457. };
  458. }
  459. case "update_focus": {
  460. const bookId = request.bookId ?? session.activeBookId;
  461. if (!bookId) {
  462. throw new Error("No active book is bound to the interaction session.");
  463. }
  464. if (!request.instruction) {
  465. throw new Error("Focus update requires instruction content.");
  466. }
  467. const toolResult = await params.tools.updateCurrentFocus(bookId, request.instruction);
  468. const metadata = extractToolMetadata(toolResult);
  469. session = bindActiveBook(session, bookId);
  470. session = appendToolEvents(session, metadata.events);
  471. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(session, request);
  472. const completed = pendingDecision
  473. ? {
  474. ...session,
  475. pendingDecision,
  476. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request),
  477. }
  478. : {
  479. ...markCompleted(session),
  480. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  481. };
  482. return {
  483. session: addEvent(completed, "task.completed", "completed", `Updated current focus for ${bookId}.`),
  484. responseText: metadata.responseText ?? (
  485. pendingDecision
  486. ? `Updated current focus for ${bookId}; waiting for your next decision.`
  487. : `Updated current focus for ${bookId}.`
  488. ),
  489. };
  490. }
  491. case "update_author_intent": {
  492. const bookId = request.bookId ?? session.activeBookId;
  493. if (!bookId) {
  494. throw new Error("No active book is bound to the interaction session.");
  495. }
  496. if (!request.instruction) {
  497. throw new Error("Author intent update requires instruction content.");
  498. }
  499. const toolResult = await params.tools.updateAuthorIntent(bookId, request.instruction);
  500. const metadata = extractToolMetadata(toolResult);
  501. session = bindActiveBook(session, bookId);
  502. session = appendToolEvents(session, metadata.events);
  503. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(session, request);
  504. const completed = pendingDecision
  505. ? {
  506. ...session,
  507. pendingDecision,
  508. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request),
  509. }
  510. : {
  511. ...markCompleted(session),
  512. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  513. };
  514. return {
  515. session: addEvent(completed, "task.completed", "completed", `Updated author intent for ${bookId}.`),
  516. responseText: metadata.responseText ?? (
  517. pendingDecision
  518. ? `Updated author intent for ${bookId}; waiting for your next decision.`
  519. : `Updated author intent for ${bookId}.`
  520. ),
  521. };
  522. }
  523. case "edit_truth": {
  524. const bookId = request.bookId ?? session.activeBookId;
  525. if (!bookId) {
  526. throw new Error("No active book is bound to the interaction session.");
  527. }
  528. if (!request.fileName || !request.instruction) {
  529. throw new Error("Truth-file edit requires a file name and content.");
  530. }
  531. const toolResult = await params.tools.writeTruthFile(bookId, request.fileName, request.instruction);
  532. const metadata = extractToolMetadata(toolResult);
  533. session = bindActiveBook(session, bookId);
  534. session = appendToolEvents(session, metadata.events);
  535. const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(session, request);
  536. const completed = pendingDecision
  537. ? {
  538. ...session,
  539. pendingDecision,
  540. currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request),
  541. }
  542. : {
  543. ...markCompleted(session),
  544. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  545. };
  546. return {
  547. session: addEvent(completed, "task.completed", "completed", `Updated ${request.fileName} for ${bookId}.`),
  548. responseText: metadata.responseText ?? (
  549. pendingDecision
  550. ? `Updated ${request.fileName} for ${bookId}; waiting for your next decision.`
  551. : `Updated ${request.fileName} for ${bookId}.`
  552. ),
  553. };
  554. }
  555. case "export_book": {
  556. const bookId = request.bookId ?? session.activeBookId;
  557. if (!params.tools.exportBook) {
  558. throw new Error("Book export is not implemented in the interaction runtime yet.");
  559. }
  560. if (!bookId) {
  561. throw new Error("No active book is bound to the interaction session.");
  562. }
  563. const toolResult = await params.tools.exportBook(bookId, {
  564. format: request.format,
  565. approvedOnly: request.approvedOnly,
  566. outputPath: request.outputPath,
  567. });
  568. const metadata = extractToolMetadata(toolResult);
  569. session = bindActiveBook(session, bookId, metadata.activeChapterNumber);
  570. session = appendToolEvents(session, metadata.events);
  571. const completed = {
  572. ...markCompleted(session),
  573. currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
  574. };
  575. return {
  576. session: addEvent(completed, "task.completed", "completed", `Exported ${bookId}.`),
  577. responseText: metadata.responseText ?? `Exported ${bookId}.`,
  578. details: metadata.details,
  579. };
  580. }
  581. case "switch_mode":
  582. session = markCompleted(session);
  583. return {
  584. session: addEvent(session, "task.completed", "completed", `Switched mode to ${session.automationMode}.`),
  585. responseText: `Switched mode to ${session.automationMode}.`,
  586. };
  587. case "pause_book": {
  588. const bookId = request.bookId ?? session.activeBookId;
  589. const paused = {
  590. ...session,
  591. currentExecution: {
  592. status: "blocked" as const,
  593. bookId,
  594. chapterNumber: session.activeChapterNumber,
  595. stageLabel: "paused by user",
  596. },
  597. };
  598. return {
  599. session: addEvent(paused, "task.completed", "blocked", `Paused ${bookId ?? "current book"}.`),
  600. responseText: `Paused ${bookId ?? "current book"}.`,
  601. };
  602. }
  603. case "resume_book": {
  604. const bookId = request.bookId ?? session.activeBookId;
  605. const resumed = {
  606. ...session,
  607. currentExecution: {
  608. status: "completed" as const,
  609. bookId,
  610. chapterNumber: session.activeChapterNumber,
  611. stageLabel: "ready to continue",
  612. },
  613. };
  614. return {
  615. session: addEvent(resumed, "task.completed", "completed", `Resumed ${bookId ?? "current book"}.`),
  616. responseText: `Resumed ${bookId ?? "current book"}.`,
  617. };
  618. }
  619. case "chat": {
  620. const bookId = request.bookId ?? session.activeBookId;
  621. const prompt = request.instruction?.trim().toLowerCase() ?? "";
  622. const toolResult = params.tools.chat
  623. ? await params.tools.chat(request.instruction ?? "", {
  624. bookId,
  625. automationMode: session.automationMode,
  626. })
  627. : undefined;
  628. const metadata = extractToolMetadata(toolResult);
  629. const responseText = metadata.responseText ?? (
  630. /^(hi|hello|hey|你好|嗨|哈喽)$/i.test(prompt)
  631. ? (bookId
  632. ? `Hi. Active book is ${bookId}. Ask me to continue, revise a chapter, or explain what is blocked.`
  633. : "Hi. No active book yet. Open a book, list books, or tell me what you want to write.")
  634. : (bookId
  635. ? `I’m here. Active book is ${bookId}. You can ask me to continue, revise a chapter, rewrite, change focus, or inspect why the pipeline stopped.`
  636. : "I’m here. No active book is bound yet. Open a book, list books, or describe what you want to write.")
  637. );
  638. const completed = markCompleted(session);
  639. return {
  640. session: addEvent(completed, "task.completed", "completed", responseText),
  641. responseText,
  642. };
  643. }
  644. case "explain_status":
  645. case "explain_failure": {
  646. const bookId = request.bookId ?? session.activeBookId;
  647. const baselineExecution = params.session.currentExecution;
  648. const stage = baselineExecution?.stageLabel ?? baselineExecution?.status ?? "idle";
  649. const summary = request.intent === "explain_failure"
  650. ? `Current failure context: ${bookId ?? "no active book"} is at ${stage}.`
  651. : `Current status: ${bookId ?? "no active book"} is at ${stage}.`;
  652. const completed = markCompleted(session);
  653. return {
  654. session: addEvent(completed, "task.completed", "completed", summary),
  655. responseText: summary,
  656. };
  657. }
  658. default:
  659. throw new Error(`Intent "${request.intent}" is not implemented in the interaction runtime yet.`);
  660. }
  661. }