novelforge-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/src/cli/index.js +125 -0
- package/dist/src/core/contextBuilder.js +128 -0
- package/dist/src/core/fileNames.js +41 -0
- package/dist/src/core/index.js +9 -0
- package/dist/src/core/projectDiscovery.js +141 -0
- package/dist/src/core/projectStore.js +85 -0
- package/dist/src/core/prompts/en-US.js +363 -0
- package/dist/src/core/prompts/types.js +1 -0
- package/dist/src/core/prompts/zh-CN.js +362 -0
- package/dist/src/core/prompts.js +15 -0
- package/dist/src/core/retrieval/chunker.js +77 -0
- package/dist/src/core/retrieval/index.js +125 -0
- package/dist/src/core/retrieval/tokenizer.js +43 -0
- package/dist/src/core/retrieval/types.js +1 -0
- package/dist/src/core/schemas.js +91 -0
- package/dist/src/core/steps/architecture.js +16 -0
- package/dist/src/core/steps/chapter.js +16 -0
- package/dist/src/core/steps/chapterReview.js +16 -0
- package/dist/src/core/steps/chapterRevision.js +20 -0
- package/dist/src/core/steps/continuityReview.js +13 -0
- package/dist/src/core/steps/crossChapterReview.js +15 -0
- package/dist/src/core/steps/index.js +20 -0
- package/dist/src/core/steps/memoryCard.js +22 -0
- package/dist/src/core/steps/novelMetadata.js +12 -0
- package/dist/src/core/steps/storyBible.js +13 -0
- package/dist/src/core/steps/types.js +7 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/workflow.js +186 -0
- package/dist/src/mcp/server.js +13 -0
- package/dist/src/mcp/tools.js +126 -0
- package/package.json +61 -0
- package/src/cli/index.ts +147 -0
- package/src/core/contextBuilder.ts +131 -0
- package/src/core/fileNames.ts +48 -0
- package/src/core/index.ts +9 -0
- package/src/core/projectDiscovery.ts +174 -0
- package/src/core/projectStore.ts +111 -0
- package/src/core/prompts/en-US.ts +376 -0
- package/src/core/prompts/types.ts +28 -0
- package/src/core/prompts/zh-CN.ts +375 -0
- package/src/core/prompts.ts +27 -0
- package/src/core/retrieval/chunker.ts +80 -0
- package/src/core/retrieval/index.ts +136 -0
- package/src/core/retrieval/tokenizer.ts +44 -0
- package/src/core/retrieval/types.ts +24 -0
- package/src/core/schemas.ts +101 -0
- package/src/core/steps/architecture.ts +17 -0
- package/src/core/steps/chapter.ts +17 -0
- package/src/core/steps/chapterReview.ts +17 -0
- package/src/core/steps/chapterRevision.ts +21 -0
- package/src/core/steps/continuityReview.ts +14 -0
- package/src/core/steps/crossChapterReview.ts +16 -0
- package/src/core/steps/index.ts +25 -0
- package/src/core/steps/memoryCard.ts +23 -0
- package/src/core/steps/novelMetadata.ts +13 -0
- package/src/core/steps/storyBible.ts +14 -0
- package/src/core/steps/types.ts +21 -0
- package/src/core/types.ts +115 -0
- package/src/core/workflow.ts +250 -0
- package/src/mcp/server.ts +15 -0
- package/src/mcp/tools.ts +227 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const CoreCastMemberSchema = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
role: z.string().min(1),
|
|
6
|
+
description: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const NovelMetadataSchema = z.object({
|
|
10
|
+
title: z.string().min(1),
|
|
11
|
+
genre: z.string().min(1),
|
|
12
|
+
premise: z.string().min(1),
|
|
13
|
+
language: z.string().min(1).default('zh-CN'),
|
|
14
|
+
style: z.string().min(1).default('清晰、连贯、适合长篇连载'),
|
|
15
|
+
coreCast: z.array(CoreCastMemberSchema).min(1),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const VolumeArchitectureSchema = z.object({
|
|
19
|
+
id: z.string().min(1),
|
|
20
|
+
title: z.string().min(1),
|
|
21
|
+
summary: z.string().min(1),
|
|
22
|
+
order: z.number().int().positive(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const ChapterArchitectureSchema = z.object({
|
|
26
|
+
chapterNumber: z.number().int().positive(),
|
|
27
|
+
title: z.string().min(1),
|
|
28
|
+
volumeId: z.string().min(1),
|
|
29
|
+
summary: z.string().min(1),
|
|
30
|
+
requiredBeats: z.array(z.string().min(1)).min(1),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const ArchitecturePayloadSchema = z.object({
|
|
34
|
+
full: z.string().min(1),
|
|
35
|
+
volumes: z.array(VolumeArchitectureSchema).min(1),
|
|
36
|
+
chapters: z.array(ChapterArchitectureSchema).min(1),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const MemoryCardSchema = z.object({
|
|
40
|
+
summary: z.string().min(1),
|
|
41
|
+
keyEvents: z.array(z.string().min(1)),
|
|
42
|
+
entities: z.array(z.object({
|
|
43
|
+
name: z.string().min(1),
|
|
44
|
+
type: z.string().min(1),
|
|
45
|
+
state: z.string().min(1),
|
|
46
|
+
})),
|
|
47
|
+
facts: z.array(z.object({
|
|
48
|
+
subject: z.string().min(1),
|
|
49
|
+
predicate: z.string().min(1),
|
|
50
|
+
object: z.string().min(1),
|
|
51
|
+
})),
|
|
52
|
+
stateChanges: z.array(z.object({
|
|
53
|
+
entity: z.string().min(1),
|
|
54
|
+
before: z.string().min(1),
|
|
55
|
+
after: z.string().min(1),
|
|
56
|
+
})),
|
|
57
|
+
openThreads: z.array(z.string().min(1)),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const ContinuityReviewSchema = z.object({
|
|
61
|
+
range: z.object({
|
|
62
|
+
start: z.number().int().positive(),
|
|
63
|
+
end: z.number().int().positive(),
|
|
64
|
+
}),
|
|
65
|
+
status: z.enum(['clean', 'issues_found']),
|
|
66
|
+
issues: z.array(z.object({
|
|
67
|
+
severity: z.enum(['low', 'medium', 'high']),
|
|
68
|
+
description: z.string().min(1),
|
|
69
|
+
evidence: z.string().min(1),
|
|
70
|
+
suggestion: z.string().min(1),
|
|
71
|
+
})),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const ChapterReviewIssueSchema = z.object({
|
|
75
|
+
severity: z.enum(['low', 'medium', 'high']),
|
|
76
|
+
category: z.enum(['character', 'world', 'timeline', 'item', 'knowledge', 'pacing', 'style', 'architecture']),
|
|
77
|
+
description: z.string().min(1),
|
|
78
|
+
evidence: z.string().min(1),
|
|
79
|
+
suggestion: z.string().min(1),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const ChapterReviewSchema = z.object({
|
|
83
|
+
chapterNumber: z.number().int().positive(),
|
|
84
|
+
status: z.enum(['clean', 'issues_found']),
|
|
85
|
+
issues: z.array(ChapterReviewIssueSchema),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const CrossChapterReviewSchema = z.object({
|
|
89
|
+
range: z.object({
|
|
90
|
+
start: z.number().int().positive(),
|
|
91
|
+
end: z.number().int().positive(),
|
|
92
|
+
}),
|
|
93
|
+
status: z.enum(['clean', 'issues_found']),
|
|
94
|
+
issues: z.array(z.object({
|
|
95
|
+
severity: z.enum(['low', 'medium', 'high']),
|
|
96
|
+
chapters: z.array(z.number().int().positive()).min(1),
|
|
97
|
+
description: z.string().min(1),
|
|
98
|
+
evidence: z.string().min(1),
|
|
99
|
+
suggestion: z.string().min(1),
|
|
100
|
+
})),
|
|
101
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ArchitecturePayloadSchema } from '../schemas.js';
|
|
2
|
+
import { saveJsonFile, saveMarkdownFile } from '../projectStore.js';
|
|
3
|
+
import { StepHandler, parseJson } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const architectureHandler: StepHandler = async (state, content) => {
|
|
6
|
+
const parsed = ArchitecturePayloadSchema.parse(parseJson(content));
|
|
7
|
+
const savedPaths = [
|
|
8
|
+
await saveMarkdownFile(state.projectPath, 'architecture/full.md', parsed.full),
|
|
9
|
+
await saveJsonFile(state.projectPath, 'architecture/volumes.json', parsed.volumes),
|
|
10
|
+
await saveJsonFile(state.projectPath, 'architecture/chapters.json', parsed.chapters),
|
|
11
|
+
];
|
|
12
|
+
return {
|
|
13
|
+
savedPaths,
|
|
14
|
+
fileEntries: { architecture: 'architecture/chapters.json' },
|
|
15
|
+
next: { kind: 'linear', nextStep: 'chapter' },
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { saveMarkdownFile } from '../projectStore.js';
|
|
3
|
+
import { chapterFileName } from '../fileNames.js';
|
|
4
|
+
import { indexChapter } from '../retrieval/index.js';
|
|
5
|
+
import { StepHandler, requireNonEmpty } from './types.js';
|
|
6
|
+
|
|
7
|
+
export const chapterHandler: StepHandler = async (state, content) => {
|
|
8
|
+
requireNonEmpty(content, 'Chapter Markdown');
|
|
9
|
+
const relative = join('chapters', chapterFileName(state.currentChapter));
|
|
10
|
+
const path = await saveMarkdownFile(state.projectPath, relative, content);
|
|
11
|
+
await indexChapter(state.projectPath, state.currentChapter, content);
|
|
12
|
+
return {
|
|
13
|
+
savedPaths: [path],
|
|
14
|
+
fileEntries: { [`chapter-${state.currentChapter}`]: relative },
|
|
15
|
+
next: { kind: 'linear', nextStep: 'memory_card' },
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { ChapterReviewSchema } from '../schemas.js';
|
|
3
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
4
|
+
import { chapterReviewFileName } from '../fileNames.js';
|
|
5
|
+
import { StepHandler, parseJson } from './types.js';
|
|
6
|
+
|
|
7
|
+
export const chapterReviewHandler: StepHandler = async (state, content) => {
|
|
8
|
+
const parsed = ChapterReviewSchema.parse(parseJson(content));
|
|
9
|
+
const target = state.pendingAction?.chapterNumber ?? parsed.chapterNumber;
|
|
10
|
+
const relative = join('reviews/chapter', chapterReviewFileName(target));
|
|
11
|
+
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
12
|
+
return {
|
|
13
|
+
savedPaths: [path],
|
|
14
|
+
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
15
|
+
next: { kind: 'sideTrackReturn' },
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { archiveChapterVersion, saveMarkdownFile } from '../projectStore.js';
|
|
3
|
+
import { chapterFileName, chapterVersionFileName } from '../fileNames.js';
|
|
4
|
+
import { indexChapter } from '../retrieval/index.js';
|
|
5
|
+
import { StepHandler, requireNonEmpty } from './types.js';
|
|
6
|
+
|
|
7
|
+
export const chapterRevisionHandler: StepHandler = async (state, content) => {
|
|
8
|
+
requireNonEmpty(content, 'Chapter revision Markdown');
|
|
9
|
+
const target = state.pendingAction?.chapterNumber ?? state.currentChapter;
|
|
10
|
+
const chapterRelative = join('chapters', chapterFileName(target));
|
|
11
|
+
const versionRelative = join('chapters/.versions', chapterVersionFileName(target, new Date().toISOString()));
|
|
12
|
+
const archived = await archiveChapterVersion(state.projectPath, chapterRelative, versionRelative);
|
|
13
|
+
const savedPaths = archived ? [archived] : [];
|
|
14
|
+
savedPaths.push(await saveMarkdownFile(state.projectPath, chapterRelative, content));
|
|
15
|
+
await indexChapter(state.projectPath, target, content);
|
|
16
|
+
return {
|
|
17
|
+
savedPaths,
|
|
18
|
+
fileEntries: { [`chapter-${target}`]: chapterRelative },
|
|
19
|
+
next: { kind: 'sideTrackReturn' },
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ContinuityReviewSchema } from '../schemas.js';
|
|
2
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { StepHandler, parseJson } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const continuityReviewHandler: StepHandler = async (state, content) => {
|
|
6
|
+
const parsed = ContinuityReviewSchema.parse(parseJson(content));
|
|
7
|
+
const relative = `reviews/continuity-${parsed.range.start}-${parsed.range.end}.json`;
|
|
8
|
+
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
9
|
+
return {
|
|
10
|
+
savedPaths: [path],
|
|
11
|
+
fileEntries: { continuityReview: relative },
|
|
12
|
+
next: { kind: 'linear', nextStep: 'complete' },
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { CrossChapterReviewSchema } from '../schemas.js';
|
|
3
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
4
|
+
import { crossChapterReviewFileName } from '../fileNames.js';
|
|
5
|
+
import { StepHandler, parseJson } from './types.js';
|
|
6
|
+
|
|
7
|
+
export const crossChapterReviewHandler: StepHandler = async (state, content) => {
|
|
8
|
+
const parsed = CrossChapterReviewSchema.parse(parseJson(content));
|
|
9
|
+
const relative = join('reviews/cross', crossChapterReviewFileName(parsed.range.start, parsed.range.end));
|
|
10
|
+
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
11
|
+
return {
|
|
12
|
+
savedPaths: [path],
|
|
13
|
+
fileEntries: { [`review-cross-${parsed.range.start}-${parsed.range.end}`]: relative },
|
|
14
|
+
next: { kind: 'sideTrackReturn' },
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WorkflowStep } from '../types.js';
|
|
2
|
+
import { architectureHandler } from './architecture.js';
|
|
3
|
+
import { chapterHandler } from './chapter.js';
|
|
4
|
+
import { chapterReviewHandler } from './chapterReview.js';
|
|
5
|
+
import { chapterRevisionHandler } from './chapterRevision.js';
|
|
6
|
+
import { continuityReviewHandler } from './continuityReview.js';
|
|
7
|
+
import { crossChapterReviewHandler } from './crossChapterReview.js';
|
|
8
|
+
import { memoryCardHandler } from './memoryCard.js';
|
|
9
|
+
import { novelMetadataHandler } from './novelMetadata.js';
|
|
10
|
+
import { storyBibleHandler } from './storyBible.js';
|
|
11
|
+
import { StepHandler } from './types.js';
|
|
12
|
+
|
|
13
|
+
export type { StepApplyNext, StepApplyResult, StepHandler } from './types.js';
|
|
14
|
+
|
|
15
|
+
export const STEP_HANDLERS: Partial<Record<WorkflowStep, StepHandler>> = {
|
|
16
|
+
novel_metadata: novelMetadataHandler,
|
|
17
|
+
story_bible: storyBibleHandler,
|
|
18
|
+
architecture: architectureHandler,
|
|
19
|
+
chapter: chapterHandler,
|
|
20
|
+
memory_card: memoryCardHandler,
|
|
21
|
+
continuity_review: continuityReviewHandler,
|
|
22
|
+
chapter_review: chapterReviewHandler,
|
|
23
|
+
chapter_revision: chapterRevisionHandler,
|
|
24
|
+
cross_chapter_review: crossChapterReviewHandler,
|
|
25
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { MemoryCardSchema } from '../schemas.js';
|
|
3
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
4
|
+
import { memoryFileName } from '../fileNames.js';
|
|
5
|
+
import { indexMemoryCard } from '../retrieval/index.js';
|
|
6
|
+
import { StepHandler, parseJson } from './types.js';
|
|
7
|
+
|
|
8
|
+
export const memoryCardHandler: StepHandler = async (state, content) => {
|
|
9
|
+
const parsed = MemoryCardSchema.parse(parseJson(content));
|
|
10
|
+
const relative = join('memory', memoryFileName(state.currentChapter));
|
|
11
|
+
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
12
|
+
await indexMemoryCard(state.projectPath, state.currentChapter, parsed);
|
|
13
|
+
const nextChapter = state.currentChapter + 1;
|
|
14
|
+
return {
|
|
15
|
+
savedPaths: [path],
|
|
16
|
+
fileEntries: { [`memory-${state.currentChapter}`]: relative },
|
|
17
|
+
next: {
|
|
18
|
+
kind: 'linear',
|
|
19
|
+
nextStep: nextChapter > state.targetChapters ? 'continuity_review' : 'chapter',
|
|
20
|
+
statePatch: { currentChapter: nextChapter },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NovelMetadataSchema } from '../schemas.js';
|
|
2
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { StepHandler, parseJson } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const novelMetadataHandler: StepHandler = async (state, content) => {
|
|
6
|
+
const parsed = NovelMetadataSchema.parse(parseJson(content));
|
|
7
|
+
const path = await saveJsonFile(state.projectPath, 'novel.json', parsed);
|
|
8
|
+
return {
|
|
9
|
+
savedPaths: [path],
|
|
10
|
+
fileEntries: { novel: 'novel.json' },
|
|
11
|
+
next: { kind: 'linear', nextStep: 'story_bible' },
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { saveMarkdownFile } from '../projectStore.js';
|
|
2
|
+
import { indexStoryBible } from '../retrieval/index.js';
|
|
3
|
+
import { StepHandler, requireNonEmpty } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const storyBibleHandler: StepHandler = async (state, content) => {
|
|
6
|
+
requireNonEmpty(content, 'Story bible Markdown');
|
|
7
|
+
const path = await saveMarkdownFile(state.projectPath, 'story-bible.md', content);
|
|
8
|
+
await indexStoryBible(state.projectPath, content);
|
|
9
|
+
return {
|
|
10
|
+
savedPaths: [path],
|
|
11
|
+
fileEntries: { storyBible: 'story-bible.md' },
|
|
12
|
+
next: { kind: 'linear', nextStep: 'architecture' },
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AgentState, WorkflowStep } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export type StepApplyNext =
|
|
4
|
+
| { kind: 'linear'; nextStep: WorkflowStep; statePatch?: Partial<AgentState> }
|
|
5
|
+
| { kind: 'sideTrackReturn' };
|
|
6
|
+
|
|
7
|
+
export interface StepApplyResult {
|
|
8
|
+
savedPaths: string[];
|
|
9
|
+
fileEntries?: Record<string, string>;
|
|
10
|
+
next: StepApplyNext;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type StepHandler = (state: AgentState, content: string) => Promise<StepApplyResult>;
|
|
14
|
+
|
|
15
|
+
export function parseJson(content: string): unknown {
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function requireNonEmpty(content: string, label: string): void {
|
|
20
|
+
if (!content.trim()) throw new Error(`${label} is empty`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type WorkflowStep =
|
|
2
|
+
| 'novel_metadata'
|
|
3
|
+
| 'story_bible'
|
|
4
|
+
| 'architecture'
|
|
5
|
+
| 'chapter'
|
|
6
|
+
| 'memory_card'
|
|
7
|
+
| 'continuity_review'
|
|
8
|
+
| 'chapter_review'
|
|
9
|
+
| 'chapter_revision'
|
|
10
|
+
| 'cross_chapter_review'
|
|
11
|
+
| 'complete';
|
|
12
|
+
|
|
13
|
+
export type ReviewSeverity = 'low' | 'medium' | 'high';
|
|
14
|
+
|
|
15
|
+
export interface ChapterReviewIssue {
|
|
16
|
+
severity: ReviewSeverity;
|
|
17
|
+
category: 'character' | 'world' | 'timeline' | 'item' | 'knowledge' | 'pacing' | 'style' | 'architecture';
|
|
18
|
+
description: string;
|
|
19
|
+
evidence: string;
|
|
20
|
+
suggestion: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ChapterReview {
|
|
24
|
+
chapterNumber: number;
|
|
25
|
+
status: 'clean' | 'issues_found';
|
|
26
|
+
issues: ChapterReviewIssue[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CrossChapterReview {
|
|
30
|
+
range: { start: number; end: number };
|
|
31
|
+
status: 'clean' | 'issues_found';
|
|
32
|
+
issues: Array<{
|
|
33
|
+
severity: ReviewSeverity;
|
|
34
|
+
chapters: number[];
|
|
35
|
+
description: string;
|
|
36
|
+
evidence: string;
|
|
37
|
+
suggestion: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CoreCastMember {
|
|
42
|
+
name: string;
|
|
43
|
+
role: string;
|
|
44
|
+
description: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface NovelMetadata {
|
|
48
|
+
title: string;
|
|
49
|
+
genre: string;
|
|
50
|
+
premise: string;
|
|
51
|
+
language: string;
|
|
52
|
+
style: string;
|
|
53
|
+
coreCast: CoreCastMember[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface VolumeArchitecture {
|
|
57
|
+
id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
summary: string;
|
|
60
|
+
order: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ChapterArchitecture {
|
|
64
|
+
chapterNumber: number;
|
|
65
|
+
title: string;
|
|
66
|
+
volumeId: string;
|
|
67
|
+
summary: string;
|
|
68
|
+
requiredBeats: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ArchitecturePayload {
|
|
72
|
+
full: string;
|
|
73
|
+
volumes: VolumeArchitecture[];
|
|
74
|
+
chapters: ChapterArchitecture[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MemoryCard {
|
|
78
|
+
summary: string;
|
|
79
|
+
keyEvents: string[];
|
|
80
|
+
entities: Array<{ name: string; type: string; state: string }>;
|
|
81
|
+
facts: Array<{ subject: string; predicate: string; object: string }>;
|
|
82
|
+
stateChanges: Array<{ entity: string; before: string; after: string }>;
|
|
83
|
+
openThreads: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PendingAction {
|
|
87
|
+
step: 'chapter_review' | 'chapter_revision' | 'cross_chapter_review';
|
|
88
|
+
chapterNumber?: number;
|
|
89
|
+
range?: { start: number; end: number };
|
|
90
|
+
feedback?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AgentState {
|
|
94
|
+
projectId: string;
|
|
95
|
+
projectPath: string;
|
|
96
|
+
initialPrompt: string;
|
|
97
|
+
language: 'zh-CN' | 'en-US';
|
|
98
|
+
targetChapters: number;
|
|
99
|
+
currentStep: WorkflowStep;
|
|
100
|
+
currentChapter: number;
|
|
101
|
+
completedSteps: WorkflowStep[];
|
|
102
|
+
files: Record<string, string>;
|
|
103
|
+
pendingAction?: PendingAction;
|
|
104
|
+
createdAt: string;
|
|
105
|
+
updatedAt: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface StepInstruction {
|
|
109
|
+
projectId: string;
|
|
110
|
+
projectPath: string;
|
|
111
|
+
currentStep: WorkflowStep;
|
|
112
|
+
instruction: string;
|
|
113
|
+
expectedFormat: string;
|
|
114
|
+
context: string;
|
|
115
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { WorkflowStep, AgentState, PendingAction, StepInstruction } from './types.js';
|
|
4
|
+
import { BuildContextInput, buildContext } from './contextBuilder.js';
|
|
5
|
+
import { buildPromptForStep } from './prompts.js';
|
|
6
|
+
import { loadState, saveJsonFile, saveRecoveryFile, saveState } from './projectStore.js';
|
|
7
|
+
import { STEP_HANDLERS } from './steps/index.js';
|
|
8
|
+
|
|
9
|
+
export interface SubmitStepInput {
|
|
10
|
+
projectPath: string;
|
|
11
|
+
step: WorkflowStep;
|
|
12
|
+
content: string;
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SubmitStepResult {
|
|
17
|
+
validation: { ok: boolean; message: string };
|
|
18
|
+
state: AgentState;
|
|
19
|
+
savedPaths: string[];
|
|
20
|
+
recoveryPath?: string;
|
|
21
|
+
next?: StepInstruction;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RequestSideTrackInput {
|
|
25
|
+
projectPath: string;
|
|
26
|
+
step: PendingAction['step'];
|
|
27
|
+
chapterNumber?: number;
|
|
28
|
+
range?: { start: number; end: number };
|
|
29
|
+
feedback?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Context recipes — one entry per step that needs packed context.
|
|
34
|
+
// Steps not listed here run with an empty context string.
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
type ContextRecipe = (state: AgentState) => Omit<BuildContextInput, 'projectPath'>;
|
|
38
|
+
|
|
39
|
+
const CONTEXT_RECIPES: Partial<Record<WorkflowStep, ContextRecipe>> = {
|
|
40
|
+
chapter: (s) => ({ purpose: 'chapter_generation', chapterNumber: s.currentChapter }),
|
|
41
|
+
memory_card: (s) => ({ purpose: 'memory_extraction', chapterNumber: s.currentChapter }),
|
|
42
|
+
continuity_review: () => ({ purpose: 'continuity_review' }),
|
|
43
|
+
chapter_review: (s) => ({
|
|
44
|
+
purpose: 'chapter_review',
|
|
45
|
+
chapterNumber: s.pendingAction?.chapterNumber ?? s.currentChapter,
|
|
46
|
+
}),
|
|
47
|
+
chapter_revision: (s) => ({
|
|
48
|
+
purpose: 'revision',
|
|
49
|
+
chapterNumber: s.pendingAction?.chapterNumber ?? s.currentChapter,
|
|
50
|
+
feedback: s.pendingAction?.feedback,
|
|
51
|
+
}),
|
|
52
|
+
cross_chapter_review: (s) => ({
|
|
53
|
+
purpose: 'cross_chapter_review',
|
|
54
|
+
range: s.pendingAction?.range,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
async function contextForStep(state: AgentState): Promise<string> {
|
|
59
|
+
const recipe = CONTEXT_RECIPES[state.currentStep];
|
|
60
|
+
if (!recipe) return '';
|
|
61
|
+
return buildContext({ projectPath: state.projectPath, ...recipe(state) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function instructionFor(state: AgentState): Promise<StepInstruction> {
|
|
65
|
+
const base = {
|
|
66
|
+
projectId: state.projectId,
|
|
67
|
+
projectPath: state.projectPath,
|
|
68
|
+
currentStep: state.currentStep,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (state.currentStep === 'complete') {
|
|
72
|
+
return { ...base, instruction: 'The workflow is complete.', expectedFormat: 'No output required', context: '' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const context = await contextForStep(state);
|
|
76
|
+
const prompt = buildPromptForStep({ state, context });
|
|
77
|
+
return { ...base, instruction: prompt.prompt, expectedFormat: prompt.expectedFormat, context };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getNextStep(projectPath: string): Promise<StepInstruction> {
|
|
81
|
+
return instructionFor(await loadState(projectPath));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// Side-track plumbing
|
|
86
|
+
// =============================================================================
|
|
87
|
+
|
|
88
|
+
interface SideTrackEntry {
|
|
89
|
+
step: PendingAction['step'];
|
|
90
|
+
resumeStep: WorkflowStep;
|
|
91
|
+
resumeChapter: number;
|
|
92
|
+
pendingAction: PendingAction;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SIDE_TRACK_FILE = '.agent-recovery/side-track.json';
|
|
96
|
+
|
|
97
|
+
async function saveSideTrack(state: AgentState, entry: SideTrackEntry): Promise<void> {
|
|
98
|
+
await saveState(state);
|
|
99
|
+
await saveJsonFile(state.projectPath, SIDE_TRACK_FILE, entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadSideTrack(projectPath: string): Promise<SideTrackEntry | undefined> {
|
|
103
|
+
try {
|
|
104
|
+
const raw = await readFile(join(projectPath, SIDE_TRACK_FILE), 'utf8');
|
|
105
|
+
return JSON.parse(raw) as SideTrackEntry;
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function clearSideTrack(projectPath: string): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
await unlink(join(projectPath, SIDE_TRACK_FILE));
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore: nothing to clear
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function maxExistingChapter(state: AgentState): number {
|
|
120
|
+
let max = 0;
|
|
121
|
+
for (const key of Object.keys(state.files)) {
|
|
122
|
+
const match = key.match(/^chapter-(\d+)$/);
|
|
123
|
+
if (match) {
|
|
124
|
+
const num = Number(match[1]);
|
|
125
|
+
if (num > max) max = num;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return max;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildPendingAction(state: AgentState, input: RequestSideTrackInput): PendingAction {
|
|
132
|
+
switch (input.step) {
|
|
133
|
+
case 'chapter_review': {
|
|
134
|
+
if (!input.chapterNumber) throw new Error('chapter_review requires chapterNumber');
|
|
135
|
+
return { step: 'chapter_review', chapterNumber: input.chapterNumber };
|
|
136
|
+
}
|
|
137
|
+
case 'chapter_revision': {
|
|
138
|
+
if (!input.chapterNumber) throw new Error('chapter_revision requires chapterNumber');
|
|
139
|
+
return { step: 'chapter_revision', chapterNumber: input.chapterNumber, feedback: input.feedback };
|
|
140
|
+
}
|
|
141
|
+
case 'cross_chapter_review': {
|
|
142
|
+
const max = maxExistingChapter(state);
|
|
143
|
+
const range = input.range ?? { start: 1, end: max || state.currentChapter };
|
|
144
|
+
if (range.start < 1 || range.end < range.start) throw new Error('Invalid range');
|
|
145
|
+
return { step: 'cross_chapter_review', range };
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unknown side-track step: ${(input as RequestSideTrackInput).step}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function requestSideTrack(input: RequestSideTrackInput): Promise<StepInstruction> {
|
|
153
|
+
const state = await loadState(input.projectPath);
|
|
154
|
+
const pendingAction = buildPendingAction(state, input);
|
|
155
|
+
const next: AgentState = { ...state, currentStep: input.step, pendingAction };
|
|
156
|
+
await saveSideTrack(next, {
|
|
157
|
+
step: input.step,
|
|
158
|
+
resumeStep: state.currentStep,
|
|
159
|
+
resumeChapter: state.currentChapter,
|
|
160
|
+
pendingAction,
|
|
161
|
+
});
|
|
162
|
+
return instructionFor(next);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// Submit dispatcher — thin glue over STEP_HANDLERS
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
function advanceLinear(
|
|
170
|
+
state: AgentState,
|
|
171
|
+
nextStep: WorkflowStep,
|
|
172
|
+
fileEntries: Record<string, string> = {},
|
|
173
|
+
statePatch: Partial<AgentState> = {}
|
|
174
|
+
): AgentState {
|
|
175
|
+
const { pendingAction: _pending, ...rest } = state;
|
|
176
|
+
return {
|
|
177
|
+
...rest,
|
|
178
|
+
...statePatch,
|
|
179
|
+
currentStep: nextStep,
|
|
180
|
+
completedSteps: [...state.completedSteps, state.currentStep],
|
|
181
|
+
files: { ...state.files, ...fileEntries },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function resumeFromSideTrack(
|
|
186
|
+
state: AgentState,
|
|
187
|
+
fileEntries: Record<string, string>
|
|
188
|
+
): Promise<AgentState> {
|
|
189
|
+
const sideTrack = await loadSideTrack(state.projectPath);
|
|
190
|
+
await clearSideTrack(state.projectPath);
|
|
191
|
+
return {
|
|
192
|
+
...state,
|
|
193
|
+
currentStep: sideTrack?.resumeStep ?? state.currentStep,
|
|
194
|
+
currentChapter: sideTrack?.resumeChapter ?? state.currentChapter,
|
|
195
|
+
completedSteps: [...state.completedSteps, state.currentStep],
|
|
196
|
+
files: { ...state.files, ...fileEntries },
|
|
197
|
+
pendingAction: undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function submitStepResult(input: SubmitStepInput): Promise<SubmitStepResult> {
|
|
202
|
+
const state = await loadState(input.projectPath);
|
|
203
|
+
|
|
204
|
+
if (state.currentStep !== input.step) {
|
|
205
|
+
const recoveryPath = await saveRecoveryFile(state.projectPath, input.step, input.content);
|
|
206
|
+
return {
|
|
207
|
+
validation: { ok: false, message: `Expected step ${state.currentStep}, got ${input.step}` },
|
|
208
|
+
state,
|
|
209
|
+
savedPaths: [],
|
|
210
|
+
recoveryPath,
|
|
211
|
+
next: await instructionFor(state),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handler = STEP_HANDLERS[input.step];
|
|
216
|
+
if (!handler) {
|
|
217
|
+
return {
|
|
218
|
+
validation: { ok: false, message: `Step ${input.step} accepts no submission` },
|
|
219
|
+
state,
|
|
220
|
+
savedPaths: [],
|
|
221
|
+
next: await instructionFor(state),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const result = await handler(state, input.content);
|
|
227
|
+
const fileEntries = result.fileEntries ?? {};
|
|
228
|
+
const nextState =
|
|
229
|
+
result.next.kind === 'linear'
|
|
230
|
+
? advanceLinear(state, result.next.nextStep, fileEntries, result.next.statePatch)
|
|
231
|
+
: await resumeFromSideTrack(state, fileEntries);
|
|
232
|
+
|
|
233
|
+
await saveState(nextState);
|
|
234
|
+
return {
|
|
235
|
+
validation: { ok: true, message: 'Saved' },
|
|
236
|
+
state: nextState,
|
|
237
|
+
savedPaths: result.savedPaths,
|
|
238
|
+
next: await instructionFor(nextState),
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const recoveryPath = await saveRecoveryFile(state.projectPath, input.step, input.content);
|
|
242
|
+
return {
|
|
243
|
+
validation: { ok: false, message: (error as Error).message },
|
|
244
|
+
state,
|
|
245
|
+
savedPaths: [],
|
|
246
|
+
recoveryPath,
|
|
247
|
+
next: await instructionFor(state),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|