@goodandready/dsh-goal 0.1.0
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/LICENSE +21 -0
- package/README.md +47 -0
- package/README.ru.md +51 -0
- package/cordis.patch.yml +4 -0
- package/docs/design/DESIGN.md +72 -0
- package/lib/client.js +694 -0
- package/lib/goal-engine.js +353 -0
- package/lib/index.js +223 -0
- package/package.json +57 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Изолированное ядро управления состоянием цели (Goal Engine).
|
|
3
|
+
* Не имеет внешних зависимостей, 100% тестируемо через node --test.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const GoalState = {
|
|
7
|
+
IDLE: 'IDLE',
|
|
8
|
+
PLANNING: 'PLANNING',
|
|
9
|
+
RUNNING: 'RUNNING',
|
|
10
|
+
PAUSED: 'PAUSED',
|
|
11
|
+
COMPLETED: 'COMPLETED',
|
|
12
|
+
FAILED: 'FAILED',
|
|
13
|
+
CANCELLED: 'CANCELLED',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MilestoneStatus = {
|
|
17
|
+
PENDING: 'pending',
|
|
18
|
+
IN_PROGRESS: 'in_progress',
|
|
19
|
+
COMPLETED: 'completed',
|
|
20
|
+
FAILED: 'failed',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Форматирование времени в лаконичную строку (например: "2s", "45s", "1m 15s", "2h 5m")
|
|
25
|
+
* @param {number} totalSeconds
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function formatElapsed(totalSeconds) {
|
|
29
|
+
const sec = Math.max(0, Math.floor(totalSeconds));
|
|
30
|
+
if (sec < 60) return `${sec}s`;
|
|
31
|
+
const mins = Math.floor(sec / 60);
|
|
32
|
+
const remainingSec = sec % 60;
|
|
33
|
+
if (mins < 60) {
|
|
34
|
+
return remainingSec > 0 ? `${mins}m ${remainingSec}s` : `${mins}m`;
|
|
35
|
+
}
|
|
36
|
+
const hours = Math.floor(mins / 60);
|
|
37
|
+
const remainingMins = mins % 60;
|
|
38
|
+
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class GoalEngine {
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
44
|
+
this.currentGoal = null;
|
|
45
|
+
this.listeners = new Set();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Подписка на изменение состояния
|
|
50
|
+
* @param {Function} callback
|
|
51
|
+
* @returns {Function} unsubscribe
|
|
52
|
+
*/
|
|
53
|
+
subscribe(callback) {
|
|
54
|
+
this.listeners.add(callback);
|
|
55
|
+
return () => this.listeners.delete(callback);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
emit() {
|
|
59
|
+
const snapshot = this.getSnapshot();
|
|
60
|
+
for (const listener of this.listeners) {
|
|
61
|
+
try {
|
|
62
|
+
listener(snapshot);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error('[GoalEngine] Listener error:', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Запуск новой цели
|
|
71
|
+
*/
|
|
72
|
+
startGoal(title, options = {}) {
|
|
73
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
74
|
+
throw new Error('Goal title cannot be empty');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cleanTitle = title.trim();
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
|
|
80
|
+
this.currentGoal = {
|
|
81
|
+
id: `goal-${now}-${Math.random().toString(36).substring(2, 7)}`,
|
|
82
|
+
title: cleanTitle,
|
|
83
|
+
description: options.description?.trim() || '',
|
|
84
|
+
state: GoalState.RUNNING,
|
|
85
|
+
startedAt: now,
|
|
86
|
+
pausedAt: null,
|
|
87
|
+
totalPausedDurationMs: 0,
|
|
88
|
+
completedAt: null,
|
|
89
|
+
iterationsCount: 0,
|
|
90
|
+
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
91
|
+
milestones: [],
|
|
92
|
+
logs: [
|
|
93
|
+
{
|
|
94
|
+
timestamp: now,
|
|
95
|
+
type: 'info',
|
|
96
|
+
message: `Goal initiated: "${cleanTitle}"`,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
resultSummary: '',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (Array.isArray(options.milestones) && options.milestones.length > 0) {
|
|
103
|
+
this.addMilestones(options.milestones, false);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.emit();
|
|
107
|
+
return this.getSnapshot();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Приостановка автономного цикла цели
|
|
112
|
+
*/
|
|
113
|
+
pause(reason = 'User requested pause') {
|
|
114
|
+
if (!this.currentGoal || this.currentGoal.state !== GoalState.RUNNING) {
|
|
115
|
+
return this.getSnapshot();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.currentGoal.state = GoalState.PAUSED;
|
|
119
|
+
this.currentGoal.pausedAt = Date.now();
|
|
120
|
+
this.currentGoal.logs.push({
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
type: 'warning',
|
|
123
|
+
message: `Paused: ${reason}`,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.emit();
|
|
127
|
+
return this.getSnapshot();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Возобновление выполнения цели
|
|
132
|
+
*/
|
|
133
|
+
resume() {
|
|
134
|
+
if (!this.currentGoal || this.currentGoal.state !== GoalState.PAUSED) {
|
|
135
|
+
return this.getSnapshot();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (this.currentGoal.pausedAt) {
|
|
140
|
+
this.currentGoal.totalPausedDurationMs += now - this.currentGoal.pausedAt;
|
|
141
|
+
this.currentGoal.pausedAt = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.currentGoal.state = GoalState.RUNNING;
|
|
145
|
+
this.currentGoal.logs.push({
|
|
146
|
+
timestamp: now,
|
|
147
|
+
type: 'info',
|
|
148
|
+
message: 'Goal resumed',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.emit();
|
|
152
|
+
return this.getSnapshot();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Отмена цели
|
|
157
|
+
*/
|
|
158
|
+
cancel(reason = 'Cancelled by user') {
|
|
159
|
+
if (!this.currentGoal) return null;
|
|
160
|
+
|
|
161
|
+
this.currentGoal.state = GoalState.CANCELLED;
|
|
162
|
+
this.currentGoal.completedAt = Date.now();
|
|
163
|
+
this.currentGoal.logs.push({
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
type: 'warning',
|
|
166
|
+
message: `Cancelled: ${reason}`,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.emit();
|
|
170
|
+
return this.getSnapshot();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Очистка / сброс цели в IDLE
|
|
175
|
+
*/
|
|
176
|
+
clear() {
|
|
177
|
+
this.currentGoal = null;
|
|
178
|
+
this.emit();
|
|
179
|
+
return this.getSnapshot();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Успешное завершение цели
|
|
184
|
+
*/
|
|
185
|
+
completeGoal(summary = '') {
|
|
186
|
+
if (!this.currentGoal) return null;
|
|
187
|
+
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
this.currentGoal.state = GoalState.COMPLETED;
|
|
190
|
+
this.currentGoal.completedAt = now;
|
|
191
|
+
this.currentGoal.resultSummary = summary;
|
|
192
|
+
this.currentGoal.logs.push({
|
|
193
|
+
timestamp: now,
|
|
194
|
+
type: 'info',
|
|
195
|
+
message: `Goal completed successfully: ${summary || 'All objectives met.'}`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Завершаем все активные milestones
|
|
199
|
+
for (const m of this.currentGoal.milestones) {
|
|
200
|
+
if (m.status === MilestoneStatus.IN_PROGRESS || m.status === MilestoneStatus.PENDING) {
|
|
201
|
+
m.status = MilestoneStatus.COMPLETED;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.emit();
|
|
206
|
+
return this.getSnapshot();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Добавление вех (milestones)
|
|
211
|
+
*/
|
|
212
|
+
addMilestones(milestonesList, shouldEmit = true) {
|
|
213
|
+
if (!this.currentGoal || !Array.isArray(milestonesList)) return;
|
|
214
|
+
|
|
215
|
+
for (const item of milestonesList) {
|
|
216
|
+
const itemTitle = typeof item === 'string' ? item : item.title;
|
|
217
|
+
if (!itemTitle || !itemTitle.trim()) continue;
|
|
218
|
+
|
|
219
|
+
const mId = (typeof item === 'object' && item.id) ? item.id : `m-${this.currentGoal.milestones.length + 1}`;
|
|
220
|
+
this.currentGoal.milestones.push({
|
|
221
|
+
id: String(mId),
|
|
222
|
+
title: itemTitle.trim(),
|
|
223
|
+
status: (typeof item === 'object' && item.status) ? item.status : MilestoneStatus.PENDING,
|
|
224
|
+
notes: (typeof item === 'object' && item.notes) ? item.notes : '',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (shouldEmit) this.emit();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Обновление конкретной вехи
|
|
233
|
+
*/
|
|
234
|
+
updateMilestone(id, status, notes = '') {
|
|
235
|
+
if (!this.currentGoal) return false;
|
|
236
|
+
|
|
237
|
+
const target = this.currentGoal.milestones.find((m) => m.id === String(id));
|
|
238
|
+
if (!target) return false;
|
|
239
|
+
|
|
240
|
+
if (status && Object.values(MilestoneStatus).includes(status)) {
|
|
241
|
+
target.status = status;
|
|
242
|
+
}
|
|
243
|
+
if (notes) {
|
|
244
|
+
target.notes = notes;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.currentGoal.logs.push({
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
type: 'milestone',
|
|
250
|
+
message: `Milestone [${target.title}] status -> ${target.status}`,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
this.emit();
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Увеличение счётчика итераций turn
|
|
259
|
+
*/
|
|
260
|
+
incrementIteration() {
|
|
261
|
+
if (!this.currentGoal || this.currentGoal.state !== GoalState.RUNNING) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.currentGoal.iterationsCount += 1;
|
|
266
|
+
|
|
267
|
+
if (this.currentGoal.iterationsCount >= this.currentGoal.maxIterations) {
|
|
268
|
+
this.currentGoal.state = GoalState.FAILED;
|
|
269
|
+
this.currentGoal.logs.push({
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
type: 'error',
|
|
272
|
+
message: `Safety limit reached: maximum ${this.currentGoal.maxIterations} iterations exceeded.`,
|
|
273
|
+
});
|
|
274
|
+
this.emit();
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.emit();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Подсчёт времени в секундах
|
|
284
|
+
*/
|
|
285
|
+
getElapsedSeconds() {
|
|
286
|
+
if (!this.currentGoal) return 0;
|
|
287
|
+
const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = this.currentGoal;
|
|
288
|
+
const endTime = completedAt || (pausedAt || Date.now());
|
|
289
|
+
const elapsedMs = Math.max(0, endTime - startedAt - totalPausedDurationMs);
|
|
290
|
+
return Math.floor(elapsedMs / 1000);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Снимок состояния для передачи клиенту / API
|
|
295
|
+
*/
|
|
296
|
+
getSnapshot() {
|
|
297
|
+
if (!this.currentGoal) {
|
|
298
|
+
return {
|
|
299
|
+
hasActiveGoal: false,
|
|
300
|
+
state: GoalState.IDLE,
|
|
301
|
+
title: '',
|
|
302
|
+
elapsedSeconds: 0,
|
|
303
|
+
formattedElapsed: '0s',
|
|
304
|
+
milestones: [],
|
|
305
|
+
progressPercent: 0,
|
|
306
|
+
iterationsCount: 0,
|
|
307
|
+
maxIterations: this.defaultMaxIterations,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const elapsed = this.getElapsedSeconds();
|
|
312
|
+
const milestones = this.currentGoal.milestones;
|
|
313
|
+
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
314
|
+
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
hasActiveGoal: true,
|
|
318
|
+
id: this.currentGoal.id,
|
|
319
|
+
state: this.currentGoal.state,
|
|
320
|
+
title: this.currentGoal.title,
|
|
321
|
+
description: this.currentGoal.description,
|
|
322
|
+
elapsedSeconds: elapsed,
|
|
323
|
+
formattedElapsed: formatElapsed(elapsed),
|
|
324
|
+
iterationsCount: this.currentGoal.iterationsCount,
|
|
325
|
+
maxIterations: this.currentGoal.maxIterations,
|
|
326
|
+
milestones,
|
|
327
|
+
progressPercent,
|
|
328
|
+
logs: this.currentGoal.logs,
|
|
329
|
+
resultSummary: this.currentGoal.resultSummary,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Формирование системного контекста для инжекта модели
|
|
335
|
+
*/
|
|
336
|
+
getStatePromptInjection() {
|
|
337
|
+
if (!this.currentGoal || this.currentGoal.state !== GoalState.RUNNING) {
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const snapshot = this.getSnapshot();
|
|
342
|
+
const milestonesText = snapshot.milestones.length > 0
|
|
343
|
+
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
344
|
+
: ' (No specific milestones decomposed yet; use goal_set_milestones to break down)';
|
|
345
|
+
|
|
346
|
+
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
347
|
+
Current Target Goal: "${snapshot.title}"
|
|
348
|
+
Elapsed Time: ${snapshot.formattedElapsed} | Turn Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
349
|
+
Milestones Progress (${snapshot.progressPercent}%):
|
|
350
|
+
${milestonesText}
|
|
351
|
+
Instructions: Keep focused strictly on achieving this goal. When sub-tasks finish, update milestones via goal_update_progress. When the target is completely accomplished, call goal_finish.`;
|
|
352
|
+
}
|
|
353
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { GoalEngine, GoalState, MilestoneStatus } from './goal-engine.js';
|
|
2
|
+
|
|
3
|
+
export const name = '@goodandready/dsh-goal';
|
|
4
|
+
export const inject = ['webServer', 'settings'];
|
|
5
|
+
|
|
6
|
+
const NS = 'dsh-goal';
|
|
7
|
+
|
|
8
|
+
export function apply(ctx, config = {}) {
|
|
9
|
+
const engine = new GoalEngine({
|
|
10
|
+
defaultMaxIterations: config?.maxIterations ?? 25,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let currentSettings = {
|
|
14
|
+
maxIterations: config?.maxIterations ?? 25,
|
|
15
|
+
autoDrive: config?.autoDrive ?? true,
|
|
16
|
+
enableSound: config?.enableSound ?? true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 1. Регистрация настроек плагина
|
|
20
|
+
ctx.inject(['settings'], (sctx) => {
|
|
21
|
+
try {
|
|
22
|
+
const scope = sctx.settings?.register?.(NS, null, { base: currentSettings });
|
|
23
|
+
if (scope) {
|
|
24
|
+
currentSettings = { ...currentSettings, ...(scope.get?.() ?? {}) };
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn('[dsh-goal] Settings register skipped:', err.message);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 2. Регистрация инструментов для модели (tools)
|
|
32
|
+
ctx.inject(['tools'], (tctx) => {
|
|
33
|
+
if (!tctx.tools?.register) return;
|
|
34
|
+
|
|
35
|
+
// Инструмент 1: Декомпозиция цели на вехи
|
|
36
|
+
tctx.tools.register({
|
|
37
|
+
name: 'goal_set_milestones',
|
|
38
|
+
description: 'Break down the current active goal into a sequence of concrete milestones/sub-tasks.',
|
|
39
|
+
parameters: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
milestones: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: { type: 'string' },
|
|
45
|
+
description: 'List of milestone titles to accomplish.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['milestones'],
|
|
49
|
+
},
|
|
50
|
+
handler: async ({ milestones }) => {
|
|
51
|
+
const snap = engine.getSnapshot();
|
|
52
|
+
if (!snap.hasActiveGoal) {
|
|
53
|
+
return { error: 'No active goal currently set. Start a goal first.' };
|
|
54
|
+
}
|
|
55
|
+
engine.addMilestones(milestones);
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
milestones: engine.getSnapshot().milestones,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Инструмент 2: Обновление статуса вехи
|
|
64
|
+
tctx.tools.register({
|
|
65
|
+
name: 'goal_update_progress',
|
|
66
|
+
description: 'Update the status of a specific goal milestone and optionally log progress notes.',
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
milestone_id: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'The ID of the milestone (e.g. "m-1", "m-2").',
|
|
73
|
+
},
|
|
74
|
+
status: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
enum: ['pending', 'in_progress', 'completed', 'failed'],
|
|
77
|
+
description: 'New status for this milestone.',
|
|
78
|
+
},
|
|
79
|
+
notes: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Brief summary of what was accomplished or why it failed.',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ['milestone_id', 'status'],
|
|
85
|
+
},
|
|
86
|
+
handler: async ({ milestone_id, status, notes }) => {
|
|
87
|
+
const ok = engine.updateMilestone(milestone_id, status, notes);
|
|
88
|
+
if (!ok) {
|
|
89
|
+
return { error: `Milestone ${milestone_id} not found or no active goal.` };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
snapshot: engine.getSnapshot(),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Инструмент 3: Успешное завершение цели
|
|
99
|
+
tctx.tools.register({
|
|
100
|
+
name: 'goal_finish',
|
|
101
|
+
description: 'Conclude the active goal successfully with a final summary and achievements.',
|
|
102
|
+
parameters: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
summary: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Final summary of the goal outcome and deliverables.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['summary'],
|
|
111
|
+
},
|
|
112
|
+
handler: async ({ summary }) => {
|
|
113
|
+
const snap = engine.completeGoal(summary);
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
completed: true,
|
|
117
|
+
summary,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 3. Регистрация HTTP REST API маршрутов
|
|
124
|
+
ctx.effect(() => {
|
|
125
|
+
if (!ctx.webServer?.register) return () => {};
|
|
126
|
+
|
|
127
|
+
const unreg = ctx.webServer.register({
|
|
128
|
+
kind: 'prefix',
|
|
129
|
+
path: '/dsh-goal',
|
|
130
|
+
handler: (req, res) => {
|
|
131
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
132
|
+
const pathname = url.pathname;
|
|
133
|
+
|
|
134
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
135
|
+
|
|
136
|
+
// GET /dsh-goal/state
|
|
137
|
+
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
138
|
+
res.statusCode = 200;
|
|
139
|
+
return res.end(JSON.stringify(engine.getSnapshot()));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// POST /dsh-goal/action
|
|
143
|
+
if (req.method === 'POST' && (pathname === '/dsh-goal/action' || pathname === '/dsh-goal/action/')) {
|
|
144
|
+
let body = '';
|
|
145
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
146
|
+
req.on('end', () => {
|
|
147
|
+
try {
|
|
148
|
+
const data = JSON.parse(body || '{}');
|
|
149
|
+
const { action, title, description, reason, milestoneId, status, notes } = data;
|
|
150
|
+
|
|
151
|
+
let result = null;
|
|
152
|
+
switch (action) {
|
|
153
|
+
case 'start':
|
|
154
|
+
result = engine.startGoal(title || 'Новая цель', { description });
|
|
155
|
+
break;
|
|
156
|
+
case 'pause':
|
|
157
|
+
result = engine.pause(reason);
|
|
158
|
+
break;
|
|
159
|
+
case 'resume':
|
|
160
|
+
result = engine.resume();
|
|
161
|
+
break;
|
|
162
|
+
case 'cancel':
|
|
163
|
+
result = engine.cancel(reason);
|
|
164
|
+
break;
|
|
165
|
+
case 'clear':
|
|
166
|
+
result = engine.clear();
|
|
167
|
+
break;
|
|
168
|
+
case 'update_milestone':
|
|
169
|
+
engine.updateMilestone(milestoneId, status, notes);
|
|
170
|
+
result = engine.getSnapshot();
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
res.statusCode = 400;
|
|
174
|
+
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
res.statusCode = 200;
|
|
178
|
+
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot() }));
|
|
179
|
+
} catch (parseErr) {
|
|
180
|
+
res.statusCode = 400;
|
|
181
|
+
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
res.statusCode = 404;
|
|
188
|
+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return () => {
|
|
193
|
+
if (typeof unreg === 'function') unreg();
|
|
194
|
+
};
|
|
195
|
+
}, 'dsh-goal: HTTP WebServer Routes');
|
|
196
|
+
|
|
197
|
+
// 4. Подписка на события сессии (автономный цикл)
|
|
198
|
+
ctx.effect(() => {
|
|
199
|
+
// Подписка на завершение turn
|
|
200
|
+
const onTurnEnd = () => {
|
|
201
|
+
const snap = engine.getSnapshot();
|
|
202
|
+
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
203
|
+
engine.incrementIteration();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Подписка на запрос подтверждения (approval/asked) -> автоматическая пауза
|
|
208
|
+
const onApprovalAsked = () => {
|
|
209
|
+
const snap = engine.getSnapshot();
|
|
210
|
+
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
211
|
+
engine.pause('Ожидание подтверждения действия оператором');
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
ctx.on?.('turn/end', onTurnEnd);
|
|
216
|
+
ctx.on?.('approval/asked', onApprovalAsked);
|
|
217
|
+
|
|
218
|
+
return () => {
|
|
219
|
+
ctx.off?.('turn/end', onTurnEnd);
|
|
220
|
+
ctx.off?.('approval/asked', onApprovalAsked);
|
|
221
|
+
};
|
|
222
|
+
}, 'dsh-goal: Session & Turn Coordinator');
|
|
223
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-goal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Goal mode & autonomous execution plugin for DeepSeek Harness with sticky top banner",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/",
|
|
15
|
+
"docs/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"README.ru.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test test/*.test.mjs"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"dsh",
|
|
25
|
+
"dsh-plugin",
|
|
26
|
+
"deepseek-harness",
|
|
27
|
+
"goal-mode",
|
|
28
|
+
"autonomous-agent",
|
|
29
|
+
"cordis"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-goal.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-goal",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-goal/issues"
|
|
38
|
+
},
|
|
39
|
+
"author": "goodandready",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dsh": {
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web",
|
|
47
|
+
"inject": [
|
|
48
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
55
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6"
|
|
56
|
+
}
|
|
57
|
+
}
|