@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.
- package/dist/agent/agent-session.d.ts +15 -0
- package/dist/agent/agent-session.d.ts.map +1 -1
- package/dist/agent/agent-session.js +256 -26
- package/dist/agent/agent-session.js.map +1 -1
- package/dist/agent/agent-system-prompt.d.ts +8 -1
- package/dist/agent/agent-system-prompt.d.ts.map +1 -1
- package/dist/agent/agent-system-prompt.js +382 -176
- package/dist/agent/agent-system-prompt.js.map +1 -1
- package/dist/agent/agent-tools.d.ts +156 -19
- package/dist/agent/agent-tools.d.ts.map +1 -1
- package/dist/agent/agent-tools.js +980 -46
- package/dist/agent/agent-tools.js.map +1 -1
- package/dist/agent/context-transform.d.ts +4 -1
- package/dist/agent/context-transform.d.ts.map +1 -1
- package/dist/agent/context-transform.js +104 -9
- package/dist/agent/context-transform.js.map +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agents/architect.d.ts +11 -1
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +242 -114
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/chapter-analyzer.js +1 -1
- package/dist/agents/chapter-analyzer.js.map +1 -1
- package/dist/agents/composer.d.ts +36 -0
- package/dist/agents/composer.d.ts.map +1 -1
- package/dist/agents/composer.js +503 -20
- package/dist/agents/composer.js.map +1 -1
- package/dist/agents/continuity.d.ts +3 -0
- package/dist/agents/continuity.d.ts.map +1 -1
- package/dist/agents/continuity.js +28 -14
- package/dist/agents/continuity.js.map +1 -1
- package/dist/agents/en-prompt-sections.d.ts.map +1 -1
- package/dist/agents/en-prompt-sections.js +15 -1
- package/dist/agents/en-prompt-sections.js.map +1 -1
- package/dist/agents/fanfic-canon-importer.d.ts +1 -0
- package/dist/agents/fanfic-canon-importer.d.ts.map +1 -1
- package/dist/agents/fanfic-canon-importer.js +53 -6
- package/dist/agents/fanfic-canon-importer.js.map +1 -1
- package/dist/agents/foundation-reviewer.d.ts +1 -0
- package/dist/agents/foundation-reviewer.d.ts.map +1 -1
- package/dist/agents/foundation-reviewer.js +17 -12
- package/dist/agents/foundation-reviewer.js.map +1 -1
- package/dist/agents/length-normalizer.d.ts +1 -0
- package/dist/agents/length-normalizer.d.ts.map +1 -1
- package/dist/agents/length-normalizer.js +16 -3
- package/dist/agents/length-normalizer.js.map +1 -1
- package/dist/agents/planner-prompts.d.ts +7 -7
- package/dist/agents/planner-prompts.d.ts.map +1 -1
- package/dist/agents/planner-prompts.js +29 -29
- package/dist/agents/planner-prompts.js.map +1 -1
- package/dist/agents/planner.d.ts +6 -5
- package/dist/agents/planner.d.ts.map +1 -1
- package/dist/agents/planner.js +90 -6
- package/dist/agents/planner.js.map +1 -1
- package/dist/agents/post-write-validator.d.ts.map +1 -1
- package/dist/agents/post-write-validator.js +49 -0
- package/dist/agents/post-write-validator.js.map +1 -1
- package/dist/agents/reviser.js +10 -0
- package/dist/agents/reviser.js.map +1 -1
- package/dist/agents/rules-reader.d.ts +6 -14
- package/dist/agents/rules-reader.d.ts.map +1 -1
- package/dist/agents/rules-reader.js +15 -28
- package/dist/agents/rules-reader.js.map +1 -1
- package/dist/agents/short-fiction.d.ts +4 -0
- package/dist/agents/short-fiction.d.ts.map +1 -1
- package/dist/agents/short-fiction.js +51 -8
- package/dist/agents/short-fiction.js.map +1 -1
- package/dist/agents/state-validator.d.ts +0 -2
- package/dist/agents/state-validator.d.ts.map +1 -1
- package/dist/agents/state-validator.js +4 -16
- package/dist/agents/state-validator.js.map +1 -1
- package/dist/agents/style-analyzer.d.ts +1 -1
- package/dist/agents/style-analyzer.d.ts.map +1 -1
- package/dist/agents/style-analyzer.js +34 -17
- package/dist/agents/style-analyzer.js.map +1 -1
- package/dist/agents/writer-prompts.d.ts.map +1 -1
- package/dist/agents/writer-prompts.js +160 -12
- package/dist/agents/writer-prompts.js.map +1 -1
- package/dist/agents/writer.d.ts.map +1 -1
- package/dist/agents/writer.js +31 -9
- package/dist/agents/writer.js.map +1 -1
- package/dist/index.d.ts +18 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/interaction/action-envelope.d.ts +261 -0
- package/dist/interaction/action-envelope.d.ts.map +1 -0
- package/dist/interaction/action-envelope.js +102 -0
- package/dist/interaction/action-envelope.js.map +1 -0
- package/dist/interaction/book-session-store.d.ts +6 -2
- package/dist/interaction/book-session-store.d.ts.map +1 -1
- package/dist/interaction/book-session-store.js +21 -3
- package/dist/interaction/book-session-store.js.map +1 -1
- package/dist/interaction/edit-controller.d.ts +5 -0
- package/dist/interaction/edit-controller.d.ts.map +1 -1
- package/dist/interaction/edit-controller.js +123 -26
- package/dist/interaction/edit-controller.js.map +1 -1
- package/dist/interaction/events.d.ts +4 -4
- package/dist/interaction/intents.d.ts +9 -6
- package/dist/interaction/intents.d.ts.map +1 -1
- package/dist/interaction/intents.js +2 -1
- package/dist/interaction/intents.js.map +1 -1
- package/dist/interaction/project-control.d.ts +3 -43
- package/dist/interaction/project-control.d.ts.map +1 -1
- package/dist/interaction/project-control.js +1 -53
- package/dist/interaction/project-control.js.map +1 -1
- package/dist/interaction/project-tools.d.ts +1 -1
- package/dist/interaction/project-tools.d.ts.map +1 -1
- package/dist/interaction/project-tools.js +41 -185
- package/dist/interaction/project-tools.js.map +1 -1
- package/dist/interaction/runtime.d.ts +1 -1
- package/dist/interaction/runtime.d.ts.map +1 -1
- package/dist/interaction/runtime.js +49 -75
- package/dist/interaction/runtime.js.map +1 -1
- package/dist/interaction/session-transcript-legacy.d.ts.map +1 -1
- package/dist/interaction/session-transcript-legacy.js +2 -0
- package/dist/interaction/session-transcript-legacy.js.map +1 -1
- package/dist/interaction/session-transcript-restore.d.ts +4 -3
- package/dist/interaction/session-transcript-restore.d.ts.map +1 -1
- package/dist/interaction/session-transcript-restore.js +234 -34
- package/dist/interaction/session-transcript-restore.js.map +1 -1
- package/dist/interaction/session-transcript-schema.d.ts +45 -12
- package/dist/interaction/session-transcript-schema.d.ts.map +1 -1
- package/dist/interaction/session-transcript-schema.js +6 -0
- package/dist/interaction/session-transcript-schema.js.map +1 -1
- package/dist/interaction/session-transcript.d.ts +8 -1
- package/dist/interaction/session-transcript.d.ts.map +1 -1
- package/dist/interaction/session-transcript.js +13 -1
- package/dist/interaction/session-transcript.js.map +1 -1
- package/dist/interaction/session.d.ts +78 -66
- package/dist/interaction/session.d.ts.map +1 -1
- package/dist/interaction/session.js +10 -2
- package/dist/interaction/session.js.map +1 -1
- package/dist/llm/provider.d.ts +32 -34
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +144 -127
- package/dist/llm/provider.js.map +1 -1
- package/dist/models/book-rules.d.ts +6 -4
- package/dist/models/book-rules.d.ts.map +1 -1
- package/dist/models/book-rules.js +187 -8
- package/dist/models/book-rules.js.map +1 -1
- package/dist/models/context-compression.d.ts +13 -0
- package/dist/models/context-compression.d.ts.map +1 -0
- package/dist/models/context-compression.js +2 -0
- package/dist/models/context-compression.js.map +1 -0
- package/dist/models/input-governance.d.ts +53 -12
- package/dist/models/input-governance.d.ts.map +1 -1
- package/dist/models/input-governance.js +16 -0
- package/dist/models/input-governance.js.map +1 -1
- package/dist/models/play.d.ts +530 -0
- package/dist/models/play.d.ts.map +1 -0
- package/dist/models/play.js +318 -0
- package/dist/models/play.js.map +1 -0
- package/dist/models/project.d.ts +8 -0
- package/dist/models/project.d.ts.map +1 -1
- package/dist/models/project.js +1 -0
- package/dist/models/project.js.map +1 -1
- package/dist/pipeline/chapter-review-cycle.d.ts.map +1 -1
- package/dist/pipeline/chapter-review-cycle.js +29 -3
- package/dist/pipeline/chapter-review-cycle.js.map +1 -1
- package/dist/pipeline/persisted-governed-plan.d.ts.map +1 -1
- package/dist/pipeline/persisted-governed-plan.js +98 -49
- package/dist/pipeline/persisted-governed-plan.js.map +1 -1
- package/dist/pipeline/runner.d.ts +31 -0
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +212 -68
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pipeline/short-fiction-runner.d.ts +14 -0
- package/dist/pipeline/short-fiction-runner.d.ts.map +1 -1
- package/dist/pipeline/short-fiction-runner.js +242 -94
- package/dist/pipeline/short-fiction-runner.js.map +1 -1
- package/dist/play/play-agents.d.ts +71 -0
- package/dist/play/play-agents.d.ts.map +1 -0
- package/dist/play/play-agents.js +511 -0
- package/dist/play/play-agents.js.map +1 -0
- package/dist/play/play-db-factory.d.ts +9 -0
- package/dist/play/play-db-factory.d.ts.map +1 -0
- package/dist/play/play-db-factory.js +18 -0
- package/dist/play/play-db-factory.js.map +1 -0
- package/dist/play/play-db.d.ts +22 -0
- package/dist/play/play-db.d.ts.map +1 -0
- package/dist/play/play-db.js +248 -0
- package/dist/play/play-db.js.map +1 -0
- package/dist/play/play-file-db.d.ts +32 -0
- package/dist/play/play-file-db.d.ts.map +1 -0
- package/dist/play/play-file-db.js +156 -0
- package/dist/play/play-file-db.js.map +1 -0
- package/dist/play/play-image.d.ts +58 -0
- package/dist/play/play-image.d.ts.map +1 -0
- package/dist/play/play-image.js +142 -0
- package/dist/play/play-image.js.map +1 -0
- package/dist/play/play-reducer.d.ts +31 -0
- package/dist/play/play-reducer.d.ts.map +1 -0
- package/dist/play/play-reducer.js +261 -0
- package/dist/play/play-reducer.js.map +1 -0
- package/dist/play/play-runner.d.ts +102 -0
- package/dist/play/play-runner.d.ts.map +1 -0
- package/dist/play/play-runner.js +465 -0
- package/dist/play/play-runner.js.map +1 -0
- package/dist/play/play-store.d.ts +112 -0
- package/dist/play/play-store.d.ts.map +1 -0
- package/dist/play/play-store.js +311 -0
- package/dist/play/play-store.js.map +1 -0
- package/dist/prompts/short-fiction.d.ts +5 -0
- package/dist/prompts/short-fiction.d.ts.map +1 -1
- package/dist/prompts/short-fiction.js +46 -22
- package/dist/prompts/short-fiction.js.map +1 -1
- package/dist/state/state-bootstrap.d.ts.map +1 -1
- package/dist/state/state-bootstrap.js +12 -25
- package/dist/state/state-bootstrap.js.map +1 -1
- package/dist/state/state-reducer.js +31 -22
- package/dist/state/state-reducer.js.map +1 -1
- package/dist/utils/book-eval.d.ts +35 -0
- package/dist/utils/book-eval.d.ts.map +1 -0
- package/dist/utils/book-eval.js +116 -0
- package/dist/utils/book-eval.js.map +1 -0
- package/dist/utils/chapter-memo-parser.d.ts +10 -7
- package/dist/utils/chapter-memo-parser.d.ts.map +1 -1
- package/dist/utils/chapter-memo-parser.js +86 -43
- package/dist/utils/chapter-memo-parser.js.map +1 -1
- package/dist/utils/context-assembly.d.ts +2 -0
- package/dist/utils/context-assembly.d.ts.map +1 -1
- package/dist/utils/context-assembly.js +38 -1
- package/dist/utils/context-assembly.js.map +1 -1
- package/dist/utils/hook-health.d.ts.map +1 -1
- package/dist/utils/hook-health.js +5 -2
- package/dist/utils/hook-health.js.map +1 -1
- package/dist/utils/hook-ledger-validator.d.ts +1 -1
- package/dist/utils/hook-ledger-validator.d.ts.map +1 -1
- package/dist/utils/hook-ledger-validator.js +5 -5
- package/dist/utils/hook-ledger-validator.js.map +1 -1
- package/dist/utils/hook-lifecycle.d.ts +1 -0
- package/dist/utils/hook-lifecycle.d.ts.map +1 -1
- package/dist/utils/hook-lifecycle.js +10 -3
- package/dist/utils/hook-lifecycle.js.map +1 -1
- package/dist/utils/language.d.ts +10 -0
- package/dist/utils/language.d.ts.map +1 -0
- package/dist/utils/language.js +18 -0
- package/dist/utils/language.js.map +1 -0
- package/dist/utils/length-metrics.d.ts +3 -0
- package/dist/utils/length-metrics.d.ts.map +1 -1
- package/dist/utils/length-metrics.js +8 -0
- package/dist/utils/length-metrics.js.map +1 -1
- package/dist/utils/memory-retrieval.d.ts.map +1 -1
- package/dist/utils/memory-retrieval.js +19 -15
- package/dist/utils/memory-retrieval.js.map +1 -1
- package/dist/utils/outline-paths.d.ts +12 -0
- package/dist/utils/outline-paths.d.ts.map +1 -1
- package/dist/utils/outline-paths.js +68 -0
- package/dist/utils/outline-paths.js.map +1 -1
- package/package.json +1 -1
- package/dist/interaction/nl-router.d.ts +0 -8
- package/dist/interaction/nl-router.d.ts.map +0 -1
- package/dist/interaction/nl-router.js +0 -218
- package/dist/interaction/nl-router.js.map +0 -1
- package/dist/pipeline/agent.d.ts +0 -15
- package/dist/pipeline/agent.d.ts.map +0 -1
- package/dist/pipeline/agent.js +0 -597
- 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
|
-
//
|
|
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:
|
|
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:
|
|
109
|
-
"
|
|
110
|
-
"
|
|
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
|
|
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 =
|
|
143
|
-
?
|
|
144
|
-
:
|
|
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:
|
|
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 ??
|
|
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
|
-
|
|
168
|
-
|
|
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:
|
|
172
|
-
title:
|
|
173
|
-
wordCount
|
|
174
|
-
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 ${
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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,
|