@hamp10/agentforge 0.2.48 → 0.2.50
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/package.json +1 -1
- package/scripts/check-task-semantics.js +38 -0
- package/src/OpenClawCLI.js +47 -3
- package/src/worker.js +22 -5
package/package.json
CHANGED
|
@@ -549,6 +549,44 @@ try {
|
|
|
549
549
|
/The task is:|Delete and rebuild both/i,
|
|
550
550
|
'missing-target retries should not put the original reset-heavy task text at the top of the retry prompt'
|
|
551
551
|
);
|
|
552
|
+
assert.throws(
|
|
553
|
+
() => cli._guardDirectFileWritePath(
|
|
554
|
+
path.join(fixture.repo, 'public_html', 'domains', 'alpha.html'),
|
|
555
|
+
fixture.repo,
|
|
556
|
+
{ task: incompleteRepairLead }
|
|
557
|
+
),
|
|
558
|
+
/focused retry pass/i,
|
|
559
|
+
'direct writes should block already-touched scoped pages during a missing-target retry pass'
|
|
560
|
+
);
|
|
561
|
+
assert.doesNotThrow(
|
|
562
|
+
() => cli._guardDirectFileWritePath(
|
|
563
|
+
path.join(fixture.repo, 'public_html', 'domains', 'beta.html'),
|
|
564
|
+
fixture.repo,
|
|
565
|
+
{ task: incompleteRepairLead }
|
|
566
|
+
),
|
|
567
|
+
'direct writes should allow the missing scoped page during a missing-target retry pass'
|
|
568
|
+
);
|
|
569
|
+
const visualRepairLead = worker._formatVisualRepairTaskLead(
|
|
570
|
+
'Work on the Example.com listing pages for Alpha.ai and Beta.ai. Make both visually polished.',
|
|
571
|
+
'beta-ai: Visual warning: heading is clipped.'
|
|
572
|
+
);
|
|
573
|
+
assert.throws(
|
|
574
|
+
() => cli._guardDirectFileWritePath(
|
|
575
|
+
path.join(fixture.repo, 'public_html', 'domains', 'alpha.html'),
|
|
576
|
+
fixture.repo,
|
|
577
|
+
{ task: visualRepairLead }
|
|
578
|
+
),
|
|
579
|
+
/focused retry pass/i,
|
|
580
|
+
'direct writes should block non-failing scoped pages during a focused visual repair pass'
|
|
581
|
+
);
|
|
582
|
+
assert.doesNotThrow(
|
|
583
|
+
() => cli._guardDirectFileWritePath(
|
|
584
|
+
path.join(fixture.repo, 'public_html', 'domains', 'beta.html'),
|
|
585
|
+
fixture.repo,
|
|
586
|
+
{ task: visualRepairLead }
|
|
587
|
+
),
|
|
588
|
+
'direct writes should allow the failing scoped page during a focused visual repair pass'
|
|
589
|
+
);
|
|
552
590
|
assert.equal(
|
|
553
591
|
worker._formatRepeatedVisualRepairNudge(2),
|
|
554
592
|
'',
|
package/src/OpenClawCLI.js
CHANGED
|
@@ -914,6 +914,24 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
914
914
|
return err;
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
_directFocusedRetryAllowedSlugs(task) {
|
|
918
|
+
const text = String(task || '');
|
|
919
|
+
const patterns = [
|
|
920
|
+
/missing-target pass for target page\(s\) not yet changed in the current diff:\s*([^\n.]+)/i,
|
|
921
|
+
/visual repair pass for currently failing target page\(s\):\s*([^\n.]+)/i,
|
|
922
|
+
];
|
|
923
|
+
for (const pattern of patterns) {
|
|
924
|
+
const match = text.match(pattern);
|
|
925
|
+
if (!match) continue;
|
|
926
|
+
const slugs = String(match[1] || '')
|
|
927
|
+
.split(/[,;]/)
|
|
928
|
+
.map(item => scopeSlug(item.trim()))
|
|
929
|
+
.filter(Boolean);
|
|
930
|
+
if (slugs.length > 0) return new Set(slugs);
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
|
|
917
935
|
_isNestedProjectCopyPath(filePath, workDir) {
|
|
918
936
|
const relative = path.relative(workDir, filePath);
|
|
919
937
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
|
|
@@ -1048,6 +1066,20 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
1048
1066
|
if (!this._directTaskScopeAllowsPath(filePath, workDir, options.task)) {
|
|
1049
1067
|
throw this._directScopeViolationError(this._formatDirectScopeError(filePath, workDir, options.task));
|
|
1050
1068
|
}
|
|
1069
|
+
const focusedRetrySlugs = this._directFocusedRetryAllowedSlugs(options.task);
|
|
1070
|
+
if (focusedRetrySlugs && slugs.length > 0) {
|
|
1071
|
+
const matchedScopedSlugs = this._directScopeSlugsMatchingText(lowerRelativePath, slugs);
|
|
1072
|
+
const matchesFocusedSlug = matchedScopedSlugs.some(slug => focusedRetrySlugs.has(slug));
|
|
1073
|
+
if (matchedScopedSlugs.length > 0 && !matchesFocusedSlug) {
|
|
1074
|
+
throw this._directScopeViolationError([
|
|
1075
|
+
'Refusing to rewrite an already-addressed scoped target during a focused retry pass.',
|
|
1076
|
+
`Requested path: ${relativePath}`,
|
|
1077
|
+
`Current retry target(s): ${[...focusedRetrySlugs].join(', ')}.`,
|
|
1078
|
+
`Path appears to belong to: ${matchedScopedSlugs.join(', ')}.`,
|
|
1079
|
+
'Continue from the current repo state and edit the focused target page(s) named by the retry instruction, then verify every requested target page locally.',
|
|
1080
|
+
].join(' '));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1051
1083
|
const collectionViolation = this._directNewHtmlPageCollectionViolation(
|
|
1052
1084
|
filePath,
|
|
1053
1085
|
workDir,
|
|
@@ -1788,7 +1820,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
1788
1820
|
if (!sessionFilePath || !existsSync(sessionFilePath)) return null;
|
|
1789
1821
|
try {
|
|
1790
1822
|
const lines = readFileSync(sessionFilePath, 'utf8').split('\n');
|
|
1791
|
-
let input = 0, output = 0, costUsd = 0;
|
|
1823
|
+
let input = 0, output = 0, costUsd = 0, hasCost = false;
|
|
1792
1824
|
for (const line of lines) {
|
|
1793
1825
|
if (!line.trim()) continue;
|
|
1794
1826
|
try {
|
|
@@ -1798,14 +1830,26 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
1798
1830
|
output += entry.message.usage.output || 0;
|
|
1799
1831
|
// openclaw writes exact cost.total — use it directly instead of re-calculating
|
|
1800
1832
|
if (entry.message.usage.cost?.total != null) {
|
|
1801
|
-
|
|
1833
|
+
const costTotal = Number(entry.message.usage.cost.total);
|
|
1834
|
+
if (Number.isFinite(costTotal) && costTotal >= 0) {
|
|
1835
|
+
costUsd += costTotal;
|
|
1836
|
+
hasCost = true;
|
|
1837
|
+
}
|
|
1802
1838
|
}
|
|
1803
1839
|
}
|
|
1804
1840
|
} catch { /* skip malformed lines */ }
|
|
1805
1841
|
}
|
|
1806
1842
|
if (input > 0 || output > 0) {
|
|
1807
1843
|
console.log(`[session-usage] Read from JSONL: input=${input} output=${output} cost=$${costUsd.toFixed(6)}`);
|
|
1808
|
-
return {
|
|
1844
|
+
return {
|
|
1845
|
+
input_tokens: input,
|
|
1846
|
+
output_tokens: output,
|
|
1847
|
+
...(hasCost ? {
|
|
1848
|
+
cost_usd: costUsd,
|
|
1849
|
+
cost_source: 'provider_reported',
|
|
1850
|
+
cost_accuracy: 'exact',
|
|
1851
|
+
} : {}),
|
|
1852
|
+
};
|
|
1809
1853
|
}
|
|
1810
1854
|
return null;
|
|
1811
1855
|
} catch (e) {
|
package/src/worker.js
CHANGED
|
@@ -3890,7 +3890,10 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
3890
3890
|
let uiRepairNudgeCount = 0;
|
|
3891
3891
|
let taskResult;
|
|
3892
3892
|
let iterationMessage = finalMessage;
|
|
3893
|
-
let totalUsage = { input_tokens: 0, output_tokens: 0
|
|
3893
|
+
let totalUsage = { input_tokens: 0, output_tokens: 0 };
|
|
3894
|
+
let totalUsageCostUsd = 0;
|
|
3895
|
+
let usageSegments = 0;
|
|
3896
|
+
let usageCostSegments = 0;
|
|
3894
3897
|
// Fallback tracking: used at most once per task
|
|
3895
3898
|
let usedFallback = false;
|
|
3896
3899
|
let toolFailureRetryCount = 0;
|
|
@@ -4175,10 +4178,19 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4175
4178
|
break;
|
|
4176
4179
|
}
|
|
4177
4180
|
if (taskResult?.usage) {
|
|
4178
|
-
|
|
4179
|
-
|
|
4181
|
+
const inputTokens = Number(taskResult.usage.input_tokens || 0);
|
|
4182
|
+
const outputTokens = Number(taskResult.usage.output_tokens || 0);
|
|
4183
|
+
totalUsage.input_tokens += Number.isFinite(inputTokens) ? inputTokens : 0;
|
|
4184
|
+
totalUsage.output_tokens += Number.isFinite(outputTokens) ? outputTokens : 0;
|
|
4185
|
+
if ((inputTokens > 0 || outputTokens > 0) || taskResult.usage.cost_usd != null) {
|
|
4186
|
+
usageSegments += 1;
|
|
4187
|
+
}
|
|
4180
4188
|
if (taskResult.usage.cost_usd != null) {
|
|
4181
|
-
|
|
4189
|
+
const costUsd = Number(taskResult.usage.cost_usd);
|
|
4190
|
+
if (Number.isFinite(costUsd) && costUsd >= 0) {
|
|
4191
|
+
totalUsageCostUsd += costUsd;
|
|
4192
|
+
usageCostSegments += 1;
|
|
4193
|
+
}
|
|
4182
4194
|
}
|
|
4183
4195
|
}
|
|
4184
4196
|
|
|
@@ -5065,7 +5077,12 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
5065
5077
|
console.log(`[${taskId}] ℹ️ Task succeeded with no text output — using default completion message`);
|
|
5066
5078
|
}
|
|
5067
5079
|
console.log(`[${taskId}] 📤 finalOutput="${finalOutput.slice(0,100)}" response=${finalOutput ? `"${finalOutput.slice(0,80)}"` : 'undefined'}`);
|
|
5068
|
-
|
|
5080
|
+
if (usageSegments > 0 && usageCostSegments === usageSegments) {
|
|
5081
|
+
totalUsage.cost_usd = totalUsageCostUsd;
|
|
5082
|
+
totalUsage.cost_source = 'provider_reported';
|
|
5083
|
+
totalUsage.cost_accuracy = 'exact';
|
|
5084
|
+
}
|
|
5085
|
+
const hasUsage = totalUsage.input_tokens > 0 || totalUsage.output_tokens > 0 || totalUsage.cost_usd != null;
|
|
5069
5086
|
const completionMessage = {
|
|
5070
5087
|
type: 'task_complete',
|
|
5071
5088
|
taskId,
|