@goodandready/dsh-goal 0.2.20 → 0.2.22
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/addon-store.js +26 -7
- package/lib/engine-milestones.js +10 -1
- package/lib/engine-prompt.js +27 -0
- package/lib/engine-reports.js +4 -4
- package/lib/engine-retrospective.js +4 -2
- package/lib/engine-sessions.js +1 -0
- package/lib/engine-store.js +4 -3
- package/lib/goal-engine.js +23 -12
- package/lib/index.js +51 -4
- package/lib/milestone-manager.js +10 -6
- package/lib/routes.js +7 -0
- package/lib/tools.js +93 -2
- package/package.json +2 -11
- package/lib/card-form-state.js +0 -147
- package/lib/command-handler.js +0 -242
package/lib/addon-store.js
CHANGED
|
@@ -22,6 +22,15 @@ export class AddonStore {
|
|
|
22
22
|
getGoalData(goalId) {
|
|
23
23
|
if (!goalId) return null;
|
|
24
24
|
if (!this.goals.has(goalId)) {
|
|
25
|
+
if (this.goals.size > 100) {
|
|
26
|
+
for (const [k, v] of this.goals.entries()) {
|
|
27
|
+
const isInactive = (!v.milestones || v.milestones.length === 0 || v.milestones.every(m => m.completed));
|
|
28
|
+
if (isInactive) {
|
|
29
|
+
this.goals.delete(k);
|
|
30
|
+
if (this.goals.size <= 80) break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
25
34
|
this.goals.set(goalId, {
|
|
26
35
|
milestones: [],
|
|
27
36
|
snapshots: [],
|
|
@@ -32,6 +41,13 @@ export class AddonStore {
|
|
|
32
41
|
return this.goals.get(goalId);
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
removeGoalData(goalId) {
|
|
45
|
+
if (!goalId) return false;
|
|
46
|
+
const deleted = this.goals.delete(goalId);
|
|
47
|
+
if (deleted) this.scheduleSave();
|
|
48
|
+
return deleted;
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
getMilestones(goalId) {
|
|
36
52
|
const data = this.getGoalData(goalId);
|
|
37
53
|
return data ? data.milestones : [];
|
|
@@ -102,13 +118,14 @@ export class AddonStore {
|
|
|
102
118
|
return;
|
|
103
119
|
}
|
|
104
120
|
if (!this.saveTimer) {
|
|
105
|
-
|
|
121
|
+
const timer = setTimeout(() => {
|
|
106
122
|
this.saveTimer = null;
|
|
107
123
|
this.writeToDiskSync();
|
|
108
124
|
}, 200);
|
|
109
|
-
if (typeof
|
|
110
|
-
|
|
125
|
+
if (typeof timer?.unref === 'function') {
|
|
126
|
+
timer.unref();
|
|
111
127
|
}
|
|
128
|
+
this.saveTimer = timer;
|
|
112
129
|
}
|
|
113
130
|
}
|
|
114
131
|
|
|
@@ -192,10 +209,12 @@ export class AddonStore {
|
|
|
192
209
|
migratedAny = true;
|
|
193
210
|
}
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
if (migratedAny) {
|
|
213
|
+
data._migratedToAddon = true;
|
|
214
|
+
data._migratedAt = Date.now();
|
|
215
|
+
fs.writeFileSync(legacyPath, JSON.stringify(data, null, 2), 'utf8');
|
|
216
|
+
this.scheduleSave(true);
|
|
217
|
+
}
|
|
199
218
|
return migratedAny;
|
|
200
219
|
} catch (err) {
|
|
201
220
|
this.logger.warn?.('[AddonStore] Legacy migration failed:', err.message);
|
package/lib/engine-milestones.js
CHANGED
|
@@ -116,7 +116,16 @@ export function applyMilestoneUpdate(target, status, notes = '', checklist = nul
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
if (status
|
|
119
|
+
if (status) {
|
|
120
|
+
if (validStatuses.length > 0 && !validStatuses.includes(status)) {
|
|
121
|
+
return {
|
|
122
|
+
prevStatus,
|
|
123
|
+
updatedStatus: prevStatus,
|
|
124
|
+
blocked: false,
|
|
125
|
+
invalidStatus: true,
|
|
126
|
+
error: `Invalid milestone status: ${status}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
120
129
|
target.status = status;
|
|
121
130
|
}
|
|
122
131
|
if (notes !== undefined && notes !== null && notes !== '') {
|
package/lib/engine-prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { detectLanguage } from "./goal-engine.js";
|
|
1
2
|
import { isMilestoneBlocked } from './engine-milestones.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -100,3 +101,29 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
100
101
|
`3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`
|
|
101
102
|
);
|
|
102
103
|
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format goal start prompt with mandatory milestone contract
|
|
108
|
+
* @param {string} goalText
|
|
109
|
+
* @param {string} [explicitLang]
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
export function formatGoalStartPrompt(goalText, explicitLang) {
|
|
113
|
+
const lang = explicitLang || detectLanguage(goalText);
|
|
114
|
+
if (lang === "zh") {
|
|
115
|
+
return `🎯 目标模式已激活 (Goal Mode): "${goalText}"\n\n` +
|
|
116
|
+
`自主执行契约:\n` +
|
|
117
|
+
`1. 第一步必选动作:在开展任何工作或回复之前,你的第一个动作必须是调用 goal_set_milestones 工具,提交包含 3-7 个具体连续步骤的工作计划。用户将在界面中实时查看此计划。\n` +
|
|
118
|
+
`2. 第二步:计划确立后开始执行任务。在开始每个步骤前,通过 goal_update_progress(milestone_id, "in_progress") 标记其为进行中。\n` +
|
|
119
|
+
`3. 第三步:完成某一步骤后,通过 goal_update_progress(milestone_id, "completed", "阶段简要成果") 标记为已完成。\n` +
|
|
120
|
+
`4. 第四步:当所有里程碑全部完成后,调用 goal_finish(summary) 工具并附带详尽的最终成果总结。`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return `🎯 Goal Mode activated: "${goalText}"\n\n` +
|
|
124
|
+
`STRICT AUTONOMOUS CONTRACT:\n` +
|
|
125
|
+
`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` +
|
|
126
|
+
`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` +
|
|
127
|
+
`3. STEP 3: Upon completing a milestone, mark it as completed via goal_update_progress(milestone_id, "completed", "brief milestone outcome").\n` +
|
|
128
|
+
`4. STEP 4: When all milestones are completed, call tool goal_finish(summary) with a comprehensive summary of achieved results.`;
|
|
129
|
+
}
|
package/lib/engine-reports.js
CHANGED
|
@@ -184,7 +184,7 @@ export function exportReportGitHubPR(state) {
|
|
|
184
184
|
md += `\n</details>\n\n`;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
md += `*Automated by [@goodandready/dsh-goal](https://github.com/
|
|
187
|
+
md += `*Automated by [@goodandready/dsh-goal](https://github.com/goodandready/dsh-goal)*\n`;
|
|
188
188
|
return md;
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -261,7 +261,7 @@ export function rollbackToCheckpoint(commitHash, cwd) {
|
|
|
261
261
|
* @param {string} [cwd]
|
|
262
262
|
* @returns {{ relativePath: string, fullPath: string, content: string }|null}
|
|
263
263
|
*/
|
|
264
|
-
export function saveGoalArtifact(state, cwd) {
|
|
264
|
+
export function saveGoalArtifact(state, cwd, addonData = {}) {
|
|
265
265
|
if (!state || (!state.title && !state.objective)) return null;
|
|
266
266
|
try {
|
|
267
267
|
const root = cwd || process.cwd();
|
|
@@ -281,7 +281,7 @@ export function saveGoalArtifact(state, cwd) {
|
|
|
281
281
|
const fullPath = path.join(targetDir, filename);
|
|
282
282
|
const relativePath = path.join('.dsh', 'goals', filename).replace(/\\/g, '/');
|
|
283
283
|
|
|
284
|
-
const content = exportReportMarkdown(state);
|
|
284
|
+
const content = exportReportMarkdown(state, addonData);
|
|
285
285
|
fs.writeFileSync(fullPath, content, 'utf8');
|
|
286
286
|
|
|
287
287
|
return { relativePath, fullPath, content };
|
|
@@ -295,5 +295,5 @@ export function saveGoalArtifact(state, cwd) {
|
|
|
295
295
|
*/
|
|
296
296
|
export function saveGoalReportFile(goal, addonData = {}, baseDir = null) {
|
|
297
297
|
const root = baseDir || process.cwd();
|
|
298
|
-
return saveGoalArtifact(goal, root)?.fullPath || null;
|
|
298
|
+
return saveGoalArtifact(goal, root, addonData)?.fullPath || null;
|
|
299
299
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { MilestoneStatus, calculateElapsedSeconds } from './goal-engine-constants.js';
|
|
3
|
+
import { isValidGitRef } from './engine-git-branch.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Generate post-goal retrospective analytics card
|
|
@@ -33,8 +34,9 @@ export function generateRetrospectiveData(goal, cwd = process.cwd()) {
|
|
|
33
34
|
let gitDiffStat = '';
|
|
34
35
|
let filesChangedCount = 0;
|
|
35
36
|
try {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
37
|
+
const rawStartRef = goal.gitStartCommit || goal.startCommit || 'HEAD~1';
|
|
38
|
+
const startRef = (rawStartRef && isValidGitRef(rawStartRef)) ? rawStartRef : 'HEAD~1';
|
|
39
|
+
const rawStat = execFileSync('git', ['diff', '--stat', `${startRef}..HEAD`, '--'], {
|
|
38
40
|
cwd,
|
|
39
41
|
encoding: 'utf8',
|
|
40
42
|
stdio: ['ignore', 'pipe', 'ignore'],
|
package/lib/engine-sessions.js
CHANGED
|
@@ -32,6 +32,7 @@ export function addNudgeToGoal(goal, text) {
|
|
|
32
32
|
if (!Array.isArray(goal.nudges)) goal.nudges = [];
|
|
33
33
|
const now = Date.now();
|
|
34
34
|
goal.nudges.push({ text: clean, timestamp: now });
|
|
35
|
+
if (goal.nudges.length > 50) goal.nudges = goal.nudges.slice(-50);
|
|
35
36
|
goal.pendingNudge = clean;
|
|
36
37
|
goal.logs.push({
|
|
37
38
|
timestamp: now,
|
package/lib/engine-store.js
CHANGED
|
@@ -93,13 +93,14 @@ export class EngineStore {
|
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
95
|
if (!this.saveTimer) {
|
|
96
|
-
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
97
|
this.saveTimer = null;
|
|
98
98
|
this.writeStateToDiskSync();
|
|
99
99
|
}, 250);
|
|
100
|
-
if (typeof
|
|
101
|
-
|
|
100
|
+
if (typeof timer?.unref === 'function') {
|
|
101
|
+
timer.unref();
|
|
102
102
|
}
|
|
103
|
+
this.saveTimer = timer;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
package/lib/goal-engine.js
CHANGED
|
@@ -18,7 +18,9 @@ export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, create
|
|
|
18
18
|
*/
|
|
19
19
|
export class GoalEngine {
|
|
20
20
|
constructor(options = {}) {
|
|
21
|
-
this.
|
|
21
|
+
this.logger = options.logger || console;
|
|
22
|
+
const rawDefIters = options.defaultMaxIterations ?? 25;
|
|
23
|
+
this.defaultMaxIterations = typeof rawDefIters === "number" ? rawDefIters : (typeof rawDefIters?.value === "number" ? rawDefIters.value : 25);
|
|
22
24
|
this.autoDrive = options.autoDrive ?? true;
|
|
23
25
|
this.enableSound = options.enableSound ?? true;
|
|
24
26
|
this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
|
|
@@ -95,11 +97,15 @@ export class GoalEngine {
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
updateConfig(config = {}) {
|
|
98
|
-
|
|
100
|
+
const rawMax = config.defaultMaxIterations ?? config.maxIterations;
|
|
101
|
+
const maxVal = typeof rawMax === 'number' ? rawMax : (typeof rawMax?.value === 'number' ? rawMax.value : undefined);
|
|
102
|
+
if (typeof maxVal === 'number' && maxVal >= 1) {
|
|
99
103
|
const prev = this.defaultMaxIterations;
|
|
100
|
-
this.defaultMaxIterations =
|
|
104
|
+
this.defaultMaxIterations = maxVal;
|
|
101
105
|
for (const [_, goal] of this.goals) {
|
|
102
|
-
if (goal && goal.maxIterations === prev
|
|
106
|
+
if (goal && (goal.maxIterations === prev || typeof goal.maxIterations !== 'number' || typeof goal.maxIterations === 'object')) {
|
|
107
|
+
goal.maxIterations = maxVal;
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
if (typeof config.autoDrive === 'boolean') this.autoDrive = config.autoDrive;
|
|
@@ -373,13 +379,18 @@ export class GoalEngine {
|
|
|
373
379
|
enforceDependencies: this.enableMilestoneDependencies,
|
|
374
380
|
allMilestones: goal.milestones,
|
|
375
381
|
});
|
|
376
|
-
if (res.blocked) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
382
|
+
if (res.invalidStatus || res.blocked) {
|
|
383
|
+
if (res.invalidStatus) {
|
|
384
|
+
this.logger?.warn?.(`[dsh-goal] Invalid milestone status "${status}" for milestone ${id}`);
|
|
385
|
+
}
|
|
386
|
+
if (res.blocked) {
|
|
387
|
+
recordActivity(goal, {
|
|
388
|
+
type: 'milestone',
|
|
389
|
+
tool: 'updateMilestone',
|
|
390
|
+
status: 'error',
|
|
391
|
+
details: `Milestone ${id} blocked by ${res.missingDependencies?.join(', ')}`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
383
394
|
return false;
|
|
384
395
|
}
|
|
385
396
|
|
|
@@ -401,7 +412,7 @@ export class GoalEngine {
|
|
|
401
412
|
recordActivity(goal, { type: 'issue', tool: 'syncIssueChecklist', status: 'success', details: `Issue #${syncRes.issueNumber} updated` });
|
|
402
413
|
this.emit(sid);
|
|
403
414
|
}
|
|
404
|
-
}).catch(() => {});
|
|
415
|
+
}).catch((err) => { this.logger?.warn?.("[dsh-goal] Failed to sync milestone with issue:", err?.message || err); });
|
|
405
416
|
}
|
|
406
417
|
}
|
|
407
418
|
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -130,9 +131,12 @@ export function apply(ctx, config = {}) {
|
|
|
130
131
|
|
|
131
132
|
// Attempt migration of legacy state if present
|
|
132
133
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const legacyStatePath = path.resolve(defaultStorageDir, 'dsh-goal-state.json');
|
|
135
|
+
if (fs.existsSync(legacyStatePath)) {
|
|
136
|
+
const agentsSvc = safeService(ctx, "agents");
|
|
137
|
+
const firstAgent = agentsSvc?.get?.(agentsSvc?.keys?.()?.next()?.value);
|
|
138
|
+
addonStore.migrateLegacyState(legacyStatePath, safeService(ctx, "goals"), firstAgent);
|
|
139
|
+
}
|
|
136
140
|
} catch (migErr) {
|
|
137
141
|
logger.warn?.('[dsh-goal] Legacy migration check failed:', migErr.message);
|
|
138
142
|
}
|
|
@@ -162,6 +166,7 @@ export function apply(ctx, config = {}) {
|
|
|
162
166
|
autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
163
167
|
enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
|
|
164
168
|
enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
|
|
169
|
+
logger,
|
|
165
170
|
});
|
|
166
171
|
}
|
|
167
172
|
|
|
@@ -192,14 +197,55 @@ export function apply(ctx, config = {}) {
|
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
const sessionAgents = new Map();
|
|
201
|
+
|
|
202
|
+
const stopRunningAgents = (sessionId = 'default') => {
|
|
203
|
+
try {
|
|
204
|
+
const agentsSvc = safeService(ctx, "agents");
|
|
205
|
+
const agent = sessionAgents.get(sessionId) || agentsSvc?.get?.(sessionId) || agentsSvc?.get?.(agentsSvc?.keys?.()?.next()?.value);
|
|
206
|
+
if (agent && typeof agent.abort === 'function') {
|
|
207
|
+
agent.abort();
|
|
208
|
+
} else if (agent && typeof agent.cancel === 'function') {
|
|
209
|
+
agent.cancel();
|
|
210
|
+
} else if (agent && typeof agent.stop === 'function') {
|
|
211
|
+
agent.stop();
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
logger.warn?.('[dsh-goal] Failed to stop agent:', err?.message || err);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const resumeActiveAgent = (prompt, sessionId = 'default') => {
|
|
219
|
+
try {
|
|
220
|
+
const agentsSvc = safeService(ctx, "agents");
|
|
221
|
+
const agent = sessionAgents.get(sessionId) || agentsSvc?.get?.(sessionId) || agentsSvc?.get?.(agentsSvc?.keys?.()?.next()?.value);
|
|
222
|
+
if (agent) {
|
|
223
|
+
if (typeof agent.resume === 'function') {
|
|
224
|
+
agent.resume(prompt);
|
|
225
|
+
} else if (typeof agent.step === 'function') {
|
|
226
|
+
agent.step(prompt);
|
|
227
|
+
} else if (typeof agent.send === 'function' && prompt) {
|
|
228
|
+
agent.send(prompt);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn?.('[dsh-goal] Failed to resume agent:', err?.message || err);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
195
236
|
// 2. WebServer HTTP & SSE routes (registered after updater so it remains primary)
|
|
196
237
|
const { broadcastEvent } = registerRoutes(ctx, {
|
|
197
238
|
engine: fallbackEngine,
|
|
198
239
|
milestoneManager,
|
|
240
|
+
snapshotManager: gitSnapshotManager,
|
|
199
241
|
gitSnapshotManager,
|
|
242
|
+
budgetGuard: tokenBudgetGuard,
|
|
200
243
|
tokenBudgetGuard,
|
|
201
244
|
addonStore,
|
|
202
245
|
goalsService: safeService(ctx, "goals"),
|
|
246
|
+
stopRunningAgents,
|
|
247
|
+
resumeActiveAgent,
|
|
248
|
+
sessionAgents,
|
|
203
249
|
getConfig: () => currentSettings,
|
|
204
250
|
sseClients,
|
|
205
251
|
logger,
|
|
@@ -243,6 +289,7 @@ export function apply(ctx, config = {}) {
|
|
|
243
289
|
|
|
244
290
|
// Inject milestone prompts and check budget during agent turns
|
|
245
291
|
ctx.on('agent/pre-step', async ({ agent, messages, step, signal }, next) => {
|
|
292
|
+
if (agent) { sessionAgents.set(sessionIdOf(agent, "default"), agent); }
|
|
246
293
|
const goalsSvc = safeService(ctx, "goals");
|
|
247
294
|
if (goalsSvc && typeof goalsSvc.get === "function" && agent) {
|
|
248
295
|
try {
|
|
@@ -284,7 +331,7 @@ export function apply(ctx, config = {}) {
|
|
|
284
331
|
|
|
285
332
|
// Compatibility events for turn/end and approval/asked
|
|
286
333
|
ctx.on('turn/end', (event) => {
|
|
287
|
-
//
|
|
334
|
+
// session isolation turn tracking
|
|
288
335
|
});
|
|
289
336
|
|
|
290
337
|
ctx.on('approval/asked', (event) => {
|
package/lib/milestone-manager.js
CHANGED
|
@@ -95,11 +95,15 @@ export class MilestoneManager {
|
|
|
95
95
|
|
|
96
96
|
// Best-effort Gitea issue checklist sync if issue data exists
|
|
97
97
|
const goalData = this.store.getGoalData(goalId);
|
|
98
|
-
if (goalData?.issue?.number) {
|
|
98
|
+
if (goalData?.issueRef?.issueNumber || goalData?.issue?.number) {
|
|
99
99
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const issueRef = goalData.issueRef || { issueNumber: goalData.issue.number };
|
|
101
|
+
const fakeGoal = { issueRef, cwd: workspaceRoot };
|
|
102
|
+
syncMilestoneWithIssue(fakeGoal, m, { cwd: workspaceRoot }).catch((err) => {
|
|
103
|
+
this.logger?.warn?.('[MilestoneManager] Issue sync failed:', err?.message || err);
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.logger?.warn?.('[MilestoneManager] Issue sync error:', err?.message || err);
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -132,8 +136,8 @@ export class MilestoneManager {
|
|
|
132
136
|
|
|
133
137
|
const uncompletedChecklist = (active.checklist || []).filter(c => !c.done);
|
|
134
138
|
let clText = '';
|
|
135
|
-
if (
|
|
136
|
-
clText = ` | Next items: ${
|
|
139
|
+
if (uncompletedChecklist.length > 0) {
|
|
140
|
+
clText = ` | Next items: ${uncompletedChecklist.map(c => `[ ] ${c.text}`).join(', ')}`;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
return `[Goal Milestones]: Current step: "${active.title}"${clText}`;
|
package/lib/routes.js
CHANGED
|
@@ -79,10 +79,14 @@ export function registerRoutes(ctx, {
|
|
|
79
79
|
sseClients = new Map(),
|
|
80
80
|
addonStore,
|
|
81
81
|
budgetGuard,
|
|
82
|
+
tokenBudgetGuard,
|
|
82
83
|
snapshotManager,
|
|
84
|
+
gitSnapshotManager,
|
|
83
85
|
milestoneManager,
|
|
84
86
|
logger = console,
|
|
85
87
|
} = {}) {
|
|
88
|
+
const activeBudgetGuard = budgetGuard || tokenBudgetGuard;
|
|
89
|
+
const activeSnapshotManager = snapshotManager || gitSnapshotManager;
|
|
86
90
|
// Subscribe to engine if provided (backward compatibility)
|
|
87
91
|
if (engine && typeof engine.subscribe === 'function') {
|
|
88
92
|
engine.subscribe((snapshot, sid) => {
|
|
@@ -396,6 +400,9 @@ export function registerRoutes(ctx, {
|
|
|
396
400
|
if (engine) {
|
|
397
401
|
result = engine.clear(sid);
|
|
398
402
|
}
|
|
403
|
+
if (addonStore && typeof addonStore.removeGoalData === 'function') {
|
|
404
|
+
addonStore.removeGoalData(sid);
|
|
405
|
+
}
|
|
399
406
|
if (typeof stopRunningAgents === 'function') {
|
|
400
407
|
stopRunningAgents(sid);
|
|
401
408
|
}
|
package/lib/tools.js
CHANGED
|
@@ -141,7 +141,6 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
141
141
|
},
|
|
142
142
|
output: JSON_OUTPUT,
|
|
143
143
|
execute: handleGoalMilestones,
|
|
144
|
-
handler: handleGoalMilestones,
|
|
145
144
|
});
|
|
146
145
|
|
|
147
146
|
// Tool 2: goal_checklist
|
|
@@ -181,7 +180,99 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
181
180
|
},
|
|
182
181
|
output: JSON_OUTPUT,
|
|
183
182
|
execute: handleGoalChecklist,
|
|
184
|
-
|
|
183
|
+
});
|
|
184
|
+
// Tool 3: goal_set_milestones (Issue #127 alias matching autonomous-contract prompt)
|
|
185
|
+
const handleGoalSetMilestones = async (args = {}, toolCtx = {}) => {
|
|
186
|
+
const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
187
|
+
let list = args.milestones;
|
|
188
|
+
if (Array.isArray(list)) {
|
|
189
|
+
list = list.map((item, idx) => {
|
|
190
|
+
if (typeof item === 'string') {
|
|
191
|
+
return { id: `m-${idx + 1}`, title: item };
|
|
192
|
+
}
|
|
193
|
+
return item;
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
list = [];
|
|
197
|
+
}
|
|
198
|
+
const res = mm.setMilestones(goalId, list);
|
|
199
|
+
return { ok: true, goalId, count: res.length, milestones: res };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
safeRegister({
|
|
203
|
+
name: 'goal_set_milestones',
|
|
204
|
+
description: 'Set initial work plan milestones for the active goal.',
|
|
205
|
+
parameters: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
milestones: {
|
|
209
|
+
type: 'array',
|
|
210
|
+
items: { type: ['string', 'object'] },
|
|
211
|
+
description: 'Array of milestone titles or objects',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ['milestones'],
|
|
215
|
+
},
|
|
216
|
+
output: JSON_OUTPUT,
|
|
217
|
+
execute: handleGoalSetMilestones,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Tool 4: goal_update_progress (Issue #127 alias matching autonomous-contract prompt)
|
|
221
|
+
const handleGoalUpdateProgress = async (args = {}, toolCtx = {}) => {
|
|
222
|
+
const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
223
|
+
const milestoneId = args.milestone_id || args.milestoneId;
|
|
224
|
+
const status = args.status || 'in_progress';
|
|
225
|
+
const notes = typeof args.notes === 'string' ? args.notes : '';
|
|
226
|
+
if (!milestoneId) throw new Error('milestone_id is required');
|
|
227
|
+
if (status === 'completed') {
|
|
228
|
+
const rawCwd = toolCtx?.session?.cwd || toolCtx?.cwd || process.cwd();
|
|
229
|
+
const safeCwd = getSafeCwd(rawCwd);
|
|
230
|
+
const completed = await mm.completeMilestone(goalId, milestoneId, notes, safeCwd);
|
|
231
|
+
return { ok: true, goalId, milestoneId, status: 'completed', milestone: completed };
|
|
232
|
+
}
|
|
233
|
+
const updated = mm.updateMilestone(goalId, milestoneId, { status, notes, checklist: args.checklist });
|
|
234
|
+
return { ok: true, goalId, milestoneId, status, milestone: updated };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
safeRegister({
|
|
238
|
+
name: 'goal_update_progress',
|
|
239
|
+
description: 'Update progress and status of a milestone.',
|
|
240
|
+
parameters: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
milestone_id: { type: 'string', description: 'Target milestone ID' },
|
|
244
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'blocked'], description: 'Milestone status' },
|
|
245
|
+
notes: { type: 'string', description: 'Brief outcome or status notes' },
|
|
246
|
+
checklist: { type: 'array', items: { type: 'string' }, description: 'Subtask items' },
|
|
247
|
+
},
|
|
248
|
+
required: ['milestone_id', 'status'],
|
|
249
|
+
},
|
|
250
|
+
output: JSON_OUTPUT,
|
|
251
|
+
execute: handleGoalUpdateProgress,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Tool 5: goal_finish (Issue #127 alias matching autonomous-contract prompt)
|
|
255
|
+
const handleGoalFinish = async (args = {}, toolCtx = {}) => {
|
|
256
|
+
const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
257
|
+
const summary = typeof args.summary === 'string' ? args.summary : '';
|
|
258
|
+
if (goalsService && typeof goalsService.complete === 'function') {
|
|
259
|
+
try { goalsService.complete(toolCtx?.agent, summary); } catch (e) { /* ignore */ }
|
|
260
|
+
}
|
|
261
|
+
return { ok: true, goalId, completed: true, summary };
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
safeRegister({
|
|
265
|
+
name: 'goal_finish',
|
|
266
|
+
description: 'Finish active goal with final achieved outcome summary.',
|
|
267
|
+
parameters: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
summary: { type: 'string', description: 'Comprehensive summary of results' },
|
|
271
|
+
},
|
|
272
|
+
required: ['summary'],
|
|
273
|
+
},
|
|
274
|
+
output: JSON_OUTPUT,
|
|
275
|
+
execute: handleGoalFinish,
|
|
185
276
|
});
|
|
186
277
|
});
|
|
187
278
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-goal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"description": "Core Goal Addon (Token Budget, Milestones, Git Snapshots & Retrospective Reports) for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -55,18 +55,9 @@
|
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
57
|
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
58
|
-
"@deepseek-ai/schemastery": "^3.18.4",
|
|
59
|
-
"@deepseek-ai/dsh-goal": "^0.1.7-rc.2 || ^0.2.0-rc.1"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
58
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
63
59
|
},
|
|
64
|
-
"
|
|
60
|
+
"devDependencies": {
|
|
65
61
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
66
|
-
},
|
|
67
|
-
"peerDependenciesMeta": {
|
|
68
|
-
"@deepseek-ai/dsh-goal": {
|
|
69
|
-
"optional": true
|
|
70
|
-
}
|
|
71
62
|
}
|
|
72
63
|
}
|
package/lib/card-form-state.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure state and validation logic for dsh-goal settings form.
|
|
3
|
-
* Independent from DOM/React, 100% testable via node --test.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_SETTINGS = {
|
|
7
|
-
maxIterations: 25,
|
|
8
|
-
autoDrive: true,
|
|
9
|
-
enableSound: true,
|
|
10
|
-
showQuickLaunchButton: true,
|
|
11
|
-
consecutiveToolFailureLimit: 3,
|
|
12
|
-
enableBrowserNotifications: true,
|
|
13
|
-
maxTokenBudget: 0,
|
|
14
|
-
budgetWarningThreshold: 80,
|
|
15
|
-
autoCheckpointOnMilestone: false,
|
|
16
|
-
storagePath: '',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const NUMERIC_FIELDS = {
|
|
20
|
-
maxIterations: 1,
|
|
21
|
-
consecutiveToolFailureLimit: 0,
|
|
22
|
-
maxTokenBudget: 0,
|
|
23
|
-
budgetWarningThreshold: 0,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parse numeric settings field:
|
|
28
|
-
* - empty string / null / undefined -> undefined (reset override)
|
|
29
|
-
* - valid integer >= min -> number
|
|
30
|
-
* - otherwise -> NaN (invalid)
|
|
31
|
-
* @param {any} raw
|
|
32
|
-
* @param {number} [min=1]
|
|
33
|
-
* @returns {number|undefined}
|
|
34
|
-
*/
|
|
35
|
-
export function parseNumberField(raw, min = 1) {
|
|
36
|
-
if (raw === '' || raw === null || raw === undefined) {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
const n = Number(raw);
|
|
40
|
-
if (!Number.isFinite(n) || n < min || !Number.isInteger(n)) {
|
|
41
|
-
return NaN;
|
|
42
|
-
}
|
|
43
|
-
return n;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Compute status for a specific setting field
|
|
48
|
-
* @param {string} field
|
|
49
|
-
* @param {any} draftValue
|
|
50
|
-
* @param {Object} snap - { value, base, user }
|
|
51
|
-
*/
|
|
52
|
-
export function getFieldStatus(field, draftValue, snap) {
|
|
53
|
-
const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
|
|
54
|
-
const userVal = snap?.user?.[field];
|
|
55
|
-
const effectiveVal = snap?.value?.[field] ?? baseVal;
|
|
56
|
-
|
|
57
|
-
const isOverridden = userVal !== undefined && userVal !== null;
|
|
58
|
-
const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
|
|
59
|
-
|
|
60
|
-
let invalid = false;
|
|
61
|
-
let isDirty = false;
|
|
62
|
-
|
|
63
|
-
if (field in NUMERIC_FIELDS) {
|
|
64
|
-
if (draftValue !== undefined) {
|
|
65
|
-
if (draftValue === '') {
|
|
66
|
-
invalid = false;
|
|
67
|
-
isDirty = userVal !== undefined;
|
|
68
|
-
} else {
|
|
69
|
-
const min = NUMERIC_FIELDS[field];
|
|
70
|
-
const parsed = parseNumberField(draftValue, min);
|
|
71
|
-
if (Number.isNaN(parsed)) {
|
|
72
|
-
invalid = true;
|
|
73
|
-
isDirty = true;
|
|
74
|
-
} else {
|
|
75
|
-
isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} else if (field === 'storagePath') {
|
|
80
|
-
if (draftValue !== undefined) {
|
|
81
|
-
const cleanVal = typeof draftValue === 'string' ? draftValue.trim() : '';
|
|
82
|
-
isDirty = cleanVal !== (userVal !== undefined ? userVal : (baseVal || ''));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
// boolean fields (autoDrive, enableSound, showQuickLaunchButton, enableBrowserNotifications, autoCheckpointOnMilestone)
|
|
86
|
-
if (draftValue !== undefined) {
|
|
87
|
-
isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
value: currentVal,
|
|
93
|
-
baseValue: baseVal,
|
|
94
|
-
userValue: userVal,
|
|
95
|
-
isOverridden,
|
|
96
|
-
isDirty,
|
|
97
|
-
invalid,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Compute list of key-value writes for scope.set
|
|
103
|
-
* @param {Object|null} draft
|
|
104
|
-
* @param {Object} snap - { value, base, user }
|
|
105
|
-
* @returns {Array<[string, any]>}
|
|
106
|
-
*/
|
|
107
|
-
export function computeSavePlan(draft, snap) {
|
|
108
|
-
if (!draft) return [];
|
|
109
|
-
|
|
110
|
-
const writes = [];
|
|
111
|
-
const fields = [
|
|
112
|
-
'maxIterations',
|
|
113
|
-
'autoDrive',
|
|
114
|
-
'enableSound',
|
|
115
|
-
'showQuickLaunchButton',
|
|
116
|
-
'consecutiveToolFailureLimit',
|
|
117
|
-
'enableBrowserNotifications',
|
|
118
|
-
'maxTokenBudget',
|
|
119
|
-
'budgetWarningThreshold',
|
|
120
|
-
'autoCheckpointOnMilestone',
|
|
121
|
-
'storagePath',
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
for (const f of fields) {
|
|
125
|
-
if (draft[f] === undefined) continue;
|
|
126
|
-
|
|
127
|
-
const status = getFieldStatus(f, draft[f], snap);
|
|
128
|
-
if (status.invalid) continue;
|
|
129
|
-
|
|
130
|
-
if (status.isDirty) {
|
|
131
|
-
if (f in NUMERIC_FIELDS) {
|
|
132
|
-
if (draft[f] === '') {
|
|
133
|
-
writes.push([f, undefined]);
|
|
134
|
-
} else {
|
|
135
|
-
writes.push([f, parseNumberField(draft[f], NUMERIC_FIELDS[f])]);
|
|
136
|
-
}
|
|
137
|
-
} else if (f === 'storagePath') {
|
|
138
|
-
const clean = typeof draft[f] === 'string' ? draft[f].trim() : '';
|
|
139
|
-
writes.push([f, clean ? clean : undefined]);
|
|
140
|
-
} else {
|
|
141
|
-
writes.push([f, Boolean(draft[f])]);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return writes;
|
|
147
|
-
}
|
package/lib/command-handler.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { GoalState, detectLanguage } from './goal-engine.js';
|
|
3
|
-
|
|
4
|
-
export const USAGE_EN = 'Usage: /goal [<goal>|clear|pause|resume]';
|
|
5
|
-
export const USAGE_ZH = '用法: /goal [<目标描述>|clear|pause|resume]';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Parse input string for /goal command
|
|
9
|
-
* @param {string} rawInput
|
|
10
|
-
* @returns {{ action: string, text?: string }}
|
|
11
|
-
*/
|
|
12
|
-
export function parseGoalInput(rawInput = '') {
|
|
13
|
-
const input = String(rawInput || '').trim();
|
|
14
|
-
if (!input) {
|
|
15
|
-
return { action: 'show' };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const lower = input.toLowerCase();
|
|
19
|
-
if (lower === 'clear') return { action: 'clear' };
|
|
20
|
-
if (lower === 'pause') return { action: 'pause' };
|
|
21
|
-
if (lower === 'resume') return { action: 'resume' };
|
|
22
|
-
|
|
23
|
-
return { action: 'start', text: input };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Create a valid identified user message for agent followup
|
|
28
|
-
* @param {string} text
|
|
29
|
-
* @returns {Object}
|
|
30
|
-
*/
|
|
31
|
-
export function createGoalUserMessage(text) {
|
|
32
|
-
return {
|
|
33
|
-
id: randomUUID(),
|
|
34
|
-
role: 'user',
|
|
35
|
-
content: [{ type: 'text', text }],
|
|
36
|
-
source: { kind: 'user' },
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Format goal start prompt with mandatory milestone contract
|
|
42
|
-
* @param {string} goalText
|
|
43
|
-
* @param {string} [explicitLang]
|
|
44
|
-
* @returns {string}
|
|
45
|
-
*/
|
|
46
|
-
export function formatGoalStartPrompt(goalText, explicitLang) {
|
|
47
|
-
const lang = explicitLang || detectLanguage(goalText);
|
|
48
|
-
if (lang === 'zh') {
|
|
49
|
-
return `🎯 目标模式已激活 (Goal Mode): "${goalText}"\n\n` +
|
|
50
|
-
`自主执行契约:\n` +
|
|
51
|
-
`1. 第一步必选动作:在开展任何工作或回复之前,你的第一个动作必须是调用 goal_set_milestones 工具,提交包含 3-7 个具体连续步骤的工作计划。用户将在界面中实时查看此计划。\n` +
|
|
52
|
-
`2. 第二步:计划确立后开始执行任务。在开始每个步骤前,通过 goal_update_progress(milestone_id, "in_progress") 标记其为进行中。\n` +
|
|
53
|
-
`3. 第三步:完成某一步骤后,通过 goal_update_progress(milestone_id, "completed", "阶段简要成果") 标记为已完成。\n` +
|
|
54
|
-
`4. 第四步:当所有里程碑全部完成后,调用 goal_finish(summary) 工具并附带详尽的最终成果总结。`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return `🎯 Goal Mode activated: "${goalText}"\n\n` +
|
|
58
|
-
`STRICT AUTONOMOUS CONTRACT:\n` +
|
|
59
|
-
`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` +
|
|
60
|
-
`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` +
|
|
61
|
-
`3. STEP 3: Upon completing a milestone, mark it as completed via goal_update_progress(milestone_id, "completed", "brief milestone outcome").\n` +
|
|
62
|
-
`4. STEP 4: When all milestones are completed, call tool goal_finish(summary) with a comprehensive summary of achieved results.`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Format response for UI and manage agent task execution
|
|
67
|
-
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
68
|
-
* @param {{ action: string, text?: string }} parsed
|
|
69
|
-
* @param {Object} [config]
|
|
70
|
-
* @param {Object} [agent]
|
|
71
|
-
* @param {string} [sessionId='default']
|
|
72
|
-
* @returns {{ kind: string, text: string }}
|
|
73
|
-
*/
|
|
74
|
-
export function executeGoalSlashCommand(engine, parsed, config = {}, agent = null, sessionId = 'default') {
|
|
75
|
-
const sid = sessionId || 'default';
|
|
76
|
-
const snap = engine.getGoalSnapshot(sid);
|
|
77
|
-
const lang = snap.lang || (parsed.text ? detectLanguage(parsed.text) : 'en');
|
|
78
|
-
const isZh = lang === 'zh';
|
|
79
|
-
const usage = isZh ? USAGE_ZH : USAGE_EN;
|
|
80
|
-
|
|
81
|
-
switch (parsed.action) {
|
|
82
|
-
case 'show': {
|
|
83
|
-
if (!snap.hasActiveGoal) {
|
|
84
|
-
return {
|
|
85
|
-
kind: 'success',
|
|
86
|
-
text: isZh ? `目标模式: 当前未设置活动目标。\n${usage}` : `Goal Mode: no active goal set.\n${usage}`,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const milestonesInfo = snap.milestones && snap.milestones.length > 0
|
|
91
|
-
? `\n\n${isZh ? '工作计划' : 'Work Plan'}:\n` +
|
|
92
|
-
snap.milestones
|
|
93
|
-
.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`)
|
|
94
|
-
.join('\n')
|
|
95
|
-
: '';
|
|
96
|
-
|
|
97
|
-
const etaText = snap.formattedETA ? ` (ETA: ${snap.formattedETA})` : '';
|
|
98
|
-
const tokensInfo = (snap.tokensUsage && snap.tokensUsage.totalTokens > 0)
|
|
99
|
-
? (isZh
|
|
100
|
-
? `\nToken: ${snap.tokensUsage.totalTokens.toLocaleString()} (输入: ${snap.tokensUsage.promptTokens.toLocaleString()}, 输出: ${snap.tokensUsage.completionTokens.toLocaleString()})`
|
|
101
|
-
: `\nTokens: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`)
|
|
102
|
-
: '';
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
kind: 'success',
|
|
106
|
-
text: isZh
|
|
107
|
-
? `🎯 目标: "${snap.title}"\n` +
|
|
108
|
-
`状态: ${snap.state}\n` +
|
|
109
|
-
`耗时: ${snap.formattedElapsed}${etaText}\n` +
|
|
110
|
-
`迭代轮次: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
111
|
-
tokensInfo +
|
|
112
|
-
milestonesInfo +
|
|
113
|
-
`\n\n快捷指令: /goal pause, /goal resume, /goal clear, /goal <新目标>`
|
|
114
|
-
: `🎯 Goal: "${snap.title}"\n` +
|
|
115
|
-
`Status: ${snap.state}\n` +
|
|
116
|
-
`Duration: ${snap.formattedElapsed}${etaText}\n` +
|
|
117
|
-
`Iterations: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
118
|
-
tokensInfo +
|
|
119
|
-
milestonesInfo +
|
|
120
|
-
`\n\nCommands: /goal pause, /goal resume, /goal clear, /goal <new goal>`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
case 'clear': {
|
|
125
|
-
if (!snap.hasActiveGoal) {
|
|
126
|
-
return {
|
|
127
|
-
kind: 'success',
|
|
128
|
-
text: isZh ? '当前未设置目标。' : 'No active goal set.',
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
engine.clear(sid);
|
|
132
|
-
if (agent && typeof agent.cancel === 'function') {
|
|
133
|
-
try {
|
|
134
|
-
agent.cancel({ kind: 'user', reason: 'Goal cleared by user' });
|
|
135
|
-
} catch (err) {
|
|
136
|
-
// agent cancellation error handled defensively
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
kind: 'success',
|
|
141
|
-
text: isZh ? '🎯 目标已清除。' : '🎯 Goal cleared.',
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
case 'pause': {
|
|
146
|
-
if (!snap.hasActiveGoal) {
|
|
147
|
-
return {
|
|
148
|
-
kind: 'error',
|
|
149
|
-
text: isZh
|
|
150
|
-
? `无法暂停:未设置活动目标。\n${usage}`
|
|
151
|
-
: `Cannot pause: no active goal.\n${usage}`,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (snap.state === GoalState.PAUSED) {
|
|
155
|
-
return {
|
|
156
|
-
kind: 'success',
|
|
157
|
-
text: isZh ? '⏸ 目标当前已处于暂停状态。' : '⏸ Goal is already paused.',
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
engine.pause(isZh ? '用户命令暂停 (/goal pause)' : 'Paused by user command (/goal pause)', sid);
|
|
161
|
-
if (agent && typeof agent.cancel === 'function') {
|
|
162
|
-
try {
|
|
163
|
-
agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
|
|
164
|
-
} catch (err) {
|
|
165
|
-
// agent cancellation error handled defensively
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
kind: 'success',
|
|
170
|
-
text: isZh
|
|
171
|
-
? `⏸ 目标已暂停: "${snap.title}"。智能体已停止,使用 /goal resume 或 ▶️ 按钮继续。`
|
|
172
|
-
: `⏸ Goal paused: "${snap.title}". Agent stopped. Use /goal resume or ▶️ button to continue.`,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
case 'resume': {
|
|
177
|
-
if (!snap.hasActiveGoal) {
|
|
178
|
-
return {
|
|
179
|
-
kind: 'error',
|
|
180
|
-
text: isZh
|
|
181
|
-
? `无法恢复:未设置活动目标。\n${usage}`
|
|
182
|
-
: `Cannot resume: no active goal.\n${usage}`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (snap.state === GoalState.RUNNING) {
|
|
186
|
-
return {
|
|
187
|
-
kind: 'success',
|
|
188
|
-
text: isZh ? '▶️ 目标已在运行中。' : '▶️ Goal is already running.',
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
engine.resume(sid);
|
|
192
|
-
if (agent && typeof agent.followup === 'function') {
|
|
193
|
-
try {
|
|
194
|
-
const resumePrompt = isZh
|
|
195
|
-
? `▶️ 用户已恢复目标运行。请继续推进工作计划中尚未完成的步骤。通过 goal_update_progress 实时更新进度。`
|
|
196
|
-
: `▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.`;
|
|
197
|
-
agent.followup(createGoalUserMessage(resumePrompt));
|
|
198
|
-
} catch (err) {
|
|
199
|
-
// agent followup error handled defensively
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
kind: 'success',
|
|
204
|
-
text: isZh
|
|
205
|
-
? `▶️ 目标已恢复: "${snap.title}"。智能体继续工作中。`
|
|
206
|
-
: `▶️ Goal resumed: "${snap.title}". Agent continues working.`,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
case 'start': {
|
|
211
|
-
const goalLang = detectLanguage(parsed.text);
|
|
212
|
-
const isZhStart = goalLang === 'zh';
|
|
213
|
-
const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
|
|
214
|
-
const cwd = config.cwd;
|
|
215
|
-
const newSnap = engine.startGoal(parsed.text, { maxIterations, lang: goalLang, cwd }, sid);
|
|
216
|
-
|
|
217
|
-
if (agent && typeof agent.followup === 'function') {
|
|
218
|
-
try {
|
|
219
|
-
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text, goalLang));
|
|
220
|
-
agent.followup(userMsg);
|
|
221
|
-
} catch (err) {
|
|
222
|
-
// agent dispatch error handled defensively
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
kind: 'success',
|
|
228
|
-
text: isZhStart
|
|
229
|
-
? `🎯 目标已激活: "${newSnap.title}" (上限 ${newSnap.maxIterations} 轮迭代)。\n` +
|
|
230
|
-
`暂停: /goal pause | 清除: /goal clear`
|
|
231
|
-
: `🎯 Goal activated: "${newSnap.title}" (max ${newSnap.maxIterations} iterations).\n` +
|
|
232
|
-
`To pause: /goal pause | To clear: /goal clear`,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
default:
|
|
237
|
-
return {
|
|
238
|
-
kind: 'error',
|
|
239
|
-
text: isZh ? `未知指令。${usage}` : `Unknown command. ${usage}`,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
}
|