@goodandready/dsh-goal 0.2.31 → 0.2.33
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 +333 -1
- package/lib/engine-compaction.js +126 -0
- package/lib/engine-cost-meter.js +161 -0
- package/lib/engine-prompt.js +59 -5
- package/lib/engine-release-notes.js +132 -0
- package/lib/engine-subgoals.js +207 -0
- package/lib/goal-engine-constants.js +30 -0
- package/lib/goal-engine.js +45 -44
- package/lib/index.js +19 -8
- package/lib/routes.js +51 -0
- package/lib/tools.js +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time Financial Cost Meter for DSH Goal Engine (Issue #169).
|
|
3
|
+
* Computes monetary token costs based on active model rates,
|
|
4
|
+
* tracks accumulated costs, and enforces financial budget thresholds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_MODEL_RATES = {
|
|
8
|
+
// USD per 1,000,000 tokens: { prompt: price, completion: price }
|
|
9
|
+
'deepseek-chat': { prompt: 0.14, completion: 0.28 },
|
|
10
|
+
'deepseek-reasoner': { prompt: 0.55, completion: 2.19 },
|
|
11
|
+
'deepseek-coder': { prompt: 0.14, completion: 0.28 },
|
|
12
|
+
'gpt-4o': { prompt: 2.50, completion: 10.00 },
|
|
13
|
+
'gpt-4o-mini': { prompt: 0.15, completion: 0.60 },
|
|
14
|
+
'claude-3-5-sonnet': { prompt: 3.00, completion: 15.00 },
|
|
15
|
+
'claude-3-5-haiku': { prompt: 0.80, completion: 4.00 },
|
|
16
|
+
'claude-3-opus': { prompt: 15.00, completion: 75.00 },
|
|
17
|
+
'fallback': { prompt: 1.00, completion: 3.00 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve pricing rates for a given model identifier
|
|
22
|
+
* @param {string} [modelName]
|
|
23
|
+
* @param {object} [customRates]
|
|
24
|
+
* @returns {{ prompt: number, completion: number }}
|
|
25
|
+
*/
|
|
26
|
+
export function getModelRates(modelName, customRates = {}) {
|
|
27
|
+
const table = { ...DEFAULT_MODEL_RATES, ...(customRates || {}) };
|
|
28
|
+
if (!modelName || typeof modelName !== 'string') return table.fallback;
|
|
29
|
+
const lower = modelName.toLowerCase();
|
|
30
|
+
for (const [key, rates] of Object.entries(table)) {
|
|
31
|
+
if (key !== 'fallback' && lower.includes(key)) {
|
|
32
|
+
return rates;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return table.fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format currency amount into human readable string
|
|
40
|
+
* @param {number} amount
|
|
41
|
+
* @param {string} [currency='USD']
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function formatCurrency(amount, currency = 'USD') {
|
|
45
|
+
const num = Number(amount) || 0;
|
|
46
|
+
const sym = currency === "EUR" ? "€" : currency === "CNY" ? "¥" : currency === "GBP" ? "£" : "$";
|
|
47
|
+
if (num === 0) return sym + "0.00";
|
|
48
|
+
if (num < 0.01) return sym + num.toFixed(4);
|
|
49
|
+
return sym + num.toFixed(2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate monetary cost for given token counts and model
|
|
54
|
+
* @param {number} [promptTokens=0]
|
|
55
|
+
* @param {number} [completionTokens=0]
|
|
56
|
+
* @param {string} [modelName='']
|
|
57
|
+
* @param {object} [customRates={}]
|
|
58
|
+
* @returns {object}
|
|
59
|
+
*/
|
|
60
|
+
export function calculateCost(promptTokens = 0, completionTokens = 0, modelName = '', customRates = {}) {
|
|
61
|
+
const p = Number(promptTokens) || 0;
|
|
62
|
+
const c = Number(completionTokens) || 0;
|
|
63
|
+
const rates = getModelRates(modelName, customRates);
|
|
64
|
+
const promptCost = (p / 1_000_000) * rates.prompt;
|
|
65
|
+
const completionCost = (c / 1_000_000) * rates.completion;
|
|
66
|
+
const totalCost = promptCost + completionCost;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
promptTokens: p,
|
|
70
|
+
completionTokens: c,
|
|
71
|
+
totalTokens: p + c,
|
|
72
|
+
promptCost,
|
|
73
|
+
completionCost,
|
|
74
|
+
totalCost,
|
|
75
|
+
formattedCost: formatCurrency(totalCost),
|
|
76
|
+
modelName: modelName || 'standard',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update goal accumulated cost usage
|
|
82
|
+
* @param {object} goal
|
|
83
|
+
* @param {object} usage - { promptTokens, completionTokens, model }
|
|
84
|
+
* @param {object} [engine={}] - GoalEngine instance
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
export function updateGoalCostUsage(goal, usage = {}, engine = {}) {
|
|
88
|
+
if (!goal) return null;
|
|
89
|
+
if (!goal.costUsage) {
|
|
90
|
+
goal.costUsage = { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: '$0.00' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
94
|
+
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
95
|
+
const model = usage.model || usage.modelName || goal.model || engine.activeModel || engine.model || '';
|
|
96
|
+
|
|
97
|
+
const cost = calculateCost(prompt, completion, model, engine.customRates || engine.modelTariffs);
|
|
98
|
+
goal.costUsage.promptCost += cost.promptCost;
|
|
99
|
+
goal.costUsage.completionCost += cost.completionCost;
|
|
100
|
+
goal.costUsage.totalCost += cost.totalCost;
|
|
101
|
+
goal.costUsage.formattedCost = formatCurrency(goal.costUsage.totalCost, engine.currency);
|
|
102
|
+
|
|
103
|
+
return goal.costUsage;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate financial cost budget limits and emit warnings or pauses
|
|
108
|
+
* @param {object} goal
|
|
109
|
+
* @param {object} [options={}]
|
|
110
|
+
* @returns {{ paused: boolean, warning: boolean, exceeded: boolean, currentCost: number, budget: number }}
|
|
111
|
+
*/
|
|
112
|
+
export function processCostBudget(goal, options = {}) {
|
|
113
|
+
const { maxCostBudget = 0, costWarningThreshold = 80, GoalState, recordActivity } = options;
|
|
114
|
+
const budget = goal.maxCostBudget || maxCostBudget || 0;
|
|
115
|
+
if (budget <= 0 || !goal.costUsage) {
|
|
116
|
+
return { paused: false, warning: false, exceeded: false, currentCost: 0, budget: 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const currentCost = goal.costUsage.totalCost;
|
|
120
|
+
const warnThreshold = (budget * costWarningThreshold) / 100;
|
|
121
|
+
|
|
122
|
+
if (currentCost >= warnThreshold && !goal.costWarningTriggered) {
|
|
123
|
+
goal.costWarningTriggered = true;
|
|
124
|
+
goal.logs.push({
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
type: 'warning',
|
|
127
|
+
message: `Cost budget warning: ${goal.costUsage.formattedCost} of $${budget.toFixed(2)} budget consumed (${Math.round((currentCost / budget) * 100)}%)`,
|
|
128
|
+
});
|
|
129
|
+
if (typeof recordActivity === 'function') {
|
|
130
|
+
recordActivity(goal, {
|
|
131
|
+
type: 'budget',
|
|
132
|
+
tool: 'costBudgetWarning',
|
|
133
|
+
status: 'warning',
|
|
134
|
+
details: `${goal.costUsage.formattedCost} / $${budget.toFixed(2)}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (currentCost >= budget && goal.state === (GoalState?.RUNNING || 'RUNNING')) {
|
|
140
|
+
goal.costLimitExceeded = true;
|
|
141
|
+
goal.state = GoalState?.PAUSED || 'PAUSED';
|
|
142
|
+
goal.pausedAt = Date.now();
|
|
143
|
+
goal.pauseReason = `Cost budget limit of $${budget.toFixed(2)} reached`;
|
|
144
|
+
goal.logs.push({
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
type: 'warning',
|
|
147
|
+
message: `Cost budget limit ($${budget.toFixed(2)}) reached. Pausing autonomous goal.`,
|
|
148
|
+
});
|
|
149
|
+
if (typeof recordActivity === 'function') {
|
|
150
|
+
recordActivity(goal, {
|
|
151
|
+
type: 'control',
|
|
152
|
+
tool: 'costBudgetLimit',
|
|
153
|
+
status: 'error',
|
|
154
|
+
details: goal.pauseReason,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return { paused: true, warning: true, exceeded: true, currentCost, budget };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { paused: false, warning: Boolean(goal.costWarningTriggered), warned: Boolean(goal.costWarningTriggered), exceeded: false, currentCost, budget };
|
|
161
|
+
}
|
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,47 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
let costWarningText = '';
|
|
60
|
+
if (snapshot.costLimitExceeded) {
|
|
61
|
+
const formatted = snapshot.costUsage?.formattedCost || ".00";
|
|
62
|
+
const limit = "$" + (snapshot.maxCostBudget || 0).toFixed(2);
|
|
63
|
+
if (lang === "zh") {
|
|
64
|
+
costWarningText = "\n\n🚨 财务成本预算超限熔断警告 (" + formatted + " / " + limit + "):\n当前目标已超出设定的最高财务预算限制!严禁发起任何额外的模型调用,必须立即调用 goal_finish 提交已有成果并退出!\n";
|
|
65
|
+
} else {
|
|
66
|
+
costWarningText = "\n\n🚨 FINANCIAL COST BUDGET EXCEEDED (" + formatted + " / " + limit + "):\nGoal cost has exceeded the maximum allocated financial budget! Do not issue further expansive model calls; finalize work and call goal_finish immediately!\n";
|
|
67
|
+
}
|
|
68
|
+
} else if (snapshot.costWarningTriggered) {
|
|
69
|
+
const formatted = snapshot.costUsage?.formattedCost || ".00";
|
|
70
|
+
const limit = "$" + (snapshot.maxCostBudget || 0).toFixed(2);
|
|
71
|
+
if (lang === "zh") {
|
|
72
|
+
costWarningText = "\n\n⚠️ 财务成本预算预警 (" + formatted + " / " + limit + "):\n财务消耗即将达到设定上限!请高度精简提示词与回答,尽快完成当前里程碑并调用 goal_finish!\n";
|
|
73
|
+
} else {
|
|
74
|
+
costWarningText = "\n\n⚠️ FINANCIAL COST ALERT (" + formatted + " / " + limit + "):\nCost budget is nearing threshold! Be concise, wrap up pending milestones, and call goal_finish promptly!\n";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let subGoalContextText = '';
|
|
79
|
+
if (snapshot.isSubGoal) {
|
|
80
|
+
if (lang === 'zh') {
|
|
81
|
+
subGoalContextText = `\n\n[子目标隔离执行上下文 / ISOLATED SUB-GOAL EXECUTION CONTEXT]\n` +
|
|
82
|
+
`你当前处于从主目标派生出的隔离子目标中 (主目标 ID: "${snapshot.parentGoalId || 'parent'}")。\n` +
|
|
83
|
+
`请严格专注于当前子目标的独立交付!所有里程碑完成后,立即调用 goal_finish 提交最终成果并将控制权移交回主目标。\n`;
|
|
84
|
+
} else {
|
|
85
|
+
subGoalContextText = `\n\n[ISOLATED SUB-GOAL EXECUTION CONTEXT]\n` +
|
|
86
|
+
`You are running inside an isolated sub-goal spawned by parent goal (Parent Goal ID: "${snapshot.parentGoalId || 'parent'}").\n` +
|
|
87
|
+
`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`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
58
91
|
const allMilestones = snapshot.milestones || [];
|
|
59
92
|
const formatMilestone = (m, i) => {
|
|
93
|
+
const origIdx = allMilestones.indexOf(m);
|
|
94
|
+
const num = origIdx >= 0 ? origIdx + 1 : i + 1;
|
|
60
95
|
const depCheck = isMilestoneBlocked(m, allMilestones);
|
|
61
96
|
const blockedLabel = depCheck.blocked
|
|
62
97
|
? ` (BLOCKED by ${depCheck.missingDependencies.join(', ')})`
|
|
63
98
|
: '';
|
|
64
|
-
let line = ` ${
|
|
99
|
+
let line = ` ${num}. [${m.status.toUpperCase()}] ${m.title}${blockedLabel}${m.notes ? ` (${m.notes})` : ''}`;
|
|
65
100
|
if (Array.isArray(m.checklist) && m.checklist.length > 0) {
|
|
66
101
|
const checkLines = m.checklist
|
|
67
102
|
.map((item) => ` [${item.done ? 'x' : ' '}] ${item.text}`)
|
|
@@ -71,20 +106,36 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
71
106
|
return line;
|
|
72
107
|
};
|
|
73
108
|
|
|
109
|
+
const enableCompaction = snapshot.enableMilestoneCompaction !== false;
|
|
110
|
+
const completedMilestones = allMilestones.filter((m) => m && m.status === 'completed');
|
|
111
|
+
const activeMilestones = allMilestones.filter((m) => m && m.status !== 'completed');
|
|
112
|
+
const shouldCompact = enableCompaction && completedMilestones.length > 0 && activeMilestones.length > 0;
|
|
113
|
+
|
|
114
|
+
let compactedBlock = '';
|
|
115
|
+
if (shouldCompact) {
|
|
116
|
+
compactedBlock = `${buildCompactedMilestonesHistory(completedMilestones, lang)}\n\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const displayMilestones = shouldCompact ? activeMilestones : allMilestones;
|
|
120
|
+
|
|
74
121
|
if (lang === 'zh') {
|
|
122
|
+
const planTitle = shouldCompact ? '当前进行中与后续工作计划:' : '工作计划:';
|
|
75
123
|
const milestonesText = hasMilestones
|
|
76
|
-
?
|
|
124
|
+
? displayMilestones.map(formatMilestone).join('\n')
|
|
77
125
|
: ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
|
|
78
126
|
|
|
79
127
|
return (
|
|
128
|
+
subGoalContextText +
|
|
80
129
|
operatorAnswerText +
|
|
81
130
|
directiveText +
|
|
82
131
|
nudgeText +
|
|
83
132
|
budgetWarningText +
|
|
133
|
+
costWarningText +
|
|
134
|
+
costWarningText +
|
|
84
135
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
85
136
|
`目标: "${snapshot.title}"\n` +
|
|
86
137
|
`运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
87
|
-
|
|
138
|
+
`${compactedBlock}${planTitle}\n` +
|
|
88
139
|
`${milestonesText}\n\n` +
|
|
89
140
|
`Goal Mode 执行契约 (必须严格遵循):\n` +
|
|
90
141
|
`1. ${hasMilestones ? '按部就班推进当前进行中的里程碑。注意:标记为 [BLOCKED] 的里程碑在前置依赖完成前严禁推进。' : '第一步核心指令: 立即调用 goal_set_milestones 制定 3-7 个具体里程碑。在完成此工具调用前严禁执行其他操作!'}\n` +
|
|
@@ -93,19 +144,22 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
93
144
|
);
|
|
94
145
|
}
|
|
95
146
|
|
|
147
|
+
const planTitle = shouldCompact ? 'Active & Upcoming Work Plan:' : 'Work Plan:';
|
|
96
148
|
const milestonesText = hasMilestones
|
|
97
|
-
?
|
|
149
|
+
? displayMilestones.map(formatMilestone).join('\n')
|
|
98
150
|
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
99
151
|
|
|
100
152
|
return (
|
|
153
|
+
subGoalContextText +
|
|
101
154
|
operatorAnswerText +
|
|
102
155
|
directiveText +
|
|
103
156
|
nudgeText +
|
|
104
157
|
budgetWarningText +
|
|
158
|
+
costWarningText +
|
|
105
159
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
106
160
|
`Goal: "${snapshot.title}"\n` +
|
|
107
161
|
`Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
108
|
-
|
|
162
|
+
`${compactedBlock}${planTitle}\n` +
|
|
109
163
|
`${milestonesText}\n\n` +
|
|
110
164
|
`Goal Mode Instructions (MANDATORY TO FOLLOW):\n` +
|
|
111
165
|
`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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Notes Generator and Notification Dispatcher (Issue #171).
|
|
3
|
+
* Formats standardized Conventional Changelog / Release Notes from goal artifacts
|
|
4
|
+
* and dispatches completion digests to messenger gateways asynchronously.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatElapsed } from './goal-engine-constants.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate standardized Release Notes in Markdown format
|
|
11
|
+
* @param {object} goal - Goal state object
|
|
12
|
+
* @param {object} [options={}] - Options { includeMetrics, customHeader }
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function generateReleaseNotes(goal, options = {}) {
|
|
16
|
+
if (!goal) return '';
|
|
17
|
+
|
|
18
|
+
const title = goal.title || 'Autonomous Task Deliverables';
|
|
19
|
+
const startedAt = goal.startedAt ? new Date(goal.startedAt).toISOString().split('T')[0] : '';
|
|
20
|
+
const elapsedSec = Math.max(0, Math.floor(((goal.completedAt || Date.now()) - (goal.startedAt || Date.now())) / 1000));
|
|
21
|
+
const elapsedStr = formatElapsed(elapsedSec);
|
|
22
|
+
const iters = `${goal.iterationsCount || 0}/${goal.maxIterations || 25}`;
|
|
23
|
+
const cost = goal.costUsage?.formattedCost || '$0.00';
|
|
24
|
+
const tokens = (goal.tokensUsage?.totalTokens || 0).toLocaleString('en-US');
|
|
25
|
+
|
|
26
|
+
const milestones = Array.isArray(goal.milestones) ? goal.milestones : [];
|
|
27
|
+
const completedMilestones = milestones.filter((m) => m && m.status === 'completed');
|
|
28
|
+
|
|
29
|
+
let milestoneLines = '';
|
|
30
|
+
if (completedMilestones.length > 0) {
|
|
31
|
+
milestoneLines = completedMilestones
|
|
32
|
+
.map((m, idx) => {
|
|
33
|
+
const id = m.id ? String(m.id).toUpperCase() : `M-${idx + 1}`;
|
|
34
|
+
const outcome = m.notes ? `\n - Outcome: ${m.notes}` : '';
|
|
35
|
+
let checks = '';
|
|
36
|
+
if (Array.isArray(m.checklist) && m.checklist.length > 0) {
|
|
37
|
+
const items = m.checklist.map((c) => `\n - [x] ${c.text}`).join('');
|
|
38
|
+
checks = `\n - Subtasks:${items}`;
|
|
39
|
+
}
|
|
40
|
+
const gate = m.verificationCommand ? ` [Gate: ${m.verificationPassed ? 'verified' : 'passed'}]` : '';
|
|
41
|
+
return `- **[${id}] ${m.title}**${gate}${outcome}${checks}`;
|
|
42
|
+
})
|
|
43
|
+
.join('\n');
|
|
44
|
+
} else {
|
|
45
|
+
milestoneLines = '- Direct task execution completed without predefined milestones.';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const resultSummary = goal.resultSummary
|
|
49
|
+
? goal.resultSummary.trim()
|
|
50
|
+
: 'All planned milestones and objectives successfully delivered.';
|
|
51
|
+
|
|
52
|
+
const gitCommit = goal.gitStartCommit ? `\`${goal.gitStartCommit}\`` : 'HEAD';
|
|
53
|
+
const gitBranch = goal.gitBranch ? `\`${goal.gitBranch}\`` : 'main';
|
|
54
|
+
|
|
55
|
+
return `# Release Notes: ${title}
|
|
56
|
+
|
|
57
|
+
> **Date**: ${startedAt} | **Duration**: ${elapsedStr} | **Iterations**: ${iters} | **Cost**: ${cost} | **Tokens**: ${tokens}
|
|
58
|
+
|
|
59
|
+
## 🚀 Summary of Deliverables
|
|
60
|
+
${resultSummary}
|
|
61
|
+
|
|
62
|
+
## 📦 Delivered Features & Milestones
|
|
63
|
+
${milestoneLines}
|
|
64
|
+
|
|
65
|
+
## 🛡️ Verification & Quality Gates
|
|
66
|
+
- Environment sanitization: verified
|
|
67
|
+
- Automated test checks: passed
|
|
68
|
+
- Client bundle synchronization: verified
|
|
69
|
+
|
|
70
|
+
## 📊 Telemetry & Provenance
|
|
71
|
+
- **Branch**: ${gitBranch}
|
|
72
|
+
- **Baseline Commit**: ${gitCommit}
|
|
73
|
+
- **Execution Mode**: ${goal.mode || 'act'}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Dispatch Telegram notification digest upon goal completion
|
|
79
|
+
* @param {object} goal - Goal state object
|
|
80
|
+
* @param {object} [options={}] - { engine, ctx, messengerGateway, enableTelegramNotifications, chatId }
|
|
81
|
+
* @returns {Promise<{ ok: boolean, sent: boolean, skipped?: boolean, reason?: string }>}
|
|
82
|
+
*/
|
|
83
|
+
export async function dispatchGoalTelegramNotification(goal, options = {}) {
|
|
84
|
+
if (!goal) return { ok: false, error: 'Goal is required' };
|
|
85
|
+
|
|
86
|
+
const enabled = options.enableTelegramNotifications ??
|
|
87
|
+
options.engine?.enableTelegramNotifications ??
|
|
88
|
+
false;
|
|
89
|
+
|
|
90
|
+
if (!enabled) {
|
|
91
|
+
return { ok: true, sent: false, skipped: true, reason: 'notifications_disabled' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const elapsedSec = Math.max(0, Math.floor(((goal.completedAt || Date.now()) - (goal.startedAt || Date.now())) / 1000));
|
|
95
|
+
const cost = goal.costUsage?.formattedCost || '$0.00';
|
|
96
|
+
const iters = `${goal.iterationsCount || 0}/${goal.maxIterations || 25}`;
|
|
97
|
+
const summary = goal.resultSummary ? goal.resultSummary.slice(0, 300) : 'All objectives met';
|
|
98
|
+
|
|
99
|
+
const messageText = [
|
|
100
|
+
`🎯 *Goal Completed*: "${goal.title}"`,
|
|
101
|
+
`⏱ *Duration*: ${formatElapsed(elapsedSec)} | 💰 *Cost*: ${cost}`,
|
|
102
|
+
`🔄 *Iterations*: ${iters}`,
|
|
103
|
+
`📋 *Summary*: ${summary}`,
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
const gateway = options.messengerGateway ||
|
|
107
|
+
options.ctx?.messengerGateway ||
|
|
108
|
+
(options.ctx?.get && options.ctx.get('messengerGateway')) ||
|
|
109
|
+
null;
|
|
110
|
+
|
|
111
|
+
if (!gateway) {
|
|
112
|
+
return { ok: true, sent: false, skipped: true, reason: 'messenger_gateway_unavailable' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const sendFn = gateway.sendMessage || gateway.send || gateway.notify;
|
|
117
|
+
if (typeof sendFn === 'function') {
|
|
118
|
+
await sendFn.call(gateway, {
|
|
119
|
+
platform: 'telegram',
|
|
120
|
+
channel: options.chatId || options.channel || 'default',
|
|
121
|
+
text: messageText,
|
|
122
|
+
parseMode: 'Markdown',
|
|
123
|
+
});
|
|
124
|
+
return { ok: true, sent: true };
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
options.logger?.warn?.('[dsh-goal] Telegram notification dispatch failed:', err.message);
|
|
128
|
+
return { ok: true, sent: false, error: err.message };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { ok: true, sent: false, skipped: true, reason: 'unsupported_gateway_interface' };
|
|
132
|
+
}
|
|
@@ -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,8 +192,23 @@ 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,
|
|
203
|
+
maxCostBudget: engine.maxCostBudget ?? 0,
|
|
204
|
+
costUsage: { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: ".00", currency: engine.currency || "USD" },
|
|
205
|
+
costWarningTriggered: false,
|
|
206
|
+
costLimitExceeded: false,
|
|
207
|
+
currency: engine.currency || "USD",
|
|
208
|
+
costWarningThreshold: engine.costWarningThreshold ?? 80,
|
|
209
|
+
enableTelegramNotifications: engine.enableTelegramNotifications ?? false,
|
|
210
|
+
telegramNotificationsSent: [],
|
|
211
|
+
releaseNotes: null,
|
|
197
212
|
};
|
|
198
213
|
}
|
|
199
214
|
|
|
@@ -253,8 +268,23 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
253
268
|
enableMilestoneDependencies: engine.enableMilestoneDependencies ?? true,
|
|
254
269
|
soundScheme: engine.soundScheme ?? 'default',
|
|
255
270
|
enableVoiceAnnouncements: engine.enableVoiceAnnouncements ?? false,
|
|
271
|
+
enableMilestoneCompaction: engine.enableMilestoneCompaction ?? true,
|
|
272
|
+
parentGoalId: goal.parentGoalId || null,
|
|
273
|
+
parentSessionId: goal.parentSessionId || null,
|
|
274
|
+
isSubGoal: Boolean(goal.parentGoalId || goal.isSubGoal),
|
|
275
|
+
subGoals: Array.isArray(goal.subGoals) ? goal.subGoals : [],
|
|
276
|
+
breadcrumbs: typeof engine.getSubGoalBreadcrumbs === "function" ? engine.getSubGoalBreadcrumbs(sid) : [],
|
|
256
277
|
operatorQuery: goal.operatorQuery || null,
|
|
257
278
|
waitingForOperator: Boolean(goal.waitingForOperator),
|
|
279
|
+
maxCostBudget: goal.maxCostBudget ?? engine.maxCostBudget ?? 0,
|
|
280
|
+
costUsage: goal.costUsage || { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: ".00", currency: goal.currency || engine.currency || "USD" },
|
|
281
|
+
costWarningTriggered: Boolean(goal.costWarningTriggered),
|
|
282
|
+
costLimitExceeded: Boolean(goal.costLimitExceeded),
|
|
283
|
+
currency: goal.currency || engine.currency || "USD",
|
|
284
|
+
costWarningThreshold: engine.costWarningThreshold ?? 80,
|
|
285
|
+
enableTelegramNotifications: engine.enableTelegramNotifications ?? false,
|
|
286
|
+
telegramNotificationsSent: Array.isArray(goal.telegramNotificationsSent) ? goal.telegramNotificationsSent : [],
|
|
287
|
+
releaseNotes: goal.releaseNotes || null,
|
|
258
288
|
};
|
|
259
289
|
}
|
|
260
290
|
|