@goodandready/dsh-goal 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/client.js +4384 -4498
- package/lib/command-handler.js +242 -276
- package/lib/engine-prompt.js +57 -0
- package/lib/engine-reports.js +114 -0
- package/lib/engine-store.js +142 -0
- package/lib/goal-engine-constants.js +100 -0
- package/lib/goal-engine.js +579 -2018
- package/lib/index.js +452 -1960
- package/lib/routes.js +294 -0
- package/lib/tools.js +294 -0
- package/lib/updater.js +317 -0
- package/package.json +62 -63
package/lib/routes.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register webServer HTTP routes and SSE stream for DSH Goal
|
|
5
|
+
* @param {any} ctx Cordis context
|
|
6
|
+
* @param {object} deps Dependencies
|
|
7
|
+
* @param {any} deps.engine GoalEngine instance
|
|
8
|
+
* @param {Function} deps.getConfig Function returning current config
|
|
9
|
+
* @param {Function} deps.stopRunningAgents Stop agents function
|
|
10
|
+
* @param {Function} deps.resumeActiveAgent Resume agent function
|
|
11
|
+
* @param {Map} deps.sessionAgents Session agent map
|
|
12
|
+
* @param {Map} [deps.sseClients] SSE clients map (sid -> Set)
|
|
13
|
+
*/
|
|
14
|
+
export function registerRoutes(ctx, {
|
|
15
|
+
engine,
|
|
16
|
+
getConfig,
|
|
17
|
+
stopRunningAgents,
|
|
18
|
+
resumeActiveAgent,
|
|
19
|
+
sessionAgents,
|
|
20
|
+
sseClients = new Map(),
|
|
21
|
+
}) {
|
|
22
|
+
// Subscribe to engine changes for realtime Server-Sent Events broadcasting
|
|
23
|
+
engine.subscribe((snapshot, sid) => {
|
|
24
|
+
const clients = sseClients.get(sid);
|
|
25
|
+
if (clients && clients.size > 0) {
|
|
26
|
+
const payload = `data: ${JSON.stringify(snapshot)}\n\n`;
|
|
27
|
+
for (const clientRes of Array.from(clients)) {
|
|
28
|
+
try {
|
|
29
|
+
clientRes.write(payload);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
clients.delete(clientRes);
|
|
32
|
+
try { clientRes.end(); } catch (e) { /* closed */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (clients.size === 0) sseClients.delete(sid);
|
|
36
|
+
}
|
|
37
|
+
if (sid !== 'default' && sseClients.has('default')) {
|
|
38
|
+
const defClients = sseClients.get('default');
|
|
39
|
+
if (defClients && defClients.size > 0) {
|
|
40
|
+
const payload = `data: ${JSON.stringify(snapshot)}\n\n`;
|
|
41
|
+
for (const clientRes of Array.from(defClients)) {
|
|
42
|
+
try {
|
|
43
|
+
clientRes.write(payload);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
defClients.delete(clientRes);
|
|
46
|
+
try { clientRes.end(); } catch (e) { /* closed */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (defClients.size === 0) sseClients.delete('default');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
ctx.effect(() => {
|
|
55
|
+
if (!ctx.webServer?.register) return () => {};
|
|
56
|
+
|
|
57
|
+
// Keepalive ping timer for SSE connections (every 20s)
|
|
58
|
+
const keepaliveTimer = setInterval(() => {
|
|
59
|
+
for (const [sid, clients] of sseClients.entries()) {
|
|
60
|
+
for (const res of Array.from(clients)) {
|
|
61
|
+
try {
|
|
62
|
+
res.write(': keepalive\n\n');
|
|
63
|
+
} catch (err) {
|
|
64
|
+
clients.delete(res);
|
|
65
|
+
try { res.end(); } catch (e) { /* closed */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (clients.size === 0) {
|
|
69
|
+
sseClients.delete(sid);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, 20000);
|
|
73
|
+
if (typeof keepaliveTimer.unref === 'function') keepaliveTimer.unref();
|
|
74
|
+
|
|
75
|
+
const unreg = ctx.webServer.register({
|
|
76
|
+
kind: 'prefix',
|
|
77
|
+
path: '/dsh-goal',
|
|
78
|
+
handler: (req, res) => {
|
|
79
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
80
|
+
const pathname = url.pathname;
|
|
81
|
+
|
|
82
|
+
// GET /dsh-goal/events: Server-Sent Events realtime snapshot stream
|
|
83
|
+
if (req.method === 'GET' && (pathname === '/dsh-goal/events' || pathname === '/dsh-goal/events/')) {
|
|
84
|
+
const sid = sessionIdOf(req, 'default');
|
|
85
|
+
res.writeHead(200, {
|
|
86
|
+
'Content-Type': 'text/event-stream',
|
|
87
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
88
|
+
'Connection': 'keep-alive',
|
|
89
|
+
'X-Accel-Buffering': 'no',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!sseClients.has(sid)) {
|
|
93
|
+
sseClients.set(sid, new Set());
|
|
94
|
+
}
|
|
95
|
+
sseClients.get(sid).add(res);
|
|
96
|
+
|
|
97
|
+
const initialSnap = engine.getSnapshot(sid);
|
|
98
|
+
res.write(`data: ${JSON.stringify(initialSnap)}\n\n`);
|
|
99
|
+
|
|
100
|
+
req.on('close', () => {
|
|
101
|
+
const set = sseClients.get(sid);
|
|
102
|
+
if (set) {
|
|
103
|
+
set.delete(res);
|
|
104
|
+
if (set.size === 0) sseClients.delete(sid);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
111
|
+
|
|
112
|
+
// GET /dsh-goal/state
|
|
113
|
+
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
114
|
+
const sid = sessionIdOf(req, 'default');
|
|
115
|
+
res.statusCode = 200;
|
|
116
|
+
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// POST /dsh-goal/action
|
|
120
|
+
// CSRF & Same-Origin guard
|
|
121
|
+
if (req.method === 'POST' && (pathname === '/dsh-goal/action' || pathname === '/dsh-goal/action/')) {
|
|
122
|
+
const secFetchSite = req.headers['sec-fetch-site'];
|
|
123
|
+
if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'same-site' && secFetchSite !== 'none') {
|
|
124
|
+
res.statusCode = 403;
|
|
125
|
+
return res.end(JSON.stringify({ error: 'Forbidden: cross-site requests are rejected' }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const origin = req.headers.origin;
|
|
129
|
+
const host = req.headers.host;
|
|
130
|
+
if (origin && host) {
|
|
131
|
+
try {
|
|
132
|
+
const originHost = new URL(origin).host;
|
|
133
|
+
if (originHost !== host) {
|
|
134
|
+
res.statusCode = 403;
|
|
135
|
+
return res.end(JSON.stringify({ error: 'Forbidden: origin mismatch' }));
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
res.statusCode = 403;
|
|
139
|
+
return res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let body = '';
|
|
144
|
+
let bodySize = 0;
|
|
145
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
146
|
+
let limitExceeded = false;
|
|
147
|
+
|
|
148
|
+
req.on('data', (chunk) => {
|
|
149
|
+
bodySize += chunk.length;
|
|
150
|
+
if (bodySize > MAX_PAYLOAD_BYTES) {
|
|
151
|
+
limitExceeded = true;
|
|
152
|
+
req.pause();
|
|
153
|
+
res.statusCode = 413;
|
|
154
|
+
return res.end(JSON.stringify({ error: 'Payload too large: max 256 KB allowed' }));
|
|
155
|
+
}
|
|
156
|
+
body += chunk;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
req.on('end', () => {
|
|
160
|
+
if (limitExceeded) return;
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(body || '{}');
|
|
163
|
+
const sid = data.sessionId || sessionIdOf(req, 'default');
|
|
164
|
+
const { action, title, description, reason, milestoneId, status, notes } = data;
|
|
165
|
+
|
|
166
|
+
let result = null;
|
|
167
|
+
switch (action) {
|
|
168
|
+
case 'start': {
|
|
169
|
+
const cleanTitle = typeof title === 'string' ? title.trim() : '';
|
|
170
|
+
if (!cleanTitle) {
|
|
171
|
+
res.statusCode = 400;
|
|
172
|
+
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
173
|
+
}
|
|
174
|
+
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
175
|
+
const conf = typeof getConfig === 'function' ? getConfig() : { maxIterations: 25 };
|
|
176
|
+
result = engine.startGoal(cleanTitle, {
|
|
177
|
+
description: typeof description === 'string' ? description.trim() : '',
|
|
178
|
+
maxIterations: conf.maxIterations || 25,
|
|
179
|
+
lang: detectedLang,
|
|
180
|
+
}, sid);
|
|
181
|
+
|
|
182
|
+
const startPrompt = detectedLang === 'zh'
|
|
183
|
+
? `🎯 目标已确立:“${cleanTitle}”。请立即通过 goal_set_milestones 制定工作计划(3-7个具体步骤)并开始执行。`
|
|
184
|
+
: `🎯 Goal established: "${cleanTitle}". Immediately formulate a work plan (3-7 concrete steps) via tool goal_set_milestones and start executing it.`;
|
|
185
|
+
|
|
186
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
187
|
+
resumeActiveAgent(startPrompt, sid);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'nudge': {
|
|
193
|
+
const text = (typeof data.text === 'string' ? data.text : (data.notes || data.nudge || '')).trim();
|
|
194
|
+
if (!text) {
|
|
195
|
+
res.statusCode = 400;
|
|
196
|
+
return res.end(JSON.stringify({ error: 'Nudge text cannot be empty' }));
|
|
197
|
+
}
|
|
198
|
+
result = engine.nudge(text, sid);
|
|
199
|
+
if (data.resume) {
|
|
200
|
+
engine.resume(sid);
|
|
201
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
202
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
203
|
+
resumeActiveAgent(prompt, sid);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'pause':
|
|
210
|
+
result = engine.pause(reason || 'Paused by user interface', sid);
|
|
211
|
+
if (typeof stopRunningAgents === 'function') {
|
|
212
|
+
stopRunningAgents(sid);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'resume':
|
|
217
|
+
result = engine.resume(sid);
|
|
218
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
219
|
+
resumeActiveAgent(undefined, sid);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'cancel':
|
|
224
|
+
result = engine.cancel(reason || 'Goal cancelled by user', sid);
|
|
225
|
+
if (typeof stopRunningAgents === 'function') {
|
|
226
|
+
stopRunningAgents(sid);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case 'clear':
|
|
231
|
+
result = engine.clear(sid);
|
|
232
|
+
if (typeof stopRunningAgents === 'function') {
|
|
233
|
+
stopRunningAgents(sid);
|
|
234
|
+
}
|
|
235
|
+
if (sessionAgents) {
|
|
236
|
+
sessionAgents.delete(sid);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'update_milestone': {
|
|
241
|
+
if (!milestoneId || !status) {
|
|
242
|
+
res.statusCode = 400;
|
|
243
|
+
return res.end(JSON.stringify({ error: 'milestoneId and status are required' }));
|
|
244
|
+
}
|
|
245
|
+
const validStatuses = Object.values(MilestoneStatus);
|
|
246
|
+
if (!validStatuses.includes(status)) {
|
|
247
|
+
res.statusCode = 400;
|
|
248
|
+
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
249
|
+
}
|
|
250
|
+
const ok = engine.updateMilestone(milestoneId, status, notes, sid);
|
|
251
|
+
if (!ok) {
|
|
252
|
+
res.statusCode = 404;
|
|
253
|
+
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
254
|
+
}
|
|
255
|
+
result = engine.getSnapshot(sid);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
default:
|
|
260
|
+
res.statusCode = 400;
|
|
261
|
+
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
res.statusCode = 200;
|
|
265
|
+
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot(sid) }));
|
|
266
|
+
} catch (parseErr) {
|
|
267
|
+
res.statusCode = 400;
|
|
268
|
+
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
res.statusCode = 404;
|
|
275
|
+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
clearInterval(keepaliveTimer);
|
|
281
|
+
if (typeof unreg === 'function') unreg();
|
|
282
|
+
for (const clients of sseClients.values()) {
|
|
283
|
+
for (const res of clients) {
|
|
284
|
+
try {
|
|
285
|
+
res.end();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
// Already closed
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
sseClients.clear();
|
|
292
|
+
};
|
|
293
|
+
}, 'dsh-goal: HTTP WebServer Routes & SSE');
|
|
294
|
+
}
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { GoalState, sessionIdOf } from './goal-engine-constants.js';
|
|
2
|
+
|
|
3
|
+
export const JSON_OUTPUT = {
|
|
4
|
+
schema: { type: 'object', additionalProperties: true },
|
|
5
|
+
render: (_args, val) => [{ type: 'text', text: JSON.stringify(val) }],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format goal snapshot into DSH core-compatible goal representation
|
|
10
|
+
* @param {any} snap
|
|
11
|
+
* @returns {object}
|
|
12
|
+
*/
|
|
13
|
+
export function formatGoalValue(snap) {
|
|
14
|
+
if (!snap || !snap.hasActiveGoal) {
|
|
15
|
+
return { goal: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let phase = 'active';
|
|
19
|
+
if (snap.state === GoalState.PAUSED) phase = 'paused';
|
|
20
|
+
else if (snap.state === GoalState.COMPLETED) phase = 'complete';
|
|
21
|
+
|
|
22
|
+
const roundsStarted = Number.isInteger(snap.iterations) ? snap.iterations : 0;
|
|
23
|
+
const maxGoalRounds = Number.isInteger(snap.maxIterations) ? snap.maxIterations : 25;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
goal: {
|
|
27
|
+
id: snap.id || 'goal-active',
|
|
28
|
+
revision: 1,
|
|
29
|
+
objective: snap.title || 'Goal',
|
|
30
|
+
phase,
|
|
31
|
+
roundsStarted,
|
|
32
|
+
maxGoalRounds,
|
|
33
|
+
},
|
|
34
|
+
activation: snap.state === GoalState.RUNNING ? 'armed' : 'disarmed',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register goal management tools into Cordis tools service
|
|
40
|
+
* @param {any} ctx
|
|
41
|
+
* @param {object} options
|
|
42
|
+
* @param {any} options.engine
|
|
43
|
+
* @param {Function} [options.resumeActiveAgent]
|
|
44
|
+
*/
|
|
45
|
+
export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
46
|
+
ctx.inject(['tools'], (tctx) => {
|
|
47
|
+
if (!tctx.tools?.register) return;
|
|
48
|
+
|
|
49
|
+
const safeRegister = (definition) => {
|
|
50
|
+
try {
|
|
51
|
+
const name = definition.name;
|
|
52
|
+
const globalTools = tctx.tools.layers?.global?.tools;
|
|
53
|
+
if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
|
|
54
|
+
globalTools.data.delete(name);
|
|
55
|
+
}
|
|
56
|
+
const unregister = tctx.tools.register(definition);
|
|
57
|
+
if (typeof unregister === 'function') {
|
|
58
|
+
tctx.effect(() => () => unregister(), `dsh-goal: tool ${name}`);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Tool 1: get_goal
|
|
66
|
+
const handleGetGoal = async (_args, toolCtx) => {
|
|
67
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
68
|
+
const snap = engine.getSnapshot(sid);
|
|
69
|
+
return formatGoalValue(snap);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
safeRegister({
|
|
73
|
+
name: 'get_goal',
|
|
74
|
+
description: 'Get current active goal and its execution phase.',
|
|
75
|
+
parameters: { type: 'object', properties: {} },
|
|
76
|
+
output: JSON_OUTPUT,
|
|
77
|
+
execute: handleGetGoal,
|
|
78
|
+
handler: handleGetGoal,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Tool 2: create_goal
|
|
82
|
+
const handleCreateGoal = async (args, toolCtx) => {
|
|
83
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
84
|
+
const maxIterations = Number(args.max_goal_rounds) || 25;
|
|
85
|
+
const snap = engine.startGoal(args.objective, { maxIterations }, sid);
|
|
86
|
+
return formatGoalValue(snap);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
safeRegister({
|
|
90
|
+
name: 'create_goal',
|
|
91
|
+
description: 'Initialize a new autonomous goal.',
|
|
92
|
+
parameters: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
objective: { type: 'string', description: 'The overarching objective to achieve' },
|
|
96
|
+
max_goal_rounds: { type: 'number', description: 'Maximum iterations allowed' },
|
|
97
|
+
},
|
|
98
|
+
required: ['objective'],
|
|
99
|
+
},
|
|
100
|
+
output: JSON_OUTPUT,
|
|
101
|
+
execute: handleCreateGoal,
|
|
102
|
+
handler: handleCreateGoal,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Tool 3: update_goal
|
|
106
|
+
const handleUpdateGoal = async (args, toolCtx) => {
|
|
107
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
108
|
+
const action = args.action;
|
|
109
|
+
|
|
110
|
+
if (action === 'complete') {
|
|
111
|
+
const summary = args.blocked_reason || args.objective || 'Goal marked complete';
|
|
112
|
+
const snap = engine.completeGoal(summary, sid);
|
|
113
|
+
if (toolCtx?.deferContext) {
|
|
114
|
+
try {
|
|
115
|
+
toolCtx.deferContext({
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: `<goal_complete>\nObjective: ${JSON.stringify(snap.title || summary)}\nThe goal is marked complete. Summarize what was accomplished for the user.\n</goal_complete>`,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
toolCtx?.logger?.debug?.('[dsh-goal] Failed to defer complete context:', err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return formatGoalValue(snap);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (action === 'pause') {
|
|
127
|
+
const reason = args.blocked_reason || 'Paused by model';
|
|
128
|
+
const snap = engine.pause(reason, sid);
|
|
129
|
+
return formatGoalValue(snap);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (action === 'resume') {
|
|
133
|
+
const snap = engine.resume(sid);
|
|
134
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
135
|
+
resumeActiveAgent(undefined, sid);
|
|
136
|
+
}
|
|
137
|
+
return formatGoalValue(snap);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (action === 'edit') {
|
|
141
|
+
const snap = engine.getSnapshot(sid);
|
|
142
|
+
if (args.objective) snap.title = args.objective;
|
|
143
|
+
if (args.max_goal_rounds) snap.maxIterations = Number(args.max_goal_rounds);
|
|
144
|
+
engine.emit(sid, true);
|
|
145
|
+
return formatGoalValue(snap);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (action === 'blocked') {
|
|
149
|
+
const reason = args.blocked_reason || 'Goal blocked';
|
|
150
|
+
const snap = engine.pause('Blocked: ' + reason, sid);
|
|
151
|
+
if (toolCtx?.deferContext) {
|
|
152
|
+
try {
|
|
153
|
+
toolCtx.deferContext({
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: `<goal_blocked>\nObjective: ${JSON.stringify(snap.title || 'Goal')}\nBlocked: ${JSON.stringify(reason)}\nExplain to the user what blocked progress.\n</goal_blocked>`,
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
toolCtx?.logger?.debug?.('[dsh-goal] Failed to defer blocked context:', err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const res = formatGoalValue(snap);
|
|
162
|
+
if (res.goal) {
|
|
163
|
+
res.goal.phase = 'blocked';
|
|
164
|
+
res.goal.blockedReason = { code: 'model-reported', message: reason };
|
|
165
|
+
}
|
|
166
|
+
return res;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return formatGoalValue(engine.getSnapshot(sid));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
safeRegister({
|
|
173
|
+
name: 'update_goal',
|
|
174
|
+
description: 'Update status or parameters of the current active goal.',
|
|
175
|
+
parameters: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
action: {
|
|
179
|
+
type: 'string',
|
|
180
|
+
enum: ['pause', 'resume', 'complete', 'edit', 'blocked'],
|
|
181
|
+
description: 'Action to perform on the goal',
|
|
182
|
+
},
|
|
183
|
+
objective: { type: 'string', description: 'Updated goal title (for edit)' },
|
|
184
|
+
max_goal_rounds: { type: 'number', description: 'Updated iterations limit (for edit)' },
|
|
185
|
+
blocked_reason: { type: 'string', description: 'Reason for pause, blocked, or completion summary' },
|
|
186
|
+
},
|
|
187
|
+
required: ['action'],
|
|
188
|
+
},
|
|
189
|
+
output: JSON_OUTPUT,
|
|
190
|
+
execute: handleUpdateGoal,
|
|
191
|
+
handler: handleUpdateGoal,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Tool 4: goal_set_milestones
|
|
195
|
+
const handleSetMilestones = async ({ milestones }, toolCtx) => {
|
|
196
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
197
|
+
const snap = engine.setMilestones(milestones, sid);
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
count: snap.milestones?.length || 0,
|
|
201
|
+
milestones: snap.milestones,
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
safeRegister({
|
|
206
|
+
name: 'goal_set_milestones',
|
|
207
|
+
description: 'Decompose the goal into 3-7 verifiable milestones.',
|
|
208
|
+
parameters: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
milestones: {
|
|
212
|
+
type: 'array',
|
|
213
|
+
items: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
id: { type: 'string', description: 'Short identifier, e.g. M1, step-1' },
|
|
217
|
+
title: { type: 'string', description: 'Concise actionable title' },
|
|
218
|
+
},
|
|
219
|
+
required: ['id', 'title'],
|
|
220
|
+
},
|
|
221
|
+
description: 'List of milestones to achieve the goal',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['milestones'],
|
|
225
|
+
},
|
|
226
|
+
output: JSON_OUTPUT,
|
|
227
|
+
execute: handleSetMilestones,
|
|
228
|
+
handler: handleSetMilestones,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Tool 5: goal_update_progress
|
|
232
|
+
const handleUpdateProgress = async ({ milestone_id, status, notes }, toolCtx) => {
|
|
233
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
234
|
+
const snap = engine.updateMilestone(milestone_id, status, notes, sid);
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
milestone_id,
|
|
238
|
+
status,
|
|
239
|
+
total_milestones: snap.milestones?.length,
|
|
240
|
+
completed: snap.milestones?.filter((m) => m.status === 'completed').length,
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
safeRegister({
|
|
245
|
+
name: 'goal_update_progress',
|
|
246
|
+
description: 'Update the progress of a specific milestone.',
|
|
247
|
+
parameters: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: {
|
|
250
|
+
milestone_id: { type: 'string', description: 'Milestone ID (e.g. M1)' },
|
|
251
|
+
status: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
enum: ['pending', 'in_progress', 'completed', 'failed'],
|
|
254
|
+
description: 'New milestone status',
|
|
255
|
+
},
|
|
256
|
+
notes: { type: 'string', description: 'Summary of actions completed or blocking issue' },
|
|
257
|
+
},
|
|
258
|
+
required: ['milestone_id', 'status'],
|
|
259
|
+
},
|
|
260
|
+
output: JSON_OUTPUT,
|
|
261
|
+
execute: handleUpdateProgress,
|
|
262
|
+
handler: handleUpdateProgress,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Tool 6: goal_finish
|
|
266
|
+
const handleGoalFinish = async ({ summary }, toolCtx) => {
|
|
267
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
268
|
+
const snap = engine.completeGoal(summary, sid);
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
completed: true,
|
|
272
|
+
summary,
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
safeRegister({
|
|
277
|
+
name: 'goal_finish',
|
|
278
|
+
description: 'Conclude the active goal successfully with a final summary and achievements.',
|
|
279
|
+
parameters: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
summary: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: 'Final summary of the goal outcome and deliverables.',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
required: ['summary'],
|
|
288
|
+
},
|
|
289
|
+
output: JSON_OUTPUT,
|
|
290
|
+
execute: handleGoalFinish,
|
|
291
|
+
handler: handleGoalFinish,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|