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