@goodandready/dsh-goal 0.1.0 → 0.1.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/README.md +14 -0
- package/README.ru.md +14 -0
- package/docs/design/DESIGN.md +9 -5
- package/lib/card-form-state.js +110 -0
- package/lib/client.js +700 -159
- package/lib/command-handler.js +189 -0
- package/lib/goal-engine.js +86 -6
- package/lib/index.js +279 -13
- package/package.json +10 -6
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { GoalState } from './goal-engine.js';
|
|
3
|
+
|
|
4
|
+
export const USAGE = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Парсинг строки ввода для команды /goal
|
|
8
|
+
* @param {string} rawInput
|
|
9
|
+
* @returns {{ action: string, text?: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function parseGoalInput(rawInput = '') {
|
|
12
|
+
const input = String(rawInput || '').trim();
|
|
13
|
+
if (!input) {
|
|
14
|
+
return { action: 'show' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const lower = input.toLowerCase();
|
|
18
|
+
if (lower === 'clear') return { action: 'clear' };
|
|
19
|
+
if (lower === 'pause') return { action: 'pause' };
|
|
20
|
+
if (lower === 'resume') return { action: 'resume' };
|
|
21
|
+
|
|
22
|
+
return { action: 'start', text: input };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Создание валидного identified сообщения пользователя для отправки агенту через followup
|
|
27
|
+
* @param {string} text
|
|
28
|
+
* @returns {Object}
|
|
29
|
+
*/
|
|
30
|
+
export function createGoalUserMessage(text) {
|
|
31
|
+
return {
|
|
32
|
+
id: randomUUID(),
|
|
33
|
+
role: 'user',
|
|
34
|
+
content: [{ type: 'text', text }],
|
|
35
|
+
source: { kind: 'user' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Формирование стартового промпта с жестким требованием составить план работ
|
|
41
|
+
* @param {string} goalText
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function formatGoalStartPrompt(goalText) {
|
|
45
|
+
return `🎯 Активирован режим цели: "${goalText}"\n\n` +
|
|
46
|
+
`ОБЯЗАТЕЛЬНЫЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones и передай массив из 3-7 конкретных пунктов плана работ для достижения этой цели. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
47
|
+
`После формирования плана последовательно выполняй каждый пункт. Перед выполнением шага обновляй его статус на in_progress через goal_update_progress, а по завершении — на completed.\n` +
|
|
48
|
+
`Когда все пункты выполнены, вызови инструмент goal_finish.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Форматирование текстового ответа для UI и постановка/управление задачей агента
|
|
53
|
+
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
54
|
+
* @param {{ action: string, text?: string }} parsed
|
|
55
|
+
* @param {Object} [config]
|
|
56
|
+
* @param {Object} [agent]
|
|
57
|
+
* @returns {{ kind: 'success' | 'error', text: string }}
|
|
58
|
+
*/
|
|
59
|
+
export function executeGoalSlashCommand(engine, parsed, config = {}, agent = null) {
|
|
60
|
+
const snap = engine.getSnapshot();
|
|
61
|
+
|
|
62
|
+
switch (parsed.action) {
|
|
63
|
+
case 'show': {
|
|
64
|
+
if (!snap.hasActiveGoal) {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'success',
|
|
67
|
+
text: `🎯 Режим цели (Goal Mode): цель не установлена.\n${USAGE}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const milestonesInfo = snap.milestones.length > 0
|
|
72
|
+
? `\nПлан работ:\n` + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
|
|
73
|
+
: '';
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
kind: 'success',
|
|
77
|
+
text: `🎯 Цель: «${snap.title}»\n` +
|
|
78
|
+
`Статус: ${snap.state}\n` +
|
|
79
|
+
`Время: ${snap.formattedElapsed}\n` +
|
|
80
|
+
`Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
81
|
+
milestonesInfo +
|
|
82
|
+
`\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'clear': {
|
|
87
|
+
if (!snap.hasActiveGoal) {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'success',
|
|
90
|
+
text: 'Цель не была установлена.',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
engine.clear();
|
|
94
|
+
if (agent && typeof agent.cancel === 'function') {
|
|
95
|
+
try {
|
|
96
|
+
agent.cancel({ kind: 'user', reason: 'Goal cleared by user' });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn('[dsh-goal] Failed to cancel agent on clear:', err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
kind: 'success',
|
|
103
|
+
text: '🎯 Цель сброшена.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'pause': {
|
|
108
|
+
if (!snap.hasActiveGoal) {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'error',
|
|
111
|
+
text: `Нельзя приостановить: активная цель отсутствует.\n${USAGE}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (snap.state === GoalState.PAUSED) {
|
|
115
|
+
return {
|
|
116
|
+
kind: 'success',
|
|
117
|
+
text: '⏸ Цель уже на паузе.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
engine.pause('Пауза по команде пользователя (/goal pause)');
|
|
121
|
+
if (agent && typeof agent.cancel === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.warn('[dsh-goal] Failed to cancel agent on pause:', err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
kind: 'success',
|
|
130
|
+
text: `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'resume': {
|
|
135
|
+
if (!snap.hasActiveGoal) {
|
|
136
|
+
return {
|
|
137
|
+
kind: 'error',
|
|
138
|
+
text: `Нельзя возобновить: активная цель отсутствует.\n${USAGE}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (snap.state === GoalState.RUNNING) {
|
|
142
|
+
return {
|
|
143
|
+
kind: 'success',
|
|
144
|
+
text: '▶️ Цель уже выполняется.',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
engine.resume();
|
|
148
|
+
if (agent && typeof agent.followup === 'function') {
|
|
149
|
+
try {
|
|
150
|
+
const resumePrompt = `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`;
|
|
151
|
+
agent.followup(createGoalUserMessage(resumePrompt));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.warn('[dsh-goal] Failed to followup agent on resume:', err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
kind: 'success',
|
|
158
|
+
text: `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'start': {
|
|
163
|
+
const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
|
|
164
|
+
const newSnap = engine.startGoal(parsed.text, { maxIterations });
|
|
165
|
+
|
|
166
|
+
// Если команда вызвана в контексте агента, отправляем цель в очередь модели
|
|
167
|
+
if (agent && typeof agent.followup === 'function') {
|
|
168
|
+
try {
|
|
169
|
+
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text));
|
|
170
|
+
agent.followup(userMsg);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn('[dsh-goal] Failed to dispatch goal to agent followup:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
kind: 'success',
|
|
178
|
+
text: `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
|
|
179
|
+
`Для паузы: /goal pause | Для сброса: /goal clear`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
default:
|
|
184
|
+
return {
|
|
185
|
+
kind: 'error',
|
|
186
|
+
text: `Неизвестная команда. ${USAGE}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Изолированное ядро управления состоянием цели (Goal Engine).
|
|
3
7
|
* Не имеет внешних зависимостей, 100% тестируемо через node --test.
|
|
@@ -41,8 +45,45 @@ export function formatElapsed(totalSeconds) {
|
|
|
41
45
|
export class GoalEngine {
|
|
42
46
|
constructor(options = {}) {
|
|
43
47
|
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
48
|
+
this.autoDrive = options.autoDrive ?? true;
|
|
49
|
+
this.enableSound = options.enableSound ?? true;
|
|
44
50
|
this.currentGoal = null;
|
|
45
51
|
this.listeners = new Set();
|
|
52
|
+
|
|
53
|
+
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
54
|
+
this.storagePath = options.storagePath ?? null;
|
|
55
|
+
|
|
56
|
+
this.loadStateFromDisk();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
loadStateFromDisk() {
|
|
60
|
+
if (!this.storagePath) return;
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(this.storagePath)) {
|
|
63
|
+
const raw = fs.readFileSync(this.storagePath, 'utf8');
|
|
64
|
+
const data = JSON.parse(raw);
|
|
65
|
+
if (data && typeof data === 'object' && data.id && data.title) {
|
|
66
|
+
this.currentGoal = data;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn('[GoalEngine] Failed to load state from disk:', err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
saveStateToDisk() {
|
|
75
|
+
if (!this.storagePath) return;
|
|
76
|
+
try {
|
|
77
|
+
if (this.currentGoal) {
|
|
78
|
+
fs.writeFileSync(this.storagePath, JSON.stringify(this.currentGoal, null, 2), 'utf8');
|
|
79
|
+
} else {
|
|
80
|
+
if (fs.existsSync(this.storagePath)) {
|
|
81
|
+
fs.unlinkSync(this.storagePath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.warn('[GoalEngine] Failed to save state to disk:', err);
|
|
86
|
+
}
|
|
46
87
|
}
|
|
47
88
|
|
|
48
89
|
/**
|
|
@@ -56,6 +97,7 @@ export class GoalEngine {
|
|
|
56
97
|
}
|
|
57
98
|
|
|
58
99
|
emit() {
|
|
100
|
+
this.saveStateToDisk();
|
|
59
101
|
const snapshot = this.getSnapshot();
|
|
60
102
|
for (const listener of this.listeners) {
|
|
61
103
|
try {
|
|
@@ -66,6 +108,27 @@ export class GoalEngine {
|
|
|
66
108
|
}
|
|
67
109
|
}
|
|
68
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Динамическое обновление настроек на лету
|
|
113
|
+
* @param {Object} config
|
|
114
|
+
*/
|
|
115
|
+
updateConfig(config = {}) {
|
|
116
|
+
if (typeof config.defaultMaxIterations === 'number' && config.defaultMaxIterations >= 1) {
|
|
117
|
+
const prev = this.defaultMaxIterations;
|
|
118
|
+
this.defaultMaxIterations = config.defaultMaxIterations;
|
|
119
|
+
if (this.currentGoal && this.currentGoal.maxIterations === prev) {
|
|
120
|
+
this.currentGoal.maxIterations = config.defaultMaxIterations;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (typeof config.autoDrive === 'boolean') {
|
|
124
|
+
this.autoDrive = config.autoDrive;
|
|
125
|
+
}
|
|
126
|
+
if (typeof config.enableSound === 'boolean') {
|
|
127
|
+
this.enableSound = config.enableSound;
|
|
128
|
+
}
|
|
129
|
+
this.emit();
|
|
130
|
+
}
|
|
131
|
+
|
|
69
132
|
/**
|
|
70
133
|
* Запуск новой цели
|
|
71
134
|
*/
|
|
@@ -299,12 +362,18 @@ export class GoalEngine {
|
|
|
299
362
|
hasActiveGoal: false,
|
|
300
363
|
state: GoalState.IDLE,
|
|
301
364
|
title: '',
|
|
365
|
+
startedAt: null,
|
|
366
|
+
pausedAt: null,
|
|
367
|
+
totalPausedDurationMs: 0,
|
|
368
|
+
completedAt: null,
|
|
302
369
|
elapsedSeconds: 0,
|
|
303
370
|
formattedElapsed: '0s',
|
|
304
371
|
milestones: [],
|
|
305
372
|
progressPercent: 0,
|
|
306
373
|
iterationsCount: 0,
|
|
307
374
|
maxIterations: this.defaultMaxIterations,
|
|
375
|
+
autoDrive: this.autoDrive,
|
|
376
|
+
enableSound: this.enableSound,
|
|
308
377
|
};
|
|
309
378
|
}
|
|
310
379
|
|
|
@@ -319,6 +388,10 @@ export class GoalEngine {
|
|
|
319
388
|
state: this.currentGoal.state,
|
|
320
389
|
title: this.currentGoal.title,
|
|
321
390
|
description: this.currentGoal.description,
|
|
391
|
+
startedAt: this.currentGoal.startedAt,
|
|
392
|
+
pausedAt: this.currentGoal.pausedAt,
|
|
393
|
+
totalPausedDurationMs: this.currentGoal.totalPausedDurationMs,
|
|
394
|
+
completedAt: this.currentGoal.completedAt,
|
|
322
395
|
elapsedSeconds: elapsed,
|
|
323
396
|
formattedElapsed: formatElapsed(elapsed),
|
|
324
397
|
iterationsCount: this.currentGoal.iterationsCount,
|
|
@@ -327,6 +400,8 @@ export class GoalEngine {
|
|
|
327
400
|
progressPercent,
|
|
328
401
|
logs: this.currentGoal.logs,
|
|
329
402
|
resultSummary: this.currentGoal.resultSummary,
|
|
403
|
+
autoDrive: this.autoDrive,
|
|
404
|
+
enableSound: this.enableSound,
|
|
330
405
|
};
|
|
331
406
|
}
|
|
332
407
|
|
|
@@ -339,15 +414,20 @@ export class GoalEngine {
|
|
|
339
414
|
}
|
|
340
415
|
|
|
341
416
|
const snapshot = this.getSnapshot();
|
|
342
|
-
const
|
|
417
|
+
const hasMilestones = snapshot.milestones.length > 0;
|
|
418
|
+
const milestonesText = hasMilestones
|
|
343
419
|
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
344
|
-
: ' (
|
|
420
|
+
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
345
421
|
|
|
346
422
|
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
423
|
+
Цель: "${snapshot.title}"
|
|
424
|
+
Время работы: ${snapshot.formattedElapsed} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
425
|
+
План работ:
|
|
350
426
|
${milestonesText}
|
|
351
|
-
|
|
427
|
+
|
|
428
|
+
Инструкции Goal Mode:
|
|
429
|
+
1. ${hasMilestones ? 'Выполняй текущий активный пункт плана работ.' : 'ТВОЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones со списком пунктов плана работ.'}
|
|
430
|
+
2. По мере выполнения каждого шага отмечай его статус через инструмент goal_update_progress (status: "in_progress" перед началом, "completed" по завершении).
|
|
431
|
+
3. Когда все пункты плана будут выполнены, вызови инструмент goal_finish с итоговым резюме.`;
|
|
352
432
|
}
|
|
353
433
|
}
|