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