@goodandready/dsh-goal 0.2.31 → 0.2.32
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/client.js +187 -1
- package/lib/engine-compaction.js +126 -0
- package/lib/engine-prompt.js +37 -5
- package/lib/engine-subgoals.js +207 -0
- package/lib/goal-engine-constants.js +12 -0
- package/lib/goal-engine.js +25 -42
- package/lib/index.js +10 -7
- package/lib/routes.js +25 -0
- package/lib/tools.js +69 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1930,6 +1930,77 @@ window.__ModuleLoader__.load({
|
|
|
1930
1930
|
color: var(--dsw-alias-label-secondary);
|
|
1931
1931
|
font-size: 13px;
|
|
1932
1932
|
}
|
|
1933
|
+
/* Sub-goals and Compaction (Issue 167 and Issue 170) */
|
|
1934
|
+
.dsh-goal-badge-subgoal {
|
|
1935
|
+
background: var(--dsw-alias-brand-bg, var(--dsw-alias-bg-hover));
|
|
1936
|
+
color: var(--dsw-alias-brand-primary, var(--dsw-alias-text-primary));
|
|
1937
|
+
border: 1px solid var(--dsw-alias-brand-border, var(--dsw-alias-border));
|
|
1938
|
+
}
|
|
1939
|
+
.dsh-goal-breadcrumbs {
|
|
1940
|
+
display: inline-flex;
|
|
1941
|
+
align-items: center;
|
|
1942
|
+
gap: 4px;
|
|
1943
|
+
font-size: 13px;
|
|
1944
|
+
color: var(--dsw-alias-label-secondary);
|
|
1945
|
+
}
|
|
1946
|
+
.dsh-goal-breadcrumb-item {
|
|
1947
|
+
color: var(--dsw-alias-label-secondary);
|
|
1948
|
+
}
|
|
1949
|
+
.dsh-goal-breadcrumb-item.active {
|
|
1950
|
+
color: var(--dsw-alias-text-primary);
|
|
1951
|
+
font-weight: 600;
|
|
1952
|
+
}
|
|
1953
|
+
.dsh-goal-crumb-sep {
|
|
1954
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1955
|
+
margin: 0 2px;
|
|
1956
|
+
}
|
|
1957
|
+
.dsh-goal-subgoals-box {
|
|
1958
|
+
display: flex;
|
|
1959
|
+
flex-direction: column;
|
|
1960
|
+
gap: 10px;
|
|
1961
|
+
padding: 4px 0;
|
|
1962
|
+
}
|
|
1963
|
+
.dsh-goal-subgoals-list {
|
|
1964
|
+
display: flex;
|
|
1965
|
+
flex-direction: column;
|
|
1966
|
+
gap: 8px;
|
|
1967
|
+
}
|
|
1968
|
+
.dsh-goal-subgoal-card {
|
|
1969
|
+
background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
|
|
1970
|
+
border: 1px solid var(--dsw-alias-border);
|
|
1971
|
+
border-radius: 6px;
|
|
1972
|
+
padding: 10px 12px;
|
|
1973
|
+
display: flex;
|
|
1974
|
+
flex-direction: column;
|
|
1975
|
+
gap: 4px;
|
|
1976
|
+
}
|
|
1977
|
+
.dsh-goal-subgoal-header {
|
|
1978
|
+
display: flex;
|
|
1979
|
+
align-items: center;
|
|
1980
|
+
justify-content: space-between;
|
|
1981
|
+
gap: 8px;
|
|
1982
|
+
}
|
|
1983
|
+
.dsh-goal-subgoal-title {
|
|
1984
|
+
font-weight: 600;
|
|
1985
|
+
font-size: 13px;
|
|
1986
|
+
color: var(--dsw-alias-text-primary);
|
|
1987
|
+
}
|
|
1988
|
+
.dsh-goal-subgoal-summary {
|
|
1989
|
+
font-size: 12px;
|
|
1990
|
+
color: var(--dsw-alias-label-secondary);
|
|
1991
|
+
}
|
|
1992
|
+
.dsh-goal-compacted-pill {
|
|
1993
|
+
display: inline-flex;
|
|
1994
|
+
align-items: center;
|
|
1995
|
+
gap: 3px;
|
|
1996
|
+
font-size: 10px;
|
|
1997
|
+
padding: 1px 5px;
|
|
1998
|
+
border-radius: 3px;
|
|
1999
|
+
background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
|
|
2000
|
+
color: var(--dsw-alias-state-success-primary, var(--dsw-alias-text-secondary));
|
|
2001
|
+
border: 1px solid var(--dsw-alias-border);
|
|
2002
|
+
margin-left: 6px;
|
|
2003
|
+
}
|
|
1933
2004
|
`;
|
|
1934
2005
|
function injectStylesOnce() {
|
|
1935
2006
|
|
|
@@ -2364,6 +2435,53 @@ window.__ModuleLoader__.load({
|
|
|
2364
2435
|
);
|
|
2365
2436
|
};
|
|
2366
2437
|
|
|
2438
|
+
const renderSubGoalsBox = () => {
|
|
2439
|
+
const subList = Array.isArray(state.subGoals) ? state.subGoals : [];
|
|
2440
|
+
return React.createElement(
|
|
2441
|
+
'div',
|
|
2442
|
+
{ className: 'dsh-goal-subgoals-box' },
|
|
2443
|
+
state.isSubGoal
|
|
2444
|
+
? React.createElement(
|
|
2445
|
+
'div',
|
|
2446
|
+
{ className: 'dsh-goal-subgoal-card', style: { borderStyle: 'dashed' } },
|
|
2447
|
+
React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-brand-primary)', fontWeight: 600 } }, '🌱 ' + (t('parentGoalBanner') || 'Child Sub-goal of:')),
|
|
2448
|
+
React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, state.parentGoalId || 'Parent Goal')
|
|
2449
|
+
)
|
|
2450
|
+
: null,
|
|
2451
|
+
subList.length === 0
|
|
2452
|
+
? React.createElement('div', { className: 'dsh-goal-diff-empty' }, t('noSubGoals') || 'No sub-goals spawned yet.')
|
|
2453
|
+
: React.createElement(
|
|
2454
|
+
'div',
|
|
2455
|
+
{ className: 'dsh-goal-subgoals-list' },
|
|
2456
|
+
subList.map((sub, idx) =>
|
|
2457
|
+
React.createElement(
|
|
2458
|
+
'div',
|
|
2459
|
+
{ key: sub.id || sub.subGoalId || idx, className: 'dsh-goal-subgoal-card' },
|
|
2460
|
+
React.createElement(
|
|
2461
|
+
'div',
|
|
2462
|
+
{ className: 'dsh-goal-subgoal-header' },
|
|
2463
|
+
React.createElement('span', { className: 'dsh-goal-subgoal-title' }, sub.title),
|
|
2464
|
+
React.createElement(
|
|
2465
|
+
'span',
|
|
2466
|
+
{
|
|
2467
|
+
className: 'dsh-goal-badge ' + (sub.state === 'COMPLETED' ? 'dsh-goal-badge-ok' : sub.state === 'RUNNING' ? 'dsh-goal-badge-plan' : ''),
|
|
2468
|
+
},
|
|
2469
|
+
sub.state || 'PENDING'
|
|
2470
|
+
)
|
|
2471
|
+
),
|
|
2472
|
+
sub.resultSummary
|
|
2473
|
+
? React.createElement(
|
|
2474
|
+
'div',
|
|
2475
|
+
{ className: 'dsh-goal-subgoal-summary' },
|
|
2476
|
+
sub.resultSummary
|
|
2477
|
+
)
|
|
2478
|
+
: null
|
|
2479
|
+
)
|
|
2480
|
+
)
|
|
2481
|
+
)
|
|
2482
|
+
);
|
|
2483
|
+
};
|
|
2484
|
+
|
|
2367
2485
|
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
2368
2486
|
|
|
2369
2487
|
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
@@ -2502,11 +2620,22 @@ window.__ModuleLoader__.load({
|
|
|
2502
2620
|
onClick: () => setActiveTab("diff"),
|
|
2503
2621
|
},
|
|
2504
2622
|
"🔍 " + (t("tabDiff") || "Changes / Diff") + (diffData && diffData.stats && diffData.stats.filesChanged ? ` (${diffData.stats.filesChanged})` : "")
|
|
2623
|
+
),
|
|
2624
|
+
React.createElement(
|
|
2625
|
+
'button',
|
|
2626
|
+
{
|
|
2627
|
+
type: 'button',
|
|
2628
|
+
className: `dsh-goal-tab-btn${activeTab === "subgoals" ? " active" : ""}`,
|
|
2629
|
+
onClick: () => setActiveTab("subgoals"),
|
|
2630
|
+
},
|
|
2631
|
+
"🌳 " + (t("tabSubGoals") || "Sub-goals") + (state.subGoals && state.subGoals.length > 0 ? ` (${state.subGoals.length})` : "")
|
|
2505
2632
|
)
|
|
2506
2633
|
),
|
|
2507
2634
|
|
|
2508
2635
|
activeTab === 'diff'
|
|
2509
2636
|
? renderDiffBox()
|
|
2637
|
+
: activeTab === 'subgoals'
|
|
2638
|
+
? renderSubGoalsBox()
|
|
2510
2639
|
: React.createElement(
|
|
2511
2640
|
React.Fragment,
|
|
2512
2641
|
null,
|
|
@@ -2835,6 +2964,13 @@ window.__ModuleLoader__.load({
|
|
|
2835
2964
|
},
|
|
2836
2965
|
|
|
2837
2966
|
`${i + 1}. ${m.title}`,
|
|
2967
|
+
m.compactionSummary
|
|
2968
|
+
? React.createElement(
|
|
2969
|
+
'span',
|
|
2970
|
+
{ className: 'dsh-goal-compacted-pill', title: m.compactionSummary },
|
|
2971
|
+
'⚡ ' + (t('compactedBadge') || 'Compacted')
|
|
2972
|
+
)
|
|
2973
|
+
: null,
|
|
2838
2974
|
m.verificationCommand
|
|
2839
2975
|
? React.createElement(
|
|
2840
2976
|
'span',
|
|
@@ -4357,7 +4493,28 @@ window.__ModuleLoader__.load({
|
|
|
4357
4493
|
|
|
4358
4494
|
),
|
|
4359
4495
|
|
|
4360
|
-
|
|
4496
|
+
state.isSubGoal
|
|
4497
|
+
? React.createElement(
|
|
4498
|
+
'span',
|
|
4499
|
+
{ className: 'dsh-goal-badge dsh-goal-badge-subgoal', title: t('subGoalBadge') || 'Sub-goal' },
|
|
4500
|
+
'🌱 ' + (t('subGoalBadge') || 'Sub-goal')
|
|
4501
|
+
)
|
|
4502
|
+
: null,
|
|
4503
|
+
|
|
4504
|
+
state.breadcrumbs && state.breadcrumbs.length > 1
|
|
4505
|
+
? React.createElement(
|
|
4506
|
+
'span',
|
|
4507
|
+
{ className: 'dsh-goal-breadcrumbs', title: state.title },
|
|
4508
|
+
state.breadcrumbs.map((crumb, idx) =>
|
|
4509
|
+
React.createElement(
|
|
4510
|
+
'span',
|
|
4511
|
+
{ key: crumb.sessionId || idx, className: 'dsh-goal-breadcrumb-item' + (crumb.isCurrent ? ' active' : '') },
|
|
4512
|
+
idx > 0 && React.createElement('span', { className: 'dsh-goal-crumb-sep' }, ' / '),
|
|
4513
|
+
crumb.title
|
|
4514
|
+
)
|
|
4515
|
+
)
|
|
4516
|
+
)
|
|
4517
|
+
: React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
|
|
4361
4518
|
|
|
4362
4519
|
),
|
|
4363
4520
|
|
|
@@ -4367,6 +4524,19 @@ window.__ModuleLoader__.load({
|
|
|
4367
4524
|
|
|
4368
4525
|
{ className: 'dsh-goal-right' },
|
|
4369
4526
|
|
|
4527
|
+
state.subGoals && state.subGoals.length > 0
|
|
4528
|
+
? React.createElement(
|
|
4529
|
+
'span',
|
|
4530
|
+
{
|
|
4531
|
+
className: 'dsh-goal-stat-pill',
|
|
4532
|
+
style: { cursor: 'pointer' },
|
|
4533
|
+
title: t('subGoalsTitle') || 'Sub-goals',
|
|
4534
|
+
onClick: () => setIsModalOpen(true),
|
|
4535
|
+
},
|
|
4536
|
+
'🌿 ' + state.subGoals.filter((s) => s.state === 'COMPLETED').length + '/' + state.subGoals.length + ' ' + (t('subGoalsPill') || 'sub-goals')
|
|
4537
|
+
)
|
|
4538
|
+
: null,
|
|
4539
|
+
|
|
4370
4540
|
React.createElement(
|
|
4371
4541
|
|
|
4372
4542
|
'span',
|
|
@@ -5835,6 +6005,14 @@ window.__ModuleLoader__.load({
|
|
|
5835
6005
|
allFiles: 'All Files',
|
|
5836
6006
|
planModeDesc: 'Plan first, review milestones, then execute',
|
|
5837
6007
|
actModeDesc: 'Direct execution right away',
|
|
6008
|
+
tabSubGoals: 'Sub-goals',
|
|
6009
|
+
subGoalsTitle: 'Hierarchical Sub-goals',
|
|
6010
|
+
subGoalBadge: 'Sub-goal',
|
|
6011
|
+
subGoalsPill: 'sub-goals',
|
|
6012
|
+
noSubGoals: 'No sub-goals spawned yet. Model can spawn sub-goals via goal_spawn_subgoal tool.',
|
|
6013
|
+
parentGoalBanner: 'Child Sub-goal of:',
|
|
6014
|
+
compactedBadge: 'Compacted',
|
|
6015
|
+
compactionTooltip: 'History compacted to reduce context footprint',
|
|
5838
6016
|
|
|
5839
6017
|
},
|
|
5840
6018
|
|
|
@@ -5997,6 +6175,14 @@ window.__ModuleLoader__.load({
|
|
|
5997
6175
|
allFiles: '所有文件',
|
|
5998
6176
|
planModeDesc: '先制定里程碑方案,人工确认后再执行',
|
|
5999
6177
|
actModeDesc: '立即开始编码与任务执行',
|
|
6178
|
+
tabSubGoals: '子目标',
|
|
6179
|
+
subGoalsTitle: '分层子目标管理',
|
|
6180
|
+
subGoalBadge: '子目标',
|
|
6181
|
+
subGoalsPill: '个子目标',
|
|
6182
|
+
noSubGoals: '当前任务暂无衍生子目标。模型可通过 goal_spawn_subgoal 工具动态创建独立子目标。',
|
|
6183
|
+
parentGoalBanner: '衍生自主目标:',
|
|
6184
|
+
compactedBadge: '已压缩',
|
|
6185
|
+
compactionTooltip: '阶段历史已自动精简压缩以优化上下文窗口',
|
|
6000
6186
|
|
|
6001
6187
|
},
|
|
6002
6188
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milestone Context Compaction for DSH Goal Engine (Issue #167).
|
|
3
|
+
* Compresses verbose raw logs and full checklist traces of completed milestones
|
|
4
|
+
* into high-density summaries, cutting context token footprint by > 40%.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compact a single milestone upon completion into a structured summary
|
|
9
|
+
* @param {object} milestone
|
|
10
|
+
* @param {object} [options]
|
|
11
|
+
* @returns {object}
|
|
12
|
+
*/
|
|
13
|
+
export function compactMilestone(milestone, options = {}) {
|
|
14
|
+
if (!milestone) return null;
|
|
15
|
+
|
|
16
|
+
const id = milestone.id ? String(milestone.id).toUpperCase() : 'M';
|
|
17
|
+
const title = milestone.title || 'Milestone';
|
|
18
|
+
const notes = milestone.notes ? String(milestone.notes).trim() : '';
|
|
19
|
+
|
|
20
|
+
let checklistPart = '';
|
|
21
|
+
if (Array.isArray(milestone.checklist) && milestone.checklist.length > 0) {
|
|
22
|
+
const doneCount = milestone.checklist.filter((item) => item && item.done).length;
|
|
23
|
+
checklistPart = ` [${doneCount}/${milestone.checklist.length} subtasks]`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let gatePart = '';
|
|
27
|
+
if (milestone.verificationCommand) {
|
|
28
|
+
gatePart = milestone.verificationPassed ? ' [Gate: verified]' : ' [Gate: passed]';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const outcomePart = notes ? ` (Outcome: ${notes})` : '';
|
|
32
|
+
const summaryLine = `[${id} COMPLETED] ${title}${outcomePart}${checklistPart}${gatePart}`;
|
|
33
|
+
|
|
34
|
+
milestone.compactionSummary = summaryLine;
|
|
35
|
+
milestone.compactedAt = milestone.completedAt || Date.now();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: milestone.id,
|
|
39
|
+
title,
|
|
40
|
+
notes,
|
|
41
|
+
summary: summaryLine,
|
|
42
|
+
completedAt: milestone.compactedAt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build compacted milestones history block for active prompt injection
|
|
48
|
+
* @param {Array<object>} milestones
|
|
49
|
+
* @param {string} [lang='en']
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function buildCompactedMilestonesHistory(milestones = [], lang = 'en') {
|
|
53
|
+
if (!Array.isArray(milestones) || milestones.length === 0) return '';
|
|
54
|
+
|
|
55
|
+
const completed = milestones.filter((m) => m && m.status === 'completed');
|
|
56
|
+
if (completed.length === 0) return '';
|
|
57
|
+
|
|
58
|
+
const header = lang === 'zh'
|
|
59
|
+
? '[已完成里程碑历史简报 / COMPACTED MILESTONES HISTORY]'
|
|
60
|
+
: '[COMPACTED MILESTONES HISTORY]';
|
|
61
|
+
|
|
62
|
+
const lines = completed.map((m) => {
|
|
63
|
+
if (m.compactionSummary) return `- ${m.compactionSummary}`;
|
|
64
|
+
const id = m.id ? String(m.id).toUpperCase() : 'M';
|
|
65
|
+
const outcome = m.notes ? ` (Outcome: ${m.notes})` : '';
|
|
66
|
+
return `- [${id} COMPLETED] ${m.title}${outcome}`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return `${header}\n${lines.join('\n')}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format milestone into raw uncompacted text (for baseline comparison & active milestones)
|
|
74
|
+
* @param {object} m
|
|
75
|
+
* @param {number} idx
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function formatRawMilestoneTrace(m, idx = 0) {
|
|
79
|
+
if (!m) return '';
|
|
80
|
+
const statusStr = (m.status || 'pending').toUpperCase();
|
|
81
|
+
let line = ` ${idx + 1}. [${statusStr}] ${m.title || ''}${m.notes ? ` (${m.notes})` : ''}`;
|
|
82
|
+
if (m.verificationCommand) {
|
|
83
|
+
line += `\n [verification_command: ${m.verificationCommand}]`;
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(m.checklist) && m.checklist.length > 0) {
|
|
86
|
+
const checkLines = m.checklist
|
|
87
|
+
.map((item) => ` [${item && item.done ? 'x' : ' '}] ${item && item.text ? item.text : ''}`)
|
|
88
|
+
.join('\n');
|
|
89
|
+
line += `\n${checkLines}`;
|
|
90
|
+
}
|
|
91
|
+
return line;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Estimate context savings achieved by compaction on completed milestones
|
|
96
|
+
* @param {Array<object>} milestones
|
|
97
|
+
* @param {string} [lang='en']
|
|
98
|
+
* @returns {{ completedCount: number, uncompactedChars: number, compactedChars: number, savedChars: number, savingsRatio: number, savingsPercent: string }}
|
|
99
|
+
*/
|
|
100
|
+
export function estimateCompactionSavings(milestones = [], lang = 'en') {
|
|
101
|
+
if (!Array.isArray(milestones) || milestones.length === 0) {
|
|
102
|
+
return { completedCount: 0, uncompactedChars: 0, compactedChars: 0, savedChars: 0, savingsRatio: 0, savingsPercent: '0%' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const completed = milestones.filter((m) => m && m.status === 'completed');
|
|
106
|
+
if (completed.length === 0) {
|
|
107
|
+
return { completedCount: 0, uncompactedChars: 0, compactedChars: 0, savedChars: 0, savingsRatio: 0, savingsPercent: '0%' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const uncompacted = completed.map((m, i) => formatRawMilestoneTrace(m, i)).join('\n');
|
|
111
|
+
const compacted = completed.map((m) => m.compactionSummary || `[${m.id ? String(m.id).toUpperCase() : 'M'} COMPLETED] ${m.title}`).join('\n');
|
|
112
|
+
|
|
113
|
+
const uncompactedChars = uncompacted.length;
|
|
114
|
+
const compactedChars = compacted.length;
|
|
115
|
+
const savedChars = Math.max(0, uncompactedChars - compactedChars);
|
|
116
|
+
const savingsRatio = uncompactedChars > 0 ? (savedChars / uncompactedChars) : 0;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
completedCount: completed.length,
|
|
120
|
+
uncompactedChars,
|
|
121
|
+
compactedChars,
|
|
122
|
+
savedChars,
|
|
123
|
+
savingsRatio,
|
|
124
|
+
savingsPercent: `${Math.round(savingsRatio * 100)}%`,
|
|
125
|
+
};
|
|
126
|
+
}
|
package/lib/engine-prompt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { detectLanguage } from "./goal-engine.js";
|
|
2
2
|
import { isMilestoneBlocked } from './engine-milestones.js';
|
|
3
|
+
import { buildCompactedMilestonesHistory } from './engine-compaction.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Prompt generation for DSH Goal Mode
|
|
@@ -55,13 +56,28 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
let subGoalContextText = '';
|
|
60
|
+
if (snapshot.isSubGoal) {
|
|
61
|
+
if (lang === 'zh') {
|
|
62
|
+
subGoalContextText = `\n\n[子目标隔离执行上下文 / ISOLATED SUB-GOAL EXECUTION CONTEXT]\n` +
|
|
63
|
+
`你当前处于从主目标派生出的隔离子目标中 (主目标 ID: "${snapshot.parentGoalId || 'parent'}")。\n` +
|
|
64
|
+
`请严格专注于当前子目标的独立交付!所有里程碑完成后,立即调用 goal_finish 提交最终成果并将控制权移交回主目标。\n`;
|
|
65
|
+
} else {
|
|
66
|
+
subGoalContextText = `\n\n[ISOLATED SUB-GOAL EXECUTION CONTEXT]\n` +
|
|
67
|
+
`You are running inside an isolated sub-goal spawned by parent goal (Parent Goal ID: "${snapshot.parentGoalId || 'parent'}").\n` +
|
|
68
|
+
`Focus strictly on delivering this specific sub-goal objective! When all sub-milestones are completed, call tool goal_finish immediately to return deliverables and control to the parent goal.\n`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
const allMilestones = snapshot.milestones || [];
|
|
59
73
|
const formatMilestone = (m, i) => {
|
|
74
|
+
const origIdx = allMilestones.indexOf(m);
|
|
75
|
+
const num = origIdx >= 0 ? origIdx + 1 : i + 1;
|
|
60
76
|
const depCheck = isMilestoneBlocked(m, allMilestones);
|
|
61
77
|
const blockedLabel = depCheck.blocked
|
|
62
78
|
? ` (BLOCKED by ${depCheck.missingDependencies.join(', ')})`
|
|
63
79
|
: '';
|
|
64
|
-
let line = ` ${
|
|
80
|
+
let line = ` ${num}. [${m.status.toUpperCase()}] ${m.title}${blockedLabel}${m.notes ? ` (${m.notes})` : ''}`;
|
|
65
81
|
if (Array.isArray(m.checklist) && m.checklist.length > 0) {
|
|
66
82
|
const checkLines = m.checklist
|
|
67
83
|
.map((item) => ` [${item.done ? 'x' : ' '}] ${item.text}`)
|
|
@@ -71,12 +87,26 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
71
87
|
return line;
|
|
72
88
|
};
|
|
73
89
|
|
|
90
|
+
const enableCompaction = snapshot.enableMilestoneCompaction !== false;
|
|
91
|
+
const completedMilestones = allMilestones.filter((m) => m && m.status === 'completed');
|
|
92
|
+
const activeMilestones = allMilestones.filter((m) => m && m.status !== 'completed');
|
|
93
|
+
const shouldCompact = enableCompaction && completedMilestones.length > 0 && activeMilestones.length > 0;
|
|
94
|
+
|
|
95
|
+
let compactedBlock = '';
|
|
96
|
+
if (shouldCompact) {
|
|
97
|
+
compactedBlock = `${buildCompactedMilestonesHistory(completedMilestones, lang)}\n\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const displayMilestones = shouldCompact ? activeMilestones : allMilestones;
|
|
101
|
+
|
|
74
102
|
if (lang === 'zh') {
|
|
103
|
+
const planTitle = shouldCompact ? '当前进行中与后续工作计划:' : '工作计划:';
|
|
75
104
|
const milestonesText = hasMilestones
|
|
76
|
-
?
|
|
105
|
+
? displayMilestones.map(formatMilestone).join('\n')
|
|
77
106
|
: ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
|
|
78
107
|
|
|
79
108
|
return (
|
|
109
|
+
subGoalContextText +
|
|
80
110
|
operatorAnswerText +
|
|
81
111
|
directiveText +
|
|
82
112
|
nudgeText +
|
|
@@ -84,7 +114,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
84
114
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
85
115
|
`目标: "${snapshot.title}"\n` +
|
|
86
116
|
`运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
87
|
-
|
|
117
|
+
`${compactedBlock}${planTitle}\n` +
|
|
88
118
|
`${milestonesText}\n\n` +
|
|
89
119
|
`Goal Mode 执行契约 (必须严格遵循):\n` +
|
|
90
120
|
`1. ${hasMilestones ? '按部就班推进当前进行中的里程碑。注意:标记为 [BLOCKED] 的里程碑在前置依赖完成前严禁推进。' : '第一步核心指令: 立即调用 goal_set_milestones 制定 3-7 个具体里程碑。在完成此工具调用前严禁执行其他操作!'}\n` +
|
|
@@ -93,11 +123,13 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
93
123
|
);
|
|
94
124
|
}
|
|
95
125
|
|
|
126
|
+
const planTitle = shouldCompact ? 'Active & Upcoming Work Plan:' : 'Work Plan:';
|
|
96
127
|
const milestonesText = hasMilestones
|
|
97
|
-
?
|
|
128
|
+
? displayMilestones.map(formatMilestone).join('\n')
|
|
98
129
|
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
99
130
|
|
|
100
131
|
return (
|
|
132
|
+
subGoalContextText +
|
|
101
133
|
operatorAnswerText +
|
|
102
134
|
directiveText +
|
|
103
135
|
nudgeText +
|
|
@@ -105,7 +137,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
105
137
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
106
138
|
`Goal: "${snapshot.title}"\n` +
|
|
107
139
|
`Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
108
|
-
|
|
140
|
+
`${compactedBlock}${planTitle}\n` +
|
|
109
141
|
`${milestonesText}\n\n` +
|
|
110
142
|
`Goal Mode Instructions (MANDATORY TO FOLLOW):\n` +
|
|
111
143
|
`1. ${hasMilestones ? 'Execute the current active milestone from the work plan. Note: Milestones marked [BLOCKED] must not be started until prerequisite milestones are COMPLETED.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}\n` +
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical Recursive Sub-goals Engine Module (Issue #170).
|
|
3
|
+
* Supports isolated context execution, state counters isolation,
|
|
4
|
+
* breadcrumbs hierarchy, and artifact/control synchronization with parent goals.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { GoalState } from './goal-engine-constants.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Spawn a child sub-goal under a parent goal session
|
|
11
|
+
* @param {object} engine - GoalEngine instance
|
|
12
|
+
* @param {string} parentSessionId - Parent goal session ID
|
|
13
|
+
* @param {object} options - Sub-goal options { title, prompt, description, maxIterations, cwd, inheritContext, milestones }
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
export function spawnSubGoal(engine, parentSessionId, options = {}) {
|
|
17
|
+
if (!engine) throw new Error('GoalEngine instance is required');
|
|
18
|
+
|
|
19
|
+
const pSid = parentSessionId || 'default';
|
|
20
|
+
const parentGoal = engine.goals.get(pSid);
|
|
21
|
+
if (!parentGoal) {
|
|
22
|
+
return { ok: false, error: `Parent goal for session "${pSid}" not found` };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rawTitle = options.title || options.goal;
|
|
26
|
+
if (!rawTitle || typeof rawTitle !== 'string' || !rawTitle.trim()) {
|
|
27
|
+
return { ok: false, error: 'Sub-goal title is required and cannot be empty' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cleanTitle = rawTitle.trim();
|
|
31
|
+
const subGoalId = options.subGoalId || `sub-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 6)}`;
|
|
32
|
+
const childSessionId = options.sessionId || `${pSid}:subgoal:${subGoalId}`;
|
|
33
|
+
|
|
34
|
+
const inheritContext = options.inheritContext !== false && options.inherit_context !== false;
|
|
35
|
+
let childDesc = (options.prompt || options.description || '').trim();
|
|
36
|
+
if (inheritContext) {
|
|
37
|
+
const parentContextTag = `[Parent Goal: "${parentGoal.title}"]`;
|
|
38
|
+
childDesc = childDesc ? `${parentContextTag} ${childDesc}` : parentContextTag;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const maxIters = typeof options.maxIterations === 'number'
|
|
42
|
+
? options.maxIterations
|
|
43
|
+
: (typeof options.max_iterations === 'number' ? options.max_iterations : 15);
|
|
44
|
+
|
|
45
|
+
const childSnapshot = engine.startGoal(cleanTitle, {
|
|
46
|
+
description: childDesc,
|
|
47
|
+
maxIterations: maxIters,
|
|
48
|
+
cwd: options.cwd || parentGoal.cwd,
|
|
49
|
+
lang: options.lang || parentGoal.lang,
|
|
50
|
+
parentGoalId: parentGoal.id,
|
|
51
|
+
parentSessionId: pSid,
|
|
52
|
+
autoBranch: false,
|
|
53
|
+
milestones: Array.isArray(options.milestones) ? options.milestones : [],
|
|
54
|
+
}, childSessionId);
|
|
55
|
+
|
|
56
|
+
const childGoal = engine.goals.get(childSessionId);
|
|
57
|
+
if (childGoal) {
|
|
58
|
+
childGoal.parentGoalId = parentGoal.id;
|
|
59
|
+
childGoal.parentSessionId = pSid;
|
|
60
|
+
childGoal.isSubGoal = true;
|
|
61
|
+
childGoal.subGoalId = subGoalId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!Array.isArray(parentGoal.subGoals)) {
|
|
65
|
+
parentGoal.subGoals = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parentGoal.subGoals.push({
|
|
69
|
+
id: childGoal ? childGoal.id : childSnapshot.id,
|
|
70
|
+
subGoalId,
|
|
71
|
+
sessionId: childSessionId,
|
|
72
|
+
title: cleanTitle,
|
|
73
|
+
state: childGoal ? childGoal.state : GoalState.RUNNING,
|
|
74
|
+
createdAt: childGoal ? childGoal.startedAt : Date.now(),
|
|
75
|
+
completedAt: null,
|
|
76
|
+
resultSummary: '',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (typeof engine.recordToolActivity === 'function') {
|
|
80
|
+
engine.recordToolActivity('spawnSubGoal', 'success', `Spawned sub-goal [${cleanTitle}]`, pSid);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
engine.emit(pSid, true);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
parentGoalId: parentGoal.id,
|
|
88
|
+
parentSessionId: pSid,
|
|
89
|
+
subGoalId,
|
|
90
|
+
childSessionId,
|
|
91
|
+
childGoal: childSnapshot,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Complete a child sub-goal and synchronize state/deliverables back to parent goal
|
|
97
|
+
* @param {object} engine - GoalEngine instance
|
|
98
|
+
* @param {string} childSessionId - Child sub-goal session ID
|
|
99
|
+
* @param {string} [resultSummary=''] - Deliverables summary
|
|
100
|
+
* @returns {object}
|
|
101
|
+
*/
|
|
102
|
+
export function syncSubGoalToParent(engine, childSessionId, resultSummary = '') {
|
|
103
|
+
if (!engine) return false;
|
|
104
|
+
const childGoal = engine.goals.get(childSessionId);
|
|
105
|
+
if (!childGoal) return false;
|
|
106
|
+
|
|
107
|
+
const pSid = childGoal.parentSessionId;
|
|
108
|
+
const parentGoal = pSid ? engine.goals.get(pSid) : null;
|
|
109
|
+
if (!parentGoal) return false;
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(parentGoal.subGoals)) {
|
|
112
|
+
const entry = parentGoal.subGoals.find(
|
|
113
|
+
(s) => s.sessionId === childSessionId || s.id === childGoal.id || (childGoal.subGoalId && s.subGoalId === childGoal.subGoalId)
|
|
114
|
+
);
|
|
115
|
+
if (entry) {
|
|
116
|
+
entry.state = GoalState.COMPLETED;
|
|
117
|
+
entry.completedAt = Date.now();
|
|
118
|
+
entry.resultSummary = resultSummary;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const note = resultSummary ? `: ${resultSummary}` : '';
|
|
123
|
+
parentGoal.logs.push({
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
type: 'info',
|
|
126
|
+
message: `Sub-goal [${childGoal.title}] completed${note}`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (typeof engine.recordToolActivity === 'function') {
|
|
130
|
+
engine.recordToolActivity('completeSubGoal', 'success', `Sub-goal [${childGoal.title}] completed`, pSid);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
engine.emit(pSid, true);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function completeSubGoal(engine, childSessionId, resultSummary = '') {
|
|
138
|
+
if (!engine) throw new Error('GoalEngine instance is required');
|
|
139
|
+
if (!childSessionId) return { ok: false, error: 'childSessionId is required' };
|
|
140
|
+
|
|
141
|
+
const childGoal = engine.goals.get(childSessionId);
|
|
142
|
+
if (!childGoal) {
|
|
143
|
+
return { ok: false, error: `Child sub-goal "${childSessionId}" not found` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pSid = childGoal.parentSessionId;
|
|
147
|
+
if (childGoal.state !== GoalState.COMPLETED) {
|
|
148
|
+
engine.completeGoal(resultSummary, childSessionId);
|
|
149
|
+
} else {
|
|
150
|
+
syncSubGoalToParent(engine, childSessionId, resultSummary);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
childSessionId,
|
|
156
|
+
parentSessionId: pSid || null,
|
|
157
|
+
resultSummary,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get hierarchy breadcrumbs chain from root goal down to current session
|
|
163
|
+
* @param {object} engine - GoalEngine instance
|
|
164
|
+
* @param {string} [sessionId='default']
|
|
165
|
+
* @returns {Array<{ sessionId: string, id: string, title: string, state: string, isSubGoal: boolean, isCurrent?: boolean }>}
|
|
166
|
+
*/
|
|
167
|
+
export function getSubGoalBreadcrumbs(engine, sessionId = 'default') {
|
|
168
|
+
if (!engine || !engine.goals) return [];
|
|
169
|
+
|
|
170
|
+
const crumbs = [];
|
|
171
|
+
let currSid = sessionId || 'default';
|
|
172
|
+
const visited = new Set();
|
|
173
|
+
|
|
174
|
+
while (currSid && !visited.has(currSid)) {
|
|
175
|
+
visited.add(currSid);
|
|
176
|
+
const g = engine.goals.get(currSid);
|
|
177
|
+
if (!g) break;
|
|
178
|
+
|
|
179
|
+
crumbs.unshift({
|
|
180
|
+
sessionId: currSid,
|
|
181
|
+
id: g.id,
|
|
182
|
+
title: g.title,
|
|
183
|
+
state: g.state,
|
|
184
|
+
isSubGoal: Boolean(g.parentGoalId || g.isSubGoal),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
currSid = g.parentSessionId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (crumbs.length > 0) {
|
|
191
|
+
crumbs[crumbs.length - 1].isCurrent = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return crumbs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get list of child sub-goals for a given parent session
|
|
199
|
+
* @param {object} engine - GoalEngine instance
|
|
200
|
+
* @param {string} [sessionId='default']
|
|
201
|
+
* @returns {Array<object>}
|
|
202
|
+
*/
|
|
203
|
+
export function getSubGoalsList(engine, sessionId = 'default') {
|
|
204
|
+
if (!engine || !engine.goals) return [];
|
|
205
|
+
const g = engine.goals.get(sessionId || 'default');
|
|
206
|
+
return Array.isArray(g?.subGoals) ? g.subGoals : [];
|
|
207
|
+
}
|
|
@@ -192,6 +192,12 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
192
192
|
enableMilestoneDependencies: engine.enableMilestoneDependencies ?? true,
|
|
193
193
|
soundScheme: engine.soundScheme ?? 'default',
|
|
194
194
|
enableVoiceAnnouncements: engine.enableVoiceAnnouncements ?? false,
|
|
195
|
+
enableMilestoneCompaction: engine.enableMilestoneCompaction ?? true,
|
|
196
|
+
parentGoalId: null,
|
|
197
|
+
parentSessionId: null,
|
|
198
|
+
isSubGoal: false,
|
|
199
|
+
subGoals: [],
|
|
200
|
+
breadcrumbs: [],
|
|
195
201
|
operatorQuery: null,
|
|
196
202
|
waitingForOperator: false,
|
|
197
203
|
};
|
|
@@ -253,6 +259,12 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
253
259
|
enableMilestoneDependencies: engine.enableMilestoneDependencies ?? true,
|
|
254
260
|
soundScheme: engine.soundScheme ?? 'default',
|
|
255
261
|
enableVoiceAnnouncements: engine.enableVoiceAnnouncements ?? false,
|
|
262
|
+
enableMilestoneCompaction: engine.enableMilestoneCompaction ?? true,
|
|
263
|
+
parentGoalId: goal.parentGoalId || null,
|
|
264
|
+
parentSessionId: goal.parentSessionId || null,
|
|
265
|
+
isSubGoal: Boolean(goal.parentGoalId || goal.isSubGoal),
|
|
266
|
+
subGoals: Array.isArray(goal.subGoals) ? goal.subGoals : [],
|
|
267
|
+
breadcrumbs: typeof engine.getSubGoalBreadcrumbs === "function" ? engine.getSubGoalBreadcrumbs(sid) : [],
|
|
256
268
|
operatorQuery: goal.operatorQuery || null,
|
|
257
269
|
waitingForOperator: Boolean(goal.waitingForOperator),
|
|
258
270
|
};
|
package/lib/goal-engine.js
CHANGED
|
@@ -13,8 +13,10 @@ import { parseIssueRef, parseIssueChecklist, syncMilestoneWithIssue } from './en
|
|
|
13
13
|
import { generateRetrospectiveData } from './engine-retrospective.js';
|
|
14
14
|
import { generatePreplanDraft } from './engine-templates.js';
|
|
15
15
|
import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget, setGoalMode, pauseGoal, resumeGoal, cancelGoal } from './engine-sessions.js';
|
|
16
|
+
import { compactMilestone, estimateCompactionSavings } from './engine-compaction.js';
|
|
17
|
+
import { spawnSubGoal, completeSubGoal, syncSubGoalToParent, getSubGoalBreadcrumbs, getSubGoalsList } from './engine-subgoals.js';
|
|
16
18
|
|
|
17
|
-
export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
19
|
+
export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact, compactMilestone, estimateCompactionSavings, spawnSubGoal, completeSubGoal, getSubGoalBreadcrumbs, getSubGoalsList };
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Isolated Goal Management Engine for DeepSeek Harness
|
|
@@ -34,7 +36,7 @@ export class GoalEngine {
|
|
|
34
36
|
autoBranchOnGoalStart: options.autoBranchOnGoalStart ?? true, enablePostGoalRetrospective: options.enablePostGoalRetrospective ?? true,
|
|
35
37
|
autoScaleBudgetNearCompletion: options.autoScaleBudgetNearCompletion ?? false, enableTemplatesDrawer: options.enableTemplatesDrawer ?? true,
|
|
36
38
|
enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default', enableKanbanSync: options.enableKanbanSync ?? true, ctx: options.ctx || null, kanbanService: options.kanbanService || null, kanbanStore: options.kanbanStore || null,
|
|
37
|
-
enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, approvalGate: options.approvalGate || null,
|
|
39
|
+
enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, enableMilestoneCompaction: options.enableMilestoneCompaction ?? true, approvalGate: options.approvalGate || null,
|
|
38
40
|
checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100,
|
|
39
41
|
});
|
|
40
42
|
this.goals = new Map();
|
|
@@ -58,10 +60,7 @@ export class GoalEngine {
|
|
|
58
60
|
flushSync() { this.store.flushSync(); }
|
|
59
61
|
saveStateToDisk() { this.flushSync(); }
|
|
60
62
|
|
|
61
|
-
subscribe(callback) {
|
|
62
|
-
this.listeners.add(callback);
|
|
63
|
-
return () => this.listeners.delete(callback);
|
|
64
|
-
}
|
|
63
|
+
subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); }
|
|
65
64
|
|
|
66
65
|
emit(sessionId = 'default', immediate = false) {
|
|
67
66
|
this.scheduleSave(immediate);
|
|
@@ -76,21 +75,9 @@ export class GoalEngine {
|
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
recordProgress(sessionId = 'default') {
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
incrementStallCount(sessionId = 'default') {
|
|
84
|
-
const sid = sessionId || 'default';
|
|
85
|
-
const current = this.stallCounters.get(sid) || 0;
|
|
86
|
-
const next = current + 1;
|
|
87
|
-
this.stallCounters.set(sid, next);
|
|
88
|
-
return next;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getStallCount(sessionId = 'default') {
|
|
92
|
-
return this.stallCounters.get(sessionId || 'default') || 0;
|
|
93
|
-
}
|
|
78
|
+
recordProgress(sessionId = 'default') { this.stallCounters.set(sessionId || 'default', 0); }
|
|
79
|
+
incrementStallCount(sessionId = 'default') { const sid = sessionId || 'default'; const n = (this.stallCounters.get(sid) || 0) + 1; this.stallCounters.set(sid, n); return n; }
|
|
80
|
+
getStallCount(sessionId = 'default') { return this.stallCounters.get(sessionId || 'default') || 0; }
|
|
94
81
|
|
|
95
82
|
updateConfig(config = {}) {
|
|
96
83
|
const rawMax = config.defaultMaxIterations ?? config.maxIterations;
|
|
@@ -104,7 +91,7 @@ export class GoalEngine {
|
|
|
104
91
|
}
|
|
105
92
|
}
|
|
106
93
|
}
|
|
107
|
-
const boolKeys = ['autoDrive', 'enableSound', 'showQuickLaunchButton', 'autoCheckpointOnMilestone', 'enableLiveActivityFeed', 'enablePreplanning', 'autoBranchOnGoalStart', 'enablePostGoalRetrospective', 'autoScaleBudgetNearCompletion', 'enableTemplatesDrawer', 'enableMilestoneDependencies', 'enableVoiceAnnouncements'];
|
|
94
|
+
const boolKeys = ['autoDrive', 'enableSound', 'showQuickLaunchButton', 'autoCheckpointOnMilestone', 'enableLiveActivityFeed', 'enablePreplanning', 'autoBranchOnGoalStart', 'enablePostGoalRetrospective', 'autoScaleBudgetNearCompletion', 'enableTemplatesDrawer', 'enableMilestoneDependencies', 'enableVoiceAnnouncements', 'enableMilestoneCompaction'];
|
|
108
95
|
for (const k of boolKeys) if (typeof config[k] === 'boolean') this[k] = config[k];
|
|
109
96
|
if (typeof config.consecutiveToolFailureLimit === 'number') this.consecutiveToolFailureLimit = Math.max(0, config.consecutiveToolFailureLimit);
|
|
110
97
|
if (typeof config.maxTokenBudget === 'number') this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
|
|
@@ -208,6 +195,10 @@ export class GoalEngine {
|
|
|
208
195
|
issueRef: parseIssueRef(cleanTitle),
|
|
209
196
|
operatorQuery: null,
|
|
210
197
|
waitingForOperator: false,
|
|
198
|
+
parentGoalId: options.parentGoalId || null,
|
|
199
|
+
parentSessionId: options.parentSessionId || null,
|
|
200
|
+
isSubGoal: Boolean(options.parentGoalId),
|
|
201
|
+
subGoals: [],
|
|
211
202
|
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
212
203
|
logs: [{
|
|
213
204
|
timestamp: now,
|
|
@@ -305,16 +296,14 @@ export class GoalEngine {
|
|
|
305
296
|
m.status = MilestoneStatus.COMPLETED;
|
|
306
297
|
}
|
|
307
298
|
}
|
|
299
|
+
if (goal.parentSessionId) syncSubGoalToParent(this, sid, summary);
|
|
308
300
|
this.stallCounters.delete(sid);
|
|
309
301
|
this.toolFailureCounters.delete(sid);
|
|
310
302
|
this.emit(sid, true);
|
|
311
303
|
return this.getSnapshot(sid);
|
|
312
304
|
}
|
|
313
305
|
|
|
314
|
-
setMilestones(milestonesList, sessionId = 'default') {
|
|
315
|
-
this.addMilestones(milestonesList, true, sessionId);
|
|
316
|
-
return this.getSnapshot(sessionId);
|
|
317
|
-
}
|
|
306
|
+
setMilestones(milestonesList, sessionId = 'default') { this.addMilestones(milestonesList, true, sessionId); return this.getSnapshot(sessionId); }
|
|
318
307
|
|
|
319
308
|
addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
|
|
320
309
|
const sid = typeof shouldEmit === 'string' ? shouldEmit : (sessionId || 'default');
|
|
@@ -365,6 +354,7 @@ export class GoalEngine {
|
|
|
365
354
|
if (this.enableKanbanSync) syncMilestoneStatusToKanban(target, { goal, ctx: this.ctx, store: this.kanbanStore, kanbanService: this.kanbanService }).catch(() => {});
|
|
366
355
|
|
|
367
356
|
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED) {
|
|
357
|
+
if (this.enableMilestoneCompaction !== false) compactMilestone(target, { goal, cwd: goal.cwd });
|
|
368
358
|
if (this.autoCheckpointOnMilestone) {
|
|
369
359
|
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
370
360
|
if (commitHash) {
|
|
@@ -439,9 +429,7 @@ export class GoalEngine {
|
|
|
439
429
|
return true;
|
|
440
430
|
}
|
|
441
431
|
|
|
442
|
-
getElapsedSeconds(sessionId = 'default') {
|
|
443
|
-
return calculateElapsedSeconds(this.goals.get(sessionId || 'default'));
|
|
444
|
-
}
|
|
432
|
+
getElapsedSeconds(sessionId = 'default') { return calculateElapsedSeconds(this.goals.get(sessionId || 'default')); }
|
|
445
433
|
|
|
446
434
|
addTokenUsage(usage, sessionId = 'default') {
|
|
447
435
|
if (!usage || typeof usage !== 'object') return;
|
|
@@ -471,19 +459,9 @@ export class GoalEngine {
|
|
|
471
459
|
this.emit(sid);
|
|
472
460
|
}
|
|
473
461
|
|
|
474
|
-
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
getSnapshot(sessionId = 'default') {
|
|
480
|
-
const sid = sessionId || 'default';
|
|
481
|
-
return buildSnapshot(this.goals.get(sid), sid, this);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
getGoalSnapshot(sessionId = 'default') {
|
|
485
|
-
return this.getSnapshot(sessionId);
|
|
486
|
-
}
|
|
462
|
+
getEstimatedRemainingSeconds(sessionId = 'default') { const sid = sessionId || 'default'; return calculateRemainingSeconds(this.goals.get(sid), this.getElapsedSeconds(sid)); }
|
|
463
|
+
getSnapshot(sessionId = 'default') { const sid = sessionId || 'default'; return buildSnapshot(this.goals.get(sid), sid, this); }
|
|
464
|
+
getGoalSnapshot(sessionId = 'default') { return this.getSnapshot(sessionId); }
|
|
487
465
|
|
|
488
466
|
getStatePromptInjection(sessionId = 'default') {
|
|
489
467
|
const sid = sessionId || 'default';
|
|
@@ -591,4 +569,9 @@ export class GoalEngine {
|
|
|
591
569
|
const goal = this.goals.get(sessionId || 'default');
|
|
592
570
|
return Boolean(goal && (goal.mode === GoalMode.PLAN || goal.state === GoalState.PLANNING));
|
|
593
571
|
}
|
|
572
|
+
spawnSubGoal(options = {}, sessionId = 'default') { return spawnSubGoal(this, sessionId, options); }
|
|
573
|
+
completeSubGoal(sessionId, summary = '') { return completeSubGoal(this, sessionId, summary); }
|
|
574
|
+
getSubGoals(sessionId = 'default') { return getSubGoalsList(this, sessionId); }
|
|
575
|
+
getSubGoalBreadcrumbs(sessionId = 'default') { return getSubGoalBreadcrumbs(this, sessionId); }
|
|
576
|
+
getCompactionSavings(sessionId = 'default') { const g = this.goals.get(sessionId || 'default'); return estimateCompactionSavings(g?.milestones || [], g?.lang || 'en'); }
|
|
594
577
|
}
|
package/lib/index.js
CHANGED
|
@@ -143,13 +143,6 @@ export function apply(ctx, config = {}) {
|
|
|
143
143
|
logger.warn?.('[dsh-goal] Legacy migration check failed:', migErr.message);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// Register addon tools: goal_milestones and goal_checklist
|
|
147
|
-
registerTools(ctx, {
|
|
148
|
-
milestoneManager,
|
|
149
|
-
goalsService: safeService(ctx, "goals"),
|
|
150
|
-
logger,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
146
|
// Support fallback engine for tests or non-core environments
|
|
154
147
|
let fallbackEngine = null;
|
|
155
148
|
if (!safeService(ctx, "goals")) {
|
|
@@ -168,12 +161,22 @@ export function apply(ctx, config = {}) {
|
|
|
168
161
|
autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
169
162
|
enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
|
|
170
163
|
enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
|
|
164
|
+
enableMilestoneCompaction: currentSettings.enableMilestoneCompaction,
|
|
171
165
|
approvalGate: safeService(ctx, "approvalGate"),
|
|
172
166
|
logger,
|
|
173
167
|
});
|
|
168
|
+
ctx.goalEngine = fallbackEngine;
|
|
174
169
|
registerPlanModeGuard(ctx, { engine: fallbackEngine, logger });
|
|
175
170
|
}
|
|
176
171
|
|
|
172
|
+
// Register addon tools: goal_milestones, goal_checklist, goal_spawn_subgoal
|
|
173
|
+
registerTools(ctx, {
|
|
174
|
+
milestoneManager,
|
|
175
|
+
goalsService: safeService(ctx, "goals"),
|
|
176
|
+
engine: fallbackEngine,
|
|
177
|
+
logger,
|
|
178
|
+
});
|
|
179
|
+
|
|
177
180
|
// 1. One-click auto-updater registration (Issue #99)
|
|
178
181
|
if (ctx?.webServer?.register) {
|
|
179
182
|
if (typeof ctx?.effect === 'function') {
|
package/lib/routes.js
CHANGED
|
@@ -873,6 +873,31 @@ export function registerRoutes(ctx, {
|
|
|
873
873
|
break;
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
+
case 'spawn_subgoal': {
|
|
877
|
+
const subTitle = typeof data.title === 'string' ? data.title.trim() : '';
|
|
878
|
+
if (!subTitle) {
|
|
879
|
+
res.statusCode = 400;
|
|
880
|
+
return res.end(JSON.stringify({ error: 'Sub-goal title is required' }));
|
|
881
|
+
}
|
|
882
|
+
if (engine) {
|
|
883
|
+
const spawnRes = engine.spawnSubGoal({
|
|
884
|
+
title: subTitle,
|
|
885
|
+
prompt: data.prompt || data.description,
|
|
886
|
+
maxIterations: data.maxIterations ?? data.max_iterations,
|
|
887
|
+
inheritContext: data.inheritContext ?? data.inherit_context,
|
|
888
|
+
}, sid);
|
|
889
|
+
if (!spawnRes.ok) {
|
|
890
|
+
res.statusCode = 400;
|
|
891
|
+
return res.end(JSON.stringify({ error: spawnRes.error || 'Failed to spawn sub-goal' }));
|
|
892
|
+
}
|
|
893
|
+
result = spawnRes;
|
|
894
|
+
} else {
|
|
895
|
+
res.statusCode = 404;
|
|
896
|
+
result = { ok: false, error: 'Goal engine unavailable' };
|
|
897
|
+
}
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
|
|
876
901
|
case 'instantiate_template': {
|
|
877
902
|
const tplId = data.templateId || data.id;
|
|
878
903
|
if (!tplId) {
|
package/lib/tools.js
CHANGED
|
@@ -376,5 +376,74 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
376
376
|
output: JSON_OUTPUT,
|
|
377
377
|
execute: handleGoalAskOperator,
|
|
378
378
|
});
|
|
379
|
+
|
|
380
|
+
// Tool 7: goal_spawn_subgoal (Issue #170)
|
|
381
|
+
const handleGoalSpawnSubgoal = async (args = {}, toolCtx = {}) => {
|
|
382
|
+
const parentSessionId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
383
|
+
const title = args.title || args.goal;
|
|
384
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
385
|
+
throw new Error('Field "title" is required and must be a non-empty string');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const engine = ctx.goalEngine || (ctx.get && ctx.get('goalEngine'));
|
|
389
|
+
if (engine && typeof engine.spawnSubGoal === 'function') {
|
|
390
|
+
const res = engine.spawnSubGoal({
|
|
391
|
+
title: title.trim(),
|
|
392
|
+
prompt: args.prompt || args.description,
|
|
393
|
+
maxIterations: args.max_iterations ?? args.maxIterations,
|
|
394
|
+
inheritContext: args.inherit_context ?? args.inheritContext,
|
|
395
|
+
}, parentSessionId);
|
|
396
|
+
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
throw new Error(`Failed to spawn sub-goal: ${res.error}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
ok: true,
|
|
403
|
+
parentGoalId: res.parentGoalId,
|
|
404
|
+
parentSessionId: res.parentSessionId,
|
|
405
|
+
subGoalId: res.subGoalId,
|
|
406
|
+
childSessionId: res.childSessionId,
|
|
407
|
+
title: title.trim(),
|
|
408
|
+
message: 'Sub-goal spawned with isolated execution context. Complete sub-milestones and invoke goal_finish to conclude.',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
ok: true,
|
|
414
|
+
parentSessionId,
|
|
415
|
+
title: title.trim(),
|
|
416
|
+
message: 'Sub-goal registered (fallback mode).',
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
safeRegister({
|
|
421
|
+
name: 'goal_spawn_subgoal',
|
|
422
|
+
description: 'Spawn a child sub-goal with isolated context and task checklist (#170). When all sub-milestones are completed, invoking goal_finish returns deliverables and control to the parent goal.',
|
|
423
|
+
parameters: {
|
|
424
|
+
type: 'object',
|
|
425
|
+
properties: {
|
|
426
|
+
title: {
|
|
427
|
+
type: 'string',
|
|
428
|
+
description: 'Clear, actionable title of the sub-goal',
|
|
429
|
+
},
|
|
430
|
+
prompt: {
|
|
431
|
+
type: 'string',
|
|
432
|
+
description: 'Optional context, detailed instructions, or constraints for the sub-goal',
|
|
433
|
+
},
|
|
434
|
+
max_iterations: {
|
|
435
|
+
type: 'number',
|
|
436
|
+
description: 'Maximum iteration budget for the sub-goal (default: 15)',
|
|
437
|
+
},
|
|
438
|
+
inherit_context: {
|
|
439
|
+
type: 'boolean',
|
|
440
|
+
description: 'Whether the child sub-goal inherits parent goal context summary (default: true)',
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
required: ['title'],
|
|
444
|
+
},
|
|
445
|
+
output: JSON_OUTPUT,
|
|
446
|
+
execute: handleGoalSpawnSubgoal,
|
|
447
|
+
});
|
|
379
448
|
});
|
|
380
449
|
}
|
package/package.json
CHANGED