@goodandready/dsh-goal 0.1.7 → 0.1.9
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 +118 -44
- package/README.zh.md +217 -0
- package/docs/design/DESIGN.md +20 -0
- package/lib/client.js +442 -54
- package/lib/command-handler.js +80 -32
- package/lib/goal-engine.js +138 -5
- package/lib/index.js +41 -9
- package/package.json +8 -7
package/lib/command-handler.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { GoalState } from './goal-engine.js';
|
|
2
|
+
import { GoalState, detectLanguage } from './goal-engine.js';
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const USAGE_RU = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
5
|
+
export const USAGE_EN = 'Usage: /goal [<goal>|clear|pause|resume]';
|
|
6
|
+
export const USAGE = USAGE_RU;
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Парсинг строки ввода для команды /goal
|
|
@@ -39,15 +41,26 @@ export function createGoalUserMessage(text) {
|
|
|
39
41
|
/**
|
|
40
42
|
* Формирование стартового промпта с жестким требованием составить план работ
|
|
41
43
|
* @param {string} goalText
|
|
44
|
+
* @param {string} [explicitLang]
|
|
42
45
|
* @returns {string}
|
|
43
46
|
*/
|
|
44
|
-
export function formatGoalStartPrompt(goalText) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
export function formatGoalStartPrompt(goalText, explicitLang) {
|
|
48
|
+
const lang = explicitLang || detectLanguage(goalText);
|
|
49
|
+
if (lang === 'ru') {
|
|
50
|
+
return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
|
|
51
|
+
`СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
|
|
52
|
+
`1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
53
|
+
`2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
|
|
54
|
+
`3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
|
|
55
|
+
`4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `🎯 Goal Mode activated: "${goalText}"\n\n` +
|
|
59
|
+
`STRICT AUTONOMOUS CONTRACT:\n` +
|
|
60
|
+
`1. MANDATORY STEP 1: Your FIRST action BEFORE doing any other work or reply MUST be calling tool goal_set_milestones with an array of 3-7 concrete, sequential milestones. The user sees this plan in real time.\n` +
|
|
61
|
+
`2. STEP 2: After establishing the plan, proceed to execute milestones. Before starting each milestone, mark it as in_progress via goal_update_progress(milestone_id, "in_progress").\n` +
|
|
62
|
+
`3. STEP 3: Upon completing a milestone, mark it as completed via goal_update_progress(milestone_id, "completed", "brief milestone outcome").\n` +
|
|
63
|
+
`4. STEP 4: When all milestones are completed, call tool goal_finish(summary) with a comprehensive summary of achieved results.`;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
/**
|
|
@@ -63,27 +76,49 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
63
76
|
const sid = sessionId || 'default';
|
|
64
77
|
const snap = engine.getSnapshot(sid);
|
|
65
78
|
|
|
79
|
+
const lang = (parsed.text ? detectLanguage(parsed.text) : (snap.lang || (snap.title ? detectLanguage(snap.title) : 'en'))) || 'en';
|
|
80
|
+
const isRu = lang === 'ru';
|
|
81
|
+
const usage = isRu ? USAGE_RU : USAGE_EN;
|
|
82
|
+
|
|
66
83
|
switch (parsed.action) {
|
|
67
84
|
case 'show': {
|
|
68
85
|
if (!snap.hasActiveGoal) {
|
|
69
86
|
return {
|
|
70
87
|
kind: 'success',
|
|
71
|
-
text:
|
|
88
|
+
text: isRu
|
|
89
|
+
? `🎯 Режим цели (Goal Mode): цель не установлена.\n${usage}`
|
|
90
|
+
: `🎯 Goal Mode: no active goal set.\n${usage}`,
|
|
72
91
|
};
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
const milestonesInfo = snap.milestones.length > 0
|
|
76
|
-
? `\nПлан работ:\n` + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
|
|
95
|
+
? (isRu ? `\nПлан работ:\n` : `\nWork plan:\n`) + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
|
|
96
|
+
: '';
|
|
97
|
+
|
|
98
|
+
const etaText = snap.formattedETA ? ` (ETA: ${snap.formattedETA})` : '';
|
|
99
|
+
const tokensInfo = (snap.tokensUsage && snap.tokensUsage.totalTokens > 0)
|
|
100
|
+
? (isRu
|
|
101
|
+
? `\nТокены: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`
|
|
102
|
+
: `\nTokens: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`)
|
|
77
103
|
: '';
|
|
78
104
|
|
|
79
105
|
return {
|
|
80
106
|
kind: 'success',
|
|
81
|
-
text:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
text: isRu
|
|
108
|
+
? `🎯 Цель: «${snap.title}»\n` +
|
|
109
|
+
`Статус: ${snap.state}\n` +
|
|
110
|
+
`Время: ${snap.formattedElapsed}${etaText}\n` +
|
|
111
|
+
`Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
112
|
+
tokensInfo +
|
|
113
|
+
milestonesInfo +
|
|
114
|
+
`\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`
|
|
115
|
+
: `🎯 Goal: "${snap.title}"\n` +
|
|
116
|
+
`Status: ${snap.state}\n` +
|
|
117
|
+
`Duration: ${snap.formattedElapsed}${etaText}\n` +
|
|
118
|
+
`Iterations: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
119
|
+
tokensInfo +
|
|
120
|
+
milestonesInfo +
|
|
121
|
+
`\n\nCommands: /goal pause, /goal resume, /goal clear, /goal <new goal>`,
|
|
87
122
|
};
|
|
88
123
|
}
|
|
89
124
|
|
|
@@ -91,7 +126,7 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
91
126
|
if (!snap.hasActiveGoal) {
|
|
92
127
|
return {
|
|
93
128
|
kind: 'success',
|
|
94
|
-
text: 'Цель не была установлена.',
|
|
129
|
+
text: isRu ? 'Цель не была установлена.' : 'No active goal set.',
|
|
95
130
|
};
|
|
96
131
|
}
|
|
97
132
|
engine.clear(sid);
|
|
@@ -104,7 +139,7 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
104
139
|
}
|
|
105
140
|
return {
|
|
106
141
|
kind: 'success',
|
|
107
|
-
text: '🎯 Цель сброшена.',
|
|
142
|
+
text: isRu ? '🎯 Цель сброшена.' : '🎯 Goal cleared.',
|
|
108
143
|
};
|
|
109
144
|
}
|
|
110
145
|
|
|
@@ -112,16 +147,18 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
112
147
|
if (!snap.hasActiveGoal) {
|
|
113
148
|
return {
|
|
114
149
|
kind: 'error',
|
|
115
|
-
text:
|
|
150
|
+
text: isRu
|
|
151
|
+
? `Нельзя приостановить: активная цель отсутствует.\n${usage}`
|
|
152
|
+
: `Cannot pause: no active goal.\n${usage}`,
|
|
116
153
|
};
|
|
117
154
|
}
|
|
118
155
|
if (snap.state === GoalState.PAUSED) {
|
|
119
156
|
return {
|
|
120
157
|
kind: 'success',
|
|
121
|
-
text: '⏸ Цель уже на паузе.',
|
|
158
|
+
text: isRu ? '⏸ Цель уже на паузе.' : '⏸ Goal is already paused.',
|
|
122
159
|
};
|
|
123
160
|
}
|
|
124
|
-
engine.pause('Пауза по команде пользователя (/goal pause)', sid);
|
|
161
|
+
engine.pause(isRu ? 'Пауза по команде пользователя (/goal pause)' : 'Paused by user command (/goal pause)', sid);
|
|
125
162
|
if (agent && typeof agent.cancel === 'function') {
|
|
126
163
|
try {
|
|
127
164
|
agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
|
|
@@ -131,7 +168,9 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
131
168
|
}
|
|
132
169
|
return {
|
|
133
170
|
kind: 'success',
|
|
134
|
-
text:
|
|
171
|
+
text: isRu
|
|
172
|
+
? `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`
|
|
173
|
+
: `⏸ Goal paused: "${snap.title}". Agent stopped. Use /goal resume or ▶️ button to continue.`,
|
|
135
174
|
};
|
|
136
175
|
}
|
|
137
176
|
|
|
@@ -139,19 +178,23 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
139
178
|
if (!snap.hasActiveGoal) {
|
|
140
179
|
return {
|
|
141
180
|
kind: 'error',
|
|
142
|
-
text:
|
|
181
|
+
text: isRu
|
|
182
|
+
? `Нельзя возобновить: активная цель отсутствует.\n${usage}`
|
|
183
|
+
: `Cannot resume: no active goal.\n${usage}`,
|
|
143
184
|
};
|
|
144
185
|
}
|
|
145
186
|
if (snap.state === GoalState.RUNNING) {
|
|
146
187
|
return {
|
|
147
188
|
kind: 'success',
|
|
148
|
-
text: '▶️ Цель уже выполняется.',
|
|
189
|
+
text: isRu ? '▶️ Цель уже выполняется.' : '▶️ Goal is already running.',
|
|
149
190
|
};
|
|
150
191
|
}
|
|
151
192
|
engine.resume(sid);
|
|
152
193
|
if (agent && typeof agent.followup === 'function') {
|
|
153
194
|
try {
|
|
154
|
-
const resumePrompt =
|
|
195
|
+
const resumePrompt = isRu
|
|
196
|
+
? `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`
|
|
197
|
+
: `▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.`;
|
|
155
198
|
agent.followup(createGoalUserMessage(resumePrompt));
|
|
156
199
|
} catch (err) {
|
|
157
200
|
console.warn('[dsh-goal] Failed to followup agent on resume:', err);
|
|
@@ -159,18 +202,20 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
159
202
|
}
|
|
160
203
|
return {
|
|
161
204
|
kind: 'success',
|
|
162
|
-
text:
|
|
205
|
+
text: isRu
|
|
206
|
+
? `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`
|
|
207
|
+
: `▶️ Goal resumed: "${snap.title}". Agent continues working.`,
|
|
163
208
|
};
|
|
164
209
|
}
|
|
165
210
|
|
|
166
211
|
case 'start': {
|
|
167
212
|
const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
|
|
168
|
-
const newSnap = engine.startGoal(parsed.text, { maxIterations }, sid);
|
|
213
|
+
const newSnap = engine.startGoal(parsed.text, { maxIterations, lang }, sid);
|
|
169
214
|
|
|
170
215
|
// Если команда вызвана в контексте агента, отправляем цель в очередь модели
|
|
171
216
|
if (agent && typeof agent.followup === 'function') {
|
|
172
217
|
try {
|
|
173
|
-
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text));
|
|
218
|
+
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text, lang));
|
|
174
219
|
agent.followup(userMsg);
|
|
175
220
|
} catch (err) {
|
|
176
221
|
console.warn('[dsh-goal] Failed to dispatch goal to agent followup:', err);
|
|
@@ -179,15 +224,18 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
179
224
|
|
|
180
225
|
return {
|
|
181
226
|
kind: 'success',
|
|
182
|
-
text:
|
|
183
|
-
|
|
227
|
+
text: isRu
|
|
228
|
+
? `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
|
|
229
|
+
`Для паузы: /goal pause | Для сброса: /goal clear`
|
|
230
|
+
: `🎯 Goal activated: "${newSnap.title}" (max ${newSnap.maxIterations} iterations).\n` +
|
|
231
|
+
`To pause: /goal pause | To clear: /goal clear`,
|
|
184
232
|
};
|
|
185
233
|
}
|
|
186
234
|
|
|
187
235
|
default:
|
|
188
236
|
return {
|
|
189
237
|
kind: 'error',
|
|
190
|
-
text: `Неизвестная команда. ${
|
|
238
|
+
text: isRu ? `Неизвестная команда. ${usage}` : `Unknown command. ${usage}`,
|
|
191
239
|
};
|
|
192
240
|
}
|
|
193
241
|
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -42,11 +42,45 @@ export function formatElapsed(totalSeconds) {
|
|
|
42
42
|
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Форматирование расчетного оставшегося времени (ETA)
|
|
47
|
+
* @param {number|null} seconds
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
export function formatETA(seconds) {
|
|
51
|
+
if (seconds == null || isNaN(seconds)) return null;
|
|
52
|
+
const sec = Math.max(0, Math.floor(seconds));
|
|
53
|
+
if (sec < 60) return `~${sec}s`;
|
|
54
|
+
const mins = Math.round(sec / 60);
|
|
55
|
+
if (mins < 60) return `~${mins}m`;
|
|
56
|
+
const hours = Math.floor(mins / 60);
|
|
57
|
+
const remainingMins = mins % 60;
|
|
58
|
+
return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Автоматическое определение языка текста (кириллица -> ru, иероглифы -> zh, иначе en)
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* @param {string} [fallback='en']
|
|
65
|
+
* @returns {'ru' | 'en' | 'zh'}
|
|
66
|
+
*/
|
|
67
|
+
export function detectLanguage(text, fallback = 'en') {
|
|
68
|
+
if (!text || typeof text !== 'string') return fallback;
|
|
69
|
+
if (/[а-яёА-ЯЁ]/i.test(text)) {
|
|
70
|
+
return 'ru';
|
|
71
|
+
}
|
|
72
|
+
if (/[\u4e00-\u9fa5]/.test(text)) {
|
|
73
|
+
return 'zh';
|
|
74
|
+
}
|
|
75
|
+
return 'en';
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
export class GoalEngine {
|
|
46
79
|
constructor(options = {}) {
|
|
47
80
|
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
48
81
|
this.autoDrive = options.autoDrive ?? true;
|
|
49
82
|
this.enableSound = options.enableSound ?? true;
|
|
83
|
+
this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
|
|
50
84
|
this.maxSessions = options.maxSessions ?? 100;
|
|
51
85
|
this.goals = new Map();
|
|
52
86
|
this.listeners = new Set();
|
|
@@ -96,6 +130,12 @@ export class GoalEngine {
|
|
|
96
130
|
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
97
131
|
dirty = true;
|
|
98
132
|
}
|
|
133
|
+
if (!goal.tokensUsage) {
|
|
134
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
135
|
+
}
|
|
136
|
+
if (!goal.lang) {
|
|
137
|
+
goal.lang = detectLanguage(goal.title);
|
|
138
|
+
}
|
|
99
139
|
this.goals.set(sid, goal);
|
|
100
140
|
}
|
|
101
141
|
}
|
|
@@ -112,6 +152,12 @@ export class GoalEngine {
|
|
|
112
152
|
if (data.logs.length > 100) data.logs = data.logs.slice(-100);
|
|
113
153
|
dirty = true;
|
|
114
154
|
}
|
|
155
|
+
if (!data.tokensUsage) {
|
|
156
|
+
data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
157
|
+
}
|
|
158
|
+
if (!data.lang) {
|
|
159
|
+
data.lang = detectLanguage(data.title);
|
|
160
|
+
}
|
|
115
161
|
this.goals.set('default', data);
|
|
116
162
|
}
|
|
117
163
|
if (dirty) {
|
|
@@ -251,6 +297,9 @@ export class GoalEngine {
|
|
|
251
297
|
if (typeof config.enableSound === 'boolean') {
|
|
252
298
|
this.enableSound = config.enableSound;
|
|
253
299
|
}
|
|
300
|
+
if (typeof config.showQuickLaunchButton === 'boolean') {
|
|
301
|
+
this.showQuickLaunchButton = config.showQuickLaunchButton;
|
|
302
|
+
}
|
|
254
303
|
this.emit();
|
|
255
304
|
}
|
|
256
305
|
|
|
@@ -304,6 +353,7 @@ export class GoalEngine {
|
|
|
304
353
|
sessionId: sid,
|
|
305
354
|
title: cleanTitle,
|
|
306
355
|
description: options.description?.trim() || '',
|
|
356
|
+
lang: options.lang || detectLanguage(cleanTitle),
|
|
307
357
|
state: GoalState.RUNNING,
|
|
308
358
|
startedAt: now,
|
|
309
359
|
pausedAt: null,
|
|
@@ -312,6 +362,7 @@ export class GoalEngine {
|
|
|
312
362
|
iterationsCount: 0,
|
|
313
363
|
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
314
364
|
milestones: [],
|
|
365
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
315
366
|
logs: [
|
|
316
367
|
{
|
|
317
368
|
timestamp: now,
|
|
@@ -561,6 +612,57 @@ export class GoalEngine {
|
|
|
561
612
|
return Math.floor(elapsedMs / 1000);
|
|
562
613
|
}
|
|
563
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Накопление статистики использования токенов сессии
|
|
617
|
+
* @param {Object} usage
|
|
618
|
+
* @param {string} [sessionId='default']
|
|
619
|
+
*/
|
|
620
|
+
addTokenUsage(usage, sessionId = 'default') {
|
|
621
|
+
if (!usage || typeof usage !== 'object') return;
|
|
622
|
+
const sid = sessionId || 'default';
|
|
623
|
+
const goal = this.goals.get(sid);
|
|
624
|
+
if (!goal) return;
|
|
625
|
+
|
|
626
|
+
if (!goal.tokensUsage) {
|
|
627
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
631
|
+
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
632
|
+
const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
|
|
633
|
+
|
|
634
|
+
goal.tokensUsage.promptTokens += prompt;
|
|
635
|
+
goal.tokensUsage.completionTokens += completion;
|
|
636
|
+
goal.tokensUsage.totalTokens += total;
|
|
637
|
+
|
|
638
|
+
this.emit(sid);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Интеллектуальный расчет прогноза оставшегося времени (ETA)
|
|
643
|
+
* на основе средней скорости выполнения завершенных вех
|
|
644
|
+
* @param {string} [sessionId='default']
|
|
645
|
+
* @returns {number|null}
|
|
646
|
+
*/
|
|
647
|
+
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
648
|
+
const sid = sessionId || 'default';
|
|
649
|
+
const goal = this.goals.get(sid);
|
|
650
|
+
if (!goal || goal.state !== GoalState.RUNNING) return null;
|
|
651
|
+
|
|
652
|
+
const total = goal.milestones.length;
|
|
653
|
+
if (total === 0) return null;
|
|
654
|
+
|
|
655
|
+
const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
656
|
+
if (completedCount === 0 || completedCount >= total) return null;
|
|
657
|
+
|
|
658
|
+
const elapsed = this.getElapsedSeconds(sid);
|
|
659
|
+
if (elapsed <= 0) return null;
|
|
660
|
+
|
|
661
|
+
const avgSecPerMilestone = elapsed / completedCount;
|
|
662
|
+
const remainingCount = total - completedCount;
|
|
663
|
+
return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
|
|
664
|
+
}
|
|
665
|
+
|
|
564
666
|
/**
|
|
565
667
|
* Снимок состояния для передачи клиенту / API
|
|
566
668
|
*/
|
|
@@ -579,12 +681,17 @@ export class GoalEngine {
|
|
|
579
681
|
completedAt: null,
|
|
580
682
|
elapsedSeconds: 0,
|
|
581
683
|
formattedElapsed: '0s',
|
|
684
|
+
estimatedRemainingSeconds: null,
|
|
685
|
+
formattedETA: null,
|
|
686
|
+
lang: 'en',
|
|
687
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
582
688
|
milestones: [],
|
|
583
689
|
progressPercent: 0,
|
|
584
690
|
iterationsCount: 0,
|
|
585
691
|
maxIterations: this.defaultMaxIterations,
|
|
586
692
|
autoDrive: this.autoDrive,
|
|
587
693
|
enableSound: this.enableSound,
|
|
694
|
+
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
588
695
|
};
|
|
589
696
|
}
|
|
590
697
|
|
|
@@ -592,6 +699,7 @@ export class GoalEngine {
|
|
|
592
699
|
const milestones = goal.milestones;
|
|
593
700
|
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
594
701
|
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
702
|
+
const estSec = this.getEstimatedRemainingSeconds(sid);
|
|
595
703
|
|
|
596
704
|
return {
|
|
597
705
|
sessionId: sid,
|
|
@@ -600,12 +708,16 @@ export class GoalEngine {
|
|
|
600
708
|
state: goal.state,
|
|
601
709
|
title: goal.title,
|
|
602
710
|
description: goal.description,
|
|
711
|
+
lang: goal.lang || detectLanguage(goal.title),
|
|
603
712
|
startedAt: goal.startedAt,
|
|
604
713
|
pausedAt: goal.pausedAt,
|
|
605
714
|
totalPausedDurationMs: goal.totalPausedDurationMs,
|
|
606
715
|
completedAt: goal.completedAt,
|
|
607
716
|
elapsedSeconds: elapsed,
|
|
608
717
|
formattedElapsed: formatElapsed(elapsed),
|
|
718
|
+
estimatedRemainingSeconds: estSec,
|
|
719
|
+
formattedETA: formatETA(estSec),
|
|
720
|
+
tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
609
721
|
iterationsCount: goal.iterationsCount,
|
|
610
722
|
maxIterations: goal.maxIterations,
|
|
611
723
|
milestones,
|
|
@@ -614,6 +726,7 @@ export class GoalEngine {
|
|
|
614
726
|
resultSummary: goal.resultSummary,
|
|
615
727
|
autoDrive: this.autoDrive,
|
|
616
728
|
enableSound: this.enableSound,
|
|
729
|
+
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
617
730
|
};
|
|
618
731
|
}
|
|
619
732
|
|
|
@@ -628,14 +741,18 @@ export class GoalEngine {
|
|
|
628
741
|
}
|
|
629
742
|
|
|
630
743
|
const snapshot = this.getSnapshot(sid);
|
|
744
|
+
const lang = goal.lang || detectLanguage(snapshot.title);
|
|
631
745
|
const hasMilestones = snapshot.milestones.length > 0;
|
|
632
|
-
const
|
|
633
|
-
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
634
|
-
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
746
|
+
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
635
747
|
|
|
636
|
-
|
|
748
|
+
if (lang === 'ru') {
|
|
749
|
+
const milestonesText = hasMilestones
|
|
750
|
+
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
751
|
+
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
752
|
+
|
|
753
|
+
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
637
754
|
Цель: "${snapshot.title}"
|
|
638
|
-
Время работы: ${snapshot.formattedElapsed} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
755
|
+
Время работы: ${snapshot.formattedElapsed}${etaText} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
639
756
|
План работ:
|
|
640
757
|
${milestonesText}
|
|
641
758
|
|
|
@@ -643,5 +760,21 @@ ${milestonesText}
|
|
|
643
760
|
1. ${hasMilestones ? 'Выполняй текущий активный пункт плана работ.' : 'ТВОЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones со списком пунктов плана работ (3-7 конкретных шагов). Запрещено выполнять работу или завершать turn без вызова goal_set_milestones!'}
|
|
644
761
|
2. По мере выполнения каждого шага обязательно отмечай его статус через инструмент goal_update_progress (status: "in_progress" перед началом шага, status: "completed" по его завершении с кратким notes).
|
|
645
762
|
3. Когда все пункты плана будут выполнены, вызови инструмент goal_finish с подробным итоговым резюме достигнутых результатов.`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const milestonesText = hasMilestones
|
|
766
|
+
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
767
|
+
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
768
|
+
|
|
769
|
+
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
770
|
+
Goal: "${snapshot.title}"
|
|
771
|
+
Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
772
|
+
Work Plan:
|
|
773
|
+
${milestonesText}
|
|
774
|
+
|
|
775
|
+
Goal Mode Instructions (MANDATORY TO FOLLOW):
|
|
776
|
+
1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}
|
|
777
|
+
2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes).
|
|
778
|
+
3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`;
|
|
646
779
|
}
|
|
647
780
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
import { GoalEngine, GoalState, MilestoneStatus } from './goal-engine.js';
|
|
4
|
+
import { GoalEngine, GoalState, MilestoneStatus, detectLanguage } from './goal-engine.js';
|
|
5
5
|
import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
|
|
6
6
|
|
|
7
7
|
export const name = '@goodandready/dsh-goal';
|
|
@@ -42,6 +42,7 @@ export const Config = z.object({
|
|
|
42
42
|
maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
|
|
43
43
|
autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
|
|
44
44
|
enableSound: z.boolean().default(true).description('Play synthesized audio chime on goal completion or failure'),
|
|
45
|
+
showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
|
|
45
46
|
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -59,6 +60,7 @@ export function apply(ctx, config = {}) {
|
|
|
59
60
|
maxIterations: config?.maxIterations ?? 25,
|
|
60
61
|
autoDrive: config?.autoDrive ?? true,
|
|
61
62
|
enableSound: config?.enableSound ?? true,
|
|
63
|
+
showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
|
|
62
64
|
storagePath: config?.storagePath,
|
|
63
65
|
};
|
|
64
66
|
|
|
@@ -71,6 +73,7 @@ export function apply(ctx, config = {}) {
|
|
|
71
73
|
defaultMaxIterations: currentSettings.maxIterations,
|
|
72
74
|
autoDrive: currentSettings.autoDrive,
|
|
73
75
|
enableSound: currentSettings.enableSound,
|
|
76
|
+
showQuickLaunchButton: currentSettings.showQuickLaunchButton,
|
|
74
77
|
storagePath,
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -163,9 +166,12 @@ export function apply(ctx, config = {}) {
|
|
|
163
166
|
|
|
164
167
|
const resumeActiveAgent = (promptText, sessionId = 'default') => {
|
|
165
168
|
const sid = sessionId || 'default';
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
const snap = engine.getSnapshot(sid);
|
|
170
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
171
|
+
const defaultResumePrompt = lang === 'ru'
|
|
172
|
+
? '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.'
|
|
173
|
+
: '▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.';
|
|
174
|
+
const resumeMsg = createGoalUserMessage(promptText || defaultResumePrompt);
|
|
169
175
|
let target = sessionAgents.get(sid);
|
|
170
176
|
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
171
177
|
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
@@ -215,6 +221,7 @@ export function apply(ctx, config = {}) {
|
|
|
215
221
|
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
216
222
|
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
217
223
|
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
224
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
218
225
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
219
226
|
};
|
|
220
227
|
}
|
|
@@ -226,6 +233,7 @@ export function apply(ctx, config = {}) {
|
|
|
226
233
|
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
227
234
|
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
228
235
|
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
236
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
229
237
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
230
238
|
};
|
|
231
239
|
}
|
|
@@ -240,6 +248,7 @@ export function apply(ctx, config = {}) {
|
|
|
240
248
|
defaultMaxIterations: live.maxIterations,
|
|
241
249
|
autoDrive: live.autoDrive,
|
|
242
250
|
enableSound: live.enableSound,
|
|
251
|
+
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
243
252
|
});
|
|
244
253
|
};
|
|
245
254
|
|
|
@@ -532,10 +541,16 @@ export function apply(ctx, config = {}) {
|
|
|
532
541
|
res.statusCode = 400;
|
|
533
542
|
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
534
543
|
}
|
|
544
|
+
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
535
545
|
result = engine.startGoal(cleanTitle, {
|
|
536
546
|
description: typeof description === 'string' ? description.trim() : '',
|
|
537
547
|
maxIterations: getConfig().maxIterations,
|
|
548
|
+
lang: detectedLang,
|
|
538
549
|
}, sid);
|
|
550
|
+
const startPrompt = detectedLang === 'ru'
|
|
551
|
+
? `🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`
|
|
552
|
+
: `🎯 Goal established: "${cleanTitle}". Immediately formulate a work plan (3-7 concrete steps) via tool goal_set_milestones and start executing it.`;
|
|
553
|
+
resumeActiveAgent(startPrompt, sid);
|
|
539
554
|
break;
|
|
540
555
|
}
|
|
541
556
|
case 'pause':
|
|
@@ -615,6 +630,13 @@ export function apply(ctx, config = {}) {
|
|
|
615
630
|
if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
|
|
616
631
|
|
|
617
632
|
const sid = sessionIdOf(turn, 'default');
|
|
633
|
+
|
|
634
|
+
// Накопление токенов по завершении хода
|
|
635
|
+
const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
|
|
636
|
+
if (usage) {
|
|
637
|
+
engine.addTokenUsage(usage, sid);
|
|
638
|
+
}
|
|
639
|
+
|
|
618
640
|
const snap = engine.getSnapshot(sid);
|
|
619
641
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
620
642
|
// Проверяем причину завершения хода
|
|
@@ -623,9 +645,13 @@ export function apply(ctx, config = {}) {
|
|
|
623
645
|
}
|
|
624
646
|
|
|
625
647
|
// Item 3: Smart Progress Guard — detect idle loops without progress
|
|
648
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
626
649
|
const stallCount = engine.incrementStallCount(sid);
|
|
627
650
|
if (stallCount >= 2) {
|
|
628
|
-
|
|
651
|
+
const stallReason = lang === 'ru'
|
|
652
|
+
? 'Агент не продвинулся по плану за последние 2 итерации — требуется внимание оператора'
|
|
653
|
+
: 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
|
|
654
|
+
engine.pause(stallReason, sid);
|
|
629
655
|
return;
|
|
630
656
|
}
|
|
631
657
|
|
|
@@ -636,9 +662,11 @@ export function apply(ctx, config = {}) {
|
|
|
636
662
|
const triggerNextTurn = () => {
|
|
637
663
|
const currentSnap = engine.getSnapshot(sid);
|
|
638
664
|
if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
665
|
+
const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
|
|
666
|
+
const promptText = currentLang === 'ru'
|
|
667
|
+
? 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.'
|
|
668
|
+
: '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.';
|
|
669
|
+
const promptMsg = createGoalUserMessage(promptText);
|
|
642
670
|
let target = sessionAgents.get(sid);
|
|
643
671
|
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
644
672
|
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
@@ -684,7 +712,11 @@ export function apply(ctx, config = {}) {
|
|
|
684
712
|
const sid = sessionIdOf(event, 'default');
|
|
685
713
|
const snap = engine.getSnapshot(sid);
|
|
686
714
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
687
|
-
|
|
715
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
716
|
+
const pauseReason = lang === 'ru'
|
|
717
|
+
? 'Ожидание подтверждения действия оператором'
|
|
718
|
+
: 'Waiting for operator confirmation/approval';
|
|
719
|
+
engine.pause(pauseReason, sid);
|
|
688
720
|
}
|
|
689
721
|
};
|
|
690
722
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-goal",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Goal
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Autonomous Goal Execution & Multi-Turn Task Tracking Engine with Sticky Header for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
"cordis.patch.yml",
|
|
16
16
|
"README.md",
|
|
17
17
|
"docs/",
|
|
18
|
-
"LICENSE"
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.zh.md"
|
|
19
20
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "node --test test/*.test.mjs"
|
|
22
|
-
},
|
|
23
21
|
"keywords": [
|
|
24
22
|
"dsh",
|
|
25
23
|
"dsh-plugin",
|
|
@@ -57,5 +55,8 @@
|
|
|
57
55
|
},
|
|
58
56
|
"dependencies": {
|
|
59
57
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "node --test test/*.test.mjs"
|
|
60
61
|
}
|
|
61
|
-
}
|
|
62
|
+
}
|