@goodandready/dsh-goal 0.2.1 → 0.2.2

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.
@@ -1,1009 +1,2018 @@
1
1
  import fs from 'node:fs';
2
+
2
3
  import path from 'node:path';
4
+
3
5
  import os from 'node:os';
6
+
4
7
  import { execSync } from 'node:child_process';
5
8
 
9
+
10
+
6
11
  /**
12
+
7
13
  * Изолированное ядро управления состоянием цели (Goal Engine).
14
+
8
15
  * Не имеет внешних зависимостей, 100% тестируемо через node --test.
16
+
9
17
  */
10
18
 
19
+
20
+
11
21
  export const GoalState = {
22
+
12
23
  IDLE: 'IDLE',
24
+
13
25
  PLANNING: 'PLANNING',
26
+
14
27
  RUNNING: 'RUNNING',
28
+
15
29
  PAUSED: 'PAUSED',
30
+
16
31
  COMPLETED: 'COMPLETED',
32
+
17
33
  FAILED: 'FAILED',
34
+
18
35
  CANCELLED: 'CANCELLED',
36
+
19
37
  };
20
38
 
39
+
40
+
21
41
  export const MilestoneStatus = {
42
+
22
43
  PENDING: 'pending',
44
+
23
45
  IN_PROGRESS: 'in_progress',
46
+
24
47
  COMPLETED: 'completed',
48
+
25
49
  FAILED: 'failed',
50
+
26
51
  };
27
52
 
53
+
54
+
28
55
  /**
56
+
29
57
  * Форматирование времени в лаконичную строку (например: "2s", "45s", "1m 15s", "2h 5m")
58
+
30
59
  * @param {number} totalSeconds
60
+
31
61
  * @returns {string}
62
+
32
63
  */
64
+
33
65
  export function formatElapsed(totalSeconds) {
66
+
34
67
  const sec = Math.max(0, Math.floor(totalSeconds));
68
+
35
69
  if (sec < 60) return `${sec}s`;
70
+
36
71
  const mins = Math.floor(sec / 60);
72
+
37
73
  const remainingSec = sec % 60;
74
+
38
75
  if (mins < 60) {
76
+
39
77
  return remainingSec > 0 ? `${mins}m ${remainingSec}s` : `${mins}m`;
78
+
40
79
  }
80
+
41
81
  const hours = Math.floor(mins / 60);
82
+
42
83
  const remainingMins = mins % 60;
84
+
43
85
  return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
86
+
44
87
  }
45
88
 
89
+
90
+
46
91
  /**
92
+
47
93
  * Форматирование расчетного оставшегося времени (ETA)
94
+
48
95
  * @param {number|null} seconds
96
+
49
97
  * @returns {string|null}
98
+
50
99
  */
100
+
51
101
  export function formatETA(seconds) {
102
+
52
103
  if (seconds == null || isNaN(seconds)) return null;
104
+
53
105
  const sec = Math.max(0, Math.floor(seconds));
106
+
54
107
  if (sec < 60) return `~${sec}s`;
108
+
55
109
  const mins = Math.round(sec / 60);
110
+
56
111
  if (mins < 60) return `~${mins}m`;
112
+
57
113
  const hours = Math.floor(mins / 60);
114
+
58
115
  const remainingMins = mins % 60;
116
+
59
117
  return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
118
+
60
119
  }
61
120
 
121
+
122
+
62
123
  /**
124
+
63
125
  * Автоматическое определение языка текста (кириллица -> ru, иероглифы -> zh, иначе en)
126
+
64
127
  * @param {string} text
128
+
65
129
  * @param {string} [fallback='en']
130
+
66
131
  * @returns {'ru' | 'en' | 'zh'}
132
+
67
133
  */
134
+
68
135
  export function detectLanguage(text, fallback = 'en') {
136
+
69
137
  if (!text || typeof text !== 'string') return fallback;
138
+
70
139
  if (/[а-яёА-ЯЁ]/i.test(text)) {
140
+
71
141
  return 'ru';
142
+
72
143
  }
144
+
73
145
  if (/[\u4e00-\u9fa5]/.test(text)) {
146
+
74
147
  return 'zh';
148
+
75
149
  }
150
+
76
151
  return 'en';
152
+
77
153
  }
78
154
 
79
155
 
156
+
157
+
158
+
80
159
  /**
160
+
81
161
  * Безопасное получение текущего короткого Git commit hash
162
+
82
163
  * @returns {string|null}
164
+
83
165
  */
166
+
84
167
  export function getGitCurrentCommit() {
168
+
85
169
  try {
170
+
86
171
  return execSync('git rev-parse --short HEAD', {
172
+
87
173
  encoding: 'utf8',
174
+
88
175
  stdio: ['ignore', 'pipe', 'ignore'],
176
+
89
177
  timeout: 1000,
178
+
90
179
  }).trim();
180
+
91
181
  } catch (_) {
182
+
92
183
  return null;
184
+
93
185
  }
186
+
94
187
  }
95
188
 
189
+
190
+
96
191
  /**
192
+
97
193
  * Генератор Markdown-отчета о результатах цели
194
+
98
195
  * @param {Object} state
196
+
99
197
  * @returns {string}
198
+
100
199
  */
200
+
101
201
  export function exportReportMarkdown(state) {
202
+
102
203
  if (!state) return '';
204
+
103
205
  const title = state.title || 'Goal Report';
206
+
104
207
  const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
208
+
105
209
  const elapsed = state.formattedElapsed || '0s';
210
+
106
211
  const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
212
+
107
213
  const totalTokens = state.tokensUsage?.totalTokens || 0;
214
+
108
215
  const promptTokens = state.tokensUsage?.promptTokens || 0;
216
+
109
217
  const compTokens = state.tokensUsage?.completionTokens || 0;
218
+
110
219
  const gitCommit = state.gitStartCommit ? ` | **Git Start:** \`${state.gitStartCommit}\`` : '';
111
220
 
221
+
222
+
112
223
  let md = `# 🎯 Goal Report: ${title}\n\n`;
224
+
113
225
  md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`${gitCommit}\n`;
226
+
114
227
  if (totalTokens > 0) {
228
+
115
229
  md += `**Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n`;
230
+
116
231
  }
232
+
117
233
  md += '\n';
118
234
 
235
+
236
+
119
237
  if (state.description) {
238
+
120
239
  md += `### Description\n${state.description}\n\n`;
240
+
121
241
  }
122
242
 
243
+
244
+
123
245
  if (state.resultSummary) {
246
+
124
247
  md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
248
+
125
249
  }
126
250
 
251
+
252
+
127
253
  const milestones = state.milestones || [];
254
+
128
255
  if (milestones.length > 0) {
256
+
129
257
  md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
258
+
130
259
  milestones.forEach((m, idx) => {
260
+
131
261
  const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
262
+
132
263
  const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
264
+
133
265
  const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
266
+
134
267
  md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
268
+
135
269
  });
270
+
136
271
  md += '\n';
272
+
137
273
  }
138
274
 
275
+
276
+
139
277
  md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
278
+
140
279
  return md;
280
+
141
281
  }
142
282
 
283
+
284
+
143
285
  /**
286
+
144
287
  * Генератор отчета в формате GitHub / Gitea PR Comment со спойлерами
288
+
145
289
  * @param {Object} state
290
+
146
291
  * @returns {string}
292
+
147
293
  */
294
+
148
295
  export function exportReportGitHubPR(state) {
296
+
149
297
  if (!state) return '';
298
+
150
299
  const title = state.title || 'Goal Report';
300
+
151
301
  const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
302
+
152
303
  const elapsed = state.formattedElapsed || '0s';
304
+
153
305
  const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
306
+
154
307
  const totalTokens = state.tokensUsage?.totalTokens || 0;
308
+
155
309
  const promptTokens = state.tokensUsage?.promptTokens || 0;
310
+
156
311
  const compTokens = state.tokensUsage?.completionTokens || 0;
312
+
157
313
  const gitCommit = state.gitStartCommit ? ` &nbsp;|&nbsp; **Git Start:** \`${state.gitStartCommit}\` 📌` : '';
158
314
 
315
+
316
+
159
317
  let md = `## 🎯 Autonomous Goal Resolution: ${title}\n\n`;
318
+
160
319
  md += `> **Status:** \`${status}\` 🚀 &nbsp;|&nbsp; **Duration:** \`${elapsed}\` ⏱️ &nbsp;|&nbsp; **Iterations:** \`${iter}\` 🔄${gitCommit}\n\n`;
161
320
 
321
+
322
+
162
323
  if (totalTokens > 0) {
324
+
163
325
  md += `### 📊 Telemetry & Token Usage\n`;
326
+
164
327
  md += `- **Total Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n\n`;
328
+
165
329
  }
166
330
 
331
+
332
+
167
333
  if (state.resultSummary) {
334
+
168
335
  md += `### 📦 Deliverables & Achievements\n${state.resultSummary}\n\n`;
336
+
169
337
  } else if (state.description) {
338
+
170
339
  md += `### 📝 Objective\n${state.description}\n\n`;
340
+
171
341
  }
172
342
 
343
+
344
+
173
345
  const milestones = state.milestones || [];
346
+
174
347
  if (milestones.length > 0) {
348
+
175
349
  const completedCount = milestones.filter(m => m.status === 'completed').length;
350
+
176
351
  md += `<details>\n<summary><b>📋 Milestones Breakdown (${completedCount}/${milestones.length} Completed)</b></summary>\n\n`;
352
+
177
353
  md += `| # | Status | Milestone | Notes |\n|---|---|---|---|\n`;
354
+
178
355
  milestones.forEach((m, idx) => {
356
+
179
357
  const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
358
+
180
359
  const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
360
+
181
361
  const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
362
+
182
363
  md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
364
+
183
365
  });
366
+
184
367
  md += `\n</details>\n\n`;
368
+
185
369
  }
186
370
 
371
+
372
+
187
373
  md += `*Automated by [@goodandready/dsh-goal](https://github.com/GooDAnDReaDY/dsh-goal)*\n`;
374
+
188
375
  return md;
376
+
189
377
  }
190
378
 
379
+
380
+
191
381
  export class GoalEngine {
382
+
192
383
  constructor(options = {}) {
384
+
193
385
  this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
386
+
194
387
  this.autoDrive = options.autoDrive ?? true;
388
+
195
389
  this.enableSound = options.enableSound ?? true;
390
+
196
391
  this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
392
+
197
393
  this.consecutiveToolFailureLimit = options.consecutiveToolFailureLimit ?? 3;
394
+
198
395
  this.maxSessions = options.maxSessions ?? 100;
396
+
199
397
  this.goals = new Map();
398
+
200
399
  this.listeners = new Set();
400
+
201
401
  this.saveTimer = null;
402
+
202
403
  this.stallCounters = new Map(); // sessionId -> number of consecutive turns without progress
404
+
203
405
  this.toolFailureCounters = new Map(); // sessionId -> number of consecutive turns with tool errors
204
406
 
407
+
408
+
205
409
  const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
410
+
206
411
  this.storagePath = options.storagePath ?? null;
207
412
 
413
+
414
+
208
415
  this.loadStateFromDisk();
416
+
209
417
  }
210
418
 
419
+
420
+
211
421
  get currentGoal() {
422
+
212
423
  return this.goals.get('default') || null;
424
+
213
425
  }
214
426
 
427
+
428
+
215
429
  set currentGoal(val) {
430
+
216
431
  if (val) {
432
+
217
433
  this.goals.set('default', val);
434
+
218
435
  } else {
436
+
219
437
  this.goals.delete('default');
438
+
220
439
  }
440
+
221
441
  }
222
442
 
443
+
444
+
223
445
  loadStateFromDisk() {
446
+
224
447
  if (!this.storagePath) return;
448
+
225
449
  try {
450
+
226
451
  if (fs.existsSync(this.storagePath)) {
452
+
227
453
  const raw = fs.readFileSync(this.storagePath, 'utf8');
454
+
228
455
  const data = JSON.parse(raw);
456
+
229
457
  if (data && typeof data === 'object') {
458
+
230
459
  let dirty = false;
460
+
231
461
  if (data.sessions && typeof data.sessions === 'object') {
462
+
232
463
  for (const [sid, goal] of Object.entries(data.sessions)) {
464
+
233
465
  if (goal && goal.id && goal.title) {
466
+
234
467
  // Item 4: Crash Hydration — если цель осталась в RUNNING после перезапуска/падения DSH,
468
+
235
469
  // переводим в PAUSED с понятной причиной и фиксацией времени
470
+
236
471
  if (goal.state === GoalState.RUNNING) {
472
+
237
473
  goal.state = GoalState.PAUSED;
474
+
238
475
  goal.pausedAt = Date.now();
476
+
239
477
  if (!Array.isArray(goal.logs)) goal.logs = [];
478
+
240
479
  goal.logs.push({
480
+
241
481
  timestamp: Date.now(),
482
+
242
483
  type: 'warning',
484
+
243
485
  message: 'Harness was restarted — click ▶️ to resume',
486
+
244
487
  });
488
+
245
489
  if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
490
+
246
491
  dirty = true;
492
+
247
493
  }
494
+
248
495
  if (!goal.tokensUsage) {
496
+
249
497
  goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
498
+
250
499
  }
500
+
251
501
  if (!goal.lang) {
502
+
252
503
  goal.lang = detectLanguage(goal.title);
504
+
253
505
  }
506
+
254
507
  this.goals.set(sid, goal);
508
+
255
509
  }
510
+
256
511
  }
512
+
257
513
  } else if (data.id && data.title) {
514
+
258
515
  if (data.state === GoalState.RUNNING) {
516
+
259
517
  data.state = GoalState.PAUSED;
518
+
260
519
  data.pausedAt = Date.now();
520
+
261
521
  if (!Array.isArray(data.logs)) data.logs = [];
522
+
262
523
  data.logs.push({
524
+
263
525
  timestamp: Date.now(),
526
+
264
527
  type: 'warning',
528
+
265
529
  message: 'Harness was restarted — click ▶️ to resume',
530
+
266
531
  });
532
+
267
533
  if (data.logs.length > 100) data.logs = data.logs.slice(-100);
534
+
268
535
  dirty = true;
536
+
269
537
  }
538
+
270
539
  if (!data.tokensUsage) {
540
+
271
541
  data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
542
+
272
543
  }
544
+
273
545
  if (!data.lang) {
546
+
274
547
  data.lang = detectLanguage(data.title);
548
+
275
549
  }
550
+
276
551
  this.goals.set('default', data);
552
+
277
553
  }
554
+
278
555
  if (dirty) {
556
+
279
557
  this.scheduleSave(true);
558
+
280
559
  }
560
+
281
561
  }
562
+
282
563
  }
564
+
283
565
  } catch (err) {
566
+
284
567
  console.warn('[GoalEngine] Failed to load state from disk:', err);
568
+
285
569
  }
570
+
286
571
  }
287
572
 
573
+
574
+
288
575
  scheduleSave(immediate = false) {
576
+
289
577
  if (!this.storagePath) return;
578
+
290
579
  if (immediate) {
580
+
291
581
  if (this.saveTimer) {
582
+
292
583
  clearTimeout(this.saveTimer);
584
+
293
585
  this.saveTimer = null;
586
+
294
587
  }
588
+
295
589
  this.writeStateToDiskSync();
590
+
296
591
  return;
592
+
297
593
  }
594
+
298
595
  if (!this.saveTimer) {
596
+
299
597
  this.saveTimer = setTimeout(() => {
598
+
300
599
  this.saveTimer = null;
600
+
301
601
  this.writeStateToDiskSync();
602
+
302
603
  }, 250);
604
+
303
605
  if (typeof this.saveTimer.unref === 'function') {
606
+
304
607
  this.saveTimer.unref();
608
+
305
609
  }
610
+
306
611
  }
612
+
307
613
  }
308
614
 
615
+
616
+
309
617
  writeStateToDiskSync() {
618
+
310
619
  if (!this.storagePath) return;
620
+
311
621
  try {
622
+
312
623
  if (this.goals.size === 0) {
624
+
313
625
  if (fs.existsSync(this.storagePath)) {
626
+
314
627
  fs.unlinkSync(this.storagePath);
628
+
315
629
  }
630
+
316
631
  return;
632
+
317
633
  }
634
+
318
635
  const sessionsObj = {};
636
+
319
637
  for (const [sid, goal] of this.goals.entries()) {
638
+
320
639
  sessionsObj[sid] = goal;
640
+
321
641
  }
642
+
322
643
  const payload = {
644
+
323
645
  version: 2,
646
+
324
647
  sessions: sessionsObj,
648
+
325
649
  ...(this.goals.has('default') ? this.goals.get('default') : {}),
650
+
326
651
  };
652
+
327
653
  const dir = path.dirname(this.storagePath);
654
+
328
655
  if (!fs.existsSync(dir)) {
656
+
329
657
  fs.mkdirSync(dir, { recursive: true });
658
+
330
659
  }
660
+
331
661
  const tmp = `${this.storagePath}.tmp.${Date.now()}`;
662
+
332
663
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
664
+
333
665
  fs.renameSync(tmp, this.storagePath);
666
+
334
667
  } catch (err) {
668
+
335
669
  console.warn('[GoalEngine] Failed to write state to disk:', err);
670
+
336
671
  }
672
+
337
673
  }
338
674
 
675
+
676
+
339
677
  flushSync() {
678
+
340
679
  if (this.saveTimer) {
680
+
341
681
  clearTimeout(this.saveTimer);
682
+
342
683
  this.saveTimer = null;
684
+
343
685
  }
686
+
344
687
  this.writeStateToDiskSync();
688
+
345
689
  }
346
690
 
691
+
692
+
347
693
  saveStateToDisk() {
694
+
348
695
  this.flushSync();
696
+
349
697
  }
350
698
 
699
+
700
+
351
701
  /**
702
+
352
703
  * Подписка на изменение состояния
704
+
353
705
  * @param {Function} callback
706
+
354
707
  * @returns {Function} unsubscribe
708
+
355
709
  */
710
+
356
711
  subscribe(callback) {
712
+
357
713
  this.listeners.add(callback);
714
+
358
715
  return () => this.listeners.delete(callback);
716
+
359
717
  }
360
718
 
719
+
720
+
361
721
  emit(sessionId = 'default', immediate = false) {
722
+
362
723
  this.scheduleSave(immediate);
724
+
363
725
  const sid = sessionId || 'default';
726
+
364
727
  const snapshot = this.getSnapshot(sid);
728
+
365
729
  for (const listener of this.listeners) {
730
+
366
731
  try {
732
+
367
733
  listener(snapshot, sid);
734
+
368
735
  } catch (err) {
736
+
369
737
  console.error('[GoalEngine] Listener error:', err);
738
+
370
739
  }
740
+
371
741
  }
742
+
372
743
  }
373
744
 
745
+
746
+
374
747
  /**
748
+
375
749
  * Отслеживание прогресса для Smart Progress Guard
750
+
376
751
  */
752
+
377
753
  recordProgress(sessionId = 'default') {
754
+
378
755
  const sid = sessionId || 'default';
756
+
379
757
  this.stallCounters.set(sid, 0);
758
+
380
759
  }
381
760
 
761
+
762
+
382
763
  incrementStallCount(sessionId = 'default') {
764
+
383
765
  const sid = sessionId || 'default';
766
+
384
767
  const current = this.stallCounters.get(sid) || 0;
768
+
385
769
  const next = current + 1;
770
+
386
771
  this.stallCounters.set(sid, next);
772
+
387
773
  return next;
774
+
388
775
  }
389
776
 
777
+
778
+
390
779
  getStallCount(sessionId = 'default') {
780
+
391
781
  const sid = sessionId || 'default';
782
+
392
783
  return this.stallCounters.get(sid) || 0;
784
+
393
785
  }
394
786
 
787
+
788
+
395
789
  /**
790
+
396
791
  * Динамическое обновление настроек на лету
792
+
397
793
  * @param {Object} config
794
+
398
795
  */
796
+
399
797
  updateConfig(config = {}) {
798
+
400
799
  if (typeof config.defaultMaxIterations === 'number' && config.defaultMaxIterations >= 1) {
800
+
401
801
  const prev = this.defaultMaxIterations;
802
+
402
803
  this.defaultMaxIterations = config.defaultMaxIterations;
804
+
403
805
  for (const [_, goal] of this.goals) {
806
+
404
807
  if (goal && goal.maxIterations === prev) {
808
+
405
809
  goal.maxIterations = config.defaultMaxIterations;
810
+
406
811
  }
812
+
407
813
  }
814
+
408
815
  }
816
+
409
817
  if (typeof config.autoDrive === 'boolean') {
818
+
410
819
  this.autoDrive = config.autoDrive;
820
+
411
821
  }
822
+
412
823
  if (typeof config.enableSound === 'boolean') {
824
+
413
825
  this.enableSound = config.enableSound;
826
+
414
827
  }
828
+
415
829
  if (typeof config.showQuickLaunchButton === 'boolean') {
830
+
416
831
  this.showQuickLaunchButton = config.showQuickLaunchButton;
832
+
417
833
  }
834
+
418
835
  if (typeof config.consecutiveToolFailureLimit === 'number') {
836
+
419
837
  this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
838
+
420
839
  }
840
+
421
841
  this.emit();
842
+
422
843
  }
423
844
 
845
+
846
+
424
847
  /**
848
+
425
849
  * Получение цели сессии
850
+
426
851
  */
852
+
427
853
  getGoal(sessionId = 'default') {
854
+
428
855
  return this.goals.get(sessionId || 'default') || null;
856
+
429
857
  }
430
858
 
859
+
860
+
431
861
  /**
862
+
432
863
  * Очистка старых неактивных сессий во избежание утечек памяти
864
+
433
865
  */
434
866
 
867
+
868
+
435
869
  /**
870
+
436
871
  * Управление счетчиком повторяющихся ошибок инструментов
872
+
437
873
  */
874
+
438
875
  incrementToolFailureCount(sessionId = 'default') {
876
+
439
877
  const sid = sessionId || 'default';
878
+
440
879
  const current = this.toolFailureCounters.get(sid) || 0;
880
+
441
881
  const next = current + 1;
882
+
442
883
  this.toolFailureCounters.set(sid, next);
884
+
443
885
  return next;
886
+
444
887
  }
445
888
 
889
+
890
+
446
891
  resetToolFailureCount(sessionId = 'default') {
892
+
447
893
  const sid = sessionId || 'default';
894
+
448
895
  this.toolFailureCounters.set(sid, 0);
896
+
449
897
  }
450
898
 
899
+
900
+
451
901
  getToolFailureCount(sessionId = 'default') {
902
+
452
903
  const sid = sessionId || 'default';
904
+
453
905
  return this.toolFailureCounters.get(sid) || 0;
906
+
454
907
  }
455
908
 
909
+
910
+
456
911
  /**
912
+
457
913
  * Живая корректировка цели / добавление уточнений (Live Steering / Nudge)
914
+
458
915
  * @param {string} text
916
+
459
917
  * @param {string} [sessionId='default']
918
+
460
919
  */
920
+
461
921
  nudge(text, sessionId = 'default') {
922
+
462
923
  const sid = sessionId || 'default';
924
+
463
925
  const goal = this.goals.get(sid);
926
+
464
927
  if (!goal) return this.getSnapshot(sid);
465
928
 
929
+
930
+
466
931
  const clean = String(text || '').trim();
932
+
467
933
  if (!clean) return this.getSnapshot(sid);
468
934
 
935
+
936
+
469
937
  if (!Array.isArray(goal.nudges)) {
938
+
470
939
  goal.nudges = [];
940
+
471
941
  }
942
+
472
943
  const now = Date.now();
944
+
473
945
  goal.nudges.push({ text: clean, timestamp: now });
946
+
474
947
  goal.pendingNudge = clean;
948
+
475
949
  goal.logs.push({
950
+
476
951
  timestamp: now,
952
+
477
953
  type: 'info',
954
+
478
955
  message: `User steering / clarification: "${clean}"`,
956
+
479
957
  });
958
+
480
959
  if (goal.logs.length > 100) {
960
+
481
961
  goal.logs = goal.logs.slice(-100);
962
+
482
963
  }
483
964
 
965
+
966
+
484
967
  this.emit(sid, true);
968
+
485
969
  return this.getSnapshot(sid);
970
+
486
971
  }
487
972
 
973
+
974
+
488
975
  consumePendingNudge(sessionId = 'default') {
976
+
489
977
  const sid = sessionId || 'default';
978
+
490
979
  const goal = this.goals.get(sid);
980
+
491
981
  if (!goal || !goal.pendingNudge) return null;
982
+
492
983
  const nudge = goal.pendingNudge;
984
+
493
985
  goal.pendingNudge = null;
986
+
494
987
  this.emit(sid);
988
+
495
989
  return nudge;
990
+
496
991
  }
497
992
 
993
+
994
+
498
995
  pruneInactiveSessions() {
996
+
499
997
  if (this.goals.size < this.maxSessions) return;
998
+
500
999
  const inactive = [];
1000
+
501
1001
  for (const [sid, goal] of this.goals.entries()) {
1002
+
502
1003
  if (sid === 'default') continue;
1004
+
503
1005
  if (goal.state === GoalState.COMPLETED || goal.state === GoalState.CANCELLED || goal.state === GoalState.FAILED) {
1006
+
504
1007
  inactive.push({ sid, completedAt: goal.completedAt || goal.startedAt || 0 });
1008
+
505
1009
  }
1010
+
506
1011
  }
1012
+
507
1013
  inactive.sort((a, b) => a.completedAt - b.completedAt);
1014
+
508
1015
  while (this.goals.size >= this.maxSessions && inactive.length > 0) {
1016
+
509
1017
  const oldest = inactive.shift();
1018
+
510
1019
  this.goals.delete(oldest.sid);
1020
+
511
1021
  this.stallCounters.delete(oldest.sid);
1022
+
512
1023
  }
1024
+
513
1025
  }
514
1026
 
1027
+
1028
+
515
1029
  /**
1030
+
516
1031
  * Запуск новой цели
1032
+
517
1033
  * @param {string} title
1034
+
518
1035
  * @param {Object} options
1036
+
519
1037
  * @param {string} [sessionId='default']
1038
+
520
1039
  */
1040
+
521
1041
  startGoal(title, options = {}, sessionId = 'default') {
1042
+
522
1043
  if (!title || typeof title !== 'string' || !title.trim()) {
1044
+
523
1045
  throw new Error('Goal title cannot be empty');
1046
+
524
1047
  }
525
1048
 
1049
+
1050
+
526
1051
  this.pruneInactiveSessions();
527
1052
 
1053
+
1054
+
528
1055
  const cleanTitle = title.trim();
1056
+
529
1057
  const now = Date.now();
1058
+
530
1059
  const sid = sessionId || 'default';
1060
+
531
1061
  this.stallCounters.set(sid, 0);
532
1062
 
1063
+
1064
+
533
1065
  const gitCommit = options.gitStartCommit !== undefined
1066
+
534
1067
  ? options.gitStartCommit
1068
+
535
1069
  : getGitCurrentCommit();
536
1070
 
1071
+
1072
+
537
1073
  this.toolFailureCounters.set(sid, 0);
538
1074
 
1075
+
1076
+
539
1077
  const goal = {
1078
+
540
1079
  id: `goal-${now}-${Math.random().toString(36).substring(2, 7)}`,
1080
+
541
1081
  sessionId: sid,
1082
+
542
1083
  title: cleanTitle,
1084
+
543
1085
  description: options.description?.trim() || '',
1086
+
544
1087
  lang: options.lang || detectLanguage(cleanTitle),
1088
+
545
1089
  state: GoalState.RUNNING,
1090
+
546
1091
  startedAt: now,
1092
+
547
1093
  pausedAt: null,
1094
+
548
1095
  totalPausedDurationMs: 0,
1096
+
549
1097
  completedAt: null,
1098
+
550
1099
  iterationsCount: 0,
1100
+
551
1101
  maxIterations: options.maxIterations ?? this.defaultMaxIterations,
1102
+
552
1103
  gitStartCommit: gitCommit || null,
1104
+
553
1105
  pendingNudge: null,
1106
+
554
1107
  nudges: [],
1108
+
555
1109
  milestones: [],
1110
+
556
1111
  tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
1112
+
557
1113
  logs: [
1114
+
558
1115
  {
1116
+
559
1117
  timestamp: now,
1118
+
560
1119
  type: 'info',
1120
+
561
1121
  message: `Goal initiated: "${cleanTitle}"` + (gitCommit ? ` (Git: ${gitCommit})` : ''),
1122
+
562
1123
  },
1124
+
563
1125
  ],
1126
+
564
1127
  resultSummary: '',
1128
+
565
1129
  };
566
1130
 
1131
+
1132
+
567
1133
  this.goals.set(sid, goal);
568
1134
 
1135
+
1136
+
569
1137
  if (Array.isArray(options.milestones) && options.milestones.length > 0) {
1138
+
570
1139
  this.addMilestones(options.milestones, false, sid);
1140
+
571
1141
  }
572
1142
 
1143
+
1144
+
573
1145
  this.emit(sid, true);
1146
+
574
1147
  return this.getSnapshot(sid);
1148
+
575
1149
  }
576
1150
 
1151
+
1152
+
577
1153
  /**
1154
+
578
1155
  * Приостановка автономного цикла цели
1156
+
579
1157
  */
1158
+
580
1159
  pause(reason = 'User requested pause', sessionId = 'default') {
1160
+
581
1161
  const sid = sessionId || 'default';
1162
+
582
1163
  const goal = this.goals.get(sid);
1164
+
583
1165
  if (!goal || goal.state !== GoalState.RUNNING) {
1166
+
584
1167
  return this.getSnapshot(sid);
1168
+
585
1169
  }
586
1170
 
1171
+
1172
+
587
1173
  goal.state = GoalState.PAUSED;
1174
+
588
1175
  goal.pausedAt = Date.now();
1176
+
589
1177
  goal.logs.push({
1178
+
590
1179
  timestamp: Date.now(),
1180
+
591
1181
  type: 'warning',
1182
+
592
1183
  message: `Paused: ${reason}`,
1184
+
593
1185
  });
594
1186
 
1187
+
1188
+
595
1189
  if (goal.logs.length > 100) {
1190
+
596
1191
  goal.logs = goal.logs.slice(-100);
1192
+
597
1193
  }
598
1194
 
1195
+
1196
+
599
1197
  this.emit(sid, true);
1198
+
600
1199
  return this.getSnapshot(sid);
1200
+
601
1201
  }
602
1202
 
1203
+
1204
+
603
1205
  /**
1206
+
604
1207
  * Возобновление выполнения цели
1208
+
605
1209
  */
1210
+
606
1211
  resume(sessionId = 'default') {
1212
+
607
1213
  const sid = sessionId || 'default';
1214
+
608
1215
  const goal = this.goals.get(sid);
1216
+
609
1217
  if (!goal || goal.state !== GoalState.PAUSED) {
1218
+
610
1219
  return this.getSnapshot(sid);
1220
+
611
1221
  }
612
1222
 
1223
+
1224
+
613
1225
  const now = Date.now();
1226
+
614
1227
  if (goal.pausedAt) {
1228
+
615
1229
  goal.totalPausedDurationMs += now - goal.pausedAt;
1230
+
616
1231
  goal.pausedAt = null;
1232
+
617
1233
  }
618
1234
 
1235
+
1236
+
619
1237
  goal.state = GoalState.RUNNING;
1238
+
620
1239
  this.stallCounters.set(sid, 0); // сбрасываем счетчик простоя при возобновлении
621
1240
 
1241
+
1242
+
622
1243
  goal.logs.push({
1244
+
623
1245
  timestamp: now,
1246
+
624
1247
  type: 'info',
1248
+
625
1249
  message: 'Goal resumed',
1250
+
626
1251
  });
627
1252
 
1253
+
1254
+
628
1255
  if (goal.logs.length > 100) {
1256
+
629
1257
  goal.logs = goal.logs.slice(-100);
1258
+
630
1259
  }
631
1260
 
1261
+
1262
+
632
1263
  this.emit(sid, true);
1264
+
633
1265
  return this.getSnapshot(sid);
1266
+
634
1267
  }
635
1268
 
1269
+
1270
+
636
1271
  /**
1272
+
637
1273
  * Отмена цели
1274
+
638
1275
  */
1276
+
639
1277
  cancel(reason = 'Cancelled by user', sessionId = 'default') {
1278
+
640
1279
  const sid = sessionId || 'default';
1280
+
641
1281
  const goal = this.goals.get(sid);
1282
+
642
1283
  if (!goal) return null;
643
1284
 
1285
+
1286
+
644
1287
  goal.state = GoalState.CANCELLED;
1288
+
645
1289
  goal.completedAt = Date.now();
1290
+
646
1291
  goal.logs.push({
1292
+
647
1293
  timestamp: Date.now(),
1294
+
648
1295
  type: 'warning',
1296
+
649
1297
  message: `Cancelled: ${reason}`,
1298
+
650
1299
  });
651
1300
 
1301
+
1302
+
652
1303
  if (goal.logs.length > 100) {
1304
+
653
1305
  goal.logs = goal.logs.slice(-100);
1306
+
654
1307
  }
655
1308
 
1309
+
1310
+
656
1311
  this.emit(sid, true);
1312
+
657
1313
  return this.getSnapshot(sid);
1314
+
658
1315
  }
659
1316
 
1317
+
1318
+
660
1319
  /**
1320
+
661
1321
  * Очистка / сброс цели в IDLE
1322
+
662
1323
  */
1324
+
663
1325
  clear(sessionId = 'default') {
1326
+
664
1327
  const sid = sessionId || 'default';
1328
+
665
1329
  this.goals.delete(sid);
1330
+
666
1331
  this.stallCounters.delete(sid);
1332
+
667
1333
  this.emit(sid, true);
1334
+
668
1335
  return this.getSnapshot(sid);
1336
+
669
1337
  }
670
1338
 
1339
+
1340
+
671
1341
  /**
1342
+
672
1343
  * Успешное завершение цели
1344
+
673
1345
  */
1346
+
674
1347
  completeGoal(summary = '', sessionId = 'default') {
1348
+
675
1349
  const sid = sessionId || 'default';
1350
+
676
1351
  const goal = this.goals.get(sid);
1352
+
677
1353
  if (!goal) return null;
678
1354
 
1355
+
1356
+
679
1357
  const now = Date.now();
1358
+
680
1359
  goal.state = GoalState.COMPLETED;
1360
+
681
1361
  goal.completedAt = now;
1362
+
682
1363
  goal.resultSummary = summary;
1364
+
683
1365
  goal.logs.push({
1366
+
684
1367
  timestamp: now,
1368
+
685
1369
  type: 'info',
1370
+
686
1371
  message: `Goal completed successfully: ${summary || 'All objectives met.'}`,
1372
+
687
1373
  });
688
1374
 
1375
+
1376
+
689
1377
  if (goal.logs.length > 100) {
1378
+
690
1379
  goal.logs = goal.logs.slice(-100);
1380
+
691
1381
  }
692
1382
 
1383
+
1384
+
693
1385
  // Завершаем все активные milestones
1386
+
694
1387
  for (const m of goal.milestones) {
1388
+
695
1389
  if (m.status === MilestoneStatus.IN_PROGRESS || m.status === MilestoneStatus.PENDING) {
1390
+
696
1391
  m.status = MilestoneStatus.COMPLETED;
1392
+
697
1393
  }
1394
+
698
1395
  }
699
1396
 
1397
+
1398
+
700
1399
  this.stallCounters.delete(sid);
1400
+
701
1401
  this.emit(sid, true);
1402
+
702
1403
  return this.getSnapshot(sid);
1404
+
703
1405
  }
704
1406
 
1407
+
1408
+
705
1409
  /**
1410
+
706
1411
  * Добавление вех (milestones)
1412
+
707
1413
  */
1414
+
708
1415
  addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
1416
+
709
1417
  const sid = sessionId || 'default';
1418
+
710
1419
  const goal = this.goals.get(sid);
1420
+
711
1421
  if (!goal || !Array.isArray(milestonesList)) return;
712
1422
 
1423
+
1424
+
713
1425
  for (const item of milestonesList) {
1426
+
714
1427
  const itemTitle = typeof item === 'string' ? item : item.title;
1428
+
715
1429
  if (!itemTitle || !itemTitle.trim()) continue;
716
1430
 
1431
+
1432
+
717
1433
  const mId = (typeof item === 'object' && item.id) ? item.id : `m-${goal.milestones.length + 1}`;
1434
+
718
1435
  goal.milestones.push({
1436
+
719
1437
  id: String(mId),
1438
+
720
1439
  title: itemTitle.trim(),
1440
+
721
1441
  status: (typeof item === 'object' && item.status) ? item.status : MilestoneStatus.PENDING,
1442
+
722
1443
  notes: (typeof item === 'object' && item.notes) ? item.notes : '',
1444
+
723
1445
  });
1446
+
724
1447
  }
725
1448
 
1449
+
1450
+
726
1451
  this.recordProgress(sid);
1452
+
727
1453
  if (shouldEmit) this.emit(sid);
1454
+
728
1455
  }
729
1456
 
1457
+
1458
+
730
1459
  /**
1460
+
731
1461
  * Обновление конкретной вехи
1462
+
732
1463
  */
1464
+
733
1465
  updateMilestone(id, status, notes = '', sessionId = 'default') {
1466
+
734
1467
  const sid = sessionId || 'default';
1468
+
735
1469
  const goal = this.goals.get(sid);
1470
+
736
1471
  if (!goal) return false;
737
1472
 
1473
+
1474
+
738
1475
  const target = goal.milestones.find((m) => m.id === String(id));
1476
+
739
1477
  if (!target) return false;
740
1478
 
1479
+
1480
+
741
1481
  if (status && Object.values(MilestoneStatus).includes(status)) {
1482
+
742
1483
  target.status = status;
1484
+
743
1485
  }
1486
+
744
1487
  if (notes) {
1488
+
745
1489
  target.notes = String(notes);
1490
+
746
1491
  }
747
1492
 
1493
+
1494
+
748
1495
  goal.logs.push({
1496
+
749
1497
  timestamp: Date.now(),
1498
+
750
1499
  type: 'milestone',
1500
+
751
1501
  message: `Milestone [${target.title}] status -> ${target.status}`,
1502
+
752
1503
  });
753
1504
 
1505
+
1506
+
754
1507
  if (goal.logs.length > 100) {
1508
+
755
1509
  goal.logs = goal.logs.slice(-100);
1510
+
756
1511
  }
757
1512
 
1513
+
1514
+
758
1515
  this.recordProgress(sid);
1516
+
759
1517
  this.emit(sid);
1518
+
760
1519
  return true;
1520
+
761
1521
  }
762
1522
 
1523
+
1524
+
763
1525
  /**
1526
+
764
1527
  * Увеличение счётчика итераций turn
1528
+
765
1529
  */
1530
+
766
1531
  incrementIteration(sessionId = 'default') {
1532
+
767
1533
  const sid = sessionId || 'default';
1534
+
768
1535
  const goal = this.goals.get(sid);
1536
+
769
1537
  if (!goal || goal.state !== GoalState.RUNNING) {
1538
+
770
1539
  return false;
1540
+
771
1541
  }
772
1542
 
1543
+
1544
+
773
1545
  goal.iterationsCount += 1;
774
1546
 
1547
+
1548
+
775
1549
  if (goal.iterationsCount >= goal.maxIterations) {
1550
+
776
1551
  goal.state = GoalState.FAILED;
1552
+
777
1553
  goal.logs.push({
1554
+
778
1555
  timestamp: Date.now(),
1556
+
779
1557
  type: 'error',
1558
+
780
1559
  message: `Safety limit reached: maximum ${goal.maxIterations} iterations exceeded.`,
1560
+
781
1561
  });
1562
+
782
1563
  if (goal.logs.length > 100) {
1564
+
783
1565
  goal.logs = goal.logs.slice(-100);
1566
+
784
1567
  }
1568
+
785
1569
  this.emit(sid, true);
1570
+
786
1571
  return false;
1572
+
787
1573
  }
788
1574
 
1575
+
1576
+
789
1577
  this.emit(sid);
1578
+
790
1579
  return true;
1580
+
791
1581
  }
792
1582
 
1583
+
1584
+
793
1585
  /**
1586
+
794
1587
  * Подсчёт времени в секундах
1588
+
795
1589
  */
1590
+
796
1591
  getElapsedSeconds(sessionId = 'default') {
1592
+
797
1593
  const sid = sessionId || 'default';
1594
+
798
1595
  const goal = this.goals.get(sid);
1596
+
799
1597
  if (!goal) return 0;
1598
+
800
1599
  const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = goal;
1600
+
801
1601
  const endTime = completedAt || (pausedAt || Date.now());
1602
+
802
1603
  const elapsedMs = Math.max(0, endTime - startedAt - totalPausedDurationMs);
1604
+
803
1605
  return Math.floor(elapsedMs / 1000);
1606
+
804
1607
  }
805
1608
 
1609
+
1610
+
806
1611
  /**
1612
+
807
1613
  * Накопление статистики использования токенов сессии
1614
+
808
1615
  * @param {Object} usage
1616
+
809
1617
  * @param {string} [sessionId='default']
1618
+
810
1619
  */
1620
+
811
1621
  addTokenUsage(usage, sessionId = 'default') {
1622
+
812
1623
  if (!usage || typeof usage !== 'object') return;
1624
+
813
1625
  const sid = sessionId || 'default';
1626
+
814
1627
  const goal = this.goals.get(sid);
1628
+
815
1629
  if (!goal) return;
816
1630
 
1631
+
1632
+
817
1633
  if (!goal.tokensUsage) {
1634
+
818
1635
  goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1636
+
819
1637
  }
820
1638
 
1639
+
1640
+
821
1641
  const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
1642
+
822
1643
  const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
1644
+
823
1645
  const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
824
1646
 
1647
+
1648
+
825
1649
  goal.tokensUsage.promptTokens += prompt;
1650
+
826
1651
  goal.tokensUsage.completionTokens += completion;
1652
+
827
1653
  goal.tokensUsage.totalTokens += total;
828
1654
 
1655
+
1656
+
829
1657
  this.emit(sid);
1658
+
830
1659
  }
831
1660
 
1661
+
1662
+
832
1663
  /**
1664
+
833
1665
  * Интеллектуальный расчет прогноза оставшегося времени (ETA)
1666
+
834
1667
  * на основе средней скорости выполнения завершенных вех
1668
+
835
1669
  * @param {string} [sessionId='default']
1670
+
836
1671
  * @returns {number|null}
1672
+
837
1673
  */
1674
+
838
1675
  getEstimatedRemainingSeconds(sessionId = 'default') {
1676
+
839
1677
  const sid = sessionId || 'default';
1678
+
840
1679
  const goal = this.goals.get(sid);
1680
+
841
1681
  if (!goal || goal.state !== GoalState.RUNNING) return null;
842
1682
 
1683
+
1684
+
843
1685
  const total = goal.milestones.length;
1686
+
844
1687
  if (total === 0) return null;
845
1688
 
1689
+
1690
+
846
1691
  const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
1692
+
847
1693
  if (completedCount === 0 || completedCount >= total) return null;
848
1694
 
1695
+
1696
+
849
1697
  const elapsed = this.getElapsedSeconds(sid);
1698
+
850
1699
  if (elapsed <= 0) return null;
851
1700
 
1701
+
1702
+
852
1703
  const avgSecPerMilestone = elapsed / completedCount;
1704
+
853
1705
  const remainingCount = total - completedCount;
1706
+
854
1707
  return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
1708
+
855
1709
  }
856
1710
 
1711
+
1712
+
857
1713
  /**
1714
+
858
1715
  * Снимок состояния для передачи клиенту / API
1716
+
859
1717
  */
1718
+
860
1719
  getSnapshot(sessionId = 'default') {
1720
+
861
1721
  const sid = sessionId || 'default';
1722
+
862
1723
  const goal = this.goals.get(sid);
1724
+
863
1725
  if (!goal) {
1726
+
864
1727
  return {
1728
+
865
1729
  sessionId: sid,
1730
+
866
1731
  hasActiveGoal: false,
1732
+
867
1733
  state: GoalState.IDLE,
1734
+
868
1735
  title: '',
1736
+
869
1737
  startedAt: null,
1738
+
870
1739
  pausedAt: null,
1740
+
871
1741
  totalPausedDurationMs: 0,
1742
+
872
1743
  completedAt: null,
1744
+
873
1745
  elapsedSeconds: 0,
1746
+
874
1747
  formattedElapsed: '0s',
1748
+
875
1749
  estimatedRemainingSeconds: null,
1750
+
876
1751
  formattedETA: null,
1752
+
877
1753
  lang: 'en',
1754
+
878
1755
  tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
1756
+
879
1757
  milestones: [],
1758
+
880
1759
  progressPercent: 0,
1760
+
881
1761
  iterationsCount: 0,
1762
+
882
1763
  maxIterations: this.defaultMaxIterations,
1764
+
883
1765
  autoDrive: this.autoDrive,
1766
+
884
1767
  enableSound: this.enableSound,
1768
+
885
1769
  showQuickLaunchButton: this.showQuickLaunchButton,
1770
+
886
1771
  gitStartCommit: null,
1772
+
887
1773
  pendingNudge: null,
1774
+
888
1775
  toolFailureCount: 0,
1776
+
889
1777
  consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
1778
+
890
1779
  };
1780
+
891
1781
  }
892
1782
 
1783
+
1784
+
893
1785
  const elapsed = this.getElapsedSeconds(sid);
1786
+
894
1787
  const milestones = goal.milestones;
1788
+
895
1789
  const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
1790
+
896
1791
  const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
1792
+
897
1793
  const estSec = this.getEstimatedRemainingSeconds(sid);
898
1794
 
1795
+
1796
+
899
1797
  return {
1798
+
900
1799
  sessionId: sid,
1800
+
901
1801
  hasActiveGoal: true,
1802
+
902
1803
  id: goal.id,
1804
+
903
1805
  state: goal.state,
1806
+
904
1807
  title: goal.title,
1808
+
905
1809
  description: goal.description,
1810
+
906
1811
  lang: goal.lang || detectLanguage(goal.title),
1812
+
907
1813
  startedAt: goal.startedAt,
1814
+
908
1815
  pausedAt: goal.pausedAt,
1816
+
909
1817
  totalPausedDurationMs: goal.totalPausedDurationMs,
1818
+
910
1819
  completedAt: goal.completedAt,
1820
+
911
1821
  elapsedSeconds: elapsed,
1822
+
912
1823
  formattedElapsed: formatElapsed(elapsed),
1824
+
913
1825
  estimatedRemainingSeconds: estSec,
1826
+
914
1827
  formattedETA: formatETA(estSec),
1828
+
915
1829
  tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
1830
+
916
1831
  iterationsCount: goal.iterationsCount,
1832
+
917
1833
  maxIterations: goal.maxIterations,
1834
+
918
1835
  milestones,
1836
+
919
1837
  progressPercent,
1838
+
920
1839
  logs: goal.logs,
1840
+
921
1841
  resultSummary: goal.resultSummary,
1842
+
922
1843
  autoDrive: this.autoDrive,
1844
+
923
1845
  enableSound: this.enableSound,
1846
+
924
1847
  showQuickLaunchButton: this.showQuickLaunchButton,
1848
+
925
1849
  gitStartCommit: goal.gitStartCommit || null,
1850
+
926
1851
  pendingNudge: goal.pendingNudge || null,
1852
+
927
1853
  toolFailureCount: this.getToolFailureCount(sid),
1854
+
928
1855
  consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
1856
+
929
1857
  };
1858
+
930
1859
  }
931
1860
 
1861
+
1862
+
932
1863
  /**
1864
+
933
1865
  * Формирование системного контекста для инжекта модели
1866
+
934
1867
  */
1868
+
935
1869
  getStatePromptInjection(sessionId = 'default') {
1870
+
936
1871
  const sid = sessionId || 'default';
1872
+
937
1873
  const goal = this.goals.get(sid);
1874
+
938
1875
  if (!goal || goal.state !== GoalState.RUNNING) {
1876
+
939
1877
  return '';
1878
+
940
1879
  }
941
1880
 
1881
+
1882
+
942
1883
  const snapshot = this.getSnapshot(sid);
1884
+
943
1885
  const lang = goal.lang || detectLanguage(snapshot.title);
1886
+
944
1887
  const hasMilestones = snapshot.milestones.length > 0;
1888
+
945
1889
  const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
946
1890
 
1891
+
1892
+
947
1893
  let nudgeText = '';
1894
+
948
1895
  if (goal.pendingNudge) {
1896
+
949
1897
  const userFeedback = goal.pendingNudge;
1898
+
950
1899
  goal.pendingNudge = null;
1900
+
951
1901
  if (lang === 'zh') {
1902
+
952
1903
  nudgeText = `\n\n🚨 用户紧急补充说明 / 调整方向:\n"${userFeedback}"\n你必须根据此说明调整近期的具体执行步骤!\n`;
1904
+
953
1905
  } else if (lang === 'ru') {
1906
+
954
1907
  nudgeText = `\n\n🚨 СРОЧНОЕ УТОЧНЕНИЕ / НАПРАВЛЕНИЕ ОТ ПОЛЬЗОВАТЕЛЯ:\n"${userFeedback}"\nОбязательно скорректируй свои ближайшие действия с учётом этого замечания!\n`;
1908
+
955
1909
  } else {
1910
+
956
1911
  nudgeText = `\n\n🚨 URGENT USER CLARIFICATION / STEERING:\n"${userFeedback}"\nYou must adjust your immediate actions according to this guidance!\n`;
1912
+
957
1913
  }
1914
+
958
1915
  }
959
1916
 
1917
+
1918
+
960
1919
  if (lang === 'zh') {
1920
+
961
1921
  const milestonesText = hasMilestones
1922
+
962
1923
  ? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
1924
+
963
1925
  : ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
964
1926
 
1927
+
1928
+
965
1929
  return nudgeText + `\n\n[DSH GOAL MODE ACTIVE]
1930
+
966
1931
  目标: "${snapshot.title}"
1932
+
967
1933
  运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}
1934
+
968
1935
  工作计划:
1936
+
969
1937
  ${milestonesText}
970
1938
 
1939
+
1940
+
971
1941
  Goal Mode 执行契约 (必须严格遵循):
1942
+
972
1943
  1. ${hasMilestones ? '按部就班推进当前进行中的里程碑。' : '第一步核心指令: 立即调用 goal_set_milestones 制定 3-7 个具体里程碑。在完成此工具调用前严禁执行其他操作!'}
1944
+
973
1945
  2. 推进里程碑时,必须通过 goal_update_progress 工具更新状态(开始前标为 in_progress,完成后标为 completed 并附简要说明)。
1946
+
974
1947
  3. 当所有里程碑全部完成后,调用 goal_finish 工具提交详细成果总结。`;
1948
+
975
1949
  }
976
1950
 
1951
+
1952
+
977
1953
  if (lang === 'ru') {
1954
+
978
1955
  const milestonesText = hasMilestones
1956
+
979
1957
  ? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
1958
+
980
1959
  : ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
981
1960
 
1961
+
1962
+
982
1963
  return nudgeText + `\n\n[DSH GOAL MODE ACTIVE]
1964
+
983
1965
  Цель: "${snapshot.title}"
1966
+
984
1967
  Время работы: ${snapshot.formattedElapsed}${etaText} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
1968
+
985
1969
  План работ:
1970
+
986
1971
  ${milestonesText}
987
1972
 
1973
+
1974
+
988
1975
  Инструкции Goal Mode (СТРОГО ОБЯЗАТЕЛЬНЫ К ВЫПОЛНЕНИЮ):
1976
+
989
1977
  1. ${hasMilestones ? 'Выполняй текущий активный пункт плана работ.' : 'ТВОЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones со списком пунктов плана работ (3-7 конкретных шагов). Запрещено выполнять работу или завершать turn без вызова goal_set_milestones!'}
1978
+
990
1979
  2. По мере выполнения каждого шага обязательно отмечай его статус через инструмент goal_update_progress (status: "in_progress" перед началом шага, status: "completed" по его завершении с кратким notes).
1980
+
991
1981
  3. Когда все пункты плана будут выполнены, вызови инструмент goal_finish с подробным итоговым резюме достигнутых результатов.`;
1982
+
992
1983
  }
993
1984
 
1985
+
1986
+
994
1987
  const milestonesText = hasMilestones
1988
+
995
1989
  ? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
1990
+
996
1991
  : ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
997
1992
 
1993
+
1994
+
998
1995
  return nudgeText + `\n\n[DSH GOAL MODE ACTIVE]
1996
+
999
1997
  Goal: "${snapshot.title}"
1998
+
1000
1999
  Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}
2000
+
1001
2001
  Work Plan:
2002
+
1002
2003
  ${milestonesText}
1003
2004
 
2005
+
2006
+
1004
2007
  Goal Mode Instructions (MANDATORY TO FOLLOW):
2008
+
1005
2009
  1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}
2010
+
1006
2011
  2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes).
2012
+
1007
2013
  3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`;
2014
+
1008
2015
  }
2016
+
1009
2017
  }
2018
+