@goodandready/dsh-goal 0.1.3 → 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 +400 -213
- package/lib/command-handler.js +192 -189
- package/lib/goal-engine.js +255 -104
- package/lib/index.js +182 -78
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -9,6 +9,32 @@ export const inject = ['webServer', 'settings'];
|
|
|
9
9
|
|
|
10
10
|
const NS = 'dsh-goal';
|
|
11
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
|
+
|
|
12
38
|
// Схема-функция: в rc.1 settings.register(NS, schema, { base }) ожидает
|
|
13
39
|
// schemastery-схему вторым аргументом.
|
|
14
40
|
// Issue #24: storagePath объявлен в схеме конфигурации плагина
|
|
@@ -23,6 +49,7 @@ export function apply(ctx, config = {}) {
|
|
|
23
49
|
let settingsScope = null;
|
|
24
50
|
let agentsService = null;
|
|
25
51
|
const runningAgents = new Set();
|
|
52
|
+
const sessionAgents = new Map(); // sessionId -> agent
|
|
26
53
|
let lastActiveAgent = null;
|
|
27
54
|
|
|
28
55
|
let currentSettings = {
|
|
@@ -54,60 +81,85 @@ export function apply(ctx, config = {}) {
|
|
|
54
81
|
if (status === 'running') {
|
|
55
82
|
runningAgents.add(agent);
|
|
56
83
|
lastActiveAgent = agent;
|
|
84
|
+
const sid = sessionIdOf(agent, null);
|
|
85
|
+
if (sid) {
|
|
86
|
+
sessionAgents.set(sid, agent);
|
|
87
|
+
}
|
|
57
88
|
} else {
|
|
58
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
|
+
}
|
|
59
97
|
}
|
|
60
98
|
});
|
|
61
99
|
|
|
62
|
-
const stopRunningAgents = () => {
|
|
63
|
-
|
|
64
|
-
|
|
100
|
+
const stopRunningAgents = (sessionId = 'default') => {
|
|
101
|
+
const sid = sessionId || 'default';
|
|
102
|
+
const specificAgent = sessionAgents.get(sid);
|
|
103
|
+
if (specificAgent && typeof specificAgent.cancel === 'function') {
|
|
65
104
|
try {
|
|
66
|
-
|
|
67
|
-
ag.cancel({ kind: 'user' });
|
|
68
|
-
}
|
|
105
|
+
specificAgent.cancel({ kind: 'user' });
|
|
69
106
|
} catch (err) {
|
|
70
|
-
console.warn('[dsh-goal] Failed to cancel
|
|
107
|
+
console.warn('[dsh-goal] Failed to cancel specific agent for session:', err);
|
|
71
108
|
}
|
|
72
109
|
}
|
|
73
|
-
runningAgents.clear();
|
|
74
110
|
|
|
75
|
-
//
|
|
76
|
-
|
|
111
|
+
// Отмена всех подходящих агентов в runningAgents
|
|
112
|
+
for (const ag of runningAgents) {
|
|
77
113
|
try {
|
|
78
|
-
|
|
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
|
+
}
|
|
79
120
|
} catch (err) {
|
|
80
|
-
console.warn('[dsh-goal] Failed to cancel
|
|
121
|
+
console.warn('[dsh-goal] Failed to cancel running agent:', err);
|
|
81
122
|
}
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} catch (_) {}
|
|
92
|
-
}
|
|
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);
|
|
93
132
|
}
|
|
94
|
-
} catch (err) {
|
|
95
|
-
console.warn('[dsh-goal] Failed to cancel agents from list:', err);
|
|
96
133
|
}
|
|
97
134
|
}
|
|
98
135
|
};
|
|
99
136
|
|
|
100
|
-
const resumeActiveAgent = (promptText) => {
|
|
137
|
+
const resumeActiveAgent = (promptText, sessionId = 'default') => {
|
|
138
|
+
const sid = sessionId || 'default';
|
|
101
139
|
const resumeMsg = createGoalUserMessage(
|
|
102
140
|
promptText || '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.',
|
|
103
141
|
);
|
|
104
|
-
let target =
|
|
105
|
-
if (lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
106
|
-
|
|
107
|
-
|
|
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') {
|
|
108
150
|
const list = agentsService.list();
|
|
109
151
|
if (list && list.length > 0) {
|
|
110
|
-
|
|
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
|
+
}
|
|
111
163
|
}
|
|
112
164
|
}
|
|
113
165
|
|
|
@@ -115,6 +167,7 @@ export function apply(ctx, config = {}) {
|
|
|
115
167
|
try {
|
|
116
168
|
target.followup(resumeMsg);
|
|
117
169
|
lastActiveAgent = target;
|
|
170
|
+
sessionAgents.set(sid, target);
|
|
118
171
|
return true;
|
|
119
172
|
} catch (err) {
|
|
120
173
|
console.warn('[dsh-goal] Failed to resume agent:', err);
|
|
@@ -197,12 +250,14 @@ export function apply(ctx, config = {}) {
|
|
|
197
250
|
description: 'Управление режимом цели (Goal Mode): активация, вехи, пауза, сброс',
|
|
198
251
|
input: { hint: '[<цель>|clear|pause|resume]' },
|
|
199
252
|
handler: async (invocation) => {
|
|
253
|
+
const sid = sessionIdOf(invocation, 'default');
|
|
200
254
|
if (invocation?.agent) {
|
|
201
255
|
lastActiveAgent = invocation.agent;
|
|
256
|
+
sessionAgents.set(sid, invocation.agent);
|
|
202
257
|
}
|
|
203
258
|
const parsed = parseGoalInput(invocation?.rawInput);
|
|
204
259
|
const liveConfig = getConfig();
|
|
205
|
-
return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent);
|
|
260
|
+
return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
|
|
206
261
|
},
|
|
207
262
|
});
|
|
208
263
|
|
|
@@ -225,7 +280,10 @@ export function apply(ctx, config = {}) {
|
|
|
225
280
|
const unregisterSection = pctx.systemPrompt.section({
|
|
226
281
|
name: 'tool:dsh-goal',
|
|
227
282
|
order,
|
|
228
|
-
text: () =>
|
|
283
|
+
text: (sessionCtx) => {
|
|
284
|
+
const sid = sessionIdOf(sessionCtx, 'default');
|
|
285
|
+
return engine.getStatePromptInjection(sid);
|
|
286
|
+
},
|
|
229
287
|
});
|
|
230
288
|
|
|
231
289
|
if (typeof unregisterSection === 'function') {
|
|
@@ -256,15 +314,16 @@ export function apply(ctx, config = {}) {
|
|
|
256
314
|
},
|
|
257
315
|
required: ['milestones'],
|
|
258
316
|
},
|
|
259
|
-
handler: async ({ milestones }) => {
|
|
260
|
-
const
|
|
317
|
+
handler: async ({ milestones }, toolCtx) => {
|
|
318
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
319
|
+
const snap = engine.getSnapshot(sid);
|
|
261
320
|
if (!snap.hasActiveGoal) {
|
|
262
321
|
return { error: 'No active goal currently set. Start a goal first.' };
|
|
263
322
|
}
|
|
264
|
-
engine.addMilestones(milestones);
|
|
323
|
+
engine.addMilestones(milestones, true, sid);
|
|
265
324
|
return {
|
|
266
325
|
success: true,
|
|
267
|
-
milestones: engine.getSnapshot().milestones,
|
|
326
|
+
milestones: engine.getSnapshot(sid).milestones,
|
|
268
327
|
};
|
|
269
328
|
},
|
|
270
329
|
});
|
|
@@ -292,14 +351,15 @@ export function apply(ctx, config = {}) {
|
|
|
292
351
|
},
|
|
293
352
|
required: ['milestone_id', 'status'],
|
|
294
353
|
},
|
|
295
|
-
handler: async ({ milestone_id, status, notes }) => {
|
|
296
|
-
const
|
|
354
|
+
handler: async ({ milestone_id, status, notes }, toolCtx) => {
|
|
355
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
356
|
+
const ok = engine.updateMilestone(milestone_id, status, notes, sid);
|
|
297
357
|
if (!ok) {
|
|
298
358
|
return { error: `Milestone ${milestone_id} not found or no active goal.` };
|
|
299
359
|
}
|
|
300
360
|
return {
|
|
301
361
|
success: true,
|
|
302
|
-
snapshot: engine.getSnapshot(),
|
|
362
|
+
snapshot: engine.getSnapshot(sid),
|
|
303
363
|
};
|
|
304
364
|
},
|
|
305
365
|
});
|
|
@@ -318,8 +378,9 @@ export function apply(ctx, config = {}) {
|
|
|
318
378
|
},
|
|
319
379
|
required: ['summary'],
|
|
320
380
|
},
|
|
321
|
-
handler: async ({ summary }) => {
|
|
322
|
-
const
|
|
381
|
+
handler: async ({ summary }, toolCtx) => {
|
|
382
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
383
|
+
const snap = engine.completeGoal(summary, sid);
|
|
323
384
|
return {
|
|
324
385
|
success: true,
|
|
325
386
|
completed: true,
|
|
@@ -344,8 +405,9 @@ export function apply(ctx, config = {}) {
|
|
|
344
405
|
|
|
345
406
|
// GET /dsh-goal/state
|
|
346
407
|
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
408
|
+
const sid = sessionIdOf(req, 'default');
|
|
347
409
|
res.statusCode = 200;
|
|
348
|
-
return res.end(JSON.stringify(engine.getSnapshot()));
|
|
410
|
+
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
349
411
|
}
|
|
350
412
|
|
|
351
413
|
// POST /dsh-goal/action
|
|
@@ -373,10 +435,26 @@ export function apply(ctx, config = {}) {
|
|
|
373
435
|
}
|
|
374
436
|
|
|
375
437
|
let body = '';
|
|
376
|
-
|
|
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
|
+
|
|
377
453
|
req.on('end', () => {
|
|
454
|
+
if (limitExceeded) return;
|
|
378
455
|
try {
|
|
379
456
|
const data = JSON.parse(body || '{}');
|
|
457
|
+
const sid = data.sessionId || sessionIdOf(req, 'default');
|
|
380
458
|
const { action, title, description, reason, milestoneId, status, notes } = data;
|
|
381
459
|
|
|
382
460
|
let result = null;
|
|
@@ -385,27 +463,32 @@ export function apply(ctx, config = {}) {
|
|
|
385
463
|
result = engine.startGoal(title || 'Новая цель', {
|
|
386
464
|
description,
|
|
387
465
|
maxIterations: getConfig().maxIterations,
|
|
388
|
-
});
|
|
466
|
+
}, sid);
|
|
389
467
|
break;
|
|
390
468
|
case 'pause':
|
|
391
|
-
result = engine.pause(reason || 'Пауза по кнопке интерфейса');
|
|
392
|
-
stopRunningAgents();
|
|
469
|
+
result = engine.pause(reason || 'Пауза по кнопке интерфейса', sid);
|
|
470
|
+
stopRunningAgents(sid);
|
|
393
471
|
break;
|
|
394
472
|
case 'resume':
|
|
395
|
-
result = engine.resume();
|
|
396
|
-
resumeActiveAgent();
|
|
473
|
+
result = engine.resume(sid);
|
|
474
|
+
resumeActiveAgent(undefined, sid);
|
|
397
475
|
break;
|
|
398
476
|
case 'cancel':
|
|
399
|
-
result = engine.cancel(reason || 'Отмена цели');
|
|
400
|
-
stopRunningAgents();
|
|
477
|
+
result = engine.cancel(reason || 'Отмена цели', sid);
|
|
478
|
+
stopRunningAgents(sid);
|
|
401
479
|
break;
|
|
402
480
|
case 'clear':
|
|
403
|
-
result = engine.clear();
|
|
404
|
-
stopRunningAgents();
|
|
481
|
+
result = engine.clear(sid);
|
|
482
|
+
stopRunningAgents(sid);
|
|
483
|
+
sessionAgents.delete(sid);
|
|
405
484
|
break;
|
|
406
485
|
case 'update_milestone':
|
|
407
|
-
|
|
408
|
-
|
|
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);
|
|
409
492
|
break;
|
|
410
493
|
default:
|
|
411
494
|
res.statusCode = 400;
|
|
@@ -413,7 +496,7 @@ export function apply(ctx, config = {}) {
|
|
|
413
496
|
}
|
|
414
497
|
|
|
415
498
|
res.statusCode = 200;
|
|
416
|
-
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot() }));
|
|
499
|
+
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot(sid) }));
|
|
417
500
|
} catch (parseErr) {
|
|
418
501
|
res.statusCode = 400;
|
|
419
502
|
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
@@ -439,42 +522,63 @@ export function apply(ctx, config = {}) {
|
|
|
439
522
|
const liveConfig = getConfig();
|
|
440
523
|
if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
|
|
441
524
|
|
|
442
|
-
const
|
|
525
|
+
const sid = sessionIdOf(turn, 'default');
|
|
526
|
+
const snap = engine.getSnapshot(sid);
|
|
443
527
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
444
|
-
|
|
528
|
+
// Проверяем причину завершения хода
|
|
529
|
+
if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const canContinue = engine.incrementIteration(sid);
|
|
445
534
|
if (!canContinue) return;
|
|
446
535
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (!
|
|
457
|
-
|
|
458
|
-
if (list && list.length > 0) target = list[list.length - 1];
|
|
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;
|
|
459
547
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
}
|
|
465
558
|
}
|
|
559
|
+
if (!target && sid === 'default') target = list[list.length - 1];
|
|
466
560
|
}
|
|
467
561
|
}
|
|
468
|
-
|
|
469
|
-
|
|
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);
|
|
470
573
|
}
|
|
471
574
|
};
|
|
472
575
|
|
|
473
576
|
// Подписка на запрос подтверждения (approval/asked) -> автоматическая пауза
|
|
474
|
-
const onApprovalAsked = () => {
|
|
475
|
-
const
|
|
577
|
+
const onApprovalAsked = (event) => {
|
|
578
|
+
const sid = sessionIdOf(event, 'default');
|
|
579
|
+
const snap = engine.getSnapshot(sid);
|
|
476
580
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
477
|
-
engine.pause('Ожидание подтверждения действия оператором');
|
|
581
|
+
engine.pause('Ожидание подтверждения действия оператором', sid);
|
|
478
582
|
}
|
|
479
583
|
};
|
|
480
584
|
|