@goodandready/dsh-goal 0.2.2 → 0.2.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 +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/client.js +4418 -4498
- package/lib/command-handler.js +242 -276
- package/lib/engine-prompt.js +57 -0
- package/lib/engine-reports.js +114 -0
- package/lib/engine-store.js +142 -0
- package/lib/goal-engine-constants.js +100 -0
- package/lib/goal-engine.js +579 -2018
- package/lib/index.js +452 -1960
- package/lib/routes.js +294 -0
- package/lib/tools.js +294 -0
- package/lib/updater.js +317 -0
- package/package.json +62 -63
package/lib/index.js
CHANGED
|
@@ -1,1960 +1,452 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
454
|
-
|
|
455
|
-
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
456
|
-
|
|
457
|
-
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
458
|
-
|
|
459
|
-
consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
|
|
460
|
-
|
|
461
|
-
enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
|
|
462
|
-
|
|
463
|
-
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
464
|
-
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
} else if (typeof settingsScope.get === 'function') {
|
|
472
|
-
|
|
473
|
-
const live = settingsScope.get();
|
|
474
|
-
|
|
475
|
-
if (live && typeof live === 'object') {
|
|
476
|
-
|
|
477
|
-
return {
|
|
478
|
-
|
|
479
|
-
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
480
|
-
|
|
481
|
-
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
482
|
-
|
|
483
|
-
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
484
|
-
|
|
485
|
-
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
486
|
-
|
|
487
|
-
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
488
|
-
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return currentSettings;
|
|
498
|
-
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const applySettings = () => {
|
|
504
|
-
|
|
505
|
-
const live = getConfig();
|
|
506
|
-
|
|
507
|
-
engine.updateConfig({
|
|
508
|
-
|
|
509
|
-
defaultMaxIterations: live.maxIterations,
|
|
510
|
-
|
|
511
|
-
autoDrive: live.autoDrive,
|
|
512
|
-
|
|
513
|
-
enableSound: live.enableSound,
|
|
514
|
-
|
|
515
|
-
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
516
|
-
|
|
517
|
-
consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
|
|
518
|
-
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// 1. Регистрация настроек плагина
|
|
526
|
-
|
|
527
|
-
ctx.inject(['settings'], (sctx) => {
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
|
|
531
|
-
const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
|
|
532
|
-
|
|
533
|
-
if (scope) {
|
|
534
|
-
|
|
535
|
-
settingsScope = scope;
|
|
536
|
-
|
|
537
|
-
applySettings();
|
|
538
|
-
|
|
539
|
-
if (typeof scope.subscribe === 'function') {
|
|
540
|
-
|
|
541
|
-
sctx.effect(() => {
|
|
542
|
-
|
|
543
|
-
const off = scope.subscribe(() => {
|
|
544
|
-
|
|
545
|
-
applySettings();
|
|
546
|
-
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
return () => {
|
|
550
|
-
|
|
551
|
-
if (typeof off === 'function') off();
|
|
552
|
-
|
|
553
|
-
settingsScope = null;
|
|
554
|
-
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
}, 'dsh-goal: settings subscription');
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
} catch (err) {
|
|
564
|
-
|
|
565
|
-
console.warn('[dsh-goal] Settings register skipped:', err.message);
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// 2. Регистрация слэш-команды /goal в чате
|
|
574
|
-
|
|
575
|
-
ctx.inject(['commands'], (cctx) => {
|
|
576
|
-
|
|
577
|
-
try {
|
|
578
|
-
|
|
579
|
-
if (typeof cctx.commands?.register !== 'function') return;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const unregister = cctx.commands.register({
|
|
584
|
-
|
|
585
|
-
name: 'goal',
|
|
586
|
-
|
|
587
|
-
description: 'Manage Goal Mode: activate, milestones, pause, resume, and clear',
|
|
588
|
-
|
|
589
|
-
input: { hint: '[<objective>|clear|pause|resume]' },
|
|
590
|
-
|
|
591
|
-
handler: async (invocation) => {
|
|
592
|
-
|
|
593
|
-
const sid = sessionIdOf(invocation, 'default');
|
|
594
|
-
|
|
595
|
-
if (invocation?.agent) {
|
|
596
|
-
|
|
597
|
-
lastActiveAgent = invocation.agent;
|
|
598
|
-
|
|
599
|
-
sessionAgents.set(sid, invocation.agent);
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const parsed = parseGoalInput(invocation?.rawInput);
|
|
604
|
-
|
|
605
|
-
const liveConfig = getConfig();
|
|
606
|
-
|
|
607
|
-
return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
|
|
608
|
-
|
|
609
|
-
},
|
|
610
|
-
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (typeof unregister === 'function') {
|
|
616
|
-
|
|
617
|
-
cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
} catch (err) {
|
|
622
|
-
|
|
623
|
-
console.warn('[dsh-goal] Commands register skipped:', err.message);
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// 3. Инжекция контекста цели в системный промпт (systemPrompt)
|
|
632
|
-
|
|
633
|
-
ctx.inject(['systemPrompt'], (pctx) => {
|
|
634
|
-
|
|
635
|
-
try {
|
|
636
|
-
|
|
637
|
-
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
638
|
-
|
|
639
|
-
// Clear conflicting core tool:goal prompt section if present
|
|
640
|
-
|
|
641
|
-
const globalSections = pctx.systemPrompt.layers?.global?.sections;
|
|
642
|
-
|
|
643
|
-
if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
|
|
644
|
-
|
|
645
|
-
globalSections.data.delete('tool:goal');
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
|
|
652
|
-
|
|
653
|
-
? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 2400)
|
|
654
|
-
|
|
655
|
-
: 2400;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const unregisterSection = pctx.systemPrompt.section({
|
|
660
|
-
|
|
661
|
-
name: 'tool:dsh-goal',
|
|
662
|
-
|
|
663
|
-
order,
|
|
664
|
-
|
|
665
|
-
text: (sessionCtx) => {
|
|
666
|
-
|
|
667
|
-
const sid = sessionIdOf(sessionCtx, 'default');
|
|
668
|
-
|
|
669
|
-
return engine.getStatePromptInjection(sid);
|
|
670
|
-
|
|
671
|
-
},
|
|
672
|
-
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (typeof unregisterSection === 'function') {
|
|
678
|
-
|
|
679
|
-
pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
} catch (err) {
|
|
686
|
-
|
|
687
|
-
console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
// 4. Регистрация инструментов для модели (tools)
|
|
696
|
-
|
|
697
|
-
ctx.inject(['tools'], (tctx) => {
|
|
698
|
-
|
|
699
|
-
if (!tctx.tools?.register) return;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
// Helper: Safely replace or register tool in ToolRuntime
|
|
704
|
-
|
|
705
|
-
const safeRegister = (definition) => {
|
|
706
|
-
|
|
707
|
-
try {
|
|
708
|
-
|
|
709
|
-
const name = definition.name;
|
|
710
|
-
|
|
711
|
-
const globalTools = tctx.tools.layers?.global?.tools;
|
|
712
|
-
|
|
713
|
-
if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
|
|
714
|
-
|
|
715
|
-
globalTools.data.delete(name);
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const unregister = tctx.tools.register(definition);
|
|
720
|
-
|
|
721
|
-
if (typeof unregister === 'function') {
|
|
722
|
-
|
|
723
|
-
tctx.effect(() => () => unregister(), `dsh-goal: tool ${name}`);
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
} catch (err) {
|
|
728
|
-
|
|
729
|
-
console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const JSON_OUTPUT = {
|
|
738
|
-
|
|
739
|
-
schema: { type: 'object', additionalProperties: true },
|
|
740
|
-
|
|
741
|
-
render: (_args, val) => [{ type: 'text', text: JSON.stringify(val) }],
|
|
742
|
-
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
function formatGoalValue(snap) {
|
|
748
|
-
|
|
749
|
-
if (!snap || !snap.hasActiveGoal) {
|
|
750
|
-
|
|
751
|
-
return { goal: null };
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
let phase = 'active';
|
|
756
|
-
|
|
757
|
-
if (snap.state === GoalState.PAUSED) phase = 'paused';
|
|
758
|
-
|
|
759
|
-
else if (snap.state === GoalState.COMPLETED) phase = 'complete';
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const roundsStarted = Number.isInteger(snap.iterations) ? snap.iterations : 0;
|
|
764
|
-
|
|
765
|
-
const maxGoalRounds = Number.isInteger(snap.maxIterations) ? snap.maxIterations : 25;
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return {
|
|
770
|
-
|
|
771
|
-
goal: {
|
|
772
|
-
|
|
773
|
-
id: snap.id || 'goal-active',
|
|
774
|
-
|
|
775
|
-
revision: 1,
|
|
776
|
-
|
|
777
|
-
objective: snap.title || 'Goal',
|
|
778
|
-
|
|
779
|
-
phase,
|
|
780
|
-
|
|
781
|
-
roundsStarted,
|
|
782
|
-
|
|
783
|
-
maxGoalRounds,
|
|
784
|
-
|
|
785
|
-
},
|
|
786
|
-
|
|
787
|
-
activation: snap.state === GoalState.RUNNING ? 'armed' : 'disarmed',
|
|
788
|
-
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
// Совместимый инструмент 1: get_goal
|
|
796
|
-
|
|
797
|
-
const handleGetGoal = async (_args, toolCtx) => {
|
|
798
|
-
|
|
799
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
800
|
-
|
|
801
|
-
const snap = engine.getSnapshot(sid);
|
|
802
|
-
|
|
803
|
-
return formatGoalValue(snap);
|
|
804
|
-
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
safeRegister({
|
|
810
|
-
|
|
811
|
-
name: 'get_goal',
|
|
812
|
-
|
|
813
|
-
description: 'Read the current same-session goal, including objective, phase, and round limits.',
|
|
814
|
-
|
|
815
|
-
parameters: { type: 'object', properties: {} },
|
|
816
|
-
|
|
817
|
-
output: JSON_OUTPUT,
|
|
818
|
-
|
|
819
|
-
execute: handleGetGoal,
|
|
820
|
-
|
|
821
|
-
handler: handleGetGoal,
|
|
822
|
-
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
// Совместимый инструмент 2: create_goal
|
|
828
|
-
|
|
829
|
-
const handleCreateGoal = async (args, toolCtx) => {
|
|
830
|
-
|
|
831
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
832
|
-
|
|
833
|
-
const maxIterations = Number(args.max_goal_rounds) || 25;
|
|
834
|
-
|
|
835
|
-
const snap = engine.startGoal(args.objective, { maxIterations }, sid);
|
|
836
|
-
|
|
837
|
-
return formatGoalValue(snap);
|
|
838
|
-
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
safeRegister({
|
|
844
|
-
|
|
845
|
-
name: 'create_goal',
|
|
846
|
-
|
|
847
|
-
description: 'Create one persisted same-session completion goal for long-running autonomous work.',
|
|
848
|
-
|
|
849
|
-
parameters: {
|
|
850
|
-
|
|
851
|
-
type: 'object',
|
|
852
|
-
|
|
853
|
-
properties: {
|
|
854
|
-
|
|
855
|
-
objective: {
|
|
856
|
-
|
|
857
|
-
type: 'string',
|
|
858
|
-
|
|
859
|
-
description: 'The concrete completion objective.',
|
|
860
|
-
|
|
861
|
-
},
|
|
862
|
-
|
|
863
|
-
max_goal_rounds: {
|
|
864
|
-
|
|
865
|
-
type: 'number',
|
|
866
|
-
|
|
867
|
-
description: 'Optional positive integer limit on automatic continuation rounds.',
|
|
868
|
-
|
|
869
|
-
},
|
|
870
|
-
|
|
871
|
-
},
|
|
872
|
-
|
|
873
|
-
required: ['objective'],
|
|
874
|
-
|
|
875
|
-
},
|
|
876
|
-
|
|
877
|
-
output: JSON_OUTPUT,
|
|
878
|
-
|
|
879
|
-
execute: handleCreateGoal,
|
|
880
|
-
|
|
881
|
-
handler: handleCreateGoal,
|
|
882
|
-
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// Совместимый инструмент 3: update_goal (перехватчик authority checks и роутер в GoalEngine)
|
|
888
|
-
|
|
889
|
-
const handleUpdateGoal = async (args, toolCtx) => {
|
|
890
|
-
|
|
891
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
892
|
-
|
|
893
|
-
const action = args.action;
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (action === 'complete') {
|
|
898
|
-
|
|
899
|
-
const summary = args.blocked_reason || args.objective || 'Goal marked complete';
|
|
900
|
-
|
|
901
|
-
const snap = engine.completeGoal(summary, sid);
|
|
902
|
-
|
|
903
|
-
if (toolCtx?.deferContext) {
|
|
904
|
-
|
|
905
|
-
try {
|
|
906
|
-
|
|
907
|
-
toolCtx.deferContext({
|
|
908
|
-
|
|
909
|
-
type: 'text',
|
|
910
|
-
|
|
911
|
-
text: `<goal_complete>\nObjective: ${JSON.stringify(snap.title || summary)}\nThe goal is marked complete. Summarize what was accomplished for the user.\n</goal_complete>`,
|
|
912
|
-
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
} catch (_) {}
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
return formatGoalValue(snap);
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if (action === 'pause') {
|
|
926
|
-
|
|
927
|
-
const reason = args.blocked_reason || 'Paused by model';
|
|
928
|
-
|
|
929
|
-
const snap = engine.pause(reason, sid);
|
|
930
|
-
|
|
931
|
-
return formatGoalValue(snap);
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (action === 'resume') {
|
|
938
|
-
|
|
939
|
-
const snap = engine.resume(sid);
|
|
940
|
-
|
|
941
|
-
resumeActiveAgent(undefined, sid);
|
|
942
|
-
|
|
943
|
-
return formatGoalValue(snap);
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (action === 'edit') {
|
|
950
|
-
|
|
951
|
-
const snap = engine.getSnapshot(sid);
|
|
952
|
-
|
|
953
|
-
if (args.objective) snap.title = args.objective;
|
|
954
|
-
|
|
955
|
-
if (args.max_goal_rounds) snap.maxIterations = Number(args.max_goal_rounds);
|
|
956
|
-
|
|
957
|
-
engine.emit(sid, true);
|
|
958
|
-
|
|
959
|
-
return formatGoalValue(snap);
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (action === 'blocked') {
|
|
966
|
-
|
|
967
|
-
const reason = args.blocked_reason || 'Goal blocked';
|
|
968
|
-
|
|
969
|
-
const snap = engine.pause('Blocked: ' + reason, sid);
|
|
970
|
-
|
|
971
|
-
if (toolCtx?.deferContext) {
|
|
972
|
-
|
|
973
|
-
try {
|
|
974
|
-
|
|
975
|
-
toolCtx.deferContext({
|
|
976
|
-
|
|
977
|
-
type: 'text',
|
|
978
|
-
|
|
979
|
-
text: `<goal_blocked>\nObjective: ${JSON.stringify(snap.title || 'Goal')}\nBlocked: ${JSON.stringify(reason)}\nExplain to the user what blocked progress.\n</goal_blocked>`,
|
|
980
|
-
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
} catch (_) {}
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const res = formatGoalValue(snap);
|
|
988
|
-
|
|
989
|
-
if (res.goal) {
|
|
990
|
-
|
|
991
|
-
res.goal.phase = 'blocked';
|
|
992
|
-
|
|
993
|
-
res.goal.blockedReason = { code: 'model-reported', message: reason };
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
return res;
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return formatGoalValue(engine.getSnapshot(sid));
|
|
1004
|
-
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
safeRegister({
|
|
1010
|
-
|
|
1011
|
-
name: 'update_goal',
|
|
1012
|
-
|
|
1013
|
-
description: 'Update the active goal: complete, pause, resume, edit, or report blocked.',
|
|
1014
|
-
|
|
1015
|
-
parameters: {
|
|
1016
|
-
|
|
1017
|
-
type: 'object',
|
|
1018
|
-
|
|
1019
|
-
properties: {
|
|
1020
|
-
|
|
1021
|
-
goal_id: { type: 'string', description: 'Exact id returned by get_goal.' },
|
|
1022
|
-
|
|
1023
|
-
revision: { type: 'number', description: 'Exact positive revision returned by get_goal.' },
|
|
1024
|
-
|
|
1025
|
-
action: {
|
|
1026
|
-
|
|
1027
|
-
type: 'string',
|
|
1028
|
-
|
|
1029
|
-
enum: ['edit', 'pause', 'resume', 'complete', 'blocked'],
|
|
1030
|
-
|
|
1031
|
-
description: 'edit | pause | resume | complete | blocked',
|
|
1032
|
-
|
|
1033
|
-
},
|
|
1034
|
-
|
|
1035
|
-
objective: { type: 'string', description: 'Replacement objective; valid with action edit.' },
|
|
1036
|
-
|
|
1037
|
-
max_goal_rounds: { type: 'number', description: 'Replacement cap; valid with action edit.' },
|
|
1038
|
-
|
|
1039
|
-
blocked_reason: { type: 'string', description: 'Concrete blocking condition; required with action blocked.' },
|
|
1040
|
-
|
|
1041
|
-
},
|
|
1042
|
-
|
|
1043
|
-
required: ['action'],
|
|
1044
|
-
|
|
1045
|
-
},
|
|
1046
|
-
|
|
1047
|
-
output: JSON_OUTPUT,
|
|
1048
|
-
|
|
1049
|
-
execute: handleUpdateGoal,
|
|
1050
|
-
|
|
1051
|
-
handler: handleUpdateGoal,
|
|
1052
|
-
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
// Инструмент 4: Декомпозиция цели на вехи
|
|
1058
|
-
|
|
1059
|
-
const handleSetMilestones = async ({ milestones }, toolCtx) => {
|
|
1060
|
-
|
|
1061
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
1062
|
-
|
|
1063
|
-
const snap = engine.getSnapshot(sid);
|
|
1064
|
-
|
|
1065
|
-
if (!snap.hasActiveGoal) {
|
|
1066
|
-
|
|
1067
|
-
return { error: 'No active goal currently set. Start a goal first.' };
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
engine.addMilestones(milestones, true, sid);
|
|
1072
|
-
|
|
1073
|
-
return {
|
|
1074
|
-
|
|
1075
|
-
success: true,
|
|
1076
|
-
|
|
1077
|
-
milestones: engine.getSnapshot(sid).milestones,
|
|
1078
|
-
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
safeRegister({
|
|
1086
|
-
|
|
1087
|
-
name: 'goal_set_milestones',
|
|
1088
|
-
|
|
1089
|
-
description: 'Break down the current active goal into a sequence of concrete milestones/sub-tasks.',
|
|
1090
|
-
|
|
1091
|
-
parameters: {
|
|
1092
|
-
|
|
1093
|
-
type: 'object',
|
|
1094
|
-
|
|
1095
|
-
properties: {
|
|
1096
|
-
|
|
1097
|
-
milestones: {
|
|
1098
|
-
|
|
1099
|
-
type: 'array',
|
|
1100
|
-
|
|
1101
|
-
items: { type: 'string' },
|
|
1102
|
-
|
|
1103
|
-
description: 'List of milestone titles to accomplish.',
|
|
1104
|
-
|
|
1105
|
-
},
|
|
1106
|
-
|
|
1107
|
-
},
|
|
1108
|
-
|
|
1109
|
-
required: ['milestones'],
|
|
1110
|
-
|
|
1111
|
-
},
|
|
1112
|
-
|
|
1113
|
-
output: JSON_OUTPUT,
|
|
1114
|
-
|
|
1115
|
-
execute: handleSetMilestones,
|
|
1116
|
-
|
|
1117
|
-
handler: handleSetMilestones,
|
|
1118
|
-
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
// Инструмент 5: Обновление статуса вехи
|
|
1124
|
-
|
|
1125
|
-
const handleUpdateProgress = async ({ milestone_id, status, notes }, toolCtx) => {
|
|
1126
|
-
|
|
1127
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
1128
|
-
|
|
1129
|
-
const ok = engine.updateMilestone(milestone_id, status, notes, sid);
|
|
1130
|
-
|
|
1131
|
-
if (!ok) {
|
|
1132
|
-
|
|
1133
|
-
return { error: `Milestone ${milestone_id} not found or no active goal.` };
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
return {
|
|
1138
|
-
|
|
1139
|
-
success: true,
|
|
1140
|
-
|
|
1141
|
-
snapshot: engine.getSnapshot(sid),
|
|
1142
|
-
|
|
1143
|
-
};
|
|
1144
|
-
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
safeRegister({
|
|
1150
|
-
|
|
1151
|
-
name: 'goal_update_progress',
|
|
1152
|
-
|
|
1153
|
-
description: 'Update the status of a specific goal milestone and optionally log progress notes.',
|
|
1154
|
-
|
|
1155
|
-
parameters: {
|
|
1156
|
-
|
|
1157
|
-
type: 'object',
|
|
1158
|
-
|
|
1159
|
-
properties: {
|
|
1160
|
-
|
|
1161
|
-
milestone_id: {
|
|
1162
|
-
|
|
1163
|
-
type: 'string',
|
|
1164
|
-
|
|
1165
|
-
description: 'The ID of the milestone (e.g. "m-1", "m-2").',
|
|
1166
|
-
|
|
1167
|
-
},
|
|
1168
|
-
|
|
1169
|
-
status: {
|
|
1170
|
-
|
|
1171
|
-
type: 'string',
|
|
1172
|
-
|
|
1173
|
-
enum: ['pending', 'in_progress', 'completed', 'failed'],
|
|
1174
|
-
|
|
1175
|
-
description: 'New status for this milestone.',
|
|
1176
|
-
|
|
1177
|
-
},
|
|
1178
|
-
|
|
1179
|
-
notes: {
|
|
1180
|
-
|
|
1181
|
-
type: 'string',
|
|
1182
|
-
|
|
1183
|
-
description: 'Brief summary of what was accomplished or why it failed.',
|
|
1184
|
-
|
|
1185
|
-
},
|
|
1186
|
-
|
|
1187
|
-
},
|
|
1188
|
-
|
|
1189
|
-
required: ['milestone_id', 'status'],
|
|
1190
|
-
|
|
1191
|
-
},
|
|
1192
|
-
|
|
1193
|
-
output: JSON_OUTPUT,
|
|
1194
|
-
|
|
1195
|
-
execute: handleUpdateProgress,
|
|
1196
|
-
|
|
1197
|
-
handler: handleUpdateProgress,
|
|
1198
|
-
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
// Инструмент 6: Успешное завершение цели
|
|
1204
|
-
|
|
1205
|
-
const handleGoalFinish = async ({ summary }, toolCtx) => {
|
|
1206
|
-
|
|
1207
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
1208
|
-
|
|
1209
|
-
const snap = engine.completeGoal(summary, sid);
|
|
1210
|
-
|
|
1211
|
-
return {
|
|
1212
|
-
|
|
1213
|
-
success: true,
|
|
1214
|
-
|
|
1215
|
-
completed: true,
|
|
1216
|
-
|
|
1217
|
-
summary,
|
|
1218
|
-
|
|
1219
|
-
};
|
|
1220
|
-
|
|
1221
|
-
};
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
safeRegister({
|
|
1226
|
-
|
|
1227
|
-
name: 'goal_finish',
|
|
1228
|
-
|
|
1229
|
-
description: 'Conclude the active goal successfully with a final summary and achievements.',
|
|
1230
|
-
|
|
1231
|
-
parameters: {
|
|
1232
|
-
|
|
1233
|
-
type: 'object',
|
|
1234
|
-
|
|
1235
|
-
properties: {
|
|
1236
|
-
|
|
1237
|
-
summary: {
|
|
1238
|
-
|
|
1239
|
-
type: 'string',
|
|
1240
|
-
|
|
1241
|
-
description: 'Final summary of the goal outcome and deliverables.',
|
|
1242
|
-
|
|
1243
|
-
},
|
|
1244
|
-
|
|
1245
|
-
},
|
|
1246
|
-
|
|
1247
|
-
required: ['summary'],
|
|
1248
|
-
|
|
1249
|
-
},
|
|
1250
|
-
|
|
1251
|
-
output: JSON_OUTPUT,
|
|
1252
|
-
|
|
1253
|
-
execute: handleGoalFinish,
|
|
1254
|
-
|
|
1255
|
-
handler: handleGoalFinish,
|
|
1256
|
-
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
});
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
// 5. Регистрация HTTP REST API маршрутов и SSE событий
|
|
1264
|
-
|
|
1265
|
-
ctx.effect(() => {
|
|
1266
|
-
|
|
1267
|
-
if (!ctx.webServer?.register) return () => {};
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// Keepalive ping timer for SSE connections (every 20s)
|
|
1272
|
-
|
|
1273
|
-
const keepaliveTimer = setInterval(() => {
|
|
1274
|
-
|
|
1275
|
-
for (const clients of sseClients.values()) {
|
|
1276
|
-
|
|
1277
|
-
for (const res of clients) {
|
|
1278
|
-
|
|
1279
|
-
try {
|
|
1280
|
-
|
|
1281
|
-
res.write(': keepalive\n\n');
|
|
1282
|
-
|
|
1283
|
-
} catch (_) {}
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
}, 20000);
|
|
1290
|
-
|
|
1291
|
-
if (typeof keepaliveTimer.unref === 'function') keepaliveTimer.unref();
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const unreg = ctx.webServer.register({
|
|
1296
|
-
|
|
1297
|
-
kind: 'prefix',
|
|
1298
|
-
|
|
1299
|
-
path: '/dsh-goal',
|
|
1300
|
-
|
|
1301
|
-
handler: (req, res) => {
|
|
1302
|
-
|
|
1303
|
-
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
1304
|
-
|
|
1305
|
-
const pathname = url.pathname;
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
// GET /dsh-goal/events — Server-Sent Events realtime snapshot stream
|
|
1310
|
-
|
|
1311
|
-
if (req.method === 'GET' && (pathname === '/dsh-goal/events' || pathname === '/dsh-goal/events/')) {
|
|
1312
|
-
|
|
1313
|
-
const sid = sessionIdOf(req, 'default');
|
|
1314
|
-
|
|
1315
|
-
res.writeHead(200, {
|
|
1316
|
-
|
|
1317
|
-
'Content-Type': 'text/event-stream',
|
|
1318
|
-
|
|
1319
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
1320
|
-
|
|
1321
|
-
'Connection': 'keep-alive',
|
|
1322
|
-
|
|
1323
|
-
'X-Accel-Buffering': 'no',
|
|
1324
|
-
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (!sseClients.has(sid)) {
|
|
1330
|
-
|
|
1331
|
-
sseClients.set(sid, new Set());
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
sseClients.get(sid).add(res);
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const initialSnap = engine.getSnapshot(sid);
|
|
1340
|
-
|
|
1341
|
-
res.write(`data: ${JSON.stringify(initialSnap)}\n\n`);
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
req.on('close', () => {
|
|
1346
|
-
|
|
1347
|
-
const set = sseClients.get(sid);
|
|
1348
|
-
|
|
1349
|
-
if (set) {
|
|
1350
|
-
|
|
1351
|
-
set.delete(res);
|
|
1352
|
-
|
|
1353
|
-
if (set.size === 0) sseClients.delete(sid);
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
return;
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
// GET /dsh-goal/state
|
|
1370
|
-
|
|
1371
|
-
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
1372
|
-
|
|
1373
|
-
const sid = sessionIdOf(req, 'default');
|
|
1374
|
-
|
|
1375
|
-
res.statusCode = 200;
|
|
1376
|
-
|
|
1377
|
-
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
// POST /dsh-goal/action
|
|
1384
|
-
|
|
1385
|
-
// Issue #22: CSRF & Same-Origin guard
|
|
1386
|
-
|
|
1387
|
-
if (req.method === 'POST' && (pathname === '/dsh-goal/action' || pathname === '/dsh-goal/action/')) {
|
|
1388
|
-
|
|
1389
|
-
const secFetchSite = req.headers['sec-fetch-site'];
|
|
1390
|
-
|
|
1391
|
-
if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'same-site' && secFetchSite !== 'none') {
|
|
1392
|
-
|
|
1393
|
-
res.statusCode = 403;
|
|
1394
|
-
|
|
1395
|
-
return res.end(JSON.stringify({ error: 'Forbidden: cross-site requests are rejected' }));
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const origin = req.headers.origin;
|
|
1402
|
-
|
|
1403
|
-
const host = req.headers.host;
|
|
1404
|
-
|
|
1405
|
-
if (origin && host) {
|
|
1406
|
-
|
|
1407
|
-
try {
|
|
1408
|
-
|
|
1409
|
-
const originHost = new URL(origin).host;
|
|
1410
|
-
|
|
1411
|
-
if (originHost !== host) {
|
|
1412
|
-
|
|
1413
|
-
res.statusCode = 403;
|
|
1414
|
-
|
|
1415
|
-
return res.end(JSON.stringify({ error: 'Forbidden: origin mismatch' }));
|
|
1416
|
-
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
} catch (_) {
|
|
1420
|
-
|
|
1421
|
-
res.statusCode = 403;
|
|
1422
|
-
|
|
1423
|
-
return res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
|
|
1424
|
-
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
let body = '';
|
|
1432
|
-
|
|
1433
|
-
let bodySize = 0;
|
|
1434
|
-
|
|
1435
|
-
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
1436
|
-
|
|
1437
|
-
let limitExceeded = false;
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
req.on('data', (chunk) => {
|
|
1442
|
-
|
|
1443
|
-
bodySize += chunk.length;
|
|
1444
|
-
|
|
1445
|
-
if (bodySize > MAX_PAYLOAD_BYTES) {
|
|
1446
|
-
|
|
1447
|
-
limitExceeded = true;
|
|
1448
|
-
|
|
1449
|
-
req.pause();
|
|
1450
|
-
|
|
1451
|
-
res.statusCode = 413;
|
|
1452
|
-
|
|
1453
|
-
return res.end(JSON.stringify({ error: 'Payload too large: max 256 KB allowed' }));
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
body += chunk;
|
|
1458
|
-
|
|
1459
|
-
});
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
req.on('end', () => {
|
|
1464
|
-
|
|
1465
|
-
if (limitExceeded) return;
|
|
1466
|
-
|
|
1467
|
-
try {
|
|
1468
|
-
|
|
1469
|
-
const data = JSON.parse(body || '{}');
|
|
1470
|
-
|
|
1471
|
-
const sid = data.sessionId || sessionIdOf(req, 'default');
|
|
1472
|
-
|
|
1473
|
-
const { action, title, description, reason, milestoneId, status, notes } = data;
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
let result = null;
|
|
1478
|
-
|
|
1479
|
-
switch (action) {
|
|
1480
|
-
|
|
1481
|
-
case 'start': {
|
|
1482
|
-
|
|
1483
|
-
const cleanTitle = typeof title === 'string' ? title.trim() : '';
|
|
1484
|
-
|
|
1485
|
-
if (!cleanTitle) {
|
|
1486
|
-
|
|
1487
|
-
res.statusCode = 400;
|
|
1488
|
-
|
|
1489
|
-
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
1494
|
-
|
|
1495
|
-
result = engine.startGoal(cleanTitle, {
|
|
1496
|
-
|
|
1497
|
-
description: typeof description === 'string' ? description.trim() : '',
|
|
1498
|
-
|
|
1499
|
-
maxIterations: getConfig().maxIterations,
|
|
1500
|
-
|
|
1501
|
-
lang: detectedLang,
|
|
1502
|
-
|
|
1503
|
-
}, sid);
|
|
1504
|
-
|
|
1505
|
-
const startPrompt = detectedLang === 'ru'
|
|
1506
|
-
|
|
1507
|
-
? `🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`
|
|
1508
|
-
|
|
1509
|
-
: `🎯 Goal established: "${cleanTitle}". Immediately formulate a work plan (3-7 concrete steps) via tool goal_set_milestones and start executing it.`;
|
|
1510
|
-
|
|
1511
|
-
resumeActiveAgent(startPrompt, sid);
|
|
1512
|
-
|
|
1513
|
-
break;
|
|
1514
|
-
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
case 'nudge': {
|
|
1518
|
-
|
|
1519
|
-
const text = (typeof data.text === 'string' ? data.text : (data.notes || data.nudge || '')).trim();
|
|
1520
|
-
|
|
1521
|
-
if (!text) {
|
|
1522
|
-
|
|
1523
|
-
res.statusCode = 400;
|
|
1524
|
-
|
|
1525
|
-
return res.end(JSON.stringify({ error: 'Nudge text cannot be empty' }));
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
result = engine.nudge(text, sid);
|
|
1530
|
-
|
|
1531
|
-
if (data.resume) {
|
|
1532
|
-
|
|
1533
|
-
engine.resume(sid);
|
|
1534
|
-
|
|
1535
|
-
const prompt = engine.getStatePromptInjection(sid);
|
|
1536
|
-
|
|
1537
|
-
resumeActiveAgent(prompt, sid);
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
break;
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
case 'pause':
|
|
1546
|
-
|
|
1547
|
-
result = engine.pause(reason || 'Paused by user interface', sid);
|
|
1548
|
-
|
|
1549
|
-
stopRunningAgents(sid);
|
|
1550
|
-
|
|
1551
|
-
break;
|
|
1552
|
-
|
|
1553
|
-
case 'resume':
|
|
1554
|
-
|
|
1555
|
-
result = engine.resume(sid);
|
|
1556
|
-
|
|
1557
|
-
resumeActiveAgent(undefined, sid);
|
|
1558
|
-
|
|
1559
|
-
break;
|
|
1560
|
-
|
|
1561
|
-
case 'cancel':
|
|
1562
|
-
|
|
1563
|
-
result = engine.cancel(reason || 'Goal cancelled by user', sid);
|
|
1564
|
-
|
|
1565
|
-
stopRunningAgents(sid);
|
|
1566
|
-
|
|
1567
|
-
break;
|
|
1568
|
-
|
|
1569
|
-
case 'clear':
|
|
1570
|
-
|
|
1571
|
-
result = engine.clear(sid);
|
|
1572
|
-
|
|
1573
|
-
stopRunningAgents(sid);
|
|
1574
|
-
|
|
1575
|
-
sessionAgents.delete(sid);
|
|
1576
|
-
|
|
1577
|
-
break;
|
|
1578
|
-
|
|
1579
|
-
case 'update_milestone': {
|
|
1580
|
-
|
|
1581
|
-
if (!milestoneId || !status) {
|
|
1582
|
-
|
|
1583
|
-
res.statusCode = 400;
|
|
1584
|
-
|
|
1585
|
-
return res.end(JSON.stringify({ error: 'milestoneId and status are required' }));
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
const validStatuses = Object.values(MilestoneStatus);
|
|
1590
|
-
|
|
1591
|
-
if (!validStatuses.includes(status)) {
|
|
1592
|
-
|
|
1593
|
-
res.statusCode = 400;
|
|
1594
|
-
|
|
1595
|
-
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
const ok = engine.updateMilestone(milestoneId, status, notes, sid);
|
|
1600
|
-
|
|
1601
|
-
if (!ok) {
|
|
1602
|
-
|
|
1603
|
-
res.statusCode = 404;
|
|
1604
|
-
|
|
1605
|
-
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
1606
|
-
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
result = engine.getSnapshot(sid);
|
|
1610
|
-
|
|
1611
|
-
break;
|
|
1612
|
-
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
default:
|
|
1616
|
-
|
|
1617
|
-
res.statusCode = 400;
|
|
1618
|
-
|
|
1619
|
-
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
1620
|
-
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
res.statusCode = 200;
|
|
1626
|
-
|
|
1627
|
-
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot(sid) }));
|
|
1628
|
-
|
|
1629
|
-
} catch (parseErr) {
|
|
1630
|
-
|
|
1631
|
-
res.statusCode = 400;
|
|
1632
|
-
|
|
1633
|
-
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
1634
|
-
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
});
|
|
1638
|
-
|
|
1639
|
-
return;
|
|
1640
|
-
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
res.statusCode = 404;
|
|
1646
|
-
|
|
1647
|
-
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
1648
|
-
|
|
1649
|
-
},
|
|
1650
|
-
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
return () => {
|
|
1656
|
-
|
|
1657
|
-
clearInterval(keepaliveTimer);
|
|
1658
|
-
|
|
1659
|
-
if (typeof unreg === 'function') unreg();
|
|
1660
|
-
|
|
1661
|
-
for (const clients of sseClients.values()) {
|
|
1662
|
-
|
|
1663
|
-
for (const res of clients) {
|
|
1664
|
-
|
|
1665
|
-
try {
|
|
1666
|
-
|
|
1667
|
-
res.end();
|
|
1668
|
-
|
|
1669
|
-
} catch (_) {}
|
|
1670
|
-
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
sseClients.clear();
|
|
1676
|
-
|
|
1677
|
-
};
|
|
1678
|
-
|
|
1679
|
-
}, 'dsh-goal: HTTP WebServer Routes & SSE');
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
// 6. Подписка на события сессии (автономный цикл)
|
|
1684
|
-
|
|
1685
|
-
ctx.effect(() => {
|
|
1686
|
-
|
|
1687
|
-
// Подписка на завершение turn
|
|
1688
|
-
|
|
1689
|
-
const onTurnEnd = (turn) => {
|
|
1690
|
-
|
|
1691
|
-
const liveConfig = getConfig();
|
|
1692
|
-
|
|
1693
|
-
if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const sid = sessionIdOf(turn, 'default');
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
// Накопление токенов по завершении хода
|
|
1702
|
-
|
|
1703
|
-
const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
|
|
1704
|
-
|
|
1705
|
-
if (usage) {
|
|
1706
|
-
|
|
1707
|
-
engine.addTokenUsage(usage, sid);
|
|
1708
|
-
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const snap = engine.getSnapshot(sid);
|
|
1714
|
-
|
|
1715
|
-
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
1716
|
-
|
|
1717
|
-
// Tool-Failure Breaker: отслеживание повторяющихся ошибок инструментов
|
|
1718
|
-
|
|
1719
|
-
const steps = turn?.steps || turn?.turn?.steps || [];
|
|
1720
|
-
|
|
1721
|
-
const hasStepError = Array.isArray(steps) && steps.some((s) => {
|
|
1722
|
-
|
|
1723
|
-
return s?.status === 'error' || s?.error || s?.toolResult?.isError;
|
|
1724
|
-
|
|
1725
|
-
});
|
|
1726
|
-
|
|
1727
|
-
const hasTurnError = Boolean(turn?.error || turn?.reason?.kind === 'error');
|
|
1728
|
-
|
|
1729
|
-
const isToolFailure = hasStepError || hasTurnError;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
if (isToolFailure) {
|
|
1734
|
-
|
|
1735
|
-
const failCount = engine.incrementToolFailureCount(sid);
|
|
1736
|
-
|
|
1737
|
-
const limit = liveConfig.consecutiveToolFailureLimit;
|
|
1738
|
-
|
|
1739
|
-
if (limit > 0 && failCount >= limit) {
|
|
1740
|
-
|
|
1741
|
-
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
1742
|
-
|
|
1743
|
-
const pauseReason = lang === 'ru'
|
|
1744
|
-
|
|
1745
|
-
? `Повторяющаяся ошибка инструментов (${failCount} подряд) — цель приостановлена для защиты от зацикливания`
|
|
1746
|
-
|
|
1747
|
-
: `Repeated tool failure (${failCount} consecutive) — goal paused to prevent token burn`;
|
|
1748
|
-
|
|
1749
|
-
engine.pause(pauseReason, sid);
|
|
1750
|
-
|
|
1751
|
-
return;
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
} else {
|
|
1756
|
-
|
|
1757
|
-
engine.resetToolFailureCount(sid);
|
|
1758
|
-
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
// Проверяем причину завершения хода
|
|
1764
|
-
|
|
1765
|
-
if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
|
|
1766
|
-
|
|
1767
|
-
return;
|
|
1768
|
-
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
// Item 3: Smart Progress Guard — detect idle loops without progress
|
|
1774
|
-
|
|
1775
|
-
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
1776
|
-
|
|
1777
|
-
const stallCount = engine.incrementStallCount(sid);
|
|
1778
|
-
|
|
1779
|
-
if (stallCount >= 2) {
|
|
1780
|
-
|
|
1781
|
-
const stallReason = lang === 'ru'
|
|
1782
|
-
|
|
1783
|
-
? 'Агент не продвинулся по плану за последние 2 итерации — требуется внимание оператора'
|
|
1784
|
-
|
|
1785
|
-
: 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
|
|
1786
|
-
|
|
1787
|
-
engine.pause(stallReason, sid);
|
|
1788
|
-
|
|
1789
|
-
return;
|
|
1790
|
-
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
const canContinue = engine.incrementIteration(sid);
|
|
1796
|
-
|
|
1797
|
-
if (!canContinue) return;
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
// Item 2: Low-Latency AutoDrive — setImmediate instead of static setTimeout
|
|
1802
|
-
|
|
1803
|
-
const triggerNextTurn = () => {
|
|
1804
|
-
|
|
1805
|
-
const currentSnap = engine.getSnapshot(sid);
|
|
1806
|
-
|
|
1807
|
-
if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
|
|
1808
|
-
|
|
1809
|
-
const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
|
|
1810
|
-
|
|
1811
|
-
let promptText = currentLang === 'ru'
|
|
1812
|
-
|
|
1813
|
-
? 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.'
|
|
1814
|
-
|
|
1815
|
-
: 'Continue autonomous goal execution according to the work plan. Mark each completed step via goal_update_progress, and when all tasks are complete, call goal_finish.';
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
if (currentSnap.pendingNudge) {
|
|
1820
|
-
|
|
1821
|
-
const userNudge = engine.consumePendingNudge(sid);
|
|
1822
|
-
|
|
1823
|
-
if (userNudge) {
|
|
1824
|
-
|
|
1825
|
-
promptText = (currentLang === 'ru'
|
|
1826
|
-
|
|
1827
|
-
? `🚨 СРОЧНОЕ УТОЧНЕНИЕ / НАПРАВЛЕНИЕ ОТ ПОЛЬЗОВАТЕЛЯ:\n"${userNudge}"\nСкорректируй выполнение с учётом этого замечания.\n\n`
|
|
1828
|
-
|
|
1829
|
-
: `🚨 URGENT USER CLARIFICATION / STEERING:\n"${userNudge}"\nAdjust execution adhering to this feedback.\n\n`) + promptText;
|
|
1830
|
-
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
const promptMsg = createGoalUserMessage(promptText);
|
|
1836
|
-
|
|
1837
|
-
let target = sessionAgents.get(sid);
|
|
1838
|
-
|
|
1839
|
-
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
1840
|
-
|
|
1841
|
-
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
1842
|
-
|
|
1843
|
-
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
1844
|
-
|
|
1845
|
-
target = lastActiveAgent;
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
if (!target && agentsService && typeof agentsService.list === 'function') {
|
|
1852
|
-
|
|
1853
|
-
const list = agentsService.list();
|
|
1854
|
-
|
|
1855
|
-
if (list && list.length > 0) {
|
|
1856
|
-
|
|
1857
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
1858
|
-
|
|
1859
|
-
const cand = list[i];
|
|
1860
|
-
|
|
1861
|
-
if (sessionIdOf(cand, null) === sid) {
|
|
1862
|
-
|
|
1863
|
-
target = cand;
|
|
1864
|
-
|
|
1865
|
-
break;
|
|
1866
|
-
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
if (!target && sid === 'default') target = list[list.length - 1];
|
|
1872
|
-
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
if (target && typeof target.followup === 'function' && target.status !== 'stopped') {
|
|
1880
|
-
|
|
1881
|
-
try {
|
|
1882
|
-
|
|
1883
|
-
target.followup(promptMsg);
|
|
1884
|
-
|
|
1885
|
-
sessionAgents.set(sid, target);
|
|
1886
|
-
|
|
1887
|
-
} catch (err) {
|
|
1888
|
-
|
|
1889
|
-
console.warn('[dsh-goal] Failed auto-drive followup:', err);
|
|
1890
|
-
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
};
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
if (typeof setImmediate === 'function') {
|
|
1902
|
-
|
|
1903
|
-
setImmediate(triggerNextTurn);
|
|
1904
|
-
|
|
1905
|
-
} else {
|
|
1906
|
-
|
|
1907
|
-
setTimeout(triggerNextTurn, 0);
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
};
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
// Подписка на запрос подтверждения (approval/asked) -> автоматическая пауза
|
|
1918
|
-
|
|
1919
|
-
const onApprovalAsked = (event) => {
|
|
1920
|
-
|
|
1921
|
-
const sid = sessionIdOf(event, 'default');
|
|
1922
|
-
|
|
1923
|
-
const snap = engine.getSnapshot(sid);
|
|
1924
|
-
|
|
1925
|
-
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
1926
|
-
|
|
1927
|
-
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
1928
|
-
|
|
1929
|
-
const pauseReason = lang === 'ru'
|
|
1930
|
-
|
|
1931
|
-
? 'Ожидание подтверждения действия оператором'
|
|
1932
|
-
|
|
1933
|
-
: 'Waiting for operator confirmation/approval';
|
|
1934
|
-
|
|
1935
|
-
engine.pause(pauseReason, sid);
|
|
1936
|
-
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
};
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
ctx.on?.('turn/end', onTurnEnd);
|
|
1944
|
-
|
|
1945
|
-
ctx.on?.('approval/asked', onApprovalAsked);
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
return () => {
|
|
1950
|
-
|
|
1951
|
-
ctx.off?.('turn/end', onTurnEnd);
|
|
1952
|
-
|
|
1953
|
-
ctx.off?.('approval/asked', onApprovalAsked);
|
|
1954
|
-
|
|
1955
|
-
};
|
|
1956
|
-
|
|
1957
|
-
}, 'dsh-goal: Session & Turn Coordinator');
|
|
1958
|
-
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import z from '@deepseek-ai/schemastery';
|
|
4
|
+
import { GoalEngine, GoalState, detectLanguage, sessionIdOf } from './goal-engine.js';
|
|
5
|
+
import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
|
|
6
|
+
import { registerRoutes } from './routes.js';
|
|
7
|
+
import { registerTools } from './tools.js';
|
|
8
|
+
import { registerPluginUpdater } from './updater.js';
|
|
9
|
+
|
|
10
|
+
export const name = '@goodandready/dsh-goal';
|
|
11
|
+
export const inject = ['webServer', 'settings'];
|
|
12
|
+
|
|
13
|
+
const NS = 'dsh-goal';
|
|
14
|
+
|
|
15
|
+
export const Config = z.object({
|
|
16
|
+
maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
|
|
17
|
+
autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
|
|
18
|
+
enableSound: z.boolean().default(true).description('Play synthesized audio chime on goal completion or failure'),
|
|
19
|
+
showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
|
|
20
|
+
consecutiveToolFailureLimit: z.number().default(3).description('Auto-pause goal if N consecutive turns encounter tool execution errors (0 to disable)'),
|
|
21
|
+
enableBrowserNotifications: z.boolean().default(true).description('Show desktop notifications on goal completion or failure'),
|
|
22
|
+
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function apply(ctx, config = {}) {
|
|
26
|
+
let settingsScope = null;
|
|
27
|
+
let agentsService = null;
|
|
28
|
+
const runningAgents = new Set();
|
|
29
|
+
const sessionAgents = new Map();
|
|
30
|
+
let lastActiveAgent = null;
|
|
31
|
+
const sseClients = new Map();
|
|
32
|
+
|
|
33
|
+
let currentSettings = {
|
|
34
|
+
maxIterations: config?.maxIterations ?? 25,
|
|
35
|
+
autoDrive: config?.autoDrive ?? true,
|
|
36
|
+
enableSound: config?.enableSound ?? true,
|
|
37
|
+
showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
|
|
38
|
+
consecutiveToolFailureLimit: config?.consecutiveToolFailureLimit ?? 3,
|
|
39
|
+
enableBrowserNotifications: config?.enableBrowserNotifications ?? true,
|
|
40
|
+
storagePath: config?.storagePath,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
44
|
+
const storagePath = currentSettings.storagePath !== undefined && currentSettings.storagePath !== null
|
|
45
|
+
? currentSettings.storagePath
|
|
46
|
+
: path.join(defaultStorageDir, 'dsh-goal-state.json');
|
|
47
|
+
|
|
48
|
+
const engine = new GoalEngine({
|
|
49
|
+
defaultMaxIterations: currentSettings.maxIterations,
|
|
50
|
+
autoDrive: currentSettings.autoDrive,
|
|
51
|
+
enableSound: currentSettings.enableSound,
|
|
52
|
+
showQuickLaunchButton: currentSettings.showQuickLaunchButton,
|
|
53
|
+
consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
|
|
54
|
+
storagePath,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Track running agents through global DSH core event
|
|
58
|
+
ctx.inject(['agents'], (actx) => {
|
|
59
|
+
agentsService = actx.agents;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
ctx.on?.('agent/status', ({ agent, status }) => {
|
|
63
|
+
if (status === 'running') {
|
|
64
|
+
runningAgents.add(agent);
|
|
65
|
+
lastActiveAgent = agent;
|
|
66
|
+
const sid = sessionIdOf(agent, null);
|
|
67
|
+
if (sid) {
|
|
68
|
+
sessionAgents.set(sid, agent);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
runningAgents.delete(agent);
|
|
72
|
+
if (lastActiveAgent === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
|
|
73
|
+
lastActiveAgent = null;
|
|
74
|
+
}
|
|
75
|
+
const sid = sessionIdOf(agent, null);
|
|
76
|
+
if (sid && sessionAgents.get(sid) === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
|
|
77
|
+
sessionAgents.delete(sid);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const stopRunningAgents = (sessionId = 'default') => {
|
|
83
|
+
const sid = sessionId || 'default';
|
|
84
|
+
const specificAgent = sessionAgents.get(sid);
|
|
85
|
+
if (specificAgent && typeof specificAgent.cancel === 'function') {
|
|
86
|
+
try {
|
|
87
|
+
specificAgent.cancel({ kind: 'user' });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.warn('[dsh-goal] Failed to cancel specific agent for session:', err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const ag of runningAgents) {
|
|
94
|
+
try {
|
|
95
|
+
const agSid = sessionIdOf(ag, null);
|
|
96
|
+
if (!agSid || agSid === sid || sid === 'default') {
|
|
97
|
+
if (typeof ag.cancel === 'function') {
|
|
98
|
+
ag.cancel({ kind: 'user' });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.warn('[dsh-goal] Failed to cancel running agent:', err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (lastActiveAgent && lastActiveAgent.status === 'running' && typeof lastActiveAgent.cancel === 'function') {
|
|
107
|
+
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
108
|
+
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
109
|
+
try {
|
|
110
|
+
lastActiveAgent.cancel({ kind: 'user' });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.warn('[dsh-goal] Failed to cancel lastActiveAgent:', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const resumeActiveAgent = (promptText, sessionId = 'default') => {
|
|
119
|
+
const sid = sessionId || 'default';
|
|
120
|
+
const snap = engine.getSnapshot(sid);
|
|
121
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
122
|
+
const defaultResumePrompt = lang === 'zh'
|
|
123
|
+
? '▶️ 用户已恢复目标。请继续执行工作计划,并通过 goal_update_progress 更新进度。'
|
|
124
|
+
: '▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.';
|
|
125
|
+
|
|
126
|
+
const resumeMsg = createGoalUserMessage(promptText || defaultResumePrompt);
|
|
127
|
+
|
|
128
|
+
let target = sessionAgents.get(sid);
|
|
129
|
+
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
130
|
+
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
131
|
+
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
132
|
+
target = lastActiveAgent;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!target && agentsService && typeof agentsService.list === 'function') {
|
|
137
|
+
const list = agentsService.list();
|
|
138
|
+
if (list && list.length > 0) {
|
|
139
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
140
|
+
const cand = list[i];
|
|
141
|
+
const candSid = sessionIdOf(cand, null);
|
|
142
|
+
if (candSid === sid) {
|
|
143
|
+
target = cand;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!target && sid === 'default') {
|
|
148
|
+
target = list[list.length - 1];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (target && typeof target.followup === 'function') {
|
|
154
|
+
try {
|
|
155
|
+
target.followup(resumeMsg);
|
|
156
|
+
lastActiveAgent = target;
|
|
157
|
+
sessionAgents.set(sid, target);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.warn('[dsh-goal] Failed to resume agent:', err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const getConfig = () => {
|
|
167
|
+
if (settingsScope) {
|
|
168
|
+
const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
|
|
169
|
+
if (snap) {
|
|
170
|
+
if (snap.status === 'ready' || snap.status === undefined) {
|
|
171
|
+
const live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
|
|
172
|
+
if (live && typeof live === 'object') {
|
|
173
|
+
return {
|
|
174
|
+
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
175
|
+
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
176
|
+
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
177
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
178
|
+
consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
|
|
179
|
+
enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
|
|
180
|
+
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else if (typeof settingsScope.get === 'function') {
|
|
185
|
+
const live = settingsScope.get();
|
|
186
|
+
if (live && typeof live === 'object') {
|
|
187
|
+
return {
|
|
188
|
+
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
189
|
+
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
190
|
+
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
191
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
192
|
+
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return currentSettings;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const applySettings = () => {
|
|
201
|
+
const live = getConfig();
|
|
202
|
+
engine.updateConfig({
|
|
203
|
+
defaultMaxIterations: live.maxIterations,
|
|
204
|
+
autoDrive: live.autoDrive,
|
|
205
|
+
enableSound: live.enableSound,
|
|
206
|
+
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
207
|
+
consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// 1. Settings registration
|
|
212
|
+
ctx.inject(['settings'], (sctx) => {
|
|
213
|
+
try {
|
|
214
|
+
const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
|
|
215
|
+
if (scope) {
|
|
216
|
+
settingsScope = scope;
|
|
217
|
+
applySettings();
|
|
218
|
+
if (typeof scope.subscribe === 'function') {
|
|
219
|
+
sctx.effect(() => {
|
|
220
|
+
const off = scope.subscribe(() => {
|
|
221
|
+
applySettings();
|
|
222
|
+
});
|
|
223
|
+
return () => {
|
|
224
|
+
if (typeof off === 'function') off();
|
|
225
|
+
settingsScope = null;
|
|
226
|
+
};
|
|
227
|
+
}, 'dsh-goal: settings subscription');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.warn('[dsh-goal] Settings register skipped:', err.message);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 2. Chat slash command /goal registration
|
|
236
|
+
ctx.inject(['commands'], (cctx) => {
|
|
237
|
+
try {
|
|
238
|
+
if (typeof cctx.commands?.register !== 'function') return;
|
|
239
|
+
const unregister = cctx.commands.register({
|
|
240
|
+
name: 'goal',
|
|
241
|
+
description: 'Manage Goal Mode: activate, milestones, pause, resume, and clear',
|
|
242
|
+
input: { hint: '[<objective>|clear|pause|resume]' },
|
|
243
|
+
handler: async (invocation) => {
|
|
244
|
+
const sid = sessionIdOf(invocation, 'default');
|
|
245
|
+
if (invocation?.agent) {
|
|
246
|
+
lastActiveAgent = invocation.agent;
|
|
247
|
+
sessionAgents.set(sid, invocation.agent);
|
|
248
|
+
}
|
|
249
|
+
const parsed = parseGoalInput(invocation?.rawInput);
|
|
250
|
+
const liveConfig = getConfig();
|
|
251
|
+
return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (typeof unregister === 'function') {
|
|
256
|
+
cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.warn('[dsh-goal] Commands register skipped:', err.message);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 3. System prompt context injection
|
|
264
|
+
ctx.inject(['systemPrompt'], (pctx) => {
|
|
265
|
+
try {
|
|
266
|
+
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
267
|
+
const globalSections = pctx.systemPrompt.layers?.global?.sections;
|
|
268
|
+
if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
|
|
269
|
+
globalSections.data.delete('tool:goal');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
|
|
273
|
+
? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 2400)
|
|
274
|
+
: 2400;
|
|
275
|
+
|
|
276
|
+
const unregisterSection = pctx.systemPrompt.section({
|
|
277
|
+
name: 'tool:dsh-goal',
|
|
278
|
+
order,
|
|
279
|
+
text: (sessionCtx) => {
|
|
280
|
+
const sid = sessionIdOf(sessionCtx, 'default');
|
|
281
|
+
return engine.getStatePromptInjection(sid);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (typeof unregisterSection === 'function') {
|
|
286
|
+
pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// 4. Tools registration
|
|
295
|
+
registerTools(ctx, { engine, resumeActiveAgent });
|
|
296
|
+
|
|
297
|
+
// 5. One-click auto-updater registration
|
|
298
|
+
if (ctx.webServer?.register) {
|
|
299
|
+
try {
|
|
300
|
+
registerPluginUpdater(ctx, {
|
|
301
|
+
packageName: '@goodandready/dsh-goal',
|
|
302
|
+
endpoint: '/api/dsh-goal/update',
|
|
303
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
304
|
+
});
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.warn('[dsh-goal] Failed to register plugin updater:', err.message);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 6. WebServer HTTP & SSE routes
|
|
311
|
+
registerRoutes(ctx, {
|
|
312
|
+
engine,
|
|
313
|
+
getConfig,
|
|
314
|
+
stopRunningAgents,
|
|
315
|
+
resumeActiveAgent,
|
|
316
|
+
sessionAgents,
|
|
317
|
+
sseClients,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// 7. Turn coordinator & tool-failure breaker
|
|
321
|
+
ctx.effect(() => {
|
|
322
|
+
const onTurnEnd = (turn) => {
|
|
323
|
+
const liveConfig = getConfig();
|
|
324
|
+
if (!liveConfig.autoDrive) return;
|
|
325
|
+
|
|
326
|
+
const sid = sessionIdOf(turn, 'default');
|
|
327
|
+
const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
|
|
328
|
+
if (usage) {
|
|
329
|
+
engine.addTokenUsage(usage, sid);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const snap = engine.getSnapshot(sid);
|
|
333
|
+
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
334
|
+
const steps = turn?.steps || turn?.turn?.steps || [];
|
|
335
|
+
const hasStepError = Array.isArray(steps) && steps.some((s) => {
|
|
336
|
+
return s?.status === 'error' || s?.error || s?.toolResult?.isError;
|
|
337
|
+
});
|
|
338
|
+
const hasTurnError = Boolean(turn?.error || turn?.reason?.kind === 'error');
|
|
339
|
+
const isToolFailure = hasStepError || hasTurnError;
|
|
340
|
+
|
|
341
|
+
if (isToolFailure) {
|
|
342
|
+
const failCount = engine.incrementToolFailureCount(sid);
|
|
343
|
+
const limit = liveConfig.consecutiveToolFailureLimit;
|
|
344
|
+
if (limit > 0 && failCount >= limit) {
|
|
345
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
346
|
+
const pauseReason = lang === 'zh'
|
|
347
|
+
? `工具重复出错(连续 ${failCount} 次)— 目标已暂停以防止死循环`
|
|
348
|
+
: `Repeated tool failure (${failCount} consecutive) — goal paused to prevent token burn`;
|
|
349
|
+
engine.pause(pauseReason, sid);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
engine.resetToolFailureCount(sid);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
361
|
+
const stallCount = engine.incrementStallCount(sid);
|
|
362
|
+
if (stallCount >= 2) {
|
|
363
|
+
const stallReason = lang === 'zh'
|
|
364
|
+
? '智能体在过去 2 次迭代中未推进工作计划 — 需要操作员关注'
|
|
365
|
+
: 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
|
|
366
|
+
engine.pause(stallReason, sid);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const canContinue = engine.incrementIteration(sid);
|
|
371
|
+
if (!canContinue) return;
|
|
372
|
+
|
|
373
|
+
const triggerNextTurn = () => {
|
|
374
|
+
const currentSnap = engine.getSnapshot(sid);
|
|
375
|
+
if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
|
|
376
|
+
const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
|
|
377
|
+
let promptText = currentLang === 'zh'
|
|
378
|
+
? '请按照工作计划继续自主执行目标。每完成一步请通过 goal_update_progress 记录,所有任务完成后调用 goal_finish。'
|
|
379
|
+
: 'Continue autonomous goal execution according to the work plan. Mark each completed step via goal_update_progress, and when all tasks are complete, call goal_finish.';
|
|
380
|
+
|
|
381
|
+
if (currentSnap.pendingNudge) {
|
|
382
|
+
const userNudge = engine.consumePendingNudge(sid);
|
|
383
|
+
if (userNudge) {
|
|
384
|
+
promptText = (currentLang === 'zh'
|
|
385
|
+
? `🚨 用户紧急说明/指导方向:\n"${userNudge}"\n请根据此反馈调整执行。\n\n`
|
|
386
|
+
: `🚨 URGENT USER CLARIFICATION / STEERING:\n"${userNudge}"\nAdjust execution adhering to this feedback.\n\n`) + promptText;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const promptMsg = createGoalUserMessage(promptText);
|
|
391
|
+
let target = sessionAgents.get(sid);
|
|
392
|
+
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
393
|
+
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
394
|
+
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
395
|
+
target = lastActiveAgent;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!target && agentsService && typeof agentsService.list === 'function') {
|
|
400
|
+
const list = agentsService.list();
|
|
401
|
+
if (list && list.length > 0) {
|
|
402
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
403
|
+
const cand = list[i];
|
|
404
|
+
if (sessionIdOf(cand, null) === sid) {
|
|
405
|
+
target = cand;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!target && sid === 'default') target = list[list.length - 1];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (target && typeof target.followup === 'function' && target.status !== 'stopped') {
|
|
414
|
+
try {
|
|
415
|
+
target.followup(promptMsg);
|
|
416
|
+
sessionAgents.set(sid, target);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.warn('[dsh-goal] Failed auto-drive followup:', err);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
if (typeof setImmediate === 'function') {
|
|
425
|
+
setImmediate(triggerNextTurn);
|
|
426
|
+
} else {
|
|
427
|
+
setTimeout(triggerNextTurn, 0);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const onApprovalAsked = (event) => {
|
|
433
|
+
const sid = sessionIdOf(event, 'default');
|
|
434
|
+
const snap = engine.getSnapshot(sid);
|
|
435
|
+
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
436
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
437
|
+
const pauseReason = lang === 'zh'
|
|
438
|
+
? '等待操作员确认/批准操作'
|
|
439
|
+
: 'Waiting for operator confirmation/approval';
|
|
440
|
+
engine.pause(pauseReason, sid);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
ctx.on?.('turn/end', onTurnEnd);
|
|
445
|
+
ctx.on?.('approval/asked', onApprovalAsked);
|
|
446
|
+
|
|
447
|
+
return () => {
|
|
448
|
+
ctx.off?.('turn/end', onTurnEnd);
|
|
449
|
+
ctx.off?.('approval/asked', onApprovalAsked);
|
|
450
|
+
};
|
|
451
|
+
}, 'dsh-goal: Session & Turn Coordinator');
|
|
452
|
+
}
|