@goodandready/dsh-goal 0.1.2 → 0.1.4
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 +120 -38
- package/docs/README.ru.md +143 -0
- package/docs/README.zh.md +143 -0
- package/lib/client.js +1305 -1235
- package/lib/command-handler.js +192 -189
- package/lib/goal-engine.js +536 -433
- package/lib/index.js +567 -489
- package/package.json +3 -3
- package/README.ru.md +0 -65
package/lib/goal-engine.js
CHANGED
|
@@ -1,433 +1,536 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Изолированное ядро управления состоянием цели (Goal Engine).
|
|
7
|
-
* Не имеет внешних зависимостей, 100% тестируемо через node --test.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const GoalState = {
|
|
11
|
-
IDLE: 'IDLE',
|
|
12
|
-
PLANNING: 'PLANNING',
|
|
13
|
-
RUNNING: 'RUNNING',
|
|
14
|
-
PAUSED: 'PAUSED',
|
|
15
|
-
COMPLETED: 'COMPLETED',
|
|
16
|
-
FAILED: 'FAILED',
|
|
17
|
-
CANCELLED: 'CANCELLED',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const MilestoneStatus = {
|
|
21
|
-
PENDING: 'pending',
|
|
22
|
-
IN_PROGRESS: 'in_progress',
|
|
23
|
-
COMPLETED: 'completed',
|
|
24
|
-
FAILED: 'failed',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Форматирование времени в лаконичную строку (например: "2s", "45s", "1m 15s", "2h 5m")
|
|
29
|
-
* @param {number} totalSeconds
|
|
30
|
-
* @returns {string}
|
|
31
|
-
*/
|
|
32
|
-
export function formatElapsed(totalSeconds) {
|
|
33
|
-
const sec = Math.max(0, Math.floor(totalSeconds));
|
|
34
|
-
if (sec < 60) return `${sec}s`;
|
|
35
|
-
const mins = Math.floor(sec / 60);
|
|
36
|
-
const remainingSec = sec % 60;
|
|
37
|
-
if (mins < 60) {
|
|
38
|
-
return remainingSec > 0 ? `${mins}m ${remainingSec}s` : `${mins}m`;
|
|
39
|
-
}
|
|
40
|
-
const hours = Math.floor(mins / 60);
|
|
41
|
-
const remainingMins = mins % 60;
|
|
42
|
-
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class GoalEngine {
|
|
46
|
-
constructor(options = {}) {
|
|
47
|
-
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
48
|
-
this.autoDrive = options.autoDrive ?? true;
|
|
49
|
-
this.enableSound = options.enableSound ?? true;
|
|
50
|
-
this.
|
|
51
|
-
this.listeners = new Set();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (this.
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Изолированное ядро управления состоянием цели (Goal Engine).
|
|
7
|
+
* Не имеет внешних зависимостей, 100% тестируемо через node --test.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const GoalState = {
|
|
11
|
+
IDLE: 'IDLE',
|
|
12
|
+
PLANNING: 'PLANNING',
|
|
13
|
+
RUNNING: 'RUNNING',
|
|
14
|
+
PAUSED: 'PAUSED',
|
|
15
|
+
COMPLETED: 'COMPLETED',
|
|
16
|
+
FAILED: 'FAILED',
|
|
17
|
+
CANCELLED: 'CANCELLED',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const MilestoneStatus = {
|
|
21
|
+
PENDING: 'pending',
|
|
22
|
+
IN_PROGRESS: 'in_progress',
|
|
23
|
+
COMPLETED: 'completed',
|
|
24
|
+
FAILED: 'failed',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Форматирование времени в лаконичную строку (например: "2s", "45s", "1m 15s", "2h 5m")
|
|
29
|
+
* @param {number} totalSeconds
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function formatElapsed(totalSeconds) {
|
|
33
|
+
const sec = Math.max(0, Math.floor(totalSeconds));
|
|
34
|
+
if (sec < 60) return `${sec}s`;
|
|
35
|
+
const mins = Math.floor(sec / 60);
|
|
36
|
+
const remainingSec = sec % 60;
|
|
37
|
+
if (mins < 60) {
|
|
38
|
+
return remainingSec > 0 ? `${mins}m ${remainingSec}s` : `${mins}m`;
|
|
39
|
+
}
|
|
40
|
+
const hours = Math.floor(mins / 60);
|
|
41
|
+
const remainingMins = mins % 60;
|
|
42
|
+
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class GoalEngine {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
48
|
+
this.autoDrive = options.autoDrive ?? true;
|
|
49
|
+
this.enableSound = options.enableSound ?? true;
|
|
50
|
+
this.goals = new Map();
|
|
51
|
+
this.listeners = new Set();
|
|
52
|
+
this.saveTimer = null;
|
|
53
|
+
|
|
54
|
+
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
55
|
+
this.storagePath = options.storagePath ?? null;
|
|
56
|
+
|
|
57
|
+
this.loadStateFromDisk();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get currentGoal() {
|
|
61
|
+
return this.goals.get('default') || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
set currentGoal(val) {
|
|
65
|
+
if (val) {
|
|
66
|
+
this.goals.set('default', val);
|
|
67
|
+
} else {
|
|
68
|
+
this.goals.delete('default');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
loadStateFromDisk() {
|
|
73
|
+
if (!this.storagePath) return;
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(this.storagePath)) {
|
|
76
|
+
const raw = fs.readFileSync(this.storagePath, 'utf8');
|
|
77
|
+
const data = JSON.parse(raw);
|
|
78
|
+
if (data && typeof data === 'object') {
|
|
79
|
+
if (data.sessions && typeof data.sessions === 'object') {
|
|
80
|
+
for (const [sid, goal] of Object.entries(data.sessions)) {
|
|
81
|
+
if (goal && goal.id && goal.title) {
|
|
82
|
+
this.goals.set(sid, goal);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (data.id && data.title) {
|
|
86
|
+
this.goals.set('default', data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.warn('[GoalEngine] Failed to load state from disk:', err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
scheduleSave(immediate = false) {
|
|
96
|
+
if (!this.storagePath) return;
|
|
97
|
+
if (immediate) {
|
|
98
|
+
if (this.saveTimer) {
|
|
99
|
+
clearTimeout(this.saveTimer);
|
|
100
|
+
this.saveTimer = null;
|
|
101
|
+
}
|
|
102
|
+
this.writeStateToDiskSync();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!this.saveTimer) {
|
|
106
|
+
this.saveTimer = setTimeout(() => {
|
|
107
|
+
this.saveTimer = null;
|
|
108
|
+
this.writeStateToDiskSync();
|
|
109
|
+
}, 250);
|
|
110
|
+
if (typeof this.saveTimer.unref === 'function') {
|
|
111
|
+
this.saveTimer.unref();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeStateToDiskSync() {
|
|
117
|
+
if (!this.storagePath) return;
|
|
118
|
+
try {
|
|
119
|
+
if (this.goals.size === 0) {
|
|
120
|
+
if (fs.existsSync(this.storagePath)) {
|
|
121
|
+
fs.unlinkSync(this.storagePath);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const sessionsObj = {};
|
|
126
|
+
for (const [sid, goal] of this.goals.entries()) {
|
|
127
|
+
sessionsObj[sid] = goal;
|
|
128
|
+
}
|
|
129
|
+
const payload = {
|
|
130
|
+
version: 2,
|
|
131
|
+
sessions: sessionsObj,
|
|
132
|
+
...(this.goals.has('default') ? this.goals.get('default') : {}),
|
|
133
|
+
};
|
|
134
|
+
const tmp = `${this.storagePath}.tmp.${Date.now()}`;
|
|
135
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
136
|
+
fs.renameSync(tmp, this.storagePath);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.warn('[GoalEngine] Failed to write state to disk:', err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
flushSync() {
|
|
143
|
+
if (this.saveTimer) {
|
|
144
|
+
clearTimeout(this.saveTimer);
|
|
145
|
+
this.saveTimer = null;
|
|
146
|
+
}
|
|
147
|
+
this.writeStateToDiskSync();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
saveStateToDisk() {
|
|
151
|
+
this.flushSync();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Подписка на изменение состояния
|
|
156
|
+
* @param {Function} callback
|
|
157
|
+
* @returns {Function} unsubscribe
|
|
158
|
+
*/
|
|
159
|
+
subscribe(callback) {
|
|
160
|
+
this.listeners.add(callback);
|
|
161
|
+
return () => this.listeners.delete(callback);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
emit(sessionId = 'default', immediate = false) {
|
|
165
|
+
this.scheduleSave(immediate);
|
|
166
|
+
const sid = sessionId || 'default';
|
|
167
|
+
const snapshot = this.getSnapshot(sid);
|
|
168
|
+
for (const listener of this.listeners) {
|
|
169
|
+
try {
|
|
170
|
+
listener(snapshot, sid);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[GoalEngine] Listener error:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Динамическое обновление настроек на лету
|
|
179
|
+
* @param {Object} config
|
|
180
|
+
*/
|
|
181
|
+
updateConfig(config = {}) {
|
|
182
|
+
if (typeof config.defaultMaxIterations === 'number' && config.defaultMaxIterations >= 1) {
|
|
183
|
+
const prev = this.defaultMaxIterations;
|
|
184
|
+
this.defaultMaxIterations = config.defaultMaxIterations;
|
|
185
|
+
if (this.currentGoal && this.currentGoal.maxIterations === prev) {
|
|
186
|
+
this.currentGoal.maxIterations = config.defaultMaxIterations;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (typeof config.autoDrive === 'boolean') {
|
|
190
|
+
this.autoDrive = config.autoDrive;
|
|
191
|
+
}
|
|
192
|
+
if (typeof config.enableSound === 'boolean') {
|
|
193
|
+
this.enableSound = config.enableSound;
|
|
194
|
+
}
|
|
195
|
+
this.emit();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Запуск новой цели
|
|
200
|
+
*/
|
|
201
|
+
getGoal(sessionId = 'default') {
|
|
202
|
+
return this.goals.get(sessionId || 'default') || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Запуск новой цели
|
|
207
|
+
* @param {string} title
|
|
208
|
+
* @param {Object} options
|
|
209
|
+
* @param {string} [sessionId='default']
|
|
210
|
+
*/
|
|
211
|
+
startGoal(title, options = {}, sessionId = 'default') {
|
|
212
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
213
|
+
throw new Error('Goal title cannot be empty');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const cleanTitle = title.trim();
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const sid = sessionId || 'default';
|
|
219
|
+
|
|
220
|
+
const goal = {
|
|
221
|
+
id: `goal-${now}-${Math.random().toString(36).substring(2, 7)}`,
|
|
222
|
+
sessionId: sid,
|
|
223
|
+
title: cleanTitle,
|
|
224
|
+
description: options.description?.trim() || '',
|
|
225
|
+
state: GoalState.RUNNING,
|
|
226
|
+
startedAt: now,
|
|
227
|
+
pausedAt: null,
|
|
228
|
+
totalPausedDurationMs: 0,
|
|
229
|
+
completedAt: null,
|
|
230
|
+
iterationsCount: 0,
|
|
231
|
+
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
232
|
+
milestones: [],
|
|
233
|
+
logs: [
|
|
234
|
+
{
|
|
235
|
+
timestamp: now,
|
|
236
|
+
type: 'info',
|
|
237
|
+
message: `Goal initiated: "${cleanTitle}"`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
resultSummary: '',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this.goals.set(sid, goal);
|
|
244
|
+
|
|
245
|
+
if (Array.isArray(options.milestones) && options.milestones.length > 0) {
|
|
246
|
+
this.addMilestones(options.milestones, false, sid);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.emit(sid, true);
|
|
250
|
+
return this.getSnapshot(sid);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Приостановка автономного цикла цели
|
|
255
|
+
*/
|
|
256
|
+
pause(reason = 'User requested pause', sessionId = 'default') {
|
|
257
|
+
const sid = sessionId || 'default';
|
|
258
|
+
const goal = this.goals.get(sid);
|
|
259
|
+
if (!goal || goal.state !== GoalState.RUNNING) {
|
|
260
|
+
return this.getSnapshot(sid);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
goal.state = GoalState.PAUSED;
|
|
264
|
+
goal.pausedAt = Date.now();
|
|
265
|
+
goal.logs.push({
|
|
266
|
+
timestamp: Date.now(),
|
|
267
|
+
type: 'warning',
|
|
268
|
+
message: `Paused: ${reason}`,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
this.emit(sid, true);
|
|
272
|
+
return this.getSnapshot(sid);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Возобновление выполнения цели
|
|
277
|
+
*/
|
|
278
|
+
resume(sessionId = 'default') {
|
|
279
|
+
const sid = sessionId || 'default';
|
|
280
|
+
const goal = this.goals.get(sid);
|
|
281
|
+
if (!goal || goal.state !== GoalState.PAUSED) {
|
|
282
|
+
return this.getSnapshot(sid);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
if (goal.pausedAt) {
|
|
287
|
+
goal.totalPausedDurationMs += now - goal.pausedAt;
|
|
288
|
+
goal.pausedAt = null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
goal.state = GoalState.RUNNING;
|
|
292
|
+
goal.logs.push({
|
|
293
|
+
timestamp: now,
|
|
294
|
+
type: 'info',
|
|
295
|
+
message: 'Goal resumed',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
this.emit(sid, true);
|
|
299
|
+
return this.getSnapshot(sid);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Отмена цели
|
|
304
|
+
*/
|
|
305
|
+
cancel(reason = 'Cancelled by user', sessionId = 'default') {
|
|
306
|
+
const sid = sessionId || 'default';
|
|
307
|
+
const goal = this.goals.get(sid);
|
|
308
|
+
if (!goal) return null;
|
|
309
|
+
|
|
310
|
+
goal.state = GoalState.CANCELLED;
|
|
311
|
+
goal.completedAt = Date.now();
|
|
312
|
+
goal.logs.push({
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
type: 'warning',
|
|
315
|
+
message: `Cancelled: ${reason}`,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
this.emit(sid, true);
|
|
319
|
+
return this.getSnapshot(sid);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Очистка / сброс цели в IDLE
|
|
324
|
+
*/
|
|
325
|
+
clear(sessionId = 'default') {
|
|
326
|
+
const sid = sessionId || 'default';
|
|
327
|
+
this.goals.delete(sid);
|
|
328
|
+
this.emit(sid, true);
|
|
329
|
+
return this.getSnapshot(sid);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Успешное завершение цели
|
|
334
|
+
*/
|
|
335
|
+
completeGoal(summary = '', sessionId = 'default') {
|
|
336
|
+
const sid = sessionId || 'default';
|
|
337
|
+
const goal = this.goals.get(sid);
|
|
338
|
+
if (!goal) return null;
|
|
339
|
+
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
goal.state = GoalState.COMPLETED;
|
|
342
|
+
goal.completedAt = now;
|
|
343
|
+
goal.resultSummary = summary;
|
|
344
|
+
goal.logs.push({
|
|
345
|
+
timestamp: now,
|
|
346
|
+
type: 'info',
|
|
347
|
+
message: `Goal completed successfully: ${summary || 'All objectives met.'}`,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Завершаем все активные milestones
|
|
351
|
+
for (const m of goal.milestones) {
|
|
352
|
+
if (m.status === MilestoneStatus.IN_PROGRESS || m.status === MilestoneStatus.PENDING) {
|
|
353
|
+
m.status = MilestoneStatus.COMPLETED;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.emit(sid, true);
|
|
358
|
+
return this.getSnapshot(sid);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Добавление вех (milestones)
|
|
363
|
+
*/
|
|
364
|
+
addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
|
|
365
|
+
const sid = sessionId || 'default';
|
|
366
|
+
const goal = this.goals.get(sid);
|
|
367
|
+
if (!goal || !Array.isArray(milestonesList)) return;
|
|
368
|
+
|
|
369
|
+
for (const item of milestonesList) {
|
|
370
|
+
const itemTitle = typeof item === 'string' ? item : item.title;
|
|
371
|
+
if (!itemTitle || !itemTitle.trim()) continue;
|
|
372
|
+
|
|
373
|
+
const mId = (typeof item === 'object' && item.id) ? item.id : `m-${goal.milestones.length + 1}`;
|
|
374
|
+
goal.milestones.push({
|
|
375
|
+
id: String(mId),
|
|
376
|
+
title: itemTitle.trim(),
|
|
377
|
+
status: (typeof item === 'object' && item.status) ? item.status : MilestoneStatus.PENDING,
|
|
378
|
+
notes: (typeof item === 'object' && item.notes) ? item.notes : '',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (shouldEmit) this.emit(sid);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Обновление конкретной вехи
|
|
387
|
+
*/
|
|
388
|
+
updateMilestone(id, status, notes = '', sessionId = 'default') {
|
|
389
|
+
const sid = sessionId || 'default';
|
|
390
|
+
const goal = this.goals.get(sid);
|
|
391
|
+
if (!goal) return false;
|
|
392
|
+
|
|
393
|
+
const target = goal.milestones.find((m) => m.id === String(id));
|
|
394
|
+
if (!target) return false;
|
|
395
|
+
|
|
396
|
+
if (status && Object.values(MilestoneStatus).includes(status)) {
|
|
397
|
+
target.status = status;
|
|
398
|
+
}
|
|
399
|
+
if (notes) {
|
|
400
|
+
target.notes = notes;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
goal.logs.push({
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
type: 'milestone',
|
|
406
|
+
message: `Milestone [${target.title}] status -> ${target.status}`,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
this.emit(sid);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Увеличение счётчика итераций turn
|
|
415
|
+
*/
|
|
416
|
+
incrementIteration(sessionId = 'default') {
|
|
417
|
+
const sid = sessionId || 'default';
|
|
418
|
+
const goal = this.goals.get(sid);
|
|
419
|
+
if (!goal || goal.state !== GoalState.RUNNING) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
goal.iterationsCount += 1;
|
|
424
|
+
|
|
425
|
+
if (goal.iterationsCount >= goal.maxIterations) {
|
|
426
|
+
goal.state = GoalState.FAILED;
|
|
427
|
+
goal.logs.push({
|
|
428
|
+
timestamp: Date.now(),
|
|
429
|
+
type: 'error',
|
|
430
|
+
message: `Safety limit reached: maximum ${goal.maxIterations} iterations exceeded.`,
|
|
431
|
+
});
|
|
432
|
+
this.emit(sid, true);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
this.emit(sid);
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Подсчёт времени в секундах
|
|
442
|
+
*/
|
|
443
|
+
getElapsedSeconds(sessionId = 'default') {
|
|
444
|
+
const sid = sessionId || 'default';
|
|
445
|
+
const goal = this.goals.get(sid);
|
|
446
|
+
if (!goal) return 0;
|
|
447
|
+
const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = goal;
|
|
448
|
+
const endTime = completedAt || (pausedAt || Date.now());
|
|
449
|
+
const elapsedMs = Math.max(0, endTime - startedAt - totalPausedDurationMs);
|
|
450
|
+
return Math.floor(elapsedMs / 1000);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Снимок состояния для передачи клиенту / API
|
|
455
|
+
*/
|
|
456
|
+
getSnapshot(sessionId = 'default') {
|
|
457
|
+
const sid = sessionId || 'default';
|
|
458
|
+
const goal = this.goals.get(sid);
|
|
459
|
+
if (!goal) {
|
|
460
|
+
return {
|
|
461
|
+
sessionId: sid,
|
|
462
|
+
hasActiveGoal: false,
|
|
463
|
+
state: GoalState.IDLE,
|
|
464
|
+
title: '',
|
|
465
|
+
startedAt: null,
|
|
466
|
+
pausedAt: null,
|
|
467
|
+
totalPausedDurationMs: 0,
|
|
468
|
+
completedAt: null,
|
|
469
|
+
elapsedSeconds: 0,
|
|
470
|
+
formattedElapsed: '0s',
|
|
471
|
+
milestones: [],
|
|
472
|
+
progressPercent: 0,
|
|
473
|
+
iterationsCount: 0,
|
|
474
|
+
maxIterations: this.defaultMaxIterations,
|
|
475
|
+
autoDrive: this.autoDrive,
|
|
476
|
+
enableSound: this.enableSound,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const elapsed = this.getElapsedSeconds(sid);
|
|
481
|
+
const milestones = goal.milestones;
|
|
482
|
+
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
483
|
+
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
sessionId: sid,
|
|
487
|
+
hasActiveGoal: true,
|
|
488
|
+
id: goal.id,
|
|
489
|
+
state: goal.state,
|
|
490
|
+
title: goal.title,
|
|
491
|
+
description: goal.description,
|
|
492
|
+
startedAt: goal.startedAt,
|
|
493
|
+
pausedAt: goal.pausedAt,
|
|
494
|
+
totalPausedDurationMs: goal.totalPausedDurationMs,
|
|
495
|
+
completedAt: goal.completedAt,
|
|
496
|
+
elapsedSeconds: elapsed,
|
|
497
|
+
formattedElapsed: formatElapsed(elapsed),
|
|
498
|
+
iterationsCount: goal.iterationsCount,
|
|
499
|
+
maxIterations: goal.maxIterations,
|
|
500
|
+
milestones,
|
|
501
|
+
progressPercent,
|
|
502
|
+
logs: goal.logs,
|
|
503
|
+
resultSummary: goal.resultSummary,
|
|
504
|
+
autoDrive: this.autoDrive,
|
|
505
|
+
enableSound: this.enableSound,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Формирование системного контекста для инжекта модели
|
|
511
|
+
*/
|
|
512
|
+
getStatePromptInjection(sessionId = 'default') {
|
|
513
|
+
const sid = sessionId || 'default';
|
|
514
|
+
const goal = this.goals.get(sid);
|
|
515
|
+
if (!goal || goal.state !== GoalState.RUNNING) {
|
|
516
|
+
return '';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const snapshot = this.getSnapshot(sid);
|
|
520
|
+
const hasMilestones = snapshot.milestones.length > 0;
|
|
521
|
+
const milestonesText = hasMilestones
|
|
522
|
+
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
523
|
+
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
524
|
+
|
|
525
|
+
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
526
|
+
Цель: "${snapshot.title}"
|
|
527
|
+
Время работы: ${snapshot.formattedElapsed} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
528
|
+
План работ:
|
|
529
|
+
${milestonesText}
|
|
530
|
+
|
|
531
|
+
Инструкции Goal Mode (СТРОГО ОБЯЗАТЕЛЬНЫ К ВЫПОЛНЕНИЮ):
|
|
532
|
+
1. ${hasMilestones ? 'Выполняй текущий активный пункт плана работ.' : 'ТВОЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones со списком пунктов плана работ (3-7 конкретных шагов). Запрещено выполнять работу или завершать turn без вызова goal_set_milestones!'}
|
|
533
|
+
2. По мере выполнения каждого шага обязательно отмечай его статус через инструмент goal_update_progress (status: "in_progress" перед началом шага, status: "completed" по его завершении с кратким notes).
|
|
534
|
+
3. Когда все пункты плана будут выполнены, вызови инструмент goal_finish с подробным итоговым резюме достигнутых результатов.`;
|
|
535
|
+
}
|
|
536
|
+
}
|