novelforge-agent 0.1.1 → 0.2.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/README.md +36 -13
- package/dist/src/cli/index.js +71 -2
- package/dist/src/core/bibleStore.js +36 -0
- package/dist/src/core/characterStore.js +74 -0
- package/dist/src/core/contextBuilder.js +44 -1
- package/dist/src/core/fileNames.js +4 -0
- package/dist/src/core/index.js +4 -0
- package/dist/src/core/projectOps.js +187 -0
- package/dist/src/core/projectStore.js +11 -0
- package/dist/src/core/prompts/en-US.js +117 -13
- package/dist/src/core/prompts/zh-CN.js +116 -12
- package/dist/src/core/retrieval/index.js +8 -0
- package/dist/src/core/schemas.js +98 -1
- package/dist/src/core/steps/architecture.js +7 -1
- package/dist/src/core/steps/chapter.js +11 -1
- package/dist/src/core/steps/chapterReview.js +25 -1
- package/dist/src/core/steps/chapterRevision.js +17 -0
- package/dist/src/core/steps/memoryCard.js +4 -0
- package/dist/src/core/steps/novelMetadata.js +4 -2
- package/dist/src/core/threadStore.js +150 -0
- package/dist/src/core/workflow.js +3 -3
- package/dist/src/mcp/tools.js +198 -18
- package/package.json +5 -1
- package/src/cli/index.ts +74 -1
- package/src/core/bibleStore.ts +57 -0
- package/src/core/characterStore.ts +93 -0
- package/src/core/contextBuilder.ts +44 -4
- package/src/core/fileNames.ts +5 -0
- package/src/core/index.ts +4 -0
- package/src/core/projectOps.ts +243 -0
- package/src/core/projectStore.ts +11 -0
- package/src/core/prompts/en-US.ts +126 -22
- package/src/core/prompts/types.ts +2 -1
- package/src/core/prompts/zh-CN.ts +118 -14
- package/src/core/retrieval/index.ts +10 -0
- package/src/core/schemas.ts +108 -1
- package/src/core/steps/architecture.ts +7 -1
- package/src/core/steps/chapter.ts +11 -1
- package/src/core/steps/chapterReview.ts +27 -1
- package/src/core/steps/chapterRevision.ts +18 -0
- package/src/core/steps/memoryCard.ts +4 -0
- package/src/core/steps/novelMetadata.ts +4 -2
- package/src/core/threadStore.ts +173 -0
- package/src/core/types.ts +102 -1
- package/src/core/workflow.ts +3 -3
- package/src/mcp/tools.ts +322 -19
|
@@ -9,9 +9,35 @@ export const chapterReviewHandler: StepHandler = async (state, content) => {
|
|
|
9
9
|
const target = state.pendingAction?.chapterNumber ?? parsed.chapterNumber;
|
|
10
10
|
const relative = join('reviews/chapter', chapterReviewFileName(target));
|
|
11
11
|
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
12
|
+
if (state.pendingAction?.mode === 'side_track') {
|
|
13
|
+
return {
|
|
14
|
+
savedPaths: [path],
|
|
15
|
+
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
16
|
+
next: { kind: 'sideTrackReturn' },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (parsed.status === 'clean') {
|
|
21
|
+
return {
|
|
22
|
+
savedPaths: [path],
|
|
23
|
+
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
24
|
+
next: { kind: 'linear', nextStep: 'memory_card' },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
return {
|
|
13
29
|
savedPaths: [path],
|
|
14
30
|
fileEntries: { [`review-chapter-${target}`]: relative },
|
|
15
|
-
next: {
|
|
31
|
+
next: {
|
|
32
|
+
kind: 'linear',
|
|
33
|
+
nextStep: 'chapter_revision',
|
|
34
|
+
statePatch: {
|
|
35
|
+
pendingAction: {
|
|
36
|
+
step: 'chapter_revision',
|
|
37
|
+
mode: 'gate',
|
|
38
|
+
chapterNumber: target,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
16
42
|
};
|
|
17
43
|
};
|
|
@@ -13,6 +13,24 @@ export const chapterRevisionHandler: StepHandler = async (state, content) => {
|
|
|
13
13
|
const savedPaths = archived ? [archived] : [];
|
|
14
14
|
savedPaths.push(await saveMarkdownFile(state.projectPath, chapterRelative, content));
|
|
15
15
|
await indexChapter(state.projectPath, target, content);
|
|
16
|
+
if (state.pendingAction?.mode === 'gate') {
|
|
17
|
+
return {
|
|
18
|
+
savedPaths,
|
|
19
|
+
fileEntries: { [`chapter-${target}`]: chapterRelative },
|
|
20
|
+
next: {
|
|
21
|
+
kind: 'linear',
|
|
22
|
+
nextStep: 'chapter_review',
|
|
23
|
+
statePatch: {
|
|
24
|
+
pendingAction: {
|
|
25
|
+
step: 'chapter_review',
|
|
26
|
+
mode: 'gate',
|
|
27
|
+
chapterNumber: target,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
return {
|
|
17
35
|
savedPaths,
|
|
18
36
|
fileEntries: { [`chapter-${target}`]: chapterRelative },
|
|
@@ -3,6 +3,8 @@ import { MemoryCardSchema } from '../schemas.js';
|
|
|
3
3
|
import { saveJsonFile } from '../projectStore.js';
|
|
4
4
|
import { memoryFileName } from '../fileNames.js';
|
|
5
5
|
import { indexMemoryCard } from '../retrieval/index.js';
|
|
6
|
+
import { ingestMemoryCardThreads } from '../threadStore.js';
|
|
7
|
+
import { applyCharacterUpdates } from '../characterStore.js';
|
|
6
8
|
import { StepHandler, parseJson } from './types.js';
|
|
7
9
|
|
|
8
10
|
export const memoryCardHandler: StepHandler = async (state, content) => {
|
|
@@ -10,6 +12,8 @@ export const memoryCardHandler: StepHandler = async (state, content) => {
|
|
|
10
12
|
const relative = join('memory', memoryFileName(state.currentChapter));
|
|
11
13
|
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
12
14
|
await indexMemoryCard(state.projectPath, state.currentChapter, parsed);
|
|
15
|
+
await ingestMemoryCardThreads(state.projectPath, state.currentChapter, parsed.threadActions);
|
|
16
|
+
await applyCharacterUpdates(state.projectPath, state.currentChapter, parsed.characterUpdates);
|
|
13
17
|
const nextChapter = state.currentChapter + 1;
|
|
14
18
|
return {
|
|
15
19
|
savedPaths: [path],
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { NovelMetadataSchema } from '../schemas.js';
|
|
2
2
|
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { initializeCharacterStates } from '../characterStore.js';
|
|
3
4
|
import { StepHandler, parseJson } from './types.js';
|
|
4
5
|
|
|
5
6
|
export const novelMetadataHandler: StepHandler = async (state, content) => {
|
|
6
7
|
const parsed = NovelMetadataSchema.parse(parseJson(content));
|
|
7
8
|
const path = await saveJsonFile(state.projectPath, 'novel.json', parsed);
|
|
9
|
+
const charactersPath = await initializeCharacterStates(state.projectPath, parsed.coreCast);
|
|
8
10
|
return {
|
|
9
|
-
savedPaths: [path],
|
|
10
|
-
fileEntries: { novel: 'novel.json' },
|
|
11
|
+
savedPaths: [path, charactersPath],
|
|
12
|
+
fileEntries: { novel: 'novel.json', characters: 'characters.json' },
|
|
11
13
|
next: { kind: 'linear', nextStep: 'story_bible' },
|
|
12
14
|
};
|
|
13
15
|
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Thread, ThreadAction, ThreadStatus } from './types.js';
|
|
5
|
+
|
|
6
|
+
const THREADS_FILE = 'threads.json';
|
|
7
|
+
|
|
8
|
+
export interface ThreadsBundle {
|
|
9
|
+
threads: Thread[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function loadThreads(projectPath: string): Promise<Thread[]> {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(join(projectPath, THREADS_FILE), 'utf8');
|
|
15
|
+
const parsed = JSON.parse(raw) as ThreadsBundle;
|
|
16
|
+
return Array.isArray(parsed.threads) ? parsed.threads : [];
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function saveThreads(projectPath: string, threads: Thread[]): Promise<string> {
|
|
23
|
+
const fullPath = join(projectPath, THREADS_FILE);
|
|
24
|
+
const bundle: ThreadsBundle = { threads };
|
|
25
|
+
await writeFile(fullPath, `${JSON.stringify(bundle, null, 2)}\n`, 'utf8');
|
|
26
|
+
return fullPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function newThreadId(existing: Set<string>): string {
|
|
30
|
+
let candidate = `t_${randomBytes(3).toString('hex')}`;
|
|
31
|
+
while (existing.has(candidate)) {
|
|
32
|
+
candidate = `t_${randomBytes(3).toString('hex')}`;
|
|
33
|
+
}
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findByDescription(threads: Thread[], description: string): Thread | undefined {
|
|
38
|
+
const target = description.trim();
|
|
39
|
+
return threads.find((t) => t.description.trim() === target);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply the threadActions emitted by a memory_card for chapter `chapterNumber`.
|
|
44
|
+
* Behavior:
|
|
45
|
+
* - 'plant' → create new thread (or reuse if identical description already planted)
|
|
46
|
+
* - 'build' → mark existing thread status = 'building', bump lastTouchedAt
|
|
47
|
+
* - 'pay' → mark existing thread status = 'paid', set paidOffAt
|
|
48
|
+
* - 'drop' → mark existing thread status = 'dropped', set droppedAt
|
|
49
|
+
* Unknown threadIds for non-plant actions are tolerated (a new thread is created and marked appropriately, so we never lose user intent).
|
|
50
|
+
*/
|
|
51
|
+
export function applyThreadActions(
|
|
52
|
+
existing: Thread[],
|
|
53
|
+
chapterNumber: number,
|
|
54
|
+
actions: ThreadAction[]
|
|
55
|
+
): Thread[] {
|
|
56
|
+
if (!actions || !actions.length) return existing;
|
|
57
|
+
const next: Thread[] = existing.map((t) => ({ ...t }));
|
|
58
|
+
const byId = new Map(next.map((t) => [t.id, t]));
|
|
59
|
+
const usedIds = new Set(next.map((t) => t.id));
|
|
60
|
+
|
|
61
|
+
for (const action of actions) {
|
|
62
|
+
if (action.kind === 'plant') {
|
|
63
|
+
const dup = findByDescription(next, action.description);
|
|
64
|
+
if (dup) {
|
|
65
|
+
dup.lastTouchedAt = Math.max(dup.lastTouchedAt, chapterNumber);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const id = action.threadId && !usedIds.has(action.threadId)
|
|
69
|
+
? action.threadId
|
|
70
|
+
: newThreadId(usedIds);
|
|
71
|
+
usedIds.add(id);
|
|
72
|
+
const planted: Thread = {
|
|
73
|
+
id,
|
|
74
|
+
description: action.description.trim(),
|
|
75
|
+
status: 'planted',
|
|
76
|
+
plantedAt: chapterNumber,
|
|
77
|
+
lastTouchedAt: chapterNumber,
|
|
78
|
+
};
|
|
79
|
+
next.push(planted);
|
|
80
|
+
byId.set(id, planted);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// build / pay / drop need an existing thread
|
|
85
|
+
let target = action.threadId ? byId.get(action.threadId) : undefined;
|
|
86
|
+
if (!target) {
|
|
87
|
+
target = findByDescription(next, action.description);
|
|
88
|
+
}
|
|
89
|
+
if (!target) {
|
|
90
|
+
// Create a placeholder so the user intent is captured; mark planted+touched on this chapter
|
|
91
|
+
const id = newThreadId(usedIds);
|
|
92
|
+
usedIds.add(id);
|
|
93
|
+
target = {
|
|
94
|
+
id,
|
|
95
|
+
description: action.description.trim(),
|
|
96
|
+
status: 'planted',
|
|
97
|
+
plantedAt: chapterNumber,
|
|
98
|
+
lastTouchedAt: chapterNumber,
|
|
99
|
+
notes: `Auto-created from a ${action.kind} action without a known threadId.`,
|
|
100
|
+
};
|
|
101
|
+
next.push(target);
|
|
102
|
+
byId.set(id, target);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
target.lastTouchedAt = chapterNumber;
|
|
106
|
+
if (action.kind === 'build') {
|
|
107
|
+
target.status = 'building';
|
|
108
|
+
} else if (action.kind === 'pay') {
|
|
109
|
+
target.status = 'paid';
|
|
110
|
+
target.paidOffAt = chapterNumber;
|
|
111
|
+
} else if (action.kind === 'drop') {
|
|
112
|
+
target.status = 'dropped';
|
|
113
|
+
target.droppedAt = chapterNumber;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function activeThreads(threads: Thread[]): Thread[] {
|
|
121
|
+
return threads.filter((t) => t.status === 'planted' || t.status === 'building');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function ingestMemoryCardThreads(
|
|
125
|
+
projectPath: string,
|
|
126
|
+
chapterNumber: number,
|
|
127
|
+
actions: ThreadAction[] | undefined
|
|
128
|
+
): Promise<Thread[]> {
|
|
129
|
+
if (!actions || !actions.length) return loadThreads(projectPath);
|
|
130
|
+
const existing = await loadThreads(projectPath);
|
|
131
|
+
const next = applyThreadActions(existing, chapterNumber, actions);
|
|
132
|
+
await saveThreads(projectPath, next);
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface UpdateThreadPatch {
|
|
137
|
+
status?: ThreadStatus;
|
|
138
|
+
plannedPayoffAt?: number | null;
|
|
139
|
+
paidOffAt?: number | null;
|
|
140
|
+
droppedAt?: number | null;
|
|
141
|
+
description?: string;
|
|
142
|
+
notes?: string | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function updateThread(
|
|
146
|
+
projectPath: string,
|
|
147
|
+
id: string,
|
|
148
|
+
patch: UpdateThreadPatch
|
|
149
|
+
): Promise<Thread> {
|
|
150
|
+
const existing = await loadThreads(projectPath);
|
|
151
|
+
const target = existing.find((t) => t.id === id);
|
|
152
|
+
if (!target) throw new Error(`Thread not found: ${id}`);
|
|
153
|
+
if (patch.status) target.status = patch.status;
|
|
154
|
+
if (patch.description) target.description = patch.description.trim();
|
|
155
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'plannedPayoffAt')) {
|
|
156
|
+
if (patch.plannedPayoffAt === null) delete target.plannedPayoffAt;
|
|
157
|
+
else if (typeof patch.plannedPayoffAt === 'number') target.plannedPayoffAt = patch.plannedPayoffAt;
|
|
158
|
+
}
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'paidOffAt')) {
|
|
160
|
+
if (patch.paidOffAt === null) delete target.paidOffAt;
|
|
161
|
+
else if (typeof patch.paidOffAt === 'number') target.paidOffAt = patch.paidOffAt;
|
|
162
|
+
}
|
|
163
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'droppedAt')) {
|
|
164
|
+
if (patch.droppedAt === null) delete target.droppedAt;
|
|
165
|
+
else if (typeof patch.droppedAt === 'number') target.droppedAt = patch.droppedAt;
|
|
166
|
+
}
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'notes')) {
|
|
168
|
+
if (patch.notes === null) delete target.notes;
|
|
169
|
+
else if (typeof patch.notes === 'string') target.notes = patch.notes;
|
|
170
|
+
}
|
|
171
|
+
await saveThreads(projectPath, existing);
|
|
172
|
+
return target;
|
|
173
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -8,21 +8,50 @@ export type WorkflowStep =
|
|
|
8
8
|
| 'chapter_review'
|
|
9
9
|
| 'chapter_revision'
|
|
10
10
|
| 'cross_chapter_review'
|
|
11
|
+
| 'story_bible_amend'
|
|
11
12
|
| 'complete';
|
|
12
13
|
|
|
13
14
|
export type ReviewSeverity = 'low' | 'medium' | 'high';
|
|
14
15
|
|
|
15
16
|
export interface ChapterReviewIssue {
|
|
16
17
|
severity: ReviewSeverity;
|
|
17
|
-
category:
|
|
18
|
+
category:
|
|
19
|
+
| 'character'
|
|
20
|
+
| 'world'
|
|
21
|
+
| 'timeline'
|
|
22
|
+
| 'item'
|
|
23
|
+
| 'knowledge'
|
|
24
|
+
| 'pacing'
|
|
25
|
+
| 'style'
|
|
26
|
+
| 'architecture'
|
|
27
|
+
| 'plot'
|
|
28
|
+
| 'foreshadow'
|
|
29
|
+
| 'hook'
|
|
30
|
+
| 'repetition';
|
|
18
31
|
description: string;
|
|
19
32
|
evidence: string;
|
|
20
33
|
suggestion: string;
|
|
21
34
|
}
|
|
22
35
|
|
|
36
|
+
export interface ChapterAcceptanceCheck {
|
|
37
|
+
status: 'pass' | 'fail';
|
|
38
|
+
evidence: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChapterAcceptanceGate {
|
|
42
|
+
requiredBeats: ChapterAcceptanceCheck & { missingBeats: string[] };
|
|
43
|
+
narrativeProgress: ChapterAcceptanceCheck;
|
|
44
|
+
characterProgress: ChapterAcceptanceCheck;
|
|
45
|
+
foreshadowProgress: ChapterAcceptanceCheck;
|
|
46
|
+
storyBibleConsistency: ChapterAcceptanceCheck;
|
|
47
|
+
endingHook: ChapterAcceptanceCheck;
|
|
48
|
+
repetition: ChapterAcceptanceCheck;
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
export interface ChapterReview {
|
|
24
52
|
chapterNumber: number;
|
|
25
53
|
status: 'clean' | 'issues_found';
|
|
54
|
+
acceptance: ChapterAcceptanceGate;
|
|
26
55
|
issues: ChapterReviewIssue[];
|
|
27
56
|
}
|
|
28
57
|
|
|
@@ -60,20 +89,88 @@ export interface VolumeArchitecture {
|
|
|
60
89
|
order: number;
|
|
61
90
|
}
|
|
62
91
|
|
|
92
|
+
export interface VolumePacingBoard {
|
|
93
|
+
volumeId: string;
|
|
94
|
+
start: string;
|
|
95
|
+
promise: string;
|
|
96
|
+
keyTurns: string[];
|
|
97
|
+
midpoint: string;
|
|
98
|
+
climax: string;
|
|
99
|
+
payoffs: string[];
|
|
100
|
+
lingeringMysteries: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type EndHookFocus = 'cliffhanger' | 'mystery' | 'emotional' | 'reveal' | 'volume_close' | 'gentle';
|
|
104
|
+
|
|
63
105
|
export interface ChapterArchitecture {
|
|
64
106
|
chapterNumber: number;
|
|
65
107
|
title: string;
|
|
66
108
|
volumeId: string;
|
|
67
109
|
summary: string;
|
|
68
110
|
requiredBeats: string[];
|
|
111
|
+
targetWords?: number;
|
|
112
|
+
requireRecap?: boolean;
|
|
113
|
+
endHookFocus?: EndHookFocus;
|
|
114
|
+
povCharacter?: string;
|
|
69
115
|
}
|
|
70
116
|
|
|
71
117
|
export interface ArchitecturePayload {
|
|
72
118
|
full: string;
|
|
73
119
|
volumes: VolumeArchitecture[];
|
|
120
|
+
volumePacing?: VolumePacingBoard[];
|
|
74
121
|
chapters: ChapterArchitecture[];
|
|
75
122
|
}
|
|
76
123
|
|
|
124
|
+
export type ThreadStatus = 'planted' | 'building' | 'paid' | 'dropped';
|
|
125
|
+
|
|
126
|
+
export type ThreadActionKind = 'plant' | 'build' | 'pay' | 'drop';
|
|
127
|
+
|
|
128
|
+
export interface ThreadAction {
|
|
129
|
+
kind: ThreadActionKind;
|
|
130
|
+
threadId?: string; // existing thread id; required for build/pay/drop
|
|
131
|
+
description: string; // for plant: the new thread description; for others: how this chapter touched it
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface Thread {
|
|
135
|
+
id: string;
|
|
136
|
+
description: string;
|
|
137
|
+
status: ThreadStatus;
|
|
138
|
+
plantedAt: number;
|
|
139
|
+
lastTouchedAt: number;
|
|
140
|
+
plannedPayoffAt?: number;
|
|
141
|
+
paidOffAt?: number;
|
|
142
|
+
droppedAt?: number;
|
|
143
|
+
notes?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface CharacterRelationshipState {
|
|
147
|
+
name: string;
|
|
148
|
+
dynamic: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CharacterState {
|
|
152
|
+
name: string;
|
|
153
|
+
role?: string;
|
|
154
|
+
goal: string;
|
|
155
|
+
belief: string;
|
|
156
|
+
relationships: CharacterRelationshipState[];
|
|
157
|
+
abilities: string[];
|
|
158
|
+
secrets: string[];
|
|
159
|
+
emotionalState: string;
|
|
160
|
+
lastUpdatedAt: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface CharacterStateUpdate {
|
|
164
|
+
name: string;
|
|
165
|
+
role?: string;
|
|
166
|
+
goal?: string;
|
|
167
|
+
belief?: string;
|
|
168
|
+
relationships?: CharacterRelationshipState[];
|
|
169
|
+
abilities?: string[];
|
|
170
|
+
secrets?: string[];
|
|
171
|
+
emotionalState?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
77
174
|
export interface MemoryCard {
|
|
78
175
|
summary: string;
|
|
79
176
|
keyEvents: string[];
|
|
@@ -81,10 +178,14 @@ export interface MemoryCard {
|
|
|
81
178
|
facts: Array<{ subject: string; predicate: string; object: string }>;
|
|
82
179
|
stateChanges: Array<{ entity: string; before: string; after: string }>;
|
|
83
180
|
openThreads: string[];
|
|
181
|
+
wordCount?: number;
|
|
182
|
+
threadActions?: ThreadAction[];
|
|
183
|
+
characterUpdates?: CharacterStateUpdate[];
|
|
84
184
|
}
|
|
85
185
|
|
|
86
186
|
export interface PendingAction {
|
|
87
187
|
step: 'chapter_review' | 'chapter_revision' | 'cross_chapter_review';
|
|
188
|
+
mode?: 'side_track' | 'gate';
|
|
88
189
|
chapterNumber?: number;
|
|
89
190
|
range?: { start: number; end: number };
|
|
90
191
|
feedback?: string;
|
package/src/core/workflow.ts
CHANGED
|
@@ -132,17 +132,17 @@ function buildPendingAction(state: AgentState, input: RequestSideTrackInput): Pe
|
|
|
132
132
|
switch (input.step) {
|
|
133
133
|
case 'chapter_review': {
|
|
134
134
|
if (!input.chapterNumber) throw new Error('chapter_review requires chapterNumber');
|
|
135
|
-
return { step: 'chapter_review', chapterNumber: input.chapterNumber };
|
|
135
|
+
return { step: 'chapter_review', mode: 'side_track', chapterNumber: input.chapterNumber };
|
|
136
136
|
}
|
|
137
137
|
case 'chapter_revision': {
|
|
138
138
|
if (!input.chapterNumber) throw new Error('chapter_revision requires chapterNumber');
|
|
139
|
-
return { step: 'chapter_revision', chapterNumber: input.chapterNumber, feedback: input.feedback };
|
|
139
|
+
return { step: 'chapter_revision', mode: 'side_track', chapterNumber: input.chapterNumber, feedback: input.feedback };
|
|
140
140
|
}
|
|
141
141
|
case 'cross_chapter_review': {
|
|
142
142
|
const max = maxExistingChapter(state);
|
|
143
143
|
const range = input.range ?? { start: 1, end: max || state.currentChapter };
|
|
144
144
|
if (range.start < 1 || range.end < range.start) throw new Error('Invalid range');
|
|
145
|
-
return { step: 'cross_chapter_review', range };
|
|
145
|
+
return { step: 'cross_chapter_review', mode: 'side_track', range };
|
|
146
146
|
}
|
|
147
147
|
default:
|
|
148
148
|
throw new Error(`Unknown side-track step: ${(input as RequestSideTrackInput).step}`);
|