@goodandready/dsh-goal 0.2.5 → 0.2.7
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 +13 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/client.js +539 -135
- package/lib/engine-milestones.js +106 -0
- package/lib/engine-prompt.js +29 -8
- package/lib/engine-reports.js +108 -2
- package/lib/goal-engine-constants.js +116 -0
- package/lib/goal-engine.js +120 -112
- package/lib/index.js +15 -0
- package/lib/routes.js +88 -1
- package/lib/tools.js +19 -3
- package/package.json +1 -1
package/lib/goal-engine.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
2
|
-
import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR } from './engine-reports.js';
|
|
1
|
+
import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot } from './goal-engine-constants.js';
|
|
2
|
+
import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
|
|
3
3
|
import { EngineStore } from './engine-store.js';
|
|
4
4
|
import { buildStatePromptInjection } from './engine-prompt.js';
|
|
5
|
+
import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
|
|
5
6
|
|
|
6
|
-
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf };
|
|
7
|
-
export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR };
|
|
7
|
+
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone };
|
|
8
|
+
export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Isolated Goal Management Engine for DeepSeek Harness
|
|
@@ -16,6 +17,9 @@ export class GoalEngine {
|
|
|
16
17
|
this.enableSound = options.enableSound ?? true;
|
|
17
18
|
this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
|
|
18
19
|
this.consecutiveToolFailureLimit = options.consecutiveToolFailureLimit ?? 3;
|
|
20
|
+
this.maxTokenBudget = options.maxTokenBudget ?? 0;
|
|
21
|
+
this.budgetWarningThreshold = options.budgetWarningThreshold ?? 80;
|
|
22
|
+
this.autoCheckpointOnMilestone = options.autoCheckpointOnMilestone ?? false;
|
|
19
23
|
this.maxSessions = options.maxSessions ?? 100;
|
|
20
24
|
this.goals = new Map();
|
|
21
25
|
this.listeners = new Set();
|
|
@@ -99,6 +103,15 @@ export class GoalEngine {
|
|
|
99
103
|
if (typeof config.consecutiveToolFailureLimit === 'number') {
|
|
100
104
|
this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
|
|
101
105
|
}
|
|
106
|
+
if (typeof config.maxTokenBudget === 'number') {
|
|
107
|
+
this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
|
|
108
|
+
}
|
|
109
|
+
if (typeof config.budgetWarningThreshold === 'number') {
|
|
110
|
+
this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
|
|
111
|
+
}
|
|
112
|
+
if (typeof config.autoCheckpointOnMilestone === 'boolean') {
|
|
113
|
+
this.autoCheckpointOnMilestone = config.autoCheckpointOnMilestone;
|
|
114
|
+
}
|
|
102
115
|
this.emit();
|
|
103
116
|
}
|
|
104
117
|
|
|
@@ -175,6 +188,7 @@ export class GoalEngine {
|
|
|
175
188
|
const oldest = inactive.shift();
|
|
176
189
|
this.goals.delete(oldest.sid);
|
|
177
190
|
this.stallCounters.delete(oldest.sid);
|
|
191
|
+
this.toolFailureCounters.delete(oldest.sid);
|
|
178
192
|
}
|
|
179
193
|
}
|
|
180
194
|
|
|
@@ -218,6 +232,8 @@ export class GoalEngine {
|
|
|
218
232
|
completedAt: null,
|
|
219
233
|
iterationsCount: 0,
|
|
220
234
|
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
235
|
+
maxTokenBudget: options.maxTokenBudget ?? this.maxTokenBudget,
|
|
236
|
+
budgetWarningTriggered: false,
|
|
221
237
|
gitStartCommit: gitCommit || null,
|
|
222
238
|
pendingNudge: null,
|
|
223
239
|
nudges: [],
|
|
@@ -321,6 +337,7 @@ export class GoalEngine {
|
|
|
321
337
|
const sid = sessionId || 'default';
|
|
322
338
|
this.goals.delete(sid);
|
|
323
339
|
this.stallCounters.delete(sid);
|
|
340
|
+
this.toolFailureCounters.delete(sid);
|
|
324
341
|
this.emit(sid, true);
|
|
325
342
|
return this.getSnapshot(sid);
|
|
326
343
|
}
|
|
@@ -351,6 +368,7 @@ export class GoalEngine {
|
|
|
351
368
|
}
|
|
352
369
|
|
|
353
370
|
this.stallCounters.delete(sid);
|
|
371
|
+
this.toolFailureCounters.delete(sid);
|
|
354
372
|
this.emit(sid, true);
|
|
355
373
|
return this.getSnapshot(sid);
|
|
356
374
|
}
|
|
@@ -360,36 +378,40 @@ export class GoalEngine {
|
|
|
360
378
|
const goal = this.goals.get(sid);
|
|
361
379
|
if (!goal || !Array.isArray(milestonesList)) return;
|
|
362
380
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!itemTitle || !itemTitle.trim()) continue;
|
|
366
|
-
|
|
367
|
-
const mId = typeof item === 'object' && item.id ? item.id : `m-${goal.milestones.length + 1}`;
|
|
368
|
-
goal.milestones.push({
|
|
369
|
-
id: String(mId),
|
|
370
|
-
title: itemTitle.trim(),
|
|
371
|
-
status: typeof item === 'object' && item.status ? item.status : MilestoneStatus.PENDING,
|
|
372
|
-
notes: typeof item === 'object' && item.notes ? item.notes : '',
|
|
373
|
-
});
|
|
374
|
-
}
|
|
381
|
+
const newItems = parseMilestoneItems(milestonesList, goal.milestones.length);
|
|
382
|
+
goal.milestones.push(...newItems);
|
|
375
383
|
|
|
376
384
|
this.recordProgress(sid);
|
|
377
385
|
if (shouldEmit) this.emit(sid);
|
|
378
386
|
}
|
|
379
387
|
|
|
380
|
-
updateMilestone(id, status, notes = '', sessionId = 'default') {
|
|
388
|
+
updateMilestone(id, status, notes = '', sessionId = 'default', checklist = null) {
|
|
381
389
|
const sid = sessionId || 'default';
|
|
382
390
|
const goal = this.goals.get(sid);
|
|
383
391
|
if (!goal) return false;
|
|
384
392
|
|
|
385
|
-
const target = goal.milestones.find((m) => m
|
|
393
|
+
const target = goal.milestones.find((m) => matchMilestone(m, id));
|
|
386
394
|
if (!target) return false;
|
|
387
395
|
|
|
388
|
-
|
|
389
|
-
target
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
396
|
+
const { prevStatus } = applyMilestoneUpdate(
|
|
397
|
+
target,
|
|
398
|
+
status,
|
|
399
|
+
notes,
|
|
400
|
+
checklist,
|
|
401
|
+
Object.values(MilestoneStatus)
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// Auto Git Checkpoint on milestone completion
|
|
405
|
+
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
|
|
406
|
+
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
407
|
+
if (commitHash) {
|
|
408
|
+
target.checkpointCommit = commitHash;
|
|
409
|
+
goal.logs.push({
|
|
410
|
+
timestamp: Date.now(),
|
|
411
|
+
type: 'info',
|
|
412
|
+
message: `Git checkpoint saved for [${target.title}]: ${commitHash}`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
393
415
|
}
|
|
394
416
|
|
|
395
417
|
goal.logs.push({
|
|
@@ -407,6 +429,48 @@ export class GoalEngine {
|
|
|
407
429
|
return true;
|
|
408
430
|
}
|
|
409
431
|
|
|
432
|
+
toggleChecklistItem(milestoneId, itemIndex, done, sessionId = 'default') {
|
|
433
|
+
const sid = sessionId || 'default';
|
|
434
|
+
const goal = this.goals.get(sid);
|
|
435
|
+
if (!goal) return false;
|
|
436
|
+
|
|
437
|
+
const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
|
|
438
|
+
const ok = toggleMilestoneChecklistItem(target, itemIndex, done);
|
|
439
|
+
if (!ok) return false;
|
|
440
|
+
|
|
441
|
+
this.recordProgress(sid);
|
|
442
|
+
this.emit(sid);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
extendBudget(addTokens = 50000, sessionId = 'default') {
|
|
447
|
+
const sid = sessionId || 'default';
|
|
448
|
+
const goal = this.goals.get(sid);
|
|
449
|
+
if (!goal) return false;
|
|
450
|
+
|
|
451
|
+
const currentTotal = goal.tokensUsage?.totalTokens || 0;
|
|
452
|
+
const currentMax = goal.maxTokenBudget || this.maxTokenBudget || 0;
|
|
453
|
+
const newBudget = Math.max(currentTotal, currentMax) + Math.max(1000, Number(addTokens) || 50000);
|
|
454
|
+
|
|
455
|
+
goal.maxTokenBudget = newBudget;
|
|
456
|
+
goal.budgetWarningTriggered = false;
|
|
457
|
+
|
|
458
|
+
if (goal.state === GoalState.PAUSED && goal.logs?.some((l) => l.message?.includes('Token budget reached'))) {
|
|
459
|
+
goal.state = GoalState.RUNNING;
|
|
460
|
+
goal.pausedAt = null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
goal.logs.push({
|
|
464
|
+
timestamp: Date.now(),
|
|
465
|
+
type: 'info',
|
|
466
|
+
message: `Token budget extended to ${newBudget.toLocaleString('en-US')} tokens`,
|
|
467
|
+
});
|
|
468
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
469
|
+
|
|
470
|
+
this.emit(sid, true);
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
|
|
410
474
|
incrementIteration(sessionId = 'default') {
|
|
411
475
|
const sid = sessionId || 'default';
|
|
412
476
|
const goal = this.goals.get(sid);
|
|
@@ -435,13 +499,7 @@ export class GoalEngine {
|
|
|
435
499
|
}
|
|
436
500
|
|
|
437
501
|
getElapsedSeconds(sessionId = 'default') {
|
|
438
|
-
|
|
439
|
-
const goal = this.goals.get(sid);
|
|
440
|
-
if (!goal) return 0;
|
|
441
|
-
const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = goal;
|
|
442
|
-
const endTime = completedAt || (pausedAt || Date.now());
|
|
443
|
-
const elapsedMs = Math.max(0, endTime - startedAt - totalPausedDurationMs);
|
|
444
|
-
return Math.floor(elapsedMs / 1000);
|
|
502
|
+
return calculateElapsedSeconds(this.goals.get(sessionId || 'default'));
|
|
445
503
|
}
|
|
446
504
|
|
|
447
505
|
addTokenUsage(usage, sessionId = 'default') {
|
|
@@ -462,98 +520,48 @@ export class GoalEngine {
|
|
|
462
520
|
goal.tokensUsage.completionTokens += completion;
|
|
463
521
|
goal.tokensUsage.totalTokens += total;
|
|
464
522
|
|
|
523
|
+
// Token Budget Guard Check
|
|
524
|
+
const budget = goal.maxTokenBudget || this.maxTokenBudget || 0;
|
|
525
|
+
if (budget > 0) {
|
|
526
|
+
const currentTotal = goal.tokensUsage.totalTokens;
|
|
527
|
+
const warnThreshold = (budget * (this.budgetWarningThreshold || 80)) / 100;
|
|
528
|
+
|
|
529
|
+
if (currentTotal >= warnThreshold && !goal.budgetWarningTriggered) {
|
|
530
|
+
goal.budgetWarningTriggered = true;
|
|
531
|
+
goal.logs.push({
|
|
532
|
+
timestamp: Date.now(),
|
|
533
|
+
type: 'warning',
|
|
534
|
+
message: `Token budget warning: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens consumed (${Math.round((currentTotal / budget) * 100)}%)`,
|
|
535
|
+
});
|
|
536
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (currentTotal >= budget && goal.state === GoalState.RUNNING) {
|
|
540
|
+
goal.state = GoalState.PAUSED;
|
|
541
|
+
goal.pausedAt = Date.now();
|
|
542
|
+
goal.logs.push({
|
|
543
|
+
timestamp: Date.now(),
|
|
544
|
+
type: 'warning',
|
|
545
|
+
message: `Token budget reached: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens. Pausing autonomous loop.`,
|
|
546
|
+
});
|
|
547
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
548
|
+
this.emit(sid, true);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
465
553
|
this.emit(sid);
|
|
466
554
|
}
|
|
467
555
|
|
|
468
556
|
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
469
557
|
const sid = sessionId || 'default';
|
|
470
558
|
const goal = this.goals.get(sid);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const total = goal.milestones.length;
|
|
474
|
-
if (total === 0) return null;
|
|
475
|
-
|
|
476
|
-
const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
477
|
-
if (completedCount === 0 || completedCount >= total) return null;
|
|
478
|
-
|
|
479
|
-
const elapsed = this.getElapsedSeconds(sid);
|
|
480
|
-
if (elapsed <= 0) return null;
|
|
481
|
-
|
|
482
|
-
const avgSecPerMilestone = elapsed / completedCount;
|
|
483
|
-
const remainingCount = total - completedCount;
|
|
484
|
-
return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
|
|
559
|
+
return calculateRemainingSeconds(goal, this.getElapsedSeconds(sid));
|
|
485
560
|
}
|
|
486
561
|
|
|
487
562
|
getSnapshot(sessionId = 'default') {
|
|
488
563
|
const sid = sessionId || 'default';
|
|
489
|
-
|
|
490
|
-
if (!goal) {
|
|
491
|
-
return {
|
|
492
|
-
sessionId: sid,
|
|
493
|
-
hasActiveGoal: false,
|
|
494
|
-
state: GoalState.IDLE,
|
|
495
|
-
title: '',
|
|
496
|
-
startedAt: null,
|
|
497
|
-
pausedAt: null,
|
|
498
|
-
totalPausedDurationMs: 0,
|
|
499
|
-
completedAt: null,
|
|
500
|
-
elapsedSeconds: 0,
|
|
501
|
-
formattedElapsed: '0s',
|
|
502
|
-
estimatedRemainingSeconds: null,
|
|
503
|
-
formattedETA: null,
|
|
504
|
-
lang: 'en',
|
|
505
|
-
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
506
|
-
milestones: [],
|
|
507
|
-
progressPercent: 0,
|
|
508
|
-
iterationsCount: 0,
|
|
509
|
-
maxIterations: this.defaultMaxIterations,
|
|
510
|
-
autoDrive: this.autoDrive,
|
|
511
|
-
enableSound: this.enableSound,
|
|
512
|
-
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
513
|
-
gitStartCommit: null,
|
|
514
|
-
pendingNudge: null,
|
|
515
|
-
toolFailureCount: 0,
|
|
516
|
-
consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const elapsed = this.getElapsedSeconds(sid);
|
|
521
|
-
const milestones = goal.milestones;
|
|
522
|
-
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
523
|
-
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
524
|
-
const estSec = this.getEstimatedRemainingSeconds(sid);
|
|
525
|
-
|
|
526
|
-
return {
|
|
527
|
-
sessionId: sid,
|
|
528
|
-
hasActiveGoal: true,
|
|
529
|
-
id: goal.id,
|
|
530
|
-
state: goal.state,
|
|
531
|
-
title: goal.title,
|
|
532
|
-
description: goal.description,
|
|
533
|
-
lang: goal.lang || detectLanguage(goal.title),
|
|
534
|
-
startedAt: goal.startedAt,
|
|
535
|
-
pausedAt: goal.pausedAt,
|
|
536
|
-
totalPausedDurationMs: goal.totalPausedDurationMs,
|
|
537
|
-
completedAt: goal.completedAt,
|
|
538
|
-
elapsedSeconds: elapsed,
|
|
539
|
-
formattedElapsed: formatElapsed(elapsed),
|
|
540
|
-
estimatedRemainingSeconds: estSec,
|
|
541
|
-
formattedETA: formatETA(estSec),
|
|
542
|
-
tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
543
|
-
iterationsCount: goal.iterationsCount,
|
|
544
|
-
maxIterations: goal.maxIterations,
|
|
545
|
-
milestones,
|
|
546
|
-
progressPercent,
|
|
547
|
-
logs: goal.logs,
|
|
548
|
-
resultSummary: goal.resultSummary,
|
|
549
|
-
autoDrive: this.autoDrive,
|
|
550
|
-
enableSound: this.enableSound,
|
|
551
|
-
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
552
|
-
gitStartCommit: goal.gitStartCommit || null,
|
|
553
|
-
pendingNudge: goal.pendingNudge || null,
|
|
554
|
-
toolFailureCount: this.getToolFailureCount(sid),
|
|
555
|
-
consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
|
|
556
|
-
};
|
|
564
|
+
return buildSnapshot(this.goals.get(sid), sid, this);
|
|
557
565
|
}
|
|
558
566
|
|
|
559
567
|
getGoalSnapshot(sessionId = 'default') {
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,9 @@ export const Config = z.object({
|
|
|
19
19
|
showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
|
|
20
20
|
consecutiveToolFailureLimit: z.number().default(3).description('Auto-pause goal if N consecutive turns encounter tool execution errors (0 to disable)'),
|
|
21
21
|
enableBrowserNotifications: z.boolean().default(true).description('Show desktop notifications on goal completion or failure'),
|
|
22
|
+
maxTokenBudget: z.number().default(0).description('Maximum token budget per goal session (0 to disable)'),
|
|
23
|
+
budgetWarningThreshold: z.number().default(80).description('Percentage of token budget consumed before model warning injection (e.g. 80)'),
|
|
24
|
+
autoCheckpointOnMilestone: z.boolean().default(false).description('Automatically create git commit checkpoint upon milestone completion'),
|
|
22
25
|
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
23
26
|
});
|
|
24
27
|
|
|
@@ -37,6 +40,9 @@ export function apply(ctx, config = {}) {
|
|
|
37
40
|
showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
|
|
38
41
|
consecutiveToolFailureLimit: config?.consecutiveToolFailureLimit ?? 3,
|
|
39
42
|
enableBrowserNotifications: config?.enableBrowserNotifications ?? true,
|
|
43
|
+
maxTokenBudget: config?.maxTokenBudget ?? 0,
|
|
44
|
+
budgetWarningThreshold: config?.budgetWarningThreshold ?? 80,
|
|
45
|
+
autoCheckpointOnMilestone: config?.autoCheckpointOnMilestone ?? false,
|
|
40
46
|
storagePath: config?.storagePath,
|
|
41
47
|
};
|
|
42
48
|
|
|
@@ -51,6 +57,9 @@ export function apply(ctx, config = {}) {
|
|
|
51
57
|
enableSound: currentSettings.enableSound,
|
|
52
58
|
showQuickLaunchButton: currentSettings.showQuickLaunchButton,
|
|
53
59
|
consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
|
|
60
|
+
maxTokenBudget: currentSettings.maxTokenBudget,
|
|
61
|
+
budgetWarningThreshold: currentSettings.budgetWarningThreshold,
|
|
62
|
+
autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
|
|
54
63
|
storagePath,
|
|
55
64
|
});
|
|
56
65
|
|
|
@@ -177,6 +186,9 @@ export function apply(ctx, config = {}) {
|
|
|
177
186
|
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
178
187
|
consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
|
|
179
188
|
enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
|
|
189
|
+
maxTokenBudget: typeof live.maxTokenBudget === 'number' ? live.maxTokenBudget : currentSettings.maxTokenBudget,
|
|
190
|
+
budgetWarningThreshold: typeof live.budgetWarningThreshold === 'number' ? live.budgetWarningThreshold : currentSettings.budgetWarningThreshold,
|
|
191
|
+
autoCheckpointOnMilestone: typeof live.autoCheckpointOnMilestone === 'boolean' ? live.autoCheckpointOnMilestone : currentSettings.autoCheckpointOnMilestone,
|
|
180
192
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
181
193
|
};
|
|
182
194
|
}
|
|
@@ -205,6 +217,9 @@ export function apply(ctx, config = {}) {
|
|
|
205
217
|
enableSound: live.enableSound,
|
|
206
218
|
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
207
219
|
consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
|
|
220
|
+
maxTokenBudget: live.maxTokenBudget,
|
|
221
|
+
budgetWarningThreshold: live.budgetWarningThreshold,
|
|
222
|
+
autoCheckpointOnMilestone: live.autoCheckpointOnMilestone,
|
|
208
223
|
});
|
|
209
224
|
};
|
|
210
225
|
|
package/lib/routes.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
4
|
+
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
5
|
+
|
|
6
|
+
function getSafeCwd(requestedCwd) {
|
|
7
|
+
if (!requestedCwd || typeof requestedCwd !== 'string') {
|
|
8
|
+
return process.cwd();
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const resolved = path.resolve(requestedCwd.trim());
|
|
12
|
+
const st = fs.statSync(resolved);
|
|
13
|
+
if (!st.isDirectory()) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return resolved;
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
/**
|
|
4
23
|
* Register webServer HTTP routes and SSE stream for DSH Goal
|
|
@@ -247,7 +266,7 @@ export function registerRoutes(ctx, {
|
|
|
247
266
|
res.statusCode = 400;
|
|
248
267
|
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
249
268
|
}
|
|
250
|
-
const ok = engine.updateMilestone(milestoneId, status, notes, sid);
|
|
269
|
+
const ok = engine.updateMilestone(milestoneId, status, notes, sid, data.checklist);
|
|
251
270
|
if (!ok) {
|
|
252
271
|
res.statusCode = 404;
|
|
253
272
|
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
@@ -256,6 +275,74 @@ export function registerRoutes(ctx, {
|
|
|
256
275
|
break;
|
|
257
276
|
}
|
|
258
277
|
|
|
278
|
+
case 'toggle_checklist_item': {
|
|
279
|
+
if (!milestoneId || data.itemIndex === undefined) {
|
|
280
|
+
res.statusCode = 400;
|
|
281
|
+
return res.end(JSON.stringify({ error: 'milestoneId and itemIndex are required' }));
|
|
282
|
+
}
|
|
283
|
+
const ok = engine.toggleChecklistItem(milestoneId, data.itemIndex, data.done, sid);
|
|
284
|
+
if (!ok) {
|
|
285
|
+
res.statusCode = 404;
|
|
286
|
+
return res.end(JSON.stringify({ error: `Milestone checklist item not found` }));
|
|
287
|
+
}
|
|
288
|
+
result = engine.getSnapshot(sid);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case 'extend_budget': {
|
|
293
|
+
const addTokens = data.addTokens || 50000;
|
|
294
|
+
const ok = engine.extendBudget(addTokens, sid);
|
|
295
|
+
if (!ok) {
|
|
296
|
+
res.statusCode = 404;
|
|
297
|
+
return res.end(JSON.stringify({ error: 'No active goal found to extend budget' }));
|
|
298
|
+
}
|
|
299
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
300
|
+
resumeActiveAgent(undefined, sid);
|
|
301
|
+
}
|
|
302
|
+
result = engine.getSnapshot(sid);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'rollback_milestone': {
|
|
307
|
+
const commit = data.commit || data.checkpointCommit;
|
|
308
|
+
if (!commit) {
|
|
309
|
+
res.statusCode = 400;
|
|
310
|
+
return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
|
|
311
|
+
}
|
|
312
|
+
const safeCwd = getSafeCwd(data.cwd);
|
|
313
|
+
if (data.cwd && !safeCwd) {
|
|
314
|
+
res.statusCode = 400;
|
|
315
|
+
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
316
|
+
}
|
|
317
|
+
const ok = rollbackToCheckpoint(commit, safeCwd);
|
|
318
|
+
if (!ok) {
|
|
319
|
+
res.statusCode = 500;
|
|
320
|
+
return res.end(JSON.stringify({ error: `Failed to rollback to checkpoint ${commit}` }));
|
|
321
|
+
}
|
|
322
|
+
result = engine.getSnapshot(sid);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case 'save_artifact': {
|
|
327
|
+
const snap = engine.getSnapshot(sid);
|
|
328
|
+
if (!snap || !snap.hasActiveGoal) {
|
|
329
|
+
res.statusCode = 404;
|
|
330
|
+
return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
|
|
331
|
+
}
|
|
332
|
+
const safeCwd = getSafeCwd(data.cwd);
|
|
333
|
+
if (data.cwd && !safeCwd) {
|
|
334
|
+
res.statusCode = 400;
|
|
335
|
+
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
336
|
+
}
|
|
337
|
+
const artifact = saveGoalArtifact(snap, safeCwd);
|
|
338
|
+
if (!artifact) {
|
|
339
|
+
res.statusCode = 500;
|
|
340
|
+
return res.end(JSON.stringify({ error: 'Failed to write goal artifact to disk' }));
|
|
341
|
+
}
|
|
342
|
+
res.statusCode = 200;
|
|
343
|
+
return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
|
|
344
|
+
}
|
|
345
|
+
|
|
259
346
|
default:
|
|
260
347
|
res.statusCode = 400;
|
|
261
348
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
package/lib/tools.js
CHANGED
|
@@ -229,13 +229,17 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
// Tool 5: goal_update_progress
|
|
232
|
-
const handleUpdateProgress = async ({ milestone_id, status, notes }, toolCtx) => {
|
|
232
|
+
const handleUpdateProgress = async ({ milestone_id, status, notes, checklist }, toolCtx) => {
|
|
233
233
|
const sid = sessionIdOf(toolCtx, 'default');
|
|
234
|
-
|
|
234
|
+
engine.updateMilestone(milestone_id, status, notes, sid, checklist);
|
|
235
|
+
const snap = engine.getSnapshot(sid);
|
|
236
|
+
const targetMilestone = snap.milestones?.find((m) => m.id === String(milestone_id) || m.id.toLowerCase().replace(/[^a-z0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^a-z0-9]/g, '') || m.id.toLowerCase().replace(/[^0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^0-9]/g, ''));
|
|
235
237
|
return {
|
|
236
238
|
success: true,
|
|
237
239
|
milestone_id,
|
|
238
240
|
status,
|
|
241
|
+
checklist: targetMilestone?.checklist || null,
|
|
242
|
+
checkpointCommit: targetMilestone?.checkpointCommit || null,
|
|
239
243
|
total_milestones: snap.milestones?.length,
|
|
240
244
|
completed: snap.milestones?.filter((m) => m.status === 'completed').length,
|
|
241
245
|
};
|
|
@@ -243,7 +247,7 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
243
247
|
|
|
244
248
|
safeRegister({
|
|
245
249
|
name: 'goal_update_progress',
|
|
246
|
-
description: 'Update the progress of a specific milestone.',
|
|
250
|
+
description: 'Update the progress of a specific milestone, optionally attaching a sub-tasks checklist.',
|
|
247
251
|
parameters: {
|
|
248
252
|
type: 'object',
|
|
249
253
|
properties: {
|
|
@@ -254,6 +258,18 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
254
258
|
description: 'New milestone status',
|
|
255
259
|
},
|
|
256
260
|
notes: { type: 'string', description: 'Summary of actions completed or blocking issue' },
|
|
261
|
+
checklist: {
|
|
262
|
+
type: 'array',
|
|
263
|
+
items: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
text: { type: 'string', description: 'Checklist task item description' },
|
|
267
|
+
done: { type: 'boolean', description: 'Whether the item is completed' },
|
|
268
|
+
},
|
|
269
|
+
required: ['text', 'done'],
|
|
270
|
+
},
|
|
271
|
+
description: 'Optional sub-tasks / checklist items within this milestone',
|
|
272
|
+
},
|
|
257
273
|
},
|
|
258
274
|
required: ['milestone_id', 'status'],
|
|
259
275
|
},
|
package/package.json
CHANGED