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