@goodandready/dsh-goal 0.1.4 → 0.1.5
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/docs/design/DESIGN.md +7 -0
- package/lib/client.js +1422 -1305
- package/lib/goal-engine.js +584 -536
- package/lib/index.js +593 -567
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,567 +1,593 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
import { GoalEngine, GoalState, MilestoneStatus } from './goal-engine.js';
|
|
5
|
-
import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
|
|
6
|
-
|
|
7
|
-
export const name = '@goodandready/dsh-goal';
|
|
8
|
-
export const inject = ['webServer', 'settings'];
|
|
9
|
-
|
|
10
|
-
const NS = 'dsh-goal';
|
|
11
|
-
|
|
12
|
-
function sessionIdOf(invocationOrReq, fallback = 'default') {
|
|
13
|
-
if (!invocationOrReq) return fallback;
|
|
14
|
-
try {
|
|
15
|
-
// 1. DSH invocation / event / turn
|
|
16
|
-
if (invocationOrReq.sessionId) return String(invocationOrReq.sessionId);
|
|
17
|
-
if (invocationOrReq.session) {
|
|
18
|
-
return String(invocationOrReq.session.id || invocationOrReq.session.header?.id || fallback);
|
|
19
|
-
}
|
|
20
|
-
if (invocationOrReq.data?.sessionId) return String(invocationOrReq.data.sessionId);
|
|
21
|
-
if (invocationOrReq.agent?.session) {
|
|
22
|
-
return String(invocationOrReq.agent.session.id || invocationOrReq.agent.session.header?.id || fallback);
|
|
23
|
-
}
|
|
24
|
-
// 2. HTTP Request (req)
|
|
25
|
-
if (invocationOrReq.headers) {
|
|
26
|
-
const headerSid = invocationOrReq.headers['x-dsh-session-id'];
|
|
27
|
-
if (headerSid) return String(headerSid);
|
|
28
|
-
if (invocationOrReq.url) {
|
|
29
|
-
const url = new URL(invocationOrReq.url, 'http://localhost');
|
|
30
|
-
const querySid = url.searchParams.get('sessionId') || url.searchParams.get('session');
|
|
31
|
-
if (querySid) return String(querySid);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
} catch (_) {}
|
|
35
|
-
return fallback;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Схема-функция: в rc.1 settings.register(NS, schema, { base }) ожидает
|
|
39
|
-
// schemastery-схему вторым аргументом.
|
|
40
|
-
// Issue #24: storagePath объявлен в схеме конфигурации плагина
|
|
41
|
-
export const Config = z.object({
|
|
42
|
-
maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
|
|
43
|
-
autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
|
|
44
|
-
enableSound: z.boolean().default(true).description('Play a sound when a goal completes'),
|
|
45
|
-
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
export function apply(ctx, config = {}) {
|
|
49
|
-
let settingsScope = null;
|
|
50
|
-
let agentsService = null;
|
|
51
|
-
const runningAgents = new Set();
|
|
52
|
-
const sessionAgents = new Map(); // sessionId -> agent
|
|
53
|
-
let lastActiveAgent = null;
|
|
54
|
-
|
|
55
|
-
let currentSettings = {
|
|
56
|
-
maxIterations: config?.maxIterations ?? 25,
|
|
57
|
-
autoDrive: config?.autoDrive ?? true,
|
|
58
|
-
enableSound: config?.enableSound ?? true,
|
|
59
|
-
storagePath: config?.storagePath,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
63
|
-
const storagePath = currentSettings.storagePath !== undefined && currentSettings.storagePath !== null
|
|
64
|
-
? currentSettings.storagePath
|
|
65
|
-
: path.join(defaultStorageDir, 'dsh-goal-state.json');
|
|
66
|
-
|
|
67
|
-
const engine = new GoalEngine({
|
|
68
|
-
defaultMaxIterations: currentSettings.maxIterations,
|
|
69
|
-
autoDrive: currentSettings.autoDrive,
|
|
70
|
-
enableSound: currentSettings.enableSound,
|
|
71
|
-
storagePath,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Динамическое внедрение сервиса agents для управления жизненным циклом
|
|
75
|
-
ctx.inject(['agents'], (actx) => {
|
|
76
|
-
agentsService = actx.agents;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Отслеживание выполняющихся агентов через глобальное событие ядра DSH
|
|
80
|
-
ctx.on?.('agent/status', ({ agent, status }) => {
|
|
81
|
-
if (status === 'running') {
|
|
82
|
-
runningAgents.add(agent);
|
|
83
|
-
lastActiveAgent = agent;
|
|
84
|
-
const sid = sessionIdOf(agent, null);
|
|
85
|
-
if (sid) {
|
|
86
|
-
sessionAgents.set(sid, agent);
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
runningAgents.delete(agent);
|
|
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
|
-
const
|
|
417
|
-
if (origin &&
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import z from '@deepseek-ai/schemastery';
|
|
4
|
+
import { GoalEngine, GoalState, MilestoneStatus } from './goal-engine.js';
|
|
5
|
+
import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
|
|
6
|
+
|
|
7
|
+
export const name = '@goodandready/dsh-goal';
|
|
8
|
+
export const inject = ['webServer', 'settings'];
|
|
9
|
+
|
|
10
|
+
const NS = 'dsh-goal';
|
|
11
|
+
|
|
12
|
+
function sessionIdOf(invocationOrReq, fallback = 'default') {
|
|
13
|
+
if (!invocationOrReq) return fallback;
|
|
14
|
+
try {
|
|
15
|
+
// 1. DSH invocation / event / turn
|
|
16
|
+
if (invocationOrReq.sessionId) return String(invocationOrReq.sessionId);
|
|
17
|
+
if (invocationOrReq.session) {
|
|
18
|
+
return String(invocationOrReq.session.id || invocationOrReq.session.header?.id || fallback);
|
|
19
|
+
}
|
|
20
|
+
if (invocationOrReq.data?.sessionId) return String(invocationOrReq.data.sessionId);
|
|
21
|
+
if (invocationOrReq.agent?.session) {
|
|
22
|
+
return String(invocationOrReq.agent.session.id || invocationOrReq.agent.session.header?.id || fallback);
|
|
23
|
+
}
|
|
24
|
+
// 2. HTTP Request (req)
|
|
25
|
+
if (invocationOrReq.headers) {
|
|
26
|
+
const headerSid = invocationOrReq.headers['x-dsh-session-id'];
|
|
27
|
+
if (headerSid) return String(headerSid);
|
|
28
|
+
if (invocationOrReq.url) {
|
|
29
|
+
const url = new URL(invocationOrReq.url, 'http://localhost');
|
|
30
|
+
const querySid = url.searchParams.get('sessionId') || url.searchParams.get('session');
|
|
31
|
+
if (querySid) return String(querySid);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Схема-функция: в rc.1 settings.register(NS, schema, { base }) ожидает
|
|
39
|
+
// schemastery-схему вторым аргументом.
|
|
40
|
+
// Issue #24: storagePath объявлен в схеме конфигурации плагина
|
|
41
|
+
export const Config = z.object({
|
|
42
|
+
maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
|
|
43
|
+
autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
|
|
44
|
+
enableSound: z.boolean().default(true).description('Play a sound when a goal completes'),
|
|
45
|
+
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function apply(ctx, config = {}) {
|
|
49
|
+
let settingsScope = null;
|
|
50
|
+
let agentsService = null;
|
|
51
|
+
const runningAgents = new Set();
|
|
52
|
+
const sessionAgents = new Map(); // sessionId -> agent
|
|
53
|
+
let lastActiveAgent = null;
|
|
54
|
+
|
|
55
|
+
let currentSettings = {
|
|
56
|
+
maxIterations: config?.maxIterations ?? 25,
|
|
57
|
+
autoDrive: config?.autoDrive ?? true,
|
|
58
|
+
enableSound: config?.enableSound ?? true,
|
|
59
|
+
storagePath: config?.storagePath,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
63
|
+
const storagePath = currentSettings.storagePath !== undefined && currentSettings.storagePath !== null
|
|
64
|
+
? currentSettings.storagePath
|
|
65
|
+
: path.join(defaultStorageDir, 'dsh-goal-state.json');
|
|
66
|
+
|
|
67
|
+
const engine = new GoalEngine({
|
|
68
|
+
defaultMaxIterations: currentSettings.maxIterations,
|
|
69
|
+
autoDrive: currentSettings.autoDrive,
|
|
70
|
+
enableSound: currentSettings.enableSound,
|
|
71
|
+
storagePath,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Динамическое внедрение сервиса agents для управления жизненным циклом
|
|
75
|
+
ctx.inject(['agents'], (actx) => {
|
|
76
|
+
agentsService = actx.agents;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Отслеживание выполняющихся агентов через глобальное событие ядра DSH
|
|
80
|
+
ctx.on?.('agent/status', ({ agent, status }) => {
|
|
81
|
+
if (status === 'running') {
|
|
82
|
+
runningAgents.add(agent);
|
|
83
|
+
lastActiveAgent = agent;
|
|
84
|
+
const sid = sessionIdOf(agent, null);
|
|
85
|
+
if (sid) {
|
|
86
|
+
sessionAgents.set(sid, agent);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
runningAgents.delete(agent);
|
|
90
|
+
if (lastActiveAgent === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
|
|
91
|
+
lastActiveAgent = null;
|
|
92
|
+
}
|
|
93
|
+
const sid = sessionIdOf(agent, null);
|
|
94
|
+
if (sid && sessionAgents.get(sid) === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
|
|
95
|
+
sessionAgents.delete(sid);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const stopRunningAgents = (sessionId = 'default') => {
|
|
101
|
+
const sid = sessionId || 'default';
|
|
102
|
+
const specificAgent = sessionAgents.get(sid);
|
|
103
|
+
if (specificAgent && typeof specificAgent.cancel === 'function') {
|
|
104
|
+
try {
|
|
105
|
+
specificAgent.cancel({ kind: 'user' });
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('[dsh-goal] Failed to cancel specific agent for session:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Отмена всех подходящих агентов в runningAgents
|
|
112
|
+
for (const ag of runningAgents) {
|
|
113
|
+
try {
|
|
114
|
+
const agSid = sessionIdOf(ag, null);
|
|
115
|
+
if (!agSid || agSid === sid || sid === 'default') {
|
|
116
|
+
if (typeof ag.cancel === 'function') {
|
|
117
|
+
ag.cancel({ kind: 'user' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn('[dsh-goal] Failed to cancel running agent:', err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (lastActiveAgent && lastActiveAgent.status === 'running' && typeof lastActiveAgent.cancel === 'function') {
|
|
126
|
+
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
127
|
+
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
128
|
+
try {
|
|
129
|
+
lastActiveAgent.cancel({ kind: 'user' });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn('[dsh-goal] Failed to cancel lastActiveAgent:', err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const resumeActiveAgent = (promptText, sessionId = 'default') => {
|
|
138
|
+
const sid = sessionId || 'default';
|
|
139
|
+
const resumeMsg = createGoalUserMessage(
|
|
140
|
+
promptText || '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.',
|
|
141
|
+
);
|
|
142
|
+
let target = sessionAgents.get(sid);
|
|
143
|
+
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
144
|
+
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
145
|
+
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
146
|
+
target = lastActiveAgent;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!target && agentsService && typeof agentsService.list === 'function') {
|
|
150
|
+
const list = agentsService.list();
|
|
151
|
+
if (list && list.length > 0) {
|
|
152
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
153
|
+
const cand = list[i];
|
|
154
|
+
const candSid = sessionIdOf(cand, null);
|
|
155
|
+
if (candSid === sid) {
|
|
156
|
+
target = cand;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!target && sid === 'default') {
|
|
161
|
+
target = list[list.length - 1];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (target && typeof target.followup === 'function') {
|
|
167
|
+
try {
|
|
168
|
+
target.followup(resumeMsg);
|
|
169
|
+
lastActiveAgent = target;
|
|
170
|
+
sessionAgents.set(sid, target);
|
|
171
|
+
return true;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.warn('[dsh-goal] Failed to resume agent:', err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Issue #21: getConfig reads snap.value ONLY when status is 'ready' (or status is undefined in unit test mocks)
|
|
180
|
+
const getConfig = () => {
|
|
181
|
+
if (settingsScope) {
|
|
182
|
+
const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
|
|
183
|
+
if (snap) {
|
|
184
|
+
if (snap.status === 'ready' || snap.status === undefined) {
|
|
185
|
+
const live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
|
|
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
|
+
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else if (typeof settingsScope.get === 'function') {
|
|
196
|
+
const live = settingsScope.get();
|
|
197
|
+
if (live && typeof live === 'object') {
|
|
198
|
+
return {
|
|
199
|
+
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
200
|
+
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
201
|
+
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
202
|
+
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return currentSettings;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const applySettings = () => {
|
|
211
|
+
const live = getConfig();
|
|
212
|
+
engine.updateConfig({
|
|
213
|
+
defaultMaxIterations: live.maxIterations,
|
|
214
|
+
autoDrive: live.autoDrive,
|
|
215
|
+
enableSound: live.enableSound,
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// 1. Регистрация настроек плагина
|
|
220
|
+
ctx.inject(['settings'], (sctx) => {
|
|
221
|
+
try {
|
|
222
|
+
const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
|
|
223
|
+
if (scope) {
|
|
224
|
+
settingsScope = scope;
|
|
225
|
+
applySettings();
|
|
226
|
+
if (typeof scope.subscribe === 'function') {
|
|
227
|
+
sctx.effect(() => {
|
|
228
|
+
const off = scope.subscribe(() => {
|
|
229
|
+
applySettings();
|
|
230
|
+
});
|
|
231
|
+
return () => {
|
|
232
|
+
if (typeof off === 'function') off();
|
|
233
|
+
settingsScope = null;
|
|
234
|
+
};
|
|
235
|
+
}, 'dsh-goal: settings subscription');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.warn('[dsh-goal] Settings register skipped:', err.message);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// 2. Регистрация слэш-команды /goal в чате
|
|
244
|
+
ctx.inject(['commands'], (cctx) => {
|
|
245
|
+
try {
|
|
246
|
+
if (typeof cctx.commands?.register !== 'function') return;
|
|
247
|
+
|
|
248
|
+
const unregister = cctx.commands.register({
|
|
249
|
+
name: 'goal',
|
|
250
|
+
description: 'Управление режимом цели (Goal Mode): активация, вехи, пауза, сброс',
|
|
251
|
+
input: { hint: '[<цель>|clear|pause|resume]' },
|
|
252
|
+
handler: async (invocation) => {
|
|
253
|
+
const sid = sessionIdOf(invocation, 'default');
|
|
254
|
+
if (invocation?.agent) {
|
|
255
|
+
lastActiveAgent = invocation.agent;
|
|
256
|
+
sessionAgents.set(sid, invocation.agent);
|
|
257
|
+
}
|
|
258
|
+
const parsed = parseGoalInput(invocation?.rawInput);
|
|
259
|
+
const liveConfig = getConfig();
|
|
260
|
+
return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (typeof unregister === 'function') {
|
|
265
|
+
cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.warn('[dsh-goal] Commands register skipped:', err.message);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// 3. Инжекция контекста цели в системный промпт (systemPrompt)
|
|
273
|
+
ctx.inject(['systemPrompt'], (pctx) => {
|
|
274
|
+
try {
|
|
275
|
+
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
276
|
+
const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
|
|
277
|
+
? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 600)
|
|
278
|
+
: 600;
|
|
279
|
+
|
|
280
|
+
const unregisterSection = pctx.systemPrompt.section({
|
|
281
|
+
name: 'tool:dsh-goal',
|
|
282
|
+
order,
|
|
283
|
+
text: (sessionCtx) => {
|
|
284
|
+
const sid = sessionIdOf(sessionCtx, 'default');
|
|
285
|
+
return engine.getStatePromptInjection(sid);
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (typeof unregisterSection === 'function') {
|
|
290
|
+
pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// 4. Регистрация инструментов для модели (tools)
|
|
299
|
+
ctx.inject(['tools'], (tctx) => {
|
|
300
|
+
if (!tctx.tools?.register) return;
|
|
301
|
+
|
|
302
|
+
// Инструмент 1: Декомпозиция цели на вехи
|
|
303
|
+
tctx.tools.register({
|
|
304
|
+
name: 'goal_set_milestones',
|
|
305
|
+
description: 'Break down the current active goal into a sequence of concrete milestones/sub-tasks.',
|
|
306
|
+
parameters: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
milestones: {
|
|
310
|
+
type: 'array',
|
|
311
|
+
items: { type: 'string' },
|
|
312
|
+
description: 'List of milestone titles to accomplish.',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
required: ['milestones'],
|
|
316
|
+
},
|
|
317
|
+
handler: async ({ milestones }, toolCtx) => {
|
|
318
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
319
|
+
const snap = engine.getSnapshot(sid);
|
|
320
|
+
if (!snap.hasActiveGoal) {
|
|
321
|
+
return { error: 'No active goal currently set. Start a goal first.' };
|
|
322
|
+
}
|
|
323
|
+
engine.addMilestones(milestones, true, sid);
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
milestones: engine.getSnapshot(sid).milestones,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Инструмент 2: Обновление статуса вехи
|
|
332
|
+
tctx.tools.register({
|
|
333
|
+
name: 'goal_update_progress',
|
|
334
|
+
description: 'Update the status of a specific goal milestone and optionally log progress notes.',
|
|
335
|
+
parameters: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
properties: {
|
|
338
|
+
milestone_id: {
|
|
339
|
+
type: 'string',
|
|
340
|
+
description: 'The ID of the milestone (e.g. "m-1", "m-2").',
|
|
341
|
+
},
|
|
342
|
+
status: {
|
|
343
|
+
type: 'string',
|
|
344
|
+
enum: ['pending', 'in_progress', 'completed', 'failed'],
|
|
345
|
+
description: 'New status for this milestone.',
|
|
346
|
+
},
|
|
347
|
+
notes: {
|
|
348
|
+
type: 'string',
|
|
349
|
+
description: 'Brief summary of what was accomplished or why it failed.',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
required: ['milestone_id', 'status'],
|
|
353
|
+
},
|
|
354
|
+
handler: async ({ milestone_id, status, notes }, toolCtx) => {
|
|
355
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
356
|
+
const ok = engine.updateMilestone(milestone_id, status, notes, sid);
|
|
357
|
+
if (!ok) {
|
|
358
|
+
return { error: `Milestone ${milestone_id} not found or no active goal.` };
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
success: true,
|
|
362
|
+
snapshot: engine.getSnapshot(sid),
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Инструмент 3: Успешное завершение цели
|
|
368
|
+
tctx.tools.register({
|
|
369
|
+
name: 'goal_finish',
|
|
370
|
+
description: 'Conclude the active goal successfully with a final summary and achievements.',
|
|
371
|
+
parameters: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
properties: {
|
|
374
|
+
summary: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
description: 'Final summary of the goal outcome and deliverables.',
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
required: ['summary'],
|
|
380
|
+
},
|
|
381
|
+
handler: async ({ summary }, toolCtx) => {
|
|
382
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
383
|
+
const snap = engine.completeGoal(summary, sid);
|
|
384
|
+
return {
|
|
385
|
+
success: true,
|
|
386
|
+
completed: true,
|
|
387
|
+
summary,
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// 5. Регистрация HTTP REST API маршрутов
|
|
394
|
+
ctx.effect(() => {
|
|
395
|
+
if (!ctx.webServer?.register) return () => {};
|
|
396
|
+
|
|
397
|
+
const unreg = ctx.webServer.register({
|
|
398
|
+
kind: 'prefix',
|
|
399
|
+
path: '/dsh-goal',
|
|
400
|
+
handler: (req, res) => {
|
|
401
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
402
|
+
const pathname = url.pathname;
|
|
403
|
+
|
|
404
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
405
|
+
|
|
406
|
+
// GET /dsh-goal/state
|
|
407
|
+
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
408
|
+
const sid = sessionIdOf(req, 'default');
|
|
409
|
+
res.statusCode = 200;
|
|
410
|
+
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// POST /dsh-goal/action
|
|
414
|
+
// Issue #22: CSRF & Same-Origin guard
|
|
415
|
+
if (req.method === 'POST' && (pathname === '/dsh-goal/action' || pathname === '/dsh-goal/action/')) {
|
|
416
|
+
const secFetchSite = req.headers['sec-fetch-site'];
|
|
417
|
+
if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'same-site' && secFetchSite !== 'none') {
|
|
418
|
+
res.statusCode = 403;
|
|
419
|
+
return res.end(JSON.stringify({ error: 'Forbidden: cross-site requests are rejected' }));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const origin = req.headers.origin;
|
|
423
|
+
const host = req.headers.host;
|
|
424
|
+
if (origin && host) {
|
|
425
|
+
try {
|
|
426
|
+
const originHost = new URL(origin).host;
|
|
427
|
+
if (originHost !== host) {
|
|
428
|
+
res.statusCode = 403;
|
|
429
|
+
return res.end(JSON.stringify({ error: 'Forbidden: origin mismatch' }));
|
|
430
|
+
}
|
|
431
|
+
} catch (_) {
|
|
432
|
+
res.statusCode = 403;
|
|
433
|
+
return res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let body = '';
|
|
438
|
+
let bodySize = 0;
|
|
439
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
440
|
+
let limitExceeded = false;
|
|
441
|
+
|
|
442
|
+
req.on('data', (chunk) => {
|
|
443
|
+
bodySize += chunk.length;
|
|
444
|
+
if (bodySize > MAX_PAYLOAD_BYTES) {
|
|
445
|
+
limitExceeded = true;
|
|
446
|
+
req.pause();
|
|
447
|
+
res.statusCode = 413;
|
|
448
|
+
return res.end(JSON.stringify({ error: 'Payload too large: max 256 KB allowed' }));
|
|
449
|
+
}
|
|
450
|
+
body += chunk;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
req.on('end', () => {
|
|
454
|
+
if (limitExceeded) return;
|
|
455
|
+
try {
|
|
456
|
+
const data = JSON.parse(body || '{}');
|
|
457
|
+
const sid = data.sessionId || sessionIdOf(req, 'default');
|
|
458
|
+
const { action, title, description, reason, milestoneId, status, notes } = data;
|
|
459
|
+
|
|
460
|
+
let result = null;
|
|
461
|
+
switch (action) {
|
|
462
|
+
case 'start':
|
|
463
|
+
result = engine.startGoal(title || 'Новая цель', {
|
|
464
|
+
description,
|
|
465
|
+
maxIterations: getConfig().maxIterations,
|
|
466
|
+
}, sid);
|
|
467
|
+
break;
|
|
468
|
+
case 'pause':
|
|
469
|
+
result = engine.pause(reason || 'Пауза по кнопке интерфейса', sid);
|
|
470
|
+
stopRunningAgents(sid);
|
|
471
|
+
break;
|
|
472
|
+
case 'resume':
|
|
473
|
+
result = engine.resume(sid);
|
|
474
|
+
resumeActiveAgent(undefined, sid);
|
|
475
|
+
break;
|
|
476
|
+
case 'cancel':
|
|
477
|
+
result = engine.cancel(reason || 'Отмена цели', sid);
|
|
478
|
+
stopRunningAgents(sid);
|
|
479
|
+
break;
|
|
480
|
+
case 'clear':
|
|
481
|
+
result = engine.clear(sid);
|
|
482
|
+
stopRunningAgents(sid);
|
|
483
|
+
sessionAgents.delete(sid);
|
|
484
|
+
break;
|
|
485
|
+
case 'update_milestone':
|
|
486
|
+
if (!milestoneId || !status) {
|
|
487
|
+
res.statusCode = 400;
|
|
488
|
+
return res.end(JSON.stringify({ error: 'milestoneId and status are required' }));
|
|
489
|
+
}
|
|
490
|
+
engine.updateMilestone(milestoneId, status, notes, sid);
|
|
491
|
+
result = engine.getSnapshot(sid);
|
|
492
|
+
break;
|
|
493
|
+
default:
|
|
494
|
+
res.statusCode = 400;
|
|
495
|
+
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
res.statusCode = 200;
|
|
499
|
+
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot(sid) }));
|
|
500
|
+
} catch (parseErr) {
|
|
501
|
+
res.statusCode = 400;
|
|
502
|
+
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
res.statusCode = 404;
|
|
509
|
+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return () => {
|
|
514
|
+
if (typeof unreg === 'function') unreg();
|
|
515
|
+
};
|
|
516
|
+
}, 'dsh-goal: HTTP WebServer Routes');
|
|
517
|
+
|
|
518
|
+
// 6. Подписка на события сессии (автономный цикл)
|
|
519
|
+
ctx.effect(() => {
|
|
520
|
+
// Подписка на завершение turn
|
|
521
|
+
const onTurnEnd = (turn) => {
|
|
522
|
+
const liveConfig = getConfig();
|
|
523
|
+
if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
|
|
524
|
+
|
|
525
|
+
const sid = sessionIdOf(turn, 'default');
|
|
526
|
+
const snap = engine.getSnapshot(sid);
|
|
527
|
+
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
528
|
+
// Проверяем причину завершения хода
|
|
529
|
+
if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const canContinue = engine.incrementIteration(sid);
|
|
534
|
+
if (!canContinue) return;
|
|
535
|
+
|
|
536
|
+
setTimeout(() => {
|
|
537
|
+
const currentSnap = engine.getSnapshot(sid);
|
|
538
|
+
if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
|
|
539
|
+
const promptMsg = createGoalUserMessage(
|
|
540
|
+
'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.',
|
|
541
|
+
);
|
|
542
|
+
let target = sessionAgents.get(sid);
|
|
543
|
+
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
544
|
+
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
545
|
+
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
546
|
+
target = lastActiveAgent;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (!target && agentsService && typeof agentsService.list === 'function') {
|
|
550
|
+
const list = agentsService.list();
|
|
551
|
+
if (list && list.length > 0) {
|
|
552
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
553
|
+
const cand = list[i];
|
|
554
|
+
if (sessionIdOf(cand, null) === sid) {
|
|
555
|
+
target = cand;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (!target && sid === 'default') target = list[list.length - 1];
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (target && typeof target.followup === 'function' && target.status !== 'stopped') {
|
|
564
|
+
try {
|
|
565
|
+
target.followup(promptMsg);
|
|
566
|
+
sessionAgents.set(sid, target);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.warn('[dsh-goal] Failed auto-drive followup:', err);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}, 300);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// Подписка на запрос подтверждения (approval/asked) -> автоматическая пауза
|
|
577
|
+
const onApprovalAsked = (event) => {
|
|
578
|
+
const sid = sessionIdOf(event, 'default');
|
|
579
|
+
const snap = engine.getSnapshot(sid);
|
|
580
|
+
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
581
|
+
engine.pause('Ожидание подтверждения действия оператором', sid);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
ctx.on?.('turn/end', onTurnEnd);
|
|
586
|
+
ctx.on?.('approval/asked', onApprovalAsked);
|
|
587
|
+
|
|
588
|
+
return () => {
|
|
589
|
+
ctx.off?.('turn/end', onTurnEnd);
|
|
590
|
+
ctx.off?.('approval/asked', onApprovalAsked);
|
|
591
|
+
};
|
|
592
|
+
}, 'dsh-goal: Session & Turn Coordinator');
|
|
593
|
+
}
|