@goodandready/dsh-goal 0.2.11 → 0.2.13
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 +213 -0
- package/lib/client.js +142 -3
- package/lib/engine-reports.js +112 -33
- package/lib/engine-store.js +4 -3
- package/lib/git-snapshot-manager.js +99 -0
- package/lib/goal-engine.js +10 -3
- package/lib/index.js +215 -394
- package/lib/milestone-manager.js +141 -0
- package/lib/routes.js +278 -137
- package/lib/token-budget-guard.js +128 -0
- package/lib/tools.js +115 -257
- package/package.json +4 -3
package/lib/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AddonStore } from './addon-store.js';
|
|
5
|
+
import { TokenBudgetGuard } from './token-budget-guard.js';
|
|
6
|
+
import { GitSnapshotManager } from './git-snapshot-manager.js';
|
|
7
|
+
import { MilestoneManager } from './milestone-manager.js';
|
|
6
8
|
import { registerRoutes } from './routes.js';
|
|
7
9
|
import { registerTools } from './tools.js';
|
|
10
|
+
import { saveGoalReportFile } from './engine-reports.js';
|
|
8
11
|
import { registerPluginUpdater } from './updater.js';
|
|
12
|
+
import { GoalEngine, GoalState } from './goal-engine.js';
|
|
13
|
+
import { sessionIdOf } from './goal-engine-constants.js';
|
|
9
14
|
|
|
10
15
|
export const name = '@goodandready/dsh-goal';
|
|
11
|
-
export const inject = ['webServer', 'settings'];
|
|
16
|
+
export const inject = ['webServer', 'settings', 'goals?'];
|
|
12
17
|
|
|
13
18
|
const NS = 'dsh-goal';
|
|
14
19
|
|
|
@@ -36,10 +41,6 @@ export const Config = z.object({
|
|
|
36
41
|
|
|
37
42
|
export function apply(ctx, config = {}) {
|
|
38
43
|
let settingsScope = null;
|
|
39
|
-
let agentsService = null;
|
|
40
|
-
const runningAgents = new Set();
|
|
41
|
-
const sessionAgents = new Map();
|
|
42
|
-
let lastActiveAgent = null;
|
|
43
44
|
const sseClients = new Map();
|
|
44
45
|
|
|
45
46
|
let currentSettings = {
|
|
@@ -65,430 +66,250 @@ export function apply(ctx, config = {}) {
|
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
68
|
-
const
|
|
69
|
-
? currentSettings.storagePath
|
|
70
|
-
:
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
const customStorage = typeof currentSettings.storagePath === 'string' && currentSettings.storagePath.trim()
|
|
70
|
+
? currentSettings.storagePath.trim()
|
|
71
|
+
: null;
|
|
72
|
+
const isTest = process.env.NODE_ENV === 'test' || Boolean(process.env.NODE_TEST_CONTEXT);
|
|
73
|
+
const storagePath = customStorage || (isTest ? null : path.join(defaultStorageDir, 'dsh-goal-state.json'));
|
|
74
|
+
const logger = ctx.logger ? ctx.logger('goal') : console;
|
|
75
|
+
|
|
76
|
+
// Initialize addon modules
|
|
77
|
+
const addonStore = new AddonStore(storagePath, logger);
|
|
78
|
+
const gitSnapshotManager = new GitSnapshotManager(addonStore, { logger });
|
|
79
|
+
const milestoneManager = new MilestoneManager(addonStore, {
|
|
80
|
+
gitSnapshotManager,
|
|
81
|
+
enforceDependencies: currentSettings.enableMilestoneDependencies,
|
|
82
|
+
logger,
|
|
83
|
+
});
|
|
84
|
+
const tokenBudgetGuard = new TokenBudgetGuard({
|
|
78
85
|
maxTokenBudget: currentSettings.maxTokenBudget,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
enablePreplanning: currentSettings.enablePreplanning,
|
|
83
|
-
autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
|
|
84
|
-
enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
|
|
85
|
-
autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
86
|
-
enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
|
|
87
|
-
enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
|
|
88
|
-
soundScheme: currentSettings.soundScheme,
|
|
89
|
-
enableVoiceAnnouncements: currentSettings.enableVoiceAnnouncements,
|
|
90
|
-
storagePath,
|
|
86
|
+
warningThreshold: currentSettings.budgetWarningThreshold,
|
|
87
|
+
autoScaleNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
88
|
+
logger,
|
|
91
89
|
});
|
|
92
90
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
// Attempt migration of legacy state if present
|
|
92
|
+
try {
|
|
93
|
+
const firstAgent = ctx.agents?.get?.(ctx.agents?.keys?.()?.next()?.value);
|
|
94
|
+
addonStore.migrateLegacyState(storagePath, ctx.goals, firstAgent);
|
|
95
|
+
} catch (migErr) {
|
|
96
|
+
logger.warn?.('[dsh-goal] Legacy migration check failed:', migErr.message);
|
|
97
|
+
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (sid) {
|
|
104
|
-
sessionAgents.set(sid, agent);
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
runningAgents.delete(agent);
|
|
108
|
-
if (lastActiveAgent === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
|
|
109
|
-
lastActiveAgent = null;
|
|
110
|
-
}
|
|
111
|
-
const sid = sessionIdOf(agent, null);
|
|
112
|
-
if (sid && sessionAgents.get(sid) === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
|
|
113
|
-
sessionAgents.delete(sid);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
99
|
+
// Register addon tools: goal_milestones and goal_checklist
|
|
100
|
+
registerTools(ctx, {
|
|
101
|
+
milestoneManager,
|
|
102
|
+
goalsService: ctx.goals,
|
|
103
|
+
logger,
|
|
116
104
|
});
|
|
117
105
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.warn('[dsh-goal] Failed to cancel running agent:', err);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
106
|
+
// Support fallback engine for tests or non-core environments
|
|
107
|
+
let fallbackEngine = null;
|
|
108
|
+
if (!ctx.goals) {
|
|
109
|
+
fallbackEngine = new GoalEngine({
|
|
110
|
+
storagePath,
|
|
111
|
+
defaultMaxIterations: currentSettings.maxIterations,
|
|
112
|
+
autoDrive: currentSettings.autoDrive,
|
|
113
|
+
consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
|
|
114
|
+
maxTokenBudget: currentSettings.maxTokenBudget,
|
|
115
|
+
budgetWarningThreshold: currentSettings.budgetWarningThreshold,
|
|
116
|
+
autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
|
|
117
|
+
enableLiveActivityFeed: currentSettings.enableLiveActivityFeed,
|
|
118
|
+
enablePreplanning: currentSettings.enablePreplanning,
|
|
119
|
+
autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
|
|
120
|
+
enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
|
|
121
|
+
autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
122
|
+
enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
|
|
123
|
+
enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
141
126
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
// 1. One-click auto-updater registration (Issue #99)
|
|
128
|
+
if (ctx?.webServer?.register) {
|
|
129
|
+
if (typeof ctx?.effect === 'function') {
|
|
130
|
+
ctx.effect(() => {
|
|
131
|
+
let unreg = null;
|
|
145
132
|
try {
|
|
146
|
-
|
|
133
|
+
unreg = registerPluginUpdater(ctx, {
|
|
134
|
+
packageName: '@goodandready/dsh-goal',
|
|
135
|
+
endpoint: '/api/dsh-goal/update',
|
|
136
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
137
|
+
});
|
|
147
138
|
} catch (err) {
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const resumeActiveAgent = (promptText, sessionId = 'default') => {
|
|
155
|
-
const sid = sessionId || 'default';
|
|
156
|
-
const snap = engine.getSnapshot(sid);
|
|
157
|
-
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
158
|
-
const defaultResumePrompt = lang === 'zh'
|
|
159
|
-
? '▶️ 用户已恢复目标。请继续执行工作计划,并通过 goal_update_progress 更新进度。'
|
|
160
|
-
: '▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.';
|
|
161
|
-
|
|
162
|
-
const resumeMsg = createGoalUserMessage(promptText || defaultResumePrompt);
|
|
163
|
-
|
|
164
|
-
let target = sessionAgents.get(sid);
|
|
165
|
-
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
166
|
-
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
167
|
-
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
168
|
-
target = lastActiveAgent;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!target && agentsService && typeof agentsService.list === 'function') {
|
|
173
|
-
const list = agentsService.list();
|
|
174
|
-
if (list && list.length > 0) {
|
|
175
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
176
|
-
const cand = list[i];
|
|
177
|
-
const candSid = sessionIdOf(cand, null);
|
|
178
|
-
if (candSid === sid) {
|
|
179
|
-
target = cand;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (!target && sid === 'default') {
|
|
184
|
-
target = list[list.length - 1];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (target && typeof target.followup === 'function') {
|
|
190
|
-
try {
|
|
191
|
-
target.followup(resumeMsg);
|
|
192
|
-
lastActiveAgent = target;
|
|
193
|
-
sessionAgents.set(sid, target);
|
|
194
|
-
return true;
|
|
195
|
-
} catch (err) {
|
|
196
|
-
console.warn('[dsh-goal] Failed to resume agent:', err);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return false;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const getConfig = () => {
|
|
203
|
-
if (settingsScope) {
|
|
204
|
-
const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
|
|
205
|
-
let live = null;
|
|
206
|
-
if (snap) {
|
|
207
|
-
if (snap.status === 'ready' || snap.status === 'writable' || snap.status === undefined) {
|
|
208
|
-
live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
|
|
139
|
+
logger.warn?.('Failed to register plugin updater:', err.message);
|
|
209
140
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
if (live && typeof live === 'object') {
|
|
214
|
-
return {
|
|
215
|
-
...currentSettings,
|
|
216
|
-
...live,
|
|
141
|
+
return () => {
|
|
142
|
+
if (typeof unreg === 'function') unreg();
|
|
217
143
|
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return currentSettings;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const applySettings = () => {
|
|
224
|
-
const live = getConfig();
|
|
225
|
-
engine.updateConfig({
|
|
226
|
-
defaultMaxIterations: live.maxIterations,
|
|
227
|
-
autoDrive: live.autoDrive,
|
|
228
|
-
enableSound: live.enableSound,
|
|
229
|
-
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
230
|
-
consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
|
|
231
|
-
maxTokenBudget: live.maxTokenBudget,
|
|
232
|
-
budgetWarningThreshold: live.budgetWarningThreshold,
|
|
233
|
-
autoCheckpointOnMilestone: live.autoCheckpointOnMilestone,
|
|
234
|
-
enableLiveActivityFeed: live.enableLiveActivityFeed,
|
|
235
|
-
enablePreplanning: live.enablePreplanning,
|
|
236
|
-
autoBranchOnGoalStart: live.autoBranchOnGoalStart,
|
|
237
|
-
enablePostGoalRetrospective: live.enablePostGoalRetrospective,
|
|
238
|
-
autoScaleBudgetNearCompletion: live.autoScaleBudgetNearCompletion,
|
|
239
|
-
enableTemplatesDrawer: live.enableTemplatesDrawer,
|
|
240
|
-
enableMilestoneDependencies: live.enableMilestoneDependencies,
|
|
241
|
-
soundScheme: live.soundScheme,
|
|
242
|
-
enableVoiceAnnouncements: live.enableVoiceAnnouncements,
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// 1. Settings registration
|
|
247
|
-
ctx.inject(['settings'], (sctx) => {
|
|
248
|
-
try {
|
|
249
|
-
const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
|
|
250
|
-
if (scope) {
|
|
251
|
-
settingsScope = scope;
|
|
252
|
-
applySettings();
|
|
253
|
-
if (typeof scope.subscribe === 'function') {
|
|
254
|
-
sctx.effect(() => {
|
|
255
|
-
const off = scope.subscribe(() => {
|
|
256
|
-
applySettings();
|
|
257
|
-
});
|
|
258
|
-
return () => {
|
|
259
|
-
if (typeof off === 'function') off();
|
|
260
|
-
settingsScope = null;
|
|
261
|
-
};
|
|
262
|
-
}, 'dsh-goal: settings subscription');
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
} catch (err) {
|
|
266
|
-
console.warn('[dsh-goal] Settings register skipped:', err.message);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// 2. Chat slash command /goal registration
|
|
271
|
-
ctx.inject(['commands'], (cctx) => {
|
|
272
|
-
try {
|
|
273
|
-
if (typeof cctx.commands?.register !== 'function') return;
|
|
274
|
-
const unregister = cctx.commands.register({
|
|
275
|
-
name: 'goal',
|
|
276
|
-
description: 'Manage Goal Mode: activate, milestones, pause, resume, and clear',
|
|
277
|
-
input: { hint: '[<objective>|clear|pause|resume]' },
|
|
278
|
-
handler: async (invocation) => {
|
|
279
|
-
const sid = sessionIdOf(invocation, 'default');
|
|
280
|
-
if (invocation?.agent) {
|
|
281
|
-
lastActiveAgent = invocation.agent;
|
|
282
|
-
sessionAgents.set(sid, invocation.agent);
|
|
283
|
-
}
|
|
284
|
-
const parsed = parseGoalInput(invocation?.rawInput);
|
|
285
|
-
const liveConfig = getConfig();
|
|
286
|
-
return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
if (typeof unregister === 'function') {
|
|
291
|
-
cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
|
|
292
|
-
}
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.warn('[dsh-goal] Commands register skipped:', err.message);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// 3. System prompt context injection
|
|
299
|
-
ctx.inject(['systemPrompt'], (pctx) => {
|
|
300
|
-
try {
|
|
301
|
-
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
302
|
-
const globalSections = pctx.systemPrompt.layers?.global?.sections;
|
|
303
|
-
let displacedPromptSection = null;
|
|
304
|
-
if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
|
|
305
|
-
displacedPromptSection = globalSections.data.get('tool:goal');
|
|
306
|
-
globalSections.data.delete('tool:goal');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
|
|
310
|
-
? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 2400)
|
|
311
|
-
: 2400;
|
|
312
|
-
|
|
313
|
-
const unregisterSection = pctx.systemPrompt.section({
|
|
314
|
-
name: 'tool:dsh-goal',
|
|
315
|
-
order,
|
|
316
|
-
text: (sessionCtx) => {
|
|
317
|
-
const sid = sessionIdOf(sessionCtx, 'default');
|
|
318
|
-
return engine.getStatePromptInjection(sid);
|
|
319
|
-
},
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
if (typeof unregisterSection === 'function') {
|
|
323
|
-
pctx.effect(() => () => {
|
|
324
|
-
unregisterSection();
|
|
325
|
-
if (displacedPromptSection && globalSections?.data instanceof Map) {
|
|
326
|
-
globalSections.data.set('tool:goal', displacedPromptSection);
|
|
327
|
-
}
|
|
328
|
-
}, 'dsh-goal: system prompt section');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
} catch (err) {
|
|
332
|
-
console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// 4. Tools registration
|
|
337
|
-
registerTools(ctx, { engine, resumeActiveAgent });
|
|
338
|
-
|
|
339
|
-
// 5. One-click auto-updater registration
|
|
340
|
-
if (ctx.webServer?.register) {
|
|
341
|
-
try {
|
|
144
|
+
}, 'dsh-goal: plugin auto-updater');
|
|
145
|
+
} else {
|
|
342
146
|
registerPluginUpdater(ctx, {
|
|
343
147
|
packageName: '@goodandready/dsh-goal',
|
|
344
148
|
endpoint: '/api/dsh-goal/update',
|
|
345
149
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
346
150
|
});
|
|
347
|
-
} catch (err) {
|
|
348
|
-
console.warn('[dsh-goal] Failed to register plugin updater:', err.message);
|
|
349
151
|
}
|
|
350
152
|
}
|
|
351
153
|
|
|
352
|
-
//
|
|
353
|
-
registerRoutes(ctx, {
|
|
354
|
-
engine,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
154
|
+
// 2. WebServer HTTP & SSE routes (registered after updater so it remains primary)
|
|
155
|
+
const { broadcastEvent } = registerRoutes(ctx, {
|
|
156
|
+
engine: fallbackEngine,
|
|
157
|
+
milestoneManager,
|
|
158
|
+
gitSnapshotManager,
|
|
159
|
+
tokenBudgetGuard,
|
|
160
|
+
addonStore,
|
|
161
|
+
goalsService: ctx.goals,
|
|
162
|
+
getConfig: () => currentSettings,
|
|
359
163
|
sseClients,
|
|
164
|
+
logger,
|
|
360
165
|
});
|
|
361
166
|
|
|
362
|
-
//
|
|
363
|
-
ctx
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
|
|
167
|
+
// 3. Listen to core goal/changed events if ctx.on exists
|
|
168
|
+
if (typeof ctx?.on === 'function') {
|
|
169
|
+
ctx.on('goal/changed', async ({ agent, change }) => {
|
|
170
|
+
if (!change) return;
|
|
171
|
+
const op = change.operation;
|
|
172
|
+
const goal = change.goal;
|
|
173
|
+
const goalId = goal?.id || change.ref?.id;
|
|
174
|
+
|
|
175
|
+
if (op === 'create' || op === 'resume') {
|
|
176
|
+
tokenBudgetGuard.resetGoal(goalId);
|
|
177
|
+
broadcastEvent('goal_status', { op, goalId, goal });
|
|
372
178
|
}
|
|
373
179
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const failCount = engine.incrementToolFailureCount(sid);
|
|
385
|
-
const limit = liveConfig.consecutiveToolFailureLimit;
|
|
386
|
-
if (limit > 0 && failCount >= limit) {
|
|
387
|
-
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
388
|
-
const pauseReason = lang === 'zh'
|
|
389
|
-
? `工具重复出错(连续 ${failCount} 次)— 目标已暂停以防止死循环`
|
|
390
|
-
: `Repeated tool failure (${failCount} consecutive) — goal paused to prevent token burn`;
|
|
391
|
-
engine.pause(pauseReason, sid);
|
|
392
|
-
return;
|
|
180
|
+
if (op === 'complete' || op === 'clear') {
|
|
181
|
+
try {
|
|
182
|
+
const milestones = milestoneManager.getMilestones(goalId);
|
|
183
|
+
const snapshots = addonStore.getSnapshots(goalId);
|
|
184
|
+
const savedPath = saveGoalReportFile(goal || { id: goalId, objective: 'Completed Goal' }, {
|
|
185
|
+
milestones,
|
|
186
|
+
snapshots,
|
|
187
|
+
});
|
|
188
|
+
if (savedPath) {
|
|
189
|
+
logger.info?.(`[dsh-goal] Saved goal report to "${savedPath}"`);
|
|
393
190
|
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
403
|
-
const stallCount = engine.incrementStallCount(sid);
|
|
404
|
-
if (stallCount >= 2) {
|
|
405
|
-
const stallReason = lang === 'zh'
|
|
406
|
-
? '智能体在过去 2 次迭代中未推进工作计划 — 需要操作员关注'
|
|
407
|
-
: 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
|
|
408
|
-
engine.pause(stallReason, sid);
|
|
409
|
-
return;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logger.warn?.('[dsh-goal] Failed to save goal report on completion:', err.message);
|
|
410
193
|
}
|
|
194
|
+
broadcastEvent('goal_status', { op, goalId, goal });
|
|
195
|
+
}
|
|
411
196
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
|
|
418
|
-
const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
|
|
419
|
-
let promptText = currentLang === 'zh'
|
|
420
|
-
? '请按照工作计划继续自主执行目标。每完成一步请通过 goal_update_progress 记录,所有任务完成后调用 goal_finish。'
|
|
421
|
-
: '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.';
|
|
422
|
-
|
|
423
|
-
if (currentSnap.pendingNudge) {
|
|
424
|
-
const userNudge = engine.consumePendingNudge(sid);
|
|
425
|
-
if (userNudge) {
|
|
426
|
-
promptText = (currentLang === 'zh'
|
|
427
|
-
? `🚨 用户紧急说明/指导方向:\n"${userNudge}"\n请根据此反馈调整执行。\n\n`
|
|
428
|
-
: `🚨 URGENT USER CLARIFICATION / STEERING:\n"${userNudge}"\nAdjust execution adhering to this feedback.\n\n`) + promptText;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const promptMsg = createGoalUserMessage(promptText);
|
|
433
|
-
let target = sessionAgents.get(sid);
|
|
434
|
-
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
435
|
-
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
436
|
-
if (!lastSid || lastSid === sid || sid === 'default') {
|
|
437
|
-
target = lastActiveAgent;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
197
|
+
if (op === 'block') {
|
|
198
|
+
logger.info?.(`[dsh-goal] Goal "${goalId}" blocked: ${goal?.blockedReason?.message || 'Policy block'}`);
|
|
199
|
+
broadcastEvent('goal_status', { op, goalId, goal });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
440
202
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
203
|
+
// Inject milestone prompts and check budget during agent turns
|
|
204
|
+
ctx.on('agent/pre-step', async ({ agent, messages, step, signal }, next) => {
|
|
205
|
+
if (ctx.goals && typeof ctx.goals.get === 'function' && agent) {
|
|
206
|
+
try {
|
|
207
|
+
const currentGoal = ctx.goals.get(agent);
|
|
208
|
+
if (currentGoal && currentGoal.phase === 'active') {
|
|
209
|
+
const goalId = currentGoal.id;
|
|
210
|
+
|
|
211
|
+
// 1. Budget check
|
|
212
|
+
const milestones = milestoneManager.getMilestones(goalId);
|
|
213
|
+
const budgetCheck = tokenBudgetGuard.checkBudget({
|
|
214
|
+
agent,
|
|
215
|
+
goal: currentGoal,
|
|
216
|
+
tokenMeter: ctx.tokenMeter,
|
|
217
|
+
goalsService: ctx.goals,
|
|
218
|
+
milestones,
|
|
219
|
+
});
|
|
454
220
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
221
|
+
// 2. Active milestone prompt guidance
|
|
222
|
+
const milestonePrompt = milestoneManager.getActiveMilestonePrompt(goalId);
|
|
223
|
+
if (milestonePrompt && Array.isArray(messages)) {
|
|
224
|
+
let promptText = milestonePrompt;
|
|
225
|
+
if (budgetCheck.status === 'warning') {
|
|
226
|
+
promptText += `\n${budgetCheck.message}`;
|
|
227
|
+
} else if (budgetCheck.status === 'auto_scaled') {
|
|
228
|
+
promptText += `\n${budgetCheck.message}`;
|
|
461
229
|
}
|
|
230
|
+
messages.push({
|
|
231
|
+
role: 'system',
|
|
232
|
+
content: promptText,
|
|
233
|
+
});
|
|
462
234
|
}
|
|
463
235
|
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (typeof setImmediate === 'function') {
|
|
467
|
-
setImmediate(triggerNextTurn);
|
|
468
|
-
} else {
|
|
469
|
-
setTimeout(triggerNextTurn, 0);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logger.warn?.('[dsh-goal] agent/pre-step error:', err.message);
|
|
470
238
|
}
|
|
471
239
|
}
|
|
472
|
-
|
|
240
|
+
return next ? next() : undefined;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Compatibility events for turn/end and approval/asked
|
|
244
|
+
ctx.on('turn/end', (event) => {
|
|
245
|
+
// noop or session isolation
|
|
246
|
+
});
|
|
473
247
|
|
|
474
|
-
|
|
248
|
+
ctx.on('approval/asked', (event) => {
|
|
249
|
+
if (!fallbackEngine) return;
|
|
475
250
|
const sid = sessionIdOf(event, 'default');
|
|
476
|
-
const snap =
|
|
477
|
-
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
251
|
+
const snap = fallbackEngine.getSnapshot(sid);
|
|
252
|
+
if (snap && snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
253
|
+
fallbackEngine.pause('Waiting for operator confirmation/approval', sid);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Settings synchronization
|
|
259
|
+
const applySettings = (patch) => {
|
|
260
|
+
if (!patch || typeof patch !== 'object') return;
|
|
261
|
+
currentSettings = { ...currentSettings, ...patch };
|
|
262
|
+
tokenBudgetGuard.updateConfig({
|
|
263
|
+
maxTokenBudget: currentSettings.maxTokenBudget,
|
|
264
|
+
warningThreshold: currentSettings.budgetWarningThreshold,
|
|
265
|
+
autoScaleNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
266
|
+
});
|
|
267
|
+
milestoneManager.enforceDependencies = Boolean(currentSettings.enableMilestoneDependencies);
|
|
268
|
+
if (typeof patch.storagePath === 'string' && patch.storagePath.trim()) {
|
|
269
|
+
addonStore.setStoragePath(patch.storagePath.trim());
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (typeof ctx?.inject === 'function') {
|
|
274
|
+
ctx.inject(['settings'], (sctx) => {
|
|
275
|
+
try {
|
|
276
|
+
const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
|
|
277
|
+
if (scope) {
|
|
278
|
+
settingsScope = scope;
|
|
279
|
+
if (typeof scope.get === 'function') {
|
|
280
|
+
applySettings(scope.get());
|
|
281
|
+
}
|
|
282
|
+
if (typeof scope.subscribe === 'function') {
|
|
283
|
+
sctx.effect?.(() => {
|
|
284
|
+
const off = scope.subscribe(() => {
|
|
285
|
+
if (typeof scope.get === 'function') applySettings(scope.get());
|
|
286
|
+
});
|
|
287
|
+
return () => {
|
|
288
|
+
if (typeof off === 'function') off();
|
|
289
|
+
settingsScope = null;
|
|
290
|
+
};
|
|
291
|
+
}, 'dsh-goal: settings subscription');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logger.warn?.('Settings register skipped:', err.message);
|
|
483
296
|
}
|
|
484
|
-
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
485
299
|
|
|
486
|
-
|
|
487
|
-
ctx.
|
|
300
|
+
if (typeof ctx?.effect === 'function') {
|
|
301
|
+
ctx.effect(() => () => {
|
|
302
|
+
try {
|
|
303
|
+
addonStore.flushSync();
|
|
304
|
+
} catch (e) { /* ignore */ }
|
|
305
|
+
}, 'dsh-goal: teardown');
|
|
306
|
+
}
|
|
488
307
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
308
|
+
return {
|
|
309
|
+
addonStore,
|
|
310
|
+
milestoneManager,
|
|
311
|
+
tokenBudgetGuard,
|
|
312
|
+
gitSnapshotManager,
|
|
313
|
+
engine: fallbackEngine,
|
|
314
|
+
};
|
|
494
315
|
}
|