dayloom 0.1.0-beta.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 +1146 -0
- package/dist/cli/daily.js +41 -0
- package/dist/cli/index.js +33 -0
- package/dist/cli/init.js +40 -0
- package/dist/cli/next.js +60 -0
- package/dist/cli/play.js +39 -0
- package/dist/cli/revise.js +41 -0
- package/dist/cli/settle.js +45 -0
- package/dist/daily/apply-plan.js +25 -0
- package/dist/daily/constants.js +16 -0
- package/dist/daily/dialogue-loop.js +147 -0
- package/dist/daily/finalize.js +23 -0
- package/dist/daily/guard.js +54 -0
- package/dist/daily/index.js +27 -0
- package/dist/daily/intent-router.js +65 -0
- package/dist/daily/mcp-gateway.js +5 -0
- package/dist/daily/mcp-tools.js +8 -0
- package/dist/daily/parse-assistant.js +38 -0
- package/dist/daily/parse-payload.js +10 -0
- package/dist/daily/player-context.js +85 -0
- package/dist/daily/project-plan.js +15 -0
- package/dist/daily/promptpile-loop.js +41 -0
- package/dist/daily/prompts.js +11 -0
- package/dist/daily/read-user-input.js +6 -0
- package/dist/daily/session.js +119 -0
- package/dist/daily/types.js +2 -0
- package/dist/daily/validate-plan.js +46 -0
- package/dist/i18n/detect.js +23 -0
- package/dist/i18n/index.js +22 -0
- package/dist/i18n/messages.js +149 -0
- package/dist/index.js +27 -0
- package/dist/init/apply-payload.js +18 -0
- package/dist/init/archive-transcript.js +28 -0
- package/dist/init/checklist.js +74 -0
- package/dist/init/cleanup.js +12 -0
- package/dist/init/constants.js +21 -0
- package/dist/init/errors.js +11 -0
- package/dist/init/finalize.js +35 -0
- package/dist/init/guard.js +31 -0
- package/dist/init/index.js +59 -0
- package/dist/init/interview-loop.js +103 -0
- package/dist/init/parse-assistant.js +50 -0
- package/dist/init/project-payload.js +78 -0
- package/dist/init/promptpile-run.js +80 -0
- package/dist/init/prompts.js +16 -0
- package/dist/init/read-user-input.js +44 -0
- package/dist/init/scaffold.js +66 -0
- package/dist/init/session.js +98 -0
- package/dist/init/types.js +2 -0
- package/dist/next/index.js +79 -0
- package/dist/next/inspect.js +90 -0
- package/dist/play/ai.js +11 -0
- package/dist/play/event-loop.js +244 -0
- package/dist/play/guard.js +14 -0
- package/dist/play/index.js +21 -0
- package/dist/play/parse-assistant.js +39 -0
- package/dist/play/player-context.js +20 -0
- package/dist/play/prompts.js +9 -0
- package/dist/play/session.js +14 -0
- package/dist/play/state.js +117 -0
- package/dist/play/types.js +2 -0
- package/dist/play/validate.js +156 -0
- package/dist/revise/apply-payload.js +58 -0
- package/dist/revise/bin-resolve.js +38 -0
- package/dist/revise/constants.js +17 -0
- package/dist/revise/dialogue-loop.js +116 -0
- package/dist/revise/diff.js +24 -0
- package/dist/revise/file-hash.js +27 -0
- package/dist/revise/finalize.js +23 -0
- package/dist/revise/guard.js +17 -0
- package/dist/revise/index.js +24 -0
- package/dist/revise/mcp-gateway.js +74 -0
- package/dist/revise/mcp-tools.js +91 -0
- package/dist/revise/parse-assistant.js +41 -0
- package/dist/revise/parse-payload.js +22 -0
- package/dist/revise/process-run.js +77 -0
- package/dist/revise/project-payload.js +62 -0
- package/dist/revise/promptpile-loop.js +41 -0
- package/dist/revise/prompts.js +16 -0
- package/dist/revise/read-user-input.js +35 -0
- package/dist/revise/session.js +119 -0
- package/dist/revise/types.js +2 -0
- package/dist/revise/validate-payload.js +47 -0
- package/dist/settle/ai.js +23 -0
- package/dist/settle/apply.js +58 -0
- package/dist/settle/context.js +69 -0
- package/dist/settle/derive.js +56 -0
- package/dist/settle/guard.js +71 -0
- package/dist/settle/index.js +105 -0
- package/dist/settle/parse-assistant.js +14 -0
- package/dist/settle/parse-payload.js +19 -0
- package/dist/settle/project.js +58 -0
- package/dist/settle/session.js +45 -0
- package/dist/settle/types.js +2 -0
- package/dist/settle/validate.js +100 -0
- package/dist/shared/filtered-stream-output.js +41 -0
- package/dist/shared/promptpile-stream.js +59 -0
- package/dist/shared/run-promptpile-with-stream.js +34 -0
- package/dist/utils/loading.js +54 -0
- package/package.json +39 -0
- package/prompts/README.md +39 -0
- package/prompts/choice.system.md +0 -0
- package/prompts/daily-dialogue.system.md +37 -0
- package/prompts/daily-finalize-plan.system.md +34 -0
- package/prompts/daily-intent-router.system.md +34 -0
- package/prompts/day-planner.system.md +0 -0
- package/prompts/day-summarizer.system.md +0 -0
- package/prompts/dialogue.system.md +0 -0
- package/prompts/diary-writer.system.md +0 -0
- package/prompts/event-runner.system.md +0 -0
- package/prompts/init-finalize.system.md +59 -0
- package/prompts/init-interviewer.system.md +37 -0
- package/prompts/memory-updater.system.md +0 -0
- package/prompts/next-day-seeder.system.md +0 -0
- package/prompts/play-event-dialogue.system.md +20 -0
- package/prompts/play-event-generator.system.md +19 -0
- package/prompts/play-event-resolver.system.md +26 -0
- package/prompts/play-replanner.system.md +21 -0
- package/prompts/revise-dialogue.system.md +22 -0
- package/prompts/revise-finalize.system.md +40 -0
- package/prompts/settle.system.md +28 -0
- package/prompts/spec.md +320 -0
- package/prompts/state-resolver.system.md +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runPlayLoop = runPlayLoop;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const mcp_gateway_1 = require("../daily/mcp-gateway");
|
|
11
|
+
const mcp_tools_1 = require("../daily/mcp-tools");
|
|
12
|
+
const read_user_input_1 = require("../daily/read-user-input");
|
|
13
|
+
const filtered_stream_output_1 = require("../shared/filtered-stream-output");
|
|
14
|
+
const ai_1 = require("./ai");
|
|
15
|
+
const player_context_1 = require("./player-context");
|
|
16
|
+
const parse_assistant_1 = require("./parse-assistant");
|
|
17
|
+
const state_1 = require("./state");
|
|
18
|
+
const validate_1 = require("./validate");
|
|
19
|
+
const loading_1 = require("../utils/loading");
|
|
20
|
+
async function runPlayLoop(worldRoot, day, options = {}) {
|
|
21
|
+
if (!process.env.DEEPSEEK_API_KEY?.trim())
|
|
22
|
+
throw new Error('DEEPSEEK_API_KEY is not set. Play requires an API key.');
|
|
23
|
+
(0, state_1.initializePlay)(worldRoot, day);
|
|
24
|
+
const serviceRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'dayloom-play-service-'));
|
|
25
|
+
const contextRoot = path_1.default.join(serviceRoot, 'player-context');
|
|
26
|
+
const toolsFile = path_1.default.join(serviceRoot, 'readonly.tools.toml');
|
|
27
|
+
let gateway;
|
|
28
|
+
const maxTools = options.maxToolRounds ?? 8;
|
|
29
|
+
const maxRounds = options.maxEventRounds ?? 20;
|
|
30
|
+
try {
|
|
31
|
+
await (0, loading_1.withLoading)('正在准备游玩上下文...', async (loading) => {
|
|
32
|
+
(0, player_context_1.buildPlayContext)(worldRoot, day, contextRoot);
|
|
33
|
+
loading.update('正在启动只读服务...');
|
|
34
|
+
gateway = await (0, mcp_gateway_1.connectOrStartGateway)(serviceRoot, contextRoot, options.mcpBaseUrl, options.mcpToken);
|
|
35
|
+
loading.update('正在准备只读工具...');
|
|
36
|
+
await (0, mcp_tools_1.exportReadonlyTools)(gateway.baseUrl, gateway.token, toolsFile);
|
|
37
|
+
await (0, mcp_tools_1.assertAllowedPlayerContextRoot)(gateway.baseUrl, gateway.token, contextRoot, serviceRoot);
|
|
38
|
+
});
|
|
39
|
+
if (!gateway)
|
|
40
|
+
throw new Error('Failed to initialize readonly gateway');
|
|
41
|
+
while (true) {
|
|
42
|
+
const state = (0, state_1.loadState)(worldRoot, day);
|
|
43
|
+
const plan = (0, state_1.loadPlan)(worldRoot, day);
|
|
44
|
+
if (state.step === 'complete' || state.phase === 'settling')
|
|
45
|
+
return;
|
|
46
|
+
if (state.step === 'ready') {
|
|
47
|
+
const beat = (0, state_1.nextExecutableBeat)(plan);
|
|
48
|
+
if (!beat) {
|
|
49
|
+
if (plan.beats.some(b => b.status === 'pending' || b.status === 'active'))
|
|
50
|
+
throw new Error('No executable beat; check depends_on references');
|
|
51
|
+
(0, state_1.finishPlay)(worldRoot, day, state);
|
|
52
|
+
process.stdout.write('All planned events resolved. Day is ready for settlement.\n');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await generate(worldRoot, day, plan, state, beat.id, toolsFile, gateway.baseUrl, gateway.token, maxTools, contextRoot);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (state.step === 'waiting_user') {
|
|
59
|
+
const exited = await dialogue(worldRoot, day, state.active_event, toolsFile, gateway.baseUrl, gateway.token, maxTools, maxRounds, contextRoot);
|
|
60
|
+
if (exited) {
|
|
61
|
+
process.stdout.write('Play progress saved.\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (state.step === 'resolving') {
|
|
67
|
+
await resolveEvent(worldRoot, day, state.active_event, state.active_beat, toolsFile, gateway.baseUrl, gateway.token, maxTools, contextRoot);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (state.step === 'replanning') {
|
|
71
|
+
await replan(worldRoot, day, state.active_event, state.active_beat, toolsFile, gateway.baseUrl, gateway.token, maxTools, contextRoot);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
throw new Error('Unsupported play step: ' + state.step);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (gateway)
|
|
79
|
+
await gateway.stop();
|
|
80
|
+
if (!options.keepSession)
|
|
81
|
+
fs_1.default.rmSync(serviceRoot, { recursive: true, force: true });
|
|
82
|
+
else
|
|
83
|
+
process.stderr.write('Play service session preserved at: ' + serviceRoot + '\n');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function generate(worldRoot, day, plan, state, beatId, tools, base, token, max, context) {
|
|
87
|
+
const beat = plan.beats.find((b) => b.id === beatId);
|
|
88
|
+
const id = (0, state_1.eventId)(state.next_event_number);
|
|
89
|
+
(0, player_context_1.buildPlayContext)(worldRoot, day, context);
|
|
90
|
+
const reply = await (0, loading_1.withLoading)('正在生成事件...', () => (0, ai_1.callPlayAi)('play-event-generator', '# Event request\n\nEvent ID: ' + id + '\nSource beat: ' + JSON.stringify(beat) + '\nCurrent plan: ' + JSON.stringify(plan, null, 2), tools, base, token, max));
|
|
91
|
+
const event = (0, parse_assistant_1.parseGeneratedEvent)(reply);
|
|
92
|
+
(0, validate_1.validateGeneratedEvent)(event, id, beatId);
|
|
93
|
+
beat.status = 'active';
|
|
94
|
+
(0, state_1.savePlan)(worldRoot, plan);
|
|
95
|
+
const dir = (0, state_1.eventRoot)(worldRoot, day, id);
|
|
96
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
97
|
+
writeJson(path_1.default.join(dir, 'event.json'), event);
|
|
98
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'transcript.md'), '# ' + event.title + '\n\n' + event.opening + '\n\n' + event.situation + '\n', 'utf8');
|
|
99
|
+
state.active_event = id;
|
|
100
|
+
state.active_beat = beatId;
|
|
101
|
+
state.step = 'waiting_user';
|
|
102
|
+
(0, state_1.saveState)(worldRoot, state);
|
|
103
|
+
}
|
|
104
|
+
async function dialogue(worldRoot, day, id, tools, base, token, maxTools, maxRounds, context) {
|
|
105
|
+
const dir = (0, state_1.eventRoot)(worldRoot, day, id);
|
|
106
|
+
const event = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, 'event.json'), 'utf8'));
|
|
107
|
+
let transcript = fs_1.default.readFileSync(path_1.default.join(dir, 'transcript.md'), 'utf8');
|
|
108
|
+
process.stdout.write('\n--- ' + event.title + ' ---\n\n' + event.opening + '\n\n' + event.situation + '\n');
|
|
109
|
+
if (event.suggested_actions.length)
|
|
110
|
+
process.stdout.write(event.suggested_actions.map((x, i) => (i + 1) + '. ' + x).join('\n') + '\n');
|
|
111
|
+
while (true) {
|
|
112
|
+
const rounds = (transcript.match(/^## User$/gm) ?? []).length;
|
|
113
|
+
if (rounds >= maxRounds)
|
|
114
|
+
throw new Error('Event exceeded max dialogue rounds (' + maxRounds + ')');
|
|
115
|
+
const input = await (0, read_user_input_1.readDailyUserInput)();
|
|
116
|
+
if (input === undefined || input === '/exit')
|
|
117
|
+
return true;
|
|
118
|
+
if (input === '/status') {
|
|
119
|
+
process.stdout.write(fs_1.default.readFileSync(path_1.default.join(dir, 'event.json'), 'utf8'));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (input === '/end-day') {
|
|
123
|
+
const status = { status: 'resolved', situation: 'The player explicitly ended the day.', needs_user_action: false, resolution_summary: 'The player explicitly ended the current day.', end_day: true };
|
|
124
|
+
const machine = '```event-status\n' + JSON.stringify(status, null, 2) + '\n```';
|
|
125
|
+
transcript += '\n## User\n\n/end-day\n\n## Assistant\n\nThe day ends here.\n\n' + machine + '\n';
|
|
126
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'transcript.md'), transcript, 'utf8');
|
|
127
|
+
writeJson(path_1.default.join(dir, 'status.json'), status);
|
|
128
|
+
const state = (0, state_1.loadState)(worldRoot, day);
|
|
129
|
+
state.step = 'resolving';
|
|
130
|
+
(0, state_1.saveState)(worldRoot, state);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
transcript += '\n## User\n\n' + input + '\n';
|
|
134
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'transcript.md'), transcript, 'utf8');
|
|
135
|
+
(0, player_context_1.buildPlayContext)(worldRoot, day, context);
|
|
136
|
+
process.stdout.write('\nAI> ');
|
|
137
|
+
const stream = (0, filtered_stream_output_1.createFilteredStreamOutput)({ hiddenBlocks: ['event-status'] });
|
|
138
|
+
const reply = await (0, ai_1.callPlayAi)('play-event-dialogue', '# Event\n\n' + JSON.stringify(event, null, 2) + '\n\n# Transcript\n\n' + transcript, tools, base, token, maxTools, text => stream.push(text));
|
|
139
|
+
stream.flush();
|
|
140
|
+
process.stdout.write('\n');
|
|
141
|
+
transcript += '\n## Assistant\n\n' + reply + '\n';
|
|
142
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'transcript.md'), transcript, 'utf8');
|
|
143
|
+
let status;
|
|
144
|
+
try {
|
|
145
|
+
status = (0, parse_assistant_1.parseEventStatus)(reply);
|
|
146
|
+
(0, validate_1.validateEventStatus)(status);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
process.stderr.write('Warning: invalid event-status metadata; continuing the event: ' + (error instanceof Error ? error.message : String(error)) + '\n');
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
writeJson(path_1.default.join(dir, 'status.json'), status);
|
|
153
|
+
if (status.status === 'resolved') {
|
|
154
|
+
const state = (0, state_1.loadState)(worldRoot, day);
|
|
155
|
+
state.step = 'resolving';
|
|
156
|
+
(0, state_1.saveState)(worldRoot, state);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function resolveEvent(worldRoot, day, id, beatId, tools, base, token, max, context) {
|
|
162
|
+
const dir = (0, state_1.eventRoot)(worldRoot, day, id);
|
|
163
|
+
const resultFile = path_1.default.join(dir, 'result.json');
|
|
164
|
+
const plan = (0, state_1.loadPlan)(worldRoot, day);
|
|
165
|
+
const statusFile = path_1.default.join(dir, 'status.json');
|
|
166
|
+
const finalStatus = fs_1.default.existsSync(statusFile) ? JSON.parse(fs_1.default.readFileSync(statusFile, 'utf8')) : { status: 'resolved', situation: 'Resolved event', needs_user_action: false, end_day: false, resolution_summary: 'Resolved event' };
|
|
167
|
+
(0, validate_1.validateEventStatus)(finalStatus);
|
|
168
|
+
let result;
|
|
169
|
+
if (fs_1.default.existsSync(resultFile))
|
|
170
|
+
result = JSON.parse(fs_1.default.readFileSync(resultFile, 'utf8'));
|
|
171
|
+
else {
|
|
172
|
+
(0, player_context_1.buildPlayContext)(worldRoot, day, context);
|
|
173
|
+
const status = JSON.stringify(finalStatus, null, 2);
|
|
174
|
+
const reply = await (0, loading_1.withLoading)('正在整理事件结果...', () => (0, ai_1.callPlayAi)('play-event-resolver', '# Event\n' + fs_1.default.readFileSync(path_1.default.join(dir, 'event.json'), 'utf8') + '\n# Final event status\n' + status + '\n# Current plan\n' + JSON.stringify(plan, null, 2) + '\n# Transcript\n' + fs_1.default.readFileSync(path_1.default.join(dir, 'transcript.md'), 'utf8'), tools, base, token, max));
|
|
175
|
+
result = (0, parse_assistant_1.parseEventResult)(reply);
|
|
176
|
+
}
|
|
177
|
+
if (finalStatus.end_day)
|
|
178
|
+
result.end_day = true;
|
|
179
|
+
(0, validate_1.validateEventResult)(result, id, beatId, plan);
|
|
180
|
+
writeJson(resultFile, result);
|
|
181
|
+
const patchFile = path_1.default.join(dir, 'state.patch.json');
|
|
182
|
+
const appliedFile = path_1.default.join(dir, 'state.patch.applied');
|
|
183
|
+
if (!fs_1.default.existsSync(patchFile))
|
|
184
|
+
writeJson(patchFile, result.state_patch);
|
|
185
|
+
if (!fs_1.default.existsSync(appliedFile)) {
|
|
186
|
+
(0, state_1.applyEventResult)(worldRoot, day, result);
|
|
187
|
+
fs_1.default.writeFileSync(appliedFile, new Date().toISOString() + '\n', 'utf8');
|
|
188
|
+
}
|
|
189
|
+
const state = (0, state_1.loadState)(worldRoot, day);
|
|
190
|
+
state.step = 'replanning';
|
|
191
|
+
(0, state_1.saveState)(worldRoot, state);
|
|
192
|
+
}
|
|
193
|
+
async function replan(worldRoot, day, id, beatId, tools, base, token, max, context) {
|
|
194
|
+
const dir = (0, state_1.eventRoot)(worldRoot, day, id);
|
|
195
|
+
const plan = (0, state_1.loadPlan)(worldRoot, day);
|
|
196
|
+
const result = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, 'result.json'), 'utf8'));
|
|
197
|
+
(0, validate_1.validateEventResult)(result, id, beatId, plan);
|
|
198
|
+
const replanFile = path_1.default.join(dir, 'replan.json');
|
|
199
|
+
let payload;
|
|
200
|
+
if (result.end_day) {
|
|
201
|
+
payload = (0, state_1.buildResultReplan)(plan, result);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
let proposed;
|
|
205
|
+
if (fs_1.default.existsSync(replanFile))
|
|
206
|
+
proposed = JSON.parse(fs_1.default.readFileSync(replanFile, 'utf8'));
|
|
207
|
+
else {
|
|
208
|
+
(0, player_context_1.buildPlayContext)(worldRoot, day, context);
|
|
209
|
+
const validBeatIds = plan.beats.map(beat => beat.id);
|
|
210
|
+
const remainingInsertSlots = Math.max(0, plan.max_events - plan.beats.length);
|
|
211
|
+
const reply = await (0, loading_1.withLoading)('正在更新后续计划...', () => (0, ai_1.callPlayAi)('play-replanner', '# Replan constraints\nValid existing beat IDs: ' + validBeatIds.join(', ') + '\nRemaining insert slots: ' + remainingInsertSlots + '\ninsert.after may only use a valid existing beat ID. New beat IDs are assigned by the system and cannot be referenced.\n\n# Current plan\n' + JSON.stringify(plan, null, 2) + '\n# Event result\n' + JSON.stringify(result, null, 2), tools, base, token, max));
|
|
212
|
+
proposed = (0, parse_assistant_1.parseReplan)(reply);
|
|
213
|
+
}
|
|
214
|
+
const normalized = (0, validate_1.normalizeReplanPayload)(proposed, plan);
|
|
215
|
+
for (const warning of normalized.warnings)
|
|
216
|
+
process.stderr.write('Warning: ' + warning + '\n');
|
|
217
|
+
payload = (0, state_1.buildResultReplan)(plan, result, normalized.payload);
|
|
218
|
+
}
|
|
219
|
+
(0, validate_1.validateReplan)(payload, plan);
|
|
220
|
+
writeJson(replanFile, payload);
|
|
221
|
+
const appliedFile = path_1.default.join(dir, 'replan.applied');
|
|
222
|
+
if (!fs_1.default.existsSync(appliedFile)) {
|
|
223
|
+
const next = (0, state_1.applyReplan)(plan, payload);
|
|
224
|
+
for (const beat of next.beats)
|
|
225
|
+
if (beat.status === 'active')
|
|
226
|
+
beat.status = 'pending';
|
|
227
|
+
(0, state_1.savePlan)(worldRoot, next);
|
|
228
|
+
fs_1.default.writeFileSync(appliedFile, new Date().toISOString() + '\n', 'utf8');
|
|
229
|
+
}
|
|
230
|
+
const state = (0, state_1.loadState)(worldRoot, day);
|
|
231
|
+
if (!state.completed_events.includes(id))
|
|
232
|
+
state.completed_events.push(id);
|
|
233
|
+
state.active_event = null;
|
|
234
|
+
state.active_beat = null;
|
|
235
|
+
state.next_event_number++;
|
|
236
|
+
if (result.end_day) {
|
|
237
|
+
(0, state_1.finishPlay)(worldRoot, day, state);
|
|
238
|
+
process.stdout.write('Day ended. World is ready for settlement.\n');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
state.step = 'ready';
|
|
242
|
+
(0, state_1.saveState)(worldRoot, state);
|
|
243
|
+
}
|
|
244
|
+
function writeJson(file, value) { fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8'); }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveWorldRoot = resolveWorldRoot;
|
|
7
|
+
exports.readCurrent = readCurrent;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
function resolveWorldRoot(dir) { return path_1.default.resolve(dir); }
|
|
11
|
+
function readCurrent(worldRoot) { const file = path_1.default.join(worldRoot, 'current.yaml'); if (!fs_1.default.existsSync(path_1.default.join(worldRoot, 'manifest.yaml')))
|
|
12
|
+
throw new Error('World save is not initialized: ' + worldRoot); const text = fs_1.default.readFileSync(file, 'utf8'); const day = text.match(/^day:\s*(\S+)/m)?.[1]; const phase = text.match(/^phase:\s*(\S+)/m)?.[1]; if (!day || !phase)
|
|
13
|
+
throw new Error('current.yaml missing day or phase'); if (phase !== 'planned' && phase !== 'playing')
|
|
14
|
+
throw new Error('Play requires current phase planned or playing, got: ' + phase); return { day, phase }; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.playInteractive = playInteractive;
|
|
18
|
+
const guard_1 = require("./guard");
|
|
19
|
+
const event_loop_1 = require("./event-loop");
|
|
20
|
+
async function playInteractive(dir, options = {}) { const worldRoot = (0, guard_1.resolveWorldRoot)(dir); const { day } = (0, guard_1.readCurrent)(worldRoot); await (0, event_loop_1.runPlayLoop)(worldRoot, day, options); }
|
|
21
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseReplan = exports.parseEventResult = exports.parseGeneratedEvent = void 0;
|
|
4
|
+
exports.parseEventStatus = parseEventStatus;
|
|
5
|
+
function block(text, label) { const m = text.match(new RegExp('```(?:json\\s+)?' + label + '\\s*\\n([\\s\\S]*?)```', 'i')); if (!m)
|
|
6
|
+
throw new Error('Assistant response missing ' + label + ' JSON block'); return m[1].trim(); }
|
|
7
|
+
function parse(text, label) { try {
|
|
8
|
+
return JSON.parse(block(text, label));
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
throw new Error('Failed to parse ' + label + ': ' + (e instanceof Error ? e.message : String(e)));
|
|
12
|
+
} }
|
|
13
|
+
const parseGeneratedEvent = (text) => parse(text, 'play-event');
|
|
14
|
+
exports.parseGeneratedEvent = parseGeneratedEvent;
|
|
15
|
+
function parseEventStatus(text) {
|
|
16
|
+
try {
|
|
17
|
+
return parse(text, 'event-status');
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const raw = block(text, 'event-status');
|
|
21
|
+
const status = raw.match(/"status"\s*:\s*"(ongoing|resolved)"/)?.[1];
|
|
22
|
+
const situation = raw.match(/"situation"\s*:\s*"([\s\S]*)"\s*,\s*"needs_user_action"\s*:/)?.[1];
|
|
23
|
+
const needsUserAction = raw.match(/"needs_user_action"\s*:\s*(true|false)/)?.[1];
|
|
24
|
+
const resolutionSummary = raw.match(/"resolution_summary"\s*:\s*"([\s\S]*)"\s*[,}]?\s*$/)?.[1];
|
|
25
|
+
if (!status || situation === undefined || !needsUserAction)
|
|
26
|
+
throw error;
|
|
27
|
+
return {
|
|
28
|
+
status: status,
|
|
29
|
+
situation,
|
|
30
|
+
needs_user_action: needsUserAction === 'true',
|
|
31
|
+
...(resolutionSummary === undefined ? {} : { resolution_summary: resolutionSummary }),
|
|
32
|
+
end_day: raw.match(/"end_day"\s*:\s*(true|false)/)?.[1] === 'true'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const parseEventResult = (text) => parse(text, 'event-result');
|
|
37
|
+
exports.parseEventResult = parseEventResult;
|
|
38
|
+
const parseReplan = (text) => parse(text, 'play-replan');
|
|
39
|
+
exports.parseReplan = parseReplan;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildPlayContext = buildPlayContext;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const player_context_1 = require("../daily/player-context");
|
|
10
|
+
function buildPlayContext(worldRoot, day, outputRoot) { (0, player_context_1.buildPlayerContext)(worldRoot, outputRoot); const today = path_1.default.join(outputRoot, 'today'); fs_1.default.mkdirSync(today, { recursive: true }); for (const name of ['plan.initial.json', 'plan.current.json', 'play.state.json', 'runtime.state.json']) {
|
|
11
|
+
const src = path_1.default.join(worldRoot, 'days', day, name);
|
|
12
|
+
if (fs_1.default.existsSync(src))
|
|
13
|
+
fs_1.default.copyFileSync(src, path_1.default.join(today, name));
|
|
14
|
+
} const events = path_1.default.join(worldRoot, 'days', day, 'events'); const summaries = []; if (fs_1.default.existsSync(events)) {
|
|
15
|
+
for (const id of fs_1.default.readdirSync(events).sort()) {
|
|
16
|
+
const result = path_1.default.join(events, id, 'result.json');
|
|
17
|
+
if (fs_1.default.existsSync(result))
|
|
18
|
+
summaries.push(fs_1.default.readFileSync(result, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
} fs_1.default.writeFileSync(path_1.default.join(today, 'completed-events.jsonl'), summaries.map(s => JSON.stringify(JSON.parse(s))).join('\n') + (summaries.length ? '\n' : ''), 'utf8'); }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadPlayPrompt = loadPlayPrompt;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function loadPlayPrompt(name) { return fs_1.default.readFileSync(path_1.default.resolve(__dirname, '..', '..', 'prompts', name + '.system.md'), 'utf8'); }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createPlayAiSession = createPlayAiSession;
|
|
7
|
+
exports.cleanupPlayAiSession = cleanupPlayAiSession;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const prompts_1 = require("./prompts");
|
|
12
|
+
function config() { return '[[llm_api]]\nname = "deepseek"\nmodel = "deepseek-chat"\nbase_url = "https://api.deepseek.com/v1"\napi_key_env = "DEEPSEEK_API_KEY"\n\n[promptpile]\nllm_api = "deepseek"\ndir = "./messages"\ntools_file = "./readonly.tools.toml"\nquiet = true\n'; }
|
|
13
|
+
function createPlayAiSession(prompt, userContent, toolsSource) { const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'dayloom-play-')); const messagesDir = path_1.default.join(root, 'messages'); fs_1.default.mkdirSync(messagesDir, { recursive: true }); const session = { root, messagesDir, toolsFile: path_1.default.join(root, 'readonly.tools.toml'), promptpileConfig: path_1.default.join(root, 'promptpile.toml'), draftFile: path_1.default.join(root, 'unused.json'), playerContextRoot: path_1.default.join(root, 'unused-context') }; fs_1.default.writeFileSync(session.promptpileConfig, config(), 'utf8'); fs_1.default.copyFileSync(toolsSource, session.toolsFile); fs_1.default.writeFileSync(path_1.default.join(messagesDir, '[0]system.md'), (0, prompts_1.loadPlayPrompt)(prompt), 'utf8'); fs_1.default.writeFileSync(path_1.default.join(messagesDir, '[1]user.md'), userContent, 'utf8'); return session; }
|
|
14
|
+
function cleanupPlayAiSession(session) { fs_1.default.rmSync(session.root, { recursive: true, force: true }); }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.eventRoot = exports.dayRoot = void 0;
|
|
7
|
+
exports.initializePlay = initializePlay;
|
|
8
|
+
exports.loadPlan = loadPlan;
|
|
9
|
+
exports.savePlan = savePlan;
|
|
10
|
+
exports.loadState = loadState;
|
|
11
|
+
exports.saveState = saveState;
|
|
12
|
+
exports.nextExecutableBeat = nextExecutableBeat;
|
|
13
|
+
exports.eventId = eventId;
|
|
14
|
+
exports.applyEventResult = applyEventResult;
|
|
15
|
+
exports.buildResultReplan = buildResultReplan;
|
|
16
|
+
exports.applyReplan = applyReplan;
|
|
17
|
+
exports.finishPlay = finishPlay;
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const dayRoot = (worldRoot, day) => path_1.default.join(worldRoot, 'days', day);
|
|
21
|
+
exports.dayRoot = dayRoot;
|
|
22
|
+
const eventRoot = (worldRoot, day, eventId) => path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'events', eventId);
|
|
23
|
+
exports.eventRoot = eventRoot;
|
|
24
|
+
const planPath = (worldRoot, day) => path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'plan.current.json');
|
|
25
|
+
const statePath = (worldRoot, day) => path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'play.state.json');
|
|
26
|
+
const runtimePath = (worldRoot, day) => path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'runtime.state.json');
|
|
27
|
+
function initializePlay(worldRoot, day) {
|
|
28
|
+
const initialFile = path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'plan.initial.json');
|
|
29
|
+
if (!fs_1.default.existsSync(initialFile))
|
|
30
|
+
throw new Error('Missing daily plan: days/' + day + '/plan.initial.json');
|
|
31
|
+
if (!fs_1.default.existsSync(planPath(worldRoot, day))) {
|
|
32
|
+
const initial = JSON.parse(fs_1.default.readFileSync(initialFile, 'utf8'));
|
|
33
|
+
const plan = { day: initial.day, user_intent: initial.user_intent, revision: 0, max_events: initial.max_events, beats: initial.planned_beats.map(b => ({ ...b, status: 'pending' })) };
|
|
34
|
+
writeJson(planPath(worldRoot, day), plan);
|
|
35
|
+
}
|
|
36
|
+
if (!fs_1.default.existsSync(statePath(worldRoot, day))) {
|
|
37
|
+
const state = { version: 1, day, phase: 'playing', next_event_number: 1, active_event: null, active_beat: null, step: 'ready', completed_events: [] };
|
|
38
|
+
writeJson(statePath(worldRoot, day), state);
|
|
39
|
+
}
|
|
40
|
+
if (!fs_1.default.existsSync(runtimePath(worldRoot, day)))
|
|
41
|
+
writeJson(runtimePath(worldRoot, day), {});
|
|
42
|
+
const phase = loadState(worldRoot, day).phase;
|
|
43
|
+
setYamlPhase(path_1.default.join(worldRoot, 'current.yaml'), phase);
|
|
44
|
+
setYamlPhase(path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'meta.yaml'), phase);
|
|
45
|
+
return { plan: loadPlan(worldRoot, day), state: loadState(worldRoot, day) };
|
|
46
|
+
}
|
|
47
|
+
function loadPlan(worldRoot, day) { return JSON.parse(fs_1.default.readFileSync(planPath(worldRoot, day), 'utf8')); }
|
|
48
|
+
function savePlan(worldRoot, plan) { writeJson(planPath(worldRoot, plan.day), plan); }
|
|
49
|
+
function loadState(worldRoot, day) { return JSON.parse(fs_1.default.readFileSync(statePath(worldRoot, day), 'utf8')); }
|
|
50
|
+
function saveState(worldRoot, state) { writeJson(statePath(worldRoot, state.day), state); }
|
|
51
|
+
function nextExecutableBeat(plan) { const completed = new Set(plan.beats.filter(b => b.status === 'completed').map(b => b.id)); return plan.beats.find(b => b.status === 'pending' && (b.depends_on ?? []).every(id => completed.has(id))); }
|
|
52
|
+
function eventId(number) { return 'event_' + String(number).padStart(3, '0'); }
|
|
53
|
+
function durationMinutes(value) { const hours = value.match(/^(\d+)h$/)?.[1]; if (hours)
|
|
54
|
+
return Number(hours) * 60; const minutes = value.match(/^(\d+)m$/)?.[1]; return Number(minutes ?? 0); }
|
|
55
|
+
function applyEventResult(worldRoot, day, result) {
|
|
56
|
+
const runtime = JSON.parse(fs_1.default.readFileSync(runtimePath(worldRoot, day), 'utf8'));
|
|
57
|
+
for (const p of result.state_patch)
|
|
58
|
+
runtime[p.key] = p.value;
|
|
59
|
+
const elapsed = (typeof runtime.day_elapsed_minutes === 'number' ? runtime.day_elapsed_minutes : 0) + durationMinutes(result.time_advanced);
|
|
60
|
+
runtime.day_elapsed_minutes = elapsed;
|
|
61
|
+
writeJson(runtimePath(worldRoot, day), runtime);
|
|
62
|
+
const log = path_1.default.join(worldRoot, 'logs', 'state_changes.jsonl');
|
|
63
|
+
fs_1.default.appendFileSync(log, JSON.stringify({ type: 'event_resolved', day, event: result.event_id, beat: result.source_beat, summary: result.summary, time_advanced: result.time_advanced, end_day: result.end_day ?? false, completed_beats: result.completed_beats ?? [], cancelled_beats: result.cancelled_beats ?? [], state_patch: result.state_patch }) + '\n', 'utf8');
|
|
64
|
+
}
|
|
65
|
+
function buildResultReplan(plan, result, payload = { operations: [] }) {
|
|
66
|
+
const completed = new Set(result.completed_beats ?? (result.completed_source_beat === false ? [] : [result.source_beat]));
|
|
67
|
+
const cancelled = new Set(result.cancelled_beats ?? []);
|
|
68
|
+
const operations = [];
|
|
69
|
+
const handled = new Set();
|
|
70
|
+
for (const beat of plan.beats) {
|
|
71
|
+
if (beat.status === 'completed' || beat.status === 'cancelled')
|
|
72
|
+
continue;
|
|
73
|
+
if (completed.has(beat.id)) {
|
|
74
|
+
operations.push({ op: 'complete', beat_id: beat.id, reason: 'Completed by event result' });
|
|
75
|
+
handled.add(beat.id);
|
|
76
|
+
}
|
|
77
|
+
else if (cancelled.has(beat.id)) {
|
|
78
|
+
operations.push({ op: 'cancel', beat_id: beat.id, reason: 'Cancelled by event result' });
|
|
79
|
+
handled.add(beat.id);
|
|
80
|
+
}
|
|
81
|
+
else if (result.end_day) {
|
|
82
|
+
operations.push({ op: 'cancel', beat_id: beat.id, reason: 'Day ended before this beat was completed' });
|
|
83
|
+
handled.add(beat.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!result.end_day) {
|
|
87
|
+
for (const op of payload.operations) {
|
|
88
|
+
if (op.op !== 'insert' && handled.has(op.beat_id))
|
|
89
|
+
continue;
|
|
90
|
+
if (op.op !== 'insert')
|
|
91
|
+
handled.add(op.beat_id);
|
|
92
|
+
operations.push(op);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { operations };
|
|
96
|
+
}
|
|
97
|
+
function applyReplan(plan, payload) { const next = JSON.parse(JSON.stringify(plan)); let counter = Math.max(0, ...next.beats.map(b => Number(b.id.match(/\d+/)?.[0] ?? 0))); for (const op of payload.operations) {
|
|
98
|
+
if (op.op === 'insert') {
|
|
99
|
+
counter++;
|
|
100
|
+
const beat = { id: 'beat_' + String(counter).padStart(2, '0'), intent: op.intent, priority: op.priority, status: 'pending' };
|
|
101
|
+
const index = op.after ? next.beats.findIndex(b => b.id === op.after) : -1;
|
|
102
|
+
next.beats.splice(index >= 0 ? index + 1 : next.beats.length, 0, beat);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const beat = next.beats.find(b => b.id === op.beat_id);
|
|
106
|
+
if (!beat)
|
|
107
|
+
continue;
|
|
108
|
+
if (op.op === 'complete')
|
|
109
|
+
beat.status = 'completed';
|
|
110
|
+
else if (op.op === 'cancel')
|
|
111
|
+
beat.status = 'cancelled';
|
|
112
|
+
else if (op.op === 'modify')
|
|
113
|
+
beat.intent = op.intent;
|
|
114
|
+
} next.revision++; return next; }
|
|
115
|
+
function finishPlay(worldRoot, day, state) { state.phase = 'settling'; state.step = 'complete'; state.active_event = null; state.active_beat = null; saveState(worldRoot, state); setYamlPhase(path_1.default.join(worldRoot, 'current.yaml'), 'settling'); setYamlPhase(path_1.default.join((0, exports.dayRoot)(worldRoot, day), 'meta.yaml'), 'settling'); }
|
|
116
|
+
function setYamlPhase(file, phase) { let text = fs_1.default.readFileSync(file, 'utf8'); text = /^phase:/m.test(text) ? text.replace(/^phase:.*$/m, 'phase: ' + phase) : text + '\nphase: ' + phase + '\n'; fs_1.default.writeFileSync(file, text, 'utf8'); }
|
|
117
|
+
function writeJson(file, value) { fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true }); const tmp = file + '.tmp'; fs_1.default.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf8'); fs_1.default.renameSync(tmp, file); }
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateGeneratedEvent = validateGeneratedEvent;
|
|
4
|
+
exports.validateEventStatus = validateEventStatus;
|
|
5
|
+
exports.normalizeTimeAdvanced = normalizeTimeAdvanced;
|
|
6
|
+
exports.validateEventResult = validateEventResult;
|
|
7
|
+
exports.normalizeReplanPayload = normalizeReplanPayload;
|
|
8
|
+
exports.validateReplan = validateReplan;
|
|
9
|
+
const ID = /^[a-z][a-z0-9_]*$/;
|
|
10
|
+
const EVENT_ID = /^event_\d{3}$/;
|
|
11
|
+
const TIME = /^\d+(?:m|h)$/;
|
|
12
|
+
function nonEmpty(v, label) { if (typeof v !== 'string' || !v.trim())
|
|
13
|
+
throw new Error(label + ' must be non-empty'); }
|
|
14
|
+
function strings(v, label) { if (!Array.isArray(v) || v.some(x => typeof x !== 'string'))
|
|
15
|
+
throw new Error(label + ' must be a string array'); }
|
|
16
|
+
function validateGeneratedEvent(v, expectedEvent, beat) { if (v.id !== expectedEvent || !EVENT_ID.test(v.id))
|
|
17
|
+
throw new Error('Invalid event id'); if (v.source_beat !== beat)
|
|
18
|
+
throw new Error('Event source beat mismatch'); nonEmpty(v.title, 'Event title'); nonEmpty(v.opening, 'Event opening'); nonEmpty(v.situation, 'Event situation'); strings(v.suggested_actions, 'Event suggested_actions'); if (v.suggested_actions.length > 5)
|
|
19
|
+
throw new Error('Event suggested_actions exceeds 5'); for (const key of ['outcome', 'result', 'success', 'failure', 'reward'])
|
|
20
|
+
if (key in v)
|
|
21
|
+
throw new Error('Generated event contains forbidden result field: ' + key); }
|
|
22
|
+
function validateEventStatus(v) {
|
|
23
|
+
if (v.status !== 'ongoing' && v.status !== 'resolved')
|
|
24
|
+
throw new Error('Invalid event status');
|
|
25
|
+
nonEmpty(v.situation, 'Event status situation');
|
|
26
|
+
if (typeof v.needs_user_action !== 'boolean')
|
|
27
|
+
throw new Error('needs_user_action must be boolean');
|
|
28
|
+
if (v.end_day === undefined)
|
|
29
|
+
v.end_day = false;
|
|
30
|
+
if (typeof v.end_day !== 'boolean')
|
|
31
|
+
throw new Error('end_day must be boolean');
|
|
32
|
+
if (v.status === 'resolved')
|
|
33
|
+
nonEmpty(v.resolution_summary, 'resolution_summary');
|
|
34
|
+
if (v.end_day && (v.status !== 'resolved' || v.needs_user_action))
|
|
35
|
+
throw new Error('end_day requires a resolved event without further user action');
|
|
36
|
+
}
|
|
37
|
+
function normalizeTimeAdvanced(value) {
|
|
38
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
39
|
+
return Math.round(value) + 'm';
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
throw new Error('time_advanced must be a duration string or minute count');
|
|
42
|
+
const compact = value.trim().toLowerCase().replace(/[约大概左右\s]/g, '');
|
|
43
|
+
if (TIME.test(compact))
|
|
44
|
+
return compact;
|
|
45
|
+
const compound = compact.match(/^(?:(\d+(?:\.\d+)?)h(?:ours?)?)?(?:(\d+)m(?:in(?:ute)?s?)?)?$/);
|
|
46
|
+
if (compound && (compound[1] || compound[2])) {
|
|
47
|
+
const minutes = Math.round(Number(compound[1] ?? 0) * 60 + Number(compound[2] ?? 0));
|
|
48
|
+
if (minutes > 0)
|
|
49
|
+
return minutes + 'm';
|
|
50
|
+
}
|
|
51
|
+
const chinese = compact.match(/^(?:(\d+(?:\.\d+)?)小时)?(?:(\d+)分钟)?$/);
|
|
52
|
+
if (chinese && (chinese[1] || chinese[2])) {
|
|
53
|
+
const minutes = Math.round(Number(chinese[1] ?? 0) * 60 + Number(chinese[2] ?? 0));
|
|
54
|
+
if (minutes > 0)
|
|
55
|
+
return minutes + 'm';
|
|
56
|
+
}
|
|
57
|
+
throw new Error('time_advanced must describe a positive duration, such as 30m, 1h30m, or 1小时30分钟');
|
|
58
|
+
}
|
|
59
|
+
function validateEventResult(v, eventId, beatId, plan) {
|
|
60
|
+
if (v.event_id !== eventId || v.source_beat !== beatId)
|
|
61
|
+
throw new Error('Event result identity mismatch');
|
|
62
|
+
nonEmpty(v.summary, 'Event result summary');
|
|
63
|
+
strings(v.protagonist_learned, 'protagonist_learned');
|
|
64
|
+
v.time_advanced = normalizeTimeAdvanced(v.time_advanced);
|
|
65
|
+
if (v.completed_source_beat !== undefined && typeof v.completed_source_beat !== 'boolean')
|
|
66
|
+
throw new Error('completed_source_beat must be boolean');
|
|
67
|
+
if (v.completed_beats === undefined)
|
|
68
|
+
v.completed_beats = v.completed_source_beat === false ? [] : [beatId];
|
|
69
|
+
if (v.cancelled_beats === undefined)
|
|
70
|
+
v.cancelled_beats = [];
|
|
71
|
+
if (v.end_day === undefined)
|
|
72
|
+
v.end_day = false;
|
|
73
|
+
strings(v.completed_beats, 'completed_beats');
|
|
74
|
+
strings(v.cancelled_beats, 'cancelled_beats');
|
|
75
|
+
if (typeof v.end_day !== 'boolean')
|
|
76
|
+
throw new Error('end_day must be boolean');
|
|
77
|
+
v.completed_source_beat = v.completed_beats.includes(beatId);
|
|
78
|
+
const completed = new Set(v.completed_beats);
|
|
79
|
+
if (v.cancelled_beats.some(id => completed.has(id)))
|
|
80
|
+
throw new Error('completed_beats and cancelled_beats overlap');
|
|
81
|
+
if (plan) {
|
|
82
|
+
const valid = new Set(plan.beats.map(beat => beat.id));
|
|
83
|
+
for (const id of [...v.completed_beats, ...v.cancelled_beats])
|
|
84
|
+
if (!valid.has(id))
|
|
85
|
+
throw new Error('Unknown beat in event result: ' + id);
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(v.state_patch) || v.state_patch.length > 20)
|
|
88
|
+
throw new Error('Invalid state_patch');
|
|
89
|
+
for (const patch of v.state_patch) {
|
|
90
|
+
if (patch.op !== 'set' || !ID.test(patch.key))
|
|
91
|
+
throw new Error('Invalid runtime patch');
|
|
92
|
+
if (!['string', 'number', 'boolean'].includes(typeof patch.value) && patch.value !== null)
|
|
93
|
+
throw new Error('Invalid runtime patch value');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function normalizeReplanPayload(v, plan) {
|
|
97
|
+
if (!Array.isArray(v.operations))
|
|
98
|
+
return { payload: v, warnings: [] };
|
|
99
|
+
const validIds = new Set(plan.beats.map(beat => beat.id));
|
|
100
|
+
let insertSlots = Math.max(0, plan.max_events - plan.beats.length);
|
|
101
|
+
const warnings = [];
|
|
102
|
+
const operations = [];
|
|
103
|
+
for (const original of v.operations) {
|
|
104
|
+
const op = JSON.parse(JSON.stringify(original));
|
|
105
|
+
if (op.op !== 'insert') {
|
|
106
|
+
operations.push(op);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (insertSlots === 0) {
|
|
110
|
+
warnings.push('Dropped insert because max_events has no remaining slots');
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (op.after && !validIds.has(op.after)) {
|
|
114
|
+
warnings.push('Removed unknown insertion anchor ' + op.after + '; appending the beat instead');
|
|
115
|
+
delete op.after;
|
|
116
|
+
}
|
|
117
|
+
operations.push(op);
|
|
118
|
+
insertSlots--;
|
|
119
|
+
}
|
|
120
|
+
return { payload: { operations }, warnings };
|
|
121
|
+
}
|
|
122
|
+
function validateReplan(v, plan) {
|
|
123
|
+
if (!Array.isArray(v.operations) || v.operations.length > 10)
|
|
124
|
+
throw new Error('Invalid replan operations');
|
|
125
|
+
const byId = new Map(plan.beats.map(beat => [beat.id, beat]));
|
|
126
|
+
const touched = new Set();
|
|
127
|
+
let inserts = 0;
|
|
128
|
+
for (const op of v.operations) {
|
|
129
|
+
if (op.op === 'insert') {
|
|
130
|
+
nonEmpty(op.intent, 'Inserted beat intent');
|
|
131
|
+
nonEmpty(op.reason, 'Inserted beat reason');
|
|
132
|
+
if (op.after && !byId.has(op.after))
|
|
133
|
+
throw new Error('Unknown insertion anchor: ' + op.after);
|
|
134
|
+
if (op.priority !== 'required' && op.priority !== 'optional')
|
|
135
|
+
throw new Error('Invalid inserted beat priority');
|
|
136
|
+
inserts++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (touched.has(op.beat_id))
|
|
140
|
+
throw new Error('Multiple replan operations for beat: ' + op.beat_id);
|
|
141
|
+
touched.add(op.beat_id);
|
|
142
|
+
const beat = byId.get(op.beat_id);
|
|
143
|
+
if (!beat)
|
|
144
|
+
throw new Error('Unknown beat in replan: ' + op.beat_id);
|
|
145
|
+
if (beat.status === 'completed' || beat.status === 'cancelled')
|
|
146
|
+
throw new Error('Cannot change finished beat: ' + op.beat_id);
|
|
147
|
+
if (op.op === 'modify') {
|
|
148
|
+
nonEmpty(op.intent, 'Modified beat intent');
|
|
149
|
+
nonEmpty(op.reason, 'Modified beat reason');
|
|
150
|
+
}
|
|
151
|
+
if (op.op === 'cancel')
|
|
152
|
+
nonEmpty(op.reason, 'Cancelled beat reason');
|
|
153
|
+
}
|
|
154
|
+
if (plan.beats.length + inserts > plan.max_events)
|
|
155
|
+
throw new Error('Replan exceeds max_events');
|
|
156
|
+
}
|