create-walle 0.9.25 → 0.9.26
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 +8 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -60,6 +60,77 @@ function getLlmClient() { if (!_llmClient) _llmClient = require('./llm/client');
|
|
|
60
60
|
function getLocalTools() { if (!_localTools) _localTools = require('./tools/local-tools'); return _localTools; }
|
|
61
61
|
function getActivityLog() { if (!_activityLog) _activityLog = require('./lib/activity-log'); return _activityLog; }
|
|
62
62
|
|
|
63
|
+
function createActionMemoryPolicyForRun(opts = {}, { logger = console } = {}) {
|
|
64
|
+
if (opts.actionMemoryPolicy !== undefined) return opts.actionMemoryPolicy;
|
|
65
|
+
const mode = String(
|
|
66
|
+
opts.actionMemoryMode
|
|
67
|
+
|| (opts.actionMemoryStore ? 'audit' : process.env.WALL_E_ACTION_MEMORY_MODE)
|
|
68
|
+
|| 'off'
|
|
69
|
+
).trim().toLowerCase();
|
|
70
|
+
if (!mode || mode === 'off' || mode === '0' || mode === 'false') return null;
|
|
71
|
+
const { ActionMemoryPolicy } = require('./coding/action-memory-policy');
|
|
72
|
+
const { ActionMemoryStore } = require('./coding/action-memory-store');
|
|
73
|
+
const store = opts.actionMemoryStore || new ActionMemoryStore({
|
|
74
|
+
dbPath: opts.actionMemoryDbPath,
|
|
75
|
+
artifactsDir: opts.actionMemoryArtifactsDir,
|
|
76
|
+
logger,
|
|
77
|
+
});
|
|
78
|
+
return new ActionMemoryPolicy({
|
|
79
|
+
store,
|
|
80
|
+
mode,
|
|
81
|
+
selector: opts.actionMemorySelector || process.env.WALL_E_ACTION_MEMORY_SELECTOR,
|
|
82
|
+
minScore: opts.actionMemoryMinScore || process.env.WALL_E_ACTION_MEMORY_MIN_SCORE,
|
|
83
|
+
minMargin: opts.actionMemoryMinMargin || process.env.WALL_E_ACTION_MEMORY_MIN_MARGIN,
|
|
84
|
+
limit: opts.actionMemoryLimit || process.env.WALL_E_ACTION_MEMORY_LIMIT,
|
|
85
|
+
maxCandidates: opts.actionMemoryMaxCandidates || process.env.WALL_E_ACTION_MEMORY_MAX_CANDIDATES,
|
|
86
|
+
memoryLimit: opts.actionMemoryMemoryLimit || process.env.WALL_E_ACTION_MEMORY_MEMORY_LIMIT,
|
|
87
|
+
logger,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stringifyIntent(intent) {
|
|
92
|
+
if (!intent) return '';
|
|
93
|
+
if (typeof intent === 'string') return intent;
|
|
94
|
+
return String(intent.id || intent.intent || intent.name || intent.type || '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function callableDecisionTool(decision = {}) {
|
|
98
|
+
return decision.selectedEntryId || 'no candidate';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function recentShellCommandsForActionMemory(toolCallHistory = [], { limit = 6 } = {}) {
|
|
102
|
+
return (toolCallHistory || [])
|
|
103
|
+
.filter((item) => callName(item) === 'run_shell')
|
|
104
|
+
.slice(-Math.max(1, Number(limit) || 6))
|
|
105
|
+
.map((item) => {
|
|
106
|
+
const command = shellCommandFromEvidence(item);
|
|
107
|
+
if (!command) return '';
|
|
108
|
+
const status = item.ok === false ? 'tool_error' : 'tool_result';
|
|
109
|
+
return `${status}: ${command}`;
|
|
110
|
+
})
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join(', ');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function knownFailuresForActionMemory(toolCallHistory = []) {
|
|
116
|
+
const failures = [];
|
|
117
|
+
if ((toolCallHistory || []).some((item) => callName(item) === 'run_shell' && item.ok === false)) {
|
|
118
|
+
failures.push('run_shell tool error');
|
|
119
|
+
}
|
|
120
|
+
return failures.join(', ');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function shellCommandFromEvidence(item = {}) {
|
|
124
|
+
const input = item.input || item.args || {};
|
|
125
|
+
if (input.command || input.cmd) return String(input.command || input.cmd).trim();
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(String(item.inputHash || ''));
|
|
128
|
+
return String(parsed.command || parsed.cmd || '').trim();
|
|
129
|
+
} catch {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
63
134
|
// Prompt builders — operating-manual content lives in coding-prompts.js
|
|
64
135
|
// for testability and to apply the Claude Code prompt patterns
|
|
65
136
|
// (edit-vs-create, no-comments, no premature abstractions, verification-
|
|
@@ -88,10 +159,18 @@ const {
|
|
|
88
159
|
} = require('./coding/capability-router');
|
|
89
160
|
const { createCodingTranscript } = require('./coding/transcript-writer');
|
|
90
161
|
const { createCodingCapabilities } = require('./coding/capability-broker');
|
|
162
|
+
const { gitRootFor } = require('./chat/code-review-context');
|
|
163
|
+
const { recordSessionWorkspace, recordSessionAnchor } = require('./coding/session-workspaces');
|
|
91
164
|
const {
|
|
92
165
|
CompactionService,
|
|
93
166
|
DEFAULT_CONTEXT_WINDOW,
|
|
94
167
|
} = require('./coding/compaction-service');
|
|
168
|
+
const {
|
|
169
|
+
createNoProgressGuard,
|
|
170
|
+
buildBlockedToolExecution,
|
|
171
|
+
buildNoProgressNudge,
|
|
172
|
+
compactRepeatedInspectionToolResults,
|
|
173
|
+
} = require('./coding/no-progress-guard');
|
|
95
174
|
const {
|
|
96
175
|
emitAgentRunContextWarnings,
|
|
97
176
|
} = require('./runtime/agent-run-context');
|
|
@@ -99,6 +178,7 @@ const {
|
|
|
99
178
|
resolveWallERuntimeProfile,
|
|
100
179
|
} = require('./runtime/walle-runtime');
|
|
101
180
|
const { estimateTokens, estimateMessagesTokens } = require('./context/token-counter');
|
|
181
|
+
const usageUtil = require('./lib/usage');
|
|
102
182
|
const { recoverAllowedTextToolCalls } = require('./llm/text-tool-calls');
|
|
103
183
|
|
|
104
184
|
const MAX_CUMULATIVE_CONTEXT = 4000;
|
|
@@ -359,7 +439,7 @@ const CODING_TOOLS = [
|
|
|
359
439
|
},
|
|
360
440
|
{
|
|
361
441
|
name: 'edit_file',
|
|
362
|
-
description: 'Make a targeted edit
|
|
442
|
+
description: 'Make a targeted edit by replacing old_string with new_string; for additive changes, include the original old_string inside new_string plus the inserted sibling code so unrelated code is preserved. Uses a 9-strategy fuzzy matching chain — tolerates minor whitespace, indentation, and Unicode differences. More efficient than write_file for modifying existing files.',
|
|
363
443
|
input_schema: {
|
|
364
444
|
type: 'object',
|
|
365
445
|
properties: {
|
|
@@ -1134,6 +1214,27 @@ function resolveRunTimeoutMs(opts = {}) {
|
|
|
1134
1214
|
return isInteractive ? 0 : 300000;
|
|
1135
1215
|
}
|
|
1136
1216
|
|
|
1217
|
+
// Compact-and-continue gate (pure, so it's unit-testable). Returns true when an
|
|
1218
|
+
// interactive coding run that is STILL making progress has reached its turn-budget
|
|
1219
|
+
// edge and should be extended (instead of force-stripping tools into a "summarize
|
|
1220
|
+
// what's left" dead-end). Extends only when: enabled + interactive, at the cap edge,
|
|
1221
|
+
// extension + absolute-ceiling budget remains, recent tool activity actually succeeded,
|
|
1222
|
+
// and it isn't spinning on one identical call (the doom-loop detector owns that case).
|
|
1223
|
+
function shouldExtendInteractiveBudget({
|
|
1224
|
+
enabled, interactive, turnIndex, turns, turnExtensions,
|
|
1225
|
+
maxExtensions, absoluteMax, recent = [], doomThreshold = 3,
|
|
1226
|
+
} = {}) {
|
|
1227
|
+
if (!enabled || !interactive) return false;
|
|
1228
|
+
if (turnIndex < turns - 1) return false;
|
|
1229
|
+
if (turnExtensions >= maxExtensions) return false;
|
|
1230
|
+
if (turns >= absoluteMax) return false;
|
|
1231
|
+
if (!recent.some(c => c && c.ok === true)) return false;
|
|
1232
|
+
const looping = recent.length >= doomThreshold
|
|
1233
|
+
&& recent.every(c => c.name === recent[0].name && c.inputHash === recent[0].inputHash);
|
|
1234
|
+
if (looping) return false;
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1137
1238
|
function providerSupportsToolCalls(provider) {
|
|
1138
1239
|
if (!provider) return true;
|
|
1139
1240
|
if (provider.capabilities?.tools === false) return false;
|
|
@@ -1179,10 +1280,87 @@ function createInitialLlmParams(opts = {}, maxTokens = 4096) {
|
|
|
1179
1280
|
if (Object.prototype.hasOwnProperty.call(opts, 'temperature')) params.temperature = opts.temperature;
|
|
1180
1281
|
if (Object.prototype.hasOwnProperty.call(opts, 'thinking')) params.thinking = opts.thinking;
|
|
1181
1282
|
if (Object.prototype.hasOwnProperty.call(opts, 'reasoningEffort')) params.reasoningEffort = opts.reasoningEffort;
|
|
1283
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'keepAlive')) params.keepAlive = opts.keepAlive;
|
|
1182
1284
|
if (opts.options && typeof opts.options === 'object') params.options = { ...opts.options };
|
|
1183
1285
|
return params;
|
|
1184
1286
|
}
|
|
1185
1287
|
|
|
1288
|
+
function providerTypeOf(provider) {
|
|
1289
|
+
if (!provider) return '';
|
|
1290
|
+
if (typeof provider === 'string') return provider;
|
|
1291
|
+
return provider.type || provider.provider || provider.id || '';
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function isFastGemma4ModelId(model) {
|
|
1295
|
+
return /^gemma4:e4b(?:$|[-:])/i.test(String(model || ''));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function ollamaKeepAliveForRun(opts = {}) {
|
|
1299
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'keepAlive')) return opts.keepAlive;
|
|
1300
|
+
return process.env.WALLE_OLLAMA_CODING_KEEP_ALIVE || process.env.OLLAMA_KEEP_ALIVE || '30m';
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async function maybePrewarmLocalOllama({ provider, model, opts = {}, emitProgress }) {
|
|
1304
|
+
if (providerTypeOf(provider) !== 'ollama') return null;
|
|
1305
|
+
if (!isFastGemma4ModelId(model)) return null;
|
|
1306
|
+
if (opts.prewarm === false || opts.ollamaPrewarm === false) return null;
|
|
1307
|
+
if (typeof provider?.prewarm !== 'function') return null;
|
|
1308
|
+
|
|
1309
|
+
const started = Date.now();
|
|
1310
|
+
const keepAlive = ollamaKeepAliveForRun(opts);
|
|
1311
|
+
emitProgress?.({
|
|
1312
|
+
phase: opts.mode || 'executing',
|
|
1313
|
+
step: -1,
|
|
1314
|
+
message: `Prewarming Ollama model ${model}...`,
|
|
1315
|
+
detail: { provider: 'ollama', model, keepAlive },
|
|
1316
|
+
});
|
|
1317
|
+
try {
|
|
1318
|
+
const result = await provider.prewarm({
|
|
1319
|
+
model,
|
|
1320
|
+
keepAlive,
|
|
1321
|
+
timeoutMs: opts.prewarmTimeoutMs || opts.ollamaPrewarmTimeoutMs || 120000,
|
|
1322
|
+
});
|
|
1323
|
+
emitProgress?.({
|
|
1324
|
+
phase: opts.mode || 'executing',
|
|
1325
|
+
step: -1,
|
|
1326
|
+
message: `Prewarmed Ollama model ${model} in ${((Date.now() - started) / 1000).toFixed(1)}s`,
|
|
1327
|
+
detail: { provider: 'ollama', model, keepAlive, result },
|
|
1328
|
+
});
|
|
1329
|
+
return result;
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
emitProgress?.({
|
|
1332
|
+
phase: opts.mode || 'executing',
|
|
1333
|
+
step: -1,
|
|
1334
|
+
message: `Ollama prewarm failed: ${String(err?.message || err).slice(0, 120)}`,
|
|
1335
|
+
detail: { provider: 'ollama', model, keepAlive },
|
|
1336
|
+
});
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function emitOllamaTurnTiming({ emitProgress, provider, model, phase, step, startedAt, usage, params, toolsCount }) {
|
|
1342
|
+
if (providerTypeOf(provider) !== 'ollama') return;
|
|
1343
|
+
const latencyMs = Date.now() - startedAt;
|
|
1344
|
+
emitProgress?.({
|
|
1345
|
+
phase,
|
|
1346
|
+
step,
|
|
1347
|
+
message: `Ollama turn completed in ${(latencyMs / 1000).toFixed(1)}s`,
|
|
1348
|
+
detail: {
|
|
1349
|
+
provider: 'ollama',
|
|
1350
|
+
model,
|
|
1351
|
+
latencyMs,
|
|
1352
|
+
inputTokens: usage?.input || 0,
|
|
1353
|
+
outputTokens: usage?.output || 0,
|
|
1354
|
+
genTokPerSec: usage?.genTokPerSec ?? null,
|
|
1355
|
+
prefillTokPerSec: usage?.prefillTokPerSec ?? null,
|
|
1356
|
+
numCtx: params?.options?.num_ctx ?? null,
|
|
1357
|
+
maxTokens: params?.maxTokens ?? null,
|
|
1358
|
+
keepAlive: params?.keepAlive ?? null,
|
|
1359
|
+
toolsCount: toolsCount ?? null,
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1186
1364
|
function createCodingCompactionService(provider, modelId, opts = {}) {
|
|
1187
1365
|
if (opts.autoCompact === false || opts.compaction === false || opts.disableCompaction === true) return null;
|
|
1188
1366
|
if (String(process.env.WALLE_CODING_AUTO_COMPACT || '').trim() === '0') return null;
|
|
@@ -2101,6 +2279,18 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2101
2279
|
}
|
|
2102
2280
|
|
|
2103
2281
|
// Build system prompt with project context.
|
|
2282
|
+
// Ground the agent in its directory every turn, and surface every root this
|
|
2283
|
+
// session has worked in (so a session reused across repos doesn't lose track of
|
|
2284
|
+
// where it built things — session c3f3af97).
|
|
2285
|
+
let gitRoot = '';
|
|
2286
|
+
try { gitRoot = gitRootFor(resolvedCwd) || ''; } catch { gitRoot = ''; }
|
|
2287
|
+
const groundingSessionKey = opts.chatSessionId || opts.session_id || sid;
|
|
2288
|
+
const sessionWorkspaces = recordSessionWorkspace(opts.brain, groundingSessionKey, resolvedCwd);
|
|
2289
|
+
// Anchor the session to its first project root (git root, else cwd) so it has a
|
|
2290
|
+
// stable "home" even when a later turn is pointed at a different directory — the
|
|
2291
|
+
// agent resolves "the project / the site / what you built" to the anchor instead
|
|
2292
|
+
// of whatever cwd this turn happens to carry (session c3f3af97).
|
|
2293
|
+
const sessionAnchor = recordSessionAnchor(opts.brain, groundingSessionKey, gitRoot || resolvedCwd);
|
|
2104
2294
|
const systemPrompt = buildAgentSystemPrompt({
|
|
2105
2295
|
resolvedCwd,
|
|
2106
2296
|
projectInfo: projectInfo && projectInfo.type !== 'unknown' ? projectInfo : null,
|
|
@@ -2115,11 +2305,17 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2115
2305
|
promptCapabilities,
|
|
2116
2306
|
artifactCapabilities,
|
|
2117
2307
|
userTask: prompt,
|
|
2308
|
+
environment: { gitRoot, isGitRepo: !!gitRoot, platform: process.platform },
|
|
2309
|
+
sessionWorkspaces,
|
|
2310
|
+
sessionAnchor,
|
|
2118
2311
|
},
|
|
2119
2312
|
});
|
|
2120
2313
|
|
|
2121
|
-
// Resume support: use restored messages if resuming from checkpoint
|
|
2122
|
-
|
|
2314
|
+
// Resume support: use restored messages if resuming from checkpoint.
|
|
2315
|
+
// For a normal turn the seed is built from the prior conversation + tool trail
|
|
2316
|
+
// (opts.contextMessages) once the compaction service exists (see below); this
|
|
2317
|
+
// placeholder is only used if there is no context to seed from.
|
|
2318
|
+
let messages = opts._resumeMessages || [{ role: 'user', content: prompt }];
|
|
2123
2319
|
let finalOutput = '';
|
|
2124
2320
|
let finalAnswerDelivered = false;
|
|
2125
2321
|
let totalInput = 0;
|
|
@@ -2130,6 +2326,147 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2130
2326
|
// Doom loop detection (6a)
|
|
2131
2327
|
const toolCallHistory = []; // { name, inputHash }
|
|
2132
2328
|
const DOOM_LOOP_THRESHOLD = 3;
|
|
2329
|
+
const noProgressGuard = createNoProgressGuard({
|
|
2330
|
+
cwd: resolvedCwd,
|
|
2331
|
+
enabled: opts.noProgressGuard !== false && process.env.WALLE_NO_PROGRESS_GUARD !== '0',
|
|
2332
|
+
threshold: opts.noProgressGuardThreshold || Number(process.env.WALLE_NO_PROGRESS_THRESHOLD) || DOOM_LOOP_THRESHOLD,
|
|
2333
|
+
});
|
|
2334
|
+
const actionMemoryPolicy = createActionMemoryPolicyForRun(opts, { logger: console });
|
|
2335
|
+
function actionMemoryContextForCall(call = {}) {
|
|
2336
|
+
const recentSequence = toolCallHistory.slice(-6).map((item) => callName(item)).filter(Boolean);
|
|
2337
|
+
const last = recentSequence[recentSequence.length - 1] || '';
|
|
2338
|
+
const shellHistory = recentShellCommandsForActionMemory(toolCallHistory);
|
|
2339
|
+
const failures = knownFailuresForActionMemory(toolCallHistory);
|
|
2340
|
+
return {
|
|
2341
|
+
sessionId: sid,
|
|
2342
|
+
cwd: resolvedCwd,
|
|
2343
|
+
repo: opts.actionMemoryRepo || opts.repo || gitRoot || resolvedCwd,
|
|
2344
|
+
familyId: opts.actionMemoryFamilyId || opts.workflowFamilyId || runtimeMode.id,
|
|
2345
|
+
intent: opts.actionMemoryIntent || stringifyIntent(codingIntent) || opts.mode || runtimeMode.id,
|
|
2346
|
+
intendedTool: callName(call),
|
|
2347
|
+
taskText: String(prompt || ''),
|
|
2348
|
+
sequence: recentSequence.join(' > '),
|
|
2349
|
+
lastTool: last,
|
|
2350
|
+
recentShellCommands: shellHistory,
|
|
2351
|
+
knownFailures: failures,
|
|
2352
|
+
verificationStatus: hasFailedVerificationAttempt(toolCallHistory)
|
|
2353
|
+
? 'failed'
|
|
2354
|
+
: (hasVerificationEvidence(toolCallHistory) ? 'passed' : ''),
|
|
2355
|
+
toolCatalogId: opts.actionMemoryToolCatalogId || `${llm.type || 'provider'}:${runtimeMode.id}`,
|
|
2356
|
+
metadata: {
|
|
2357
|
+
mode: opts.mode || '',
|
|
2358
|
+
runtimeMode: runtimeMode.id,
|
|
2359
|
+
projectType: projectInfo?.type || '',
|
|
2360
|
+
},
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
function actionMemoryModelContext() {
|
|
2364
|
+
return {
|
|
2365
|
+
provider: llm.type || '',
|
|
2366
|
+
modelId,
|
|
2367
|
+
toolFormat: opts.actionMemoryToolFormat || '',
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
async function executeWithActionMemoryPolicy({ call, context, execute, allowRewrite = true }) {
|
|
2371
|
+
if (!actionMemoryPolicy) return execute(call, { mode: 'off', action: 'skip', accepted: false });
|
|
2372
|
+
if (!allowRewrite && actionMemoryPolicy.mode !== 'audit') {
|
|
2373
|
+
return execute(call, {
|
|
2374
|
+
mode: actionMemoryPolicy.mode,
|
|
2375
|
+
action: 'skip',
|
|
2376
|
+
accepted: false,
|
|
2377
|
+
reason: 'stream_permission_prechecked',
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
const { executeToolCallWithActionMemoryPolicy } = require('./coding/action-memory-policy');
|
|
2381
|
+
return executeToolCallWithActionMemoryPolicy({
|
|
2382
|
+
call,
|
|
2383
|
+
context,
|
|
2384
|
+
model: actionMemoryModelContext(),
|
|
2385
|
+
sessionId: sid,
|
|
2386
|
+
prompt,
|
|
2387
|
+
policy: actionMemoryPolicy,
|
|
2388
|
+
execute,
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
function emitActionMemoryDecision(execution, step) {
|
|
2392
|
+
const decision = execution?.actionMemory?.decision;
|
|
2393
|
+
if (!decision || decision.action === 'skip') return;
|
|
2394
|
+
const detail = {
|
|
2395
|
+
...decision,
|
|
2396
|
+
selectedEntryId: decision.selectedEntryId || '',
|
|
2397
|
+
candidateCount: execution.actionMemory.candidateCount || 0,
|
|
2398
|
+
};
|
|
2399
|
+
emitProgress({
|
|
2400
|
+
type: 'action_memory',
|
|
2401
|
+
phase: opts.mode || 'executing',
|
|
2402
|
+
step,
|
|
2403
|
+
message: `Action memory ${decision.action}: ${callableDecisionTool(decision)}`,
|
|
2404
|
+
detail,
|
|
2405
|
+
});
|
|
2406
|
+
events.emit('action_memory.decision', { sessionId: sid, ...detail });
|
|
2407
|
+
}
|
|
2408
|
+
function compactNoProgressState(step, reason) {
|
|
2409
|
+
if (opts.noProgressGuard === false || process.env.WALLE_NO_PROGRESS_GUARD === '0') return;
|
|
2410
|
+
const compacted = compactRepeatedInspectionToolResults(messages, { cwd: resolvedCwd });
|
|
2411
|
+
if (compacted.compacted > 0) {
|
|
2412
|
+
emitProgress({
|
|
2413
|
+
phase: opts.mode || 'executing',
|
|
2414
|
+
step,
|
|
2415
|
+
message: `Compacted ${compacted.compacted} repeated inspection result(s)`,
|
|
2416
|
+
detail: { reason, groups: compacted.groups },
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// Compact-and-continue (interactive coding): a fixed turn cap that force-strips
|
|
2422
|
+
// tools and makes the model "summarize what's left" turns "out of steps" into a
|
|
2423
|
+
// dead-end ("I investigated; confirm and I'll apply it"). Interactive runs already
|
|
2424
|
+
// have NO wall-clock deadline (run until the agent finishes or the user stops, like
|
|
2425
|
+
// Claude Code / opencode) — the turn counter is the last hard ceiling contradicting
|
|
2426
|
+
// that. When an interactive run is STILL making progress at the cap edge, extend the
|
|
2427
|
+
// budget (bounded) + nudge it to land the edit, instead of forcing the wrap-up.
|
|
2428
|
+
// Headless/benchmark runs keep the hard cap (CI/background must not hang).
|
|
2429
|
+
const codingTurnContinueEnabled = process.env.WALLE_CODING_TURN_CONTINUE !== '0';
|
|
2430
|
+
const ABSOLUTE_MAX_TURNS = Math.max(turns, 200);
|
|
2431
|
+
const TURN_EXTENSION_BATCH = 10;
|
|
2432
|
+
const MAX_TURN_EXTENSIONS = 12;
|
|
2433
|
+
let turnExtensions = 0;
|
|
2434
|
+
// Called at the top of a turn, BEFORE tools get stripped on the final budgeted
|
|
2435
|
+
// round. When an interactive run is still making progress at the cap edge, bump
|
|
2436
|
+
// `turns`, append a directive nudging the model to land the edit (appended to the
|
|
2437
|
+
// last message's content, not a new user message — consecutive user messages are
|
|
2438
|
+
// rejected by the API), and return true. Returns false to let the normal wrap-up run.
|
|
2439
|
+
function tryExtendInteractiveTurnBudget(turnIndex) {
|
|
2440
|
+
if (!shouldExtendInteractiveBudget({
|
|
2441
|
+
enabled: codingTurnContinueEnabled,
|
|
2442
|
+
interactive: interactiveRun,
|
|
2443
|
+
turnIndex,
|
|
2444
|
+
turns,
|
|
2445
|
+
turnExtensions,
|
|
2446
|
+
maxExtensions: MAX_TURN_EXTENSIONS,
|
|
2447
|
+
absoluteMax: ABSOLUTE_MAX_TURNS,
|
|
2448
|
+
recent: toolCallHistory.slice(-DOOM_LOOP_THRESHOLD),
|
|
2449
|
+
doomThreshold: DOOM_LOOP_THRESHOLD,
|
|
2450
|
+
})) return false;
|
|
2451
|
+
turns = Math.min(ABSOLUTE_MAX_TURNS, turns + TURN_EXTENSION_BATCH);
|
|
2452
|
+
turnExtensions += 1;
|
|
2453
|
+
emitProgress({
|
|
2454
|
+
phase: opts.mode || 'executing',
|
|
2455
|
+
step: turnIndex,
|
|
2456
|
+
message: `Coding budget extended (${turnExtensions}/${MAX_TURN_EXTENSIONS}) — still making progress`,
|
|
2457
|
+
detail: { turns },
|
|
2458
|
+
});
|
|
2459
|
+
const directive = '[System: You still have budget. You have gathered enough context — STOP investigating and APPLY the change now with edit_file/multi_edit/apply_patch, then verify. Do not ask the user to confirm before applying; apply, then report what changed.]';
|
|
2460
|
+
const lastMsg = messages[messages.length - 1];
|
|
2461
|
+
if (lastMsg && Array.isArray(lastMsg.content)) {
|
|
2462
|
+
lastMsg.content.push({ type: 'text', text: directive });
|
|
2463
|
+
} else if (lastMsg && lastMsg.role === 'user' && typeof lastMsg.content === 'string') {
|
|
2464
|
+
lastMsg.content += '\n\n' + directive;
|
|
2465
|
+
} else {
|
|
2466
|
+
messages.push({ role: 'user', content: directive });
|
|
2467
|
+
}
|
|
2468
|
+
return true;
|
|
2469
|
+
}
|
|
2133
2470
|
|
|
2134
2471
|
// Screenshots captured by browser_screenshot calls. Drives the
|
|
2135
2472
|
// visual review pass in coding-review.js.
|
|
@@ -2138,6 +2475,59 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2138
2475
|
// Per-step cost/token tracking (6q)
|
|
2139
2476
|
const turnCosts = [];
|
|
2140
2477
|
let budgetUsed = 0;
|
|
2478
|
+
let totalCostUsd = 0; // cumulative $ (accurate, cache-discounted)
|
|
2479
|
+
let lastTurnRawUsage = null; // most recent turn's usage → context occupancy
|
|
2480
|
+
|
|
2481
|
+
// Emit a live token/cost snapshot to any watching client (per-session + top-bar
|
|
2482
|
+
// counters). Same shape coding + chat use; see lib/usage.js. `type:'usage'` flows
|
|
2483
|
+
// through emitProgress → CTM emitWalleProgress → browser handleProgress(ev.type).
|
|
2484
|
+
let _usageRequestSeq = 0;
|
|
2485
|
+
function emitUsageSnapshot(rawUsage) {
|
|
2486
|
+
if (rawUsage) lastTurnRawUsage = rawUsage;
|
|
2487
|
+
try {
|
|
2488
|
+
const snap = usageUtil.usageSnapshot({
|
|
2489
|
+
rawUsage: lastTurnRawUsage,
|
|
2490
|
+
cumulativeInput: totalInput,
|
|
2491
|
+
cumulativeOutput: totalOutput,
|
|
2492
|
+
costUsd: totalCostUsd,
|
|
2493
|
+
model: modelId,
|
|
2494
|
+
provider: llm?.type || '',
|
|
2495
|
+
});
|
|
2496
|
+
emitProgress({ type: 'usage', usage: snap });
|
|
2497
|
+
} catch { /* never let telemetry break the run */ }
|
|
2498
|
+
}
|
|
2499
|
+
// Usage fields for the final transcript assistant message — Claude-Code parity:
|
|
2500
|
+
// `usage` mirrors its per-message `message.usage`; `tokens`/`cost` stay for compat.
|
|
2501
|
+
function transcriptUsageFields() {
|
|
2502
|
+
const usage = lastTurnRawUsage
|
|
2503
|
+
? usageUtil.normalizeUsage(lastTurnRawUsage, { model: modelId, provider: llm?.type || '' })
|
|
2504
|
+
: null;
|
|
2505
|
+
return {
|
|
2506
|
+
tokens: { input: totalInput, output: totalOutput, cost_usd: totalCostUsd },
|
|
2507
|
+
usage,
|
|
2508
|
+
cost: totalCostUsd,
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
// Record one turn's usage to the queryable ledger (parity with chat.js, which the
|
|
2512
|
+
// coding agent previously skipped). Deduped per turn via a synthetic requestId.
|
|
2513
|
+
function recordTurnUsageLedger(rawUsage, turnIndex) {
|
|
2514
|
+
if (!rawUsage) return;
|
|
2515
|
+
try {
|
|
2516
|
+
require('./brain').recordModelUsage?.({
|
|
2517
|
+
source: 'wall-e.coding',
|
|
2518
|
+
feature: 'coding_turn',
|
|
2519
|
+
sessionId: sid,
|
|
2520
|
+
providerType: llm?.type || '',
|
|
2521
|
+
modelId,
|
|
2522
|
+
requestId: `${sid}:${turnIndex}:${_usageRequestSeq++}`,
|
|
2523
|
+
usage: usageUtil.normalizeUsage(rawUsage, { model: modelId, provider: llm?.type || '' }),
|
|
2524
|
+
stopReason: rawUsage.stopReason || null,
|
|
2525
|
+
costUsd: usageUtil.costFor(rawUsage, modelId, llm?.type || ''),
|
|
2526
|
+
costSource: 'wall-e.usage',
|
|
2527
|
+
metadata: { turn: turnIndex, runtimeMode: opts.mode || 'executing' },
|
|
2528
|
+
});
|
|
2529
|
+
} catch { /* ledger is best-effort */ }
|
|
2530
|
+
}
|
|
2141
2531
|
|
|
2142
2532
|
// In-flight todo tracking (6m)
|
|
2143
2533
|
let currentTodos = [];
|
|
@@ -2165,6 +2555,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2165
2555
|
});
|
|
2166
2556
|
const { AgentCatalog } = require('./coding/agent-catalog');
|
|
2167
2557
|
const agentCatalog = opts.agentCatalog || new AgentCatalog({ projectRoot: resolvedCwd });
|
|
2558
|
+
await maybePrewarmLocalOllama({ provider: llm, model: modelId, opts, emitProgress });
|
|
2168
2559
|
const defaultTaskRunner = async ({ prompt: childPrompt, agent, taskId, ctx }) => {
|
|
2169
2560
|
const childTools = agentCatalog.toolsForAgent(requestedTools, agent);
|
|
2170
2561
|
const childResult = await runAgentLoop(childPrompt, {
|
|
@@ -2222,6 +2613,20 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2222
2613
|
// Inspired by OpenCode Question service (packages/opencode/src/question/index.ts)
|
|
2223
2614
|
const questionManager = opts.questionManager || new QuestionManager(events);
|
|
2224
2615
|
const compactionService = createCodingCompactionService(llm, modelId, opts);
|
|
2616
|
+
|
|
2617
|
+
// Seed the turn from the prior conversation + tool trail so the agent reads
|
|
2618
|
+
// its own history (a bare "go ahead." now has the proposed plan to refer to,
|
|
2619
|
+
// and the agent does not re-investigate what it already explored). Skipped on
|
|
2620
|
+
// a checkpoint resume (those messages are authoritative). Bounded to the
|
|
2621
|
+
// context window via the compaction service just created above.
|
|
2622
|
+
if (!opts._resumeMessages) {
|
|
2623
|
+
const { buildInitialCodingMessages } = require('./coding/initial-messages');
|
|
2624
|
+
messages = buildInitialCodingMessages({
|
|
2625
|
+
prompt,
|
|
2626
|
+
contextMessages: opts.contextMessages,
|
|
2627
|
+
compactionService,
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2225
2630
|
const { RuntimeEventWriter } = require('./coding/runtime-events');
|
|
2226
2631
|
const {
|
|
2227
2632
|
appendPromptManifest,
|
|
@@ -2314,6 +2719,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2314
2719
|
model: modelId,
|
|
2315
2720
|
mode: opts.mode || '',
|
|
2316
2721
|
runtimeMode: runtimeMode.id,
|
|
2722
|
+
goal: String(prompt || ''),
|
|
2317
2723
|
headless: Boolean(opts.headless),
|
|
2318
2724
|
benchmark: Boolean(opts.benchmark),
|
|
2319
2725
|
userHooks,
|
|
@@ -2353,6 +2759,22 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2353
2759
|
provider: llm,
|
|
2354
2760
|
model: modelId,
|
|
2355
2761
|
transcript,
|
|
2762
|
+
prepareToolCall: actionMemoryPolicy
|
|
2763
|
+
? async (call) => {
|
|
2764
|
+
const evaluated = await actionMemoryPolicy.evaluateToolCall({
|
|
2765
|
+
call,
|
|
2766
|
+
context: actionMemoryContextForCall(call),
|
|
2767
|
+
model: actionMemoryModelContext(),
|
|
2768
|
+
sessionId: sid,
|
|
2769
|
+
prompt,
|
|
2770
|
+
});
|
|
2771
|
+
return {
|
|
2772
|
+
call: evaluated.call,
|
|
2773
|
+
decision: evaluated.decision,
|
|
2774
|
+
candidateCount: evaluated.candidates.length,
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
: null,
|
|
2356
2778
|
snapshotService: opts.snapshotService || new SnapshotService({
|
|
2357
2779
|
cwd: resolvedCwd,
|
|
2358
2780
|
// Whole-worktree step snapshots + restart-surviving boundaries for
|
|
@@ -2362,7 +2784,32 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2362
2784
|
}),
|
|
2363
2785
|
permissionService,
|
|
2364
2786
|
headless: Boolean(opts.headless || opts.benchmark),
|
|
2365
|
-
|
|
2787
|
+
goal: String(prompt || ''),
|
|
2788
|
+
toolExecutor: async (call, runtimeCtx = {}) => {
|
|
2789
|
+
const guardBlock = noProgressGuard.check(call, { cwd: resolvedCwd });
|
|
2790
|
+
if (guardBlock) {
|
|
2791
|
+
const blockedExecution = buildBlockedToolExecution(call, guardBlock);
|
|
2792
|
+
noProgressGuard.record({ name: call.name, input: blockedExecution.evidenceInput }, {
|
|
2793
|
+
ok: false,
|
|
2794
|
+
result: blockedExecution.result,
|
|
2795
|
+
blocked: true,
|
|
2796
|
+
cwd: resolvedCwd,
|
|
2797
|
+
});
|
|
2798
|
+
emitProgress({
|
|
2799
|
+
phase: opts.mode || 'executing',
|
|
2800
|
+
step: 0,
|
|
2801
|
+
message: `No-progress guard blocked repeated ${call.name}`,
|
|
2802
|
+
detail: { tool: call.name, target: guardBlock.target, repeatCount: guardBlock.repeatCount },
|
|
2803
|
+
});
|
|
2804
|
+
events.emit('no_progress.detected', {
|
|
2805
|
+
tool: call.name,
|
|
2806
|
+
target: guardBlock.target,
|
|
2807
|
+
repeatCount: guardBlock.repeatCount,
|
|
2808
|
+
threshold: guardBlock.threshold,
|
|
2809
|
+
sessionId: sid,
|
|
2810
|
+
});
|
|
2811
|
+
return blockedExecution.result;
|
|
2812
|
+
}
|
|
2366
2813
|
const execution = await streamToolExecutionController.execute(call, {
|
|
2367
2814
|
sessionId: sid,
|
|
2368
2815
|
cwd: resolvedCwd,
|
|
@@ -2373,7 +2820,26 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2373
2820
|
runtimeMode: runtimeMode.id,
|
|
2374
2821
|
interactive: opts.interactive,
|
|
2375
2822
|
onTodos: (todos) => { currentTodos = todos; },
|
|
2823
|
+
// Forward the turn's abort signal so user Stop kills an in-flight
|
|
2824
|
+
// run_shell child, and a heartbeat so a long command shows live motion.
|
|
2825
|
+
signal: runtimeCtx.signal,
|
|
2826
|
+
onProgress: (info) => emitProgress({
|
|
2827
|
+
phase: opts.mode || 'executing',
|
|
2828
|
+
step: 0,
|
|
2829
|
+
message: info?.message || 'still running…',
|
|
2830
|
+
detail: info,
|
|
2831
|
+
}),
|
|
2832
|
+
});
|
|
2833
|
+
const guardRecord = noProgressGuard.record({ name: call.name, input: execution.evidenceInput || call.input }, {
|
|
2834
|
+
ok: execution.ok,
|
|
2835
|
+
result: execution.result,
|
|
2836
|
+
blocked: execution.blocked,
|
|
2837
|
+
cwd: resolvedCwd,
|
|
2376
2838
|
});
|
|
2839
|
+
const guardNudge = buildNoProgressNudge(guardRecord);
|
|
2840
|
+
if (guardNudge && execution.result && typeof execution.result === 'object' && !execution.result.error) {
|
|
2841
|
+
execution.result._walleNoProgressAdvice = guardNudge.trim();
|
|
2842
|
+
}
|
|
2377
2843
|
return execution.result;
|
|
2378
2844
|
},
|
|
2379
2845
|
});
|
|
@@ -2389,6 +2855,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2389
2855
|
// card and waiting state without a reload.
|
|
2390
2856
|
if (evt && [
|
|
2391
2857
|
'tool_call', 'tool_result', 'tool_done', 'skill_loaded', 'skill_load_failed',
|
|
2858
|
+
'action_memory',
|
|
2392
2859
|
'permission_request', 'permission_resolved', 'permission_denied', 'lane_event',
|
|
2393
2860
|
].includes(evt.type)) {
|
|
2394
2861
|
emitProgress(evt);
|
|
@@ -2408,6 +2875,9 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2408
2875
|
const streamErrors = [];
|
|
2409
2876
|
let streamProviderError = null;
|
|
2410
2877
|
let streamHadEdit = false;
|
|
2878
|
+
let completionReportRequested = false;
|
|
2879
|
+
const maxNoProgressGuardBlocks = opts.noProgressGuardMaxBlocks || Number(process.env.WALLE_NO_PROGRESS_MAX_BLOCKS) || 2;
|
|
2880
|
+
let noProgressGuardBlockStreak = 0;
|
|
2411
2881
|
for (let turnIndex = opts._resumeTurn || 0; turnIndex < turns; turnIndex++) {
|
|
2412
2882
|
throwIfExternalAbort();
|
|
2413
2883
|
const remaining = deadline - Date.now();
|
|
@@ -2417,6 +2887,10 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2417
2887
|
finalOutput += '\n[Timeout reached]';
|
|
2418
2888
|
break;
|
|
2419
2889
|
}
|
|
2890
|
+
// Compact-and-continue: if we're at the budget edge but still making progress,
|
|
2891
|
+
// extend the turn budget (+ nudge to land the edit) BEFORE tools get stripped
|
|
2892
|
+
// below, instead of forcing the model into a tool-less "summarize what's left".
|
|
2893
|
+
tryExtendInteractiveTurnBudget(turnIndex);
|
|
2420
2894
|
emitProgress({
|
|
2421
2895
|
phase: opts.mode || 'executing',
|
|
2422
2896
|
step: turnIndex,
|
|
@@ -2429,7 +2903,9 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2429
2903
|
let turn;
|
|
2430
2904
|
let toolsForTurn = [];
|
|
2431
2905
|
try {
|
|
2432
|
-
|
|
2906
|
+
// No tools on the last budgeted round, and none on the completion-report
|
|
2907
|
+
// round (the model must answer, not start new work).
|
|
2908
|
+
toolsForTurn = (turnIndex >= turns - 1 || completionReportRequested)
|
|
2433
2909
|
? []
|
|
2434
2910
|
: await toolRegistry.getDefinitions({
|
|
2435
2911
|
provider: llm.type,
|
|
@@ -2459,21 +2935,35 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2459
2935
|
reason: 'stream_pre_turn',
|
|
2460
2936
|
opts,
|
|
2461
2937
|
});
|
|
2938
|
+
compactNoProgressState(turnIndex, 'stream_pre_turn');
|
|
2939
|
+
const requestKeepAlive = llmCtx.params.keepAlive ?? ollamaKeepAliveForRun(opts);
|
|
2940
|
+
const llmStartedAt = Date.now();
|
|
2462
2941
|
turn = await processor.runTurn({
|
|
2463
2942
|
sessionId: sid,
|
|
2464
2943
|
cwd: resolvedCwd,
|
|
2465
2944
|
system: llmCtx.system,
|
|
2466
2945
|
messages,
|
|
2467
2946
|
tools: toolsForTurn,
|
|
2468
|
-
maxTokens: taskFileHints.length >= 4 ? 8192 : 4096,
|
|
2469
2947
|
signal: ac.signal,
|
|
2470
2948
|
maxTokens: llmCtx.params.maxTokens,
|
|
2471
2949
|
temperature: llmCtx.params.temperature,
|
|
2472
2950
|
thinking: llmCtx.params.thinking,
|
|
2473
2951
|
reasoningEffort: llmCtx.params.reasoningEffort,
|
|
2474
2952
|
options: llmCtx.params.options,
|
|
2953
|
+
keepAlive: requestKeepAlive,
|
|
2475
2954
|
promptCache: true,
|
|
2476
2955
|
});
|
|
2956
|
+
emitOllamaTurnTiming({
|
|
2957
|
+
emitProgress,
|
|
2958
|
+
provider: llm,
|
|
2959
|
+
model: modelId,
|
|
2960
|
+
phase: opts.mode || 'executing',
|
|
2961
|
+
step: turnIndex,
|
|
2962
|
+
startedAt: llmStartedAt,
|
|
2963
|
+
usage: turn.usage,
|
|
2964
|
+
params: { ...llmCtx.params, keepAlive: requestKeepAlive },
|
|
2965
|
+
toolsCount: toolsForTurn.length,
|
|
2966
|
+
});
|
|
2477
2967
|
} finally {
|
|
2478
2968
|
clearTimeout(timer);
|
|
2479
2969
|
unlinkExternalAbort();
|
|
@@ -2481,6 +2971,13 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2481
2971
|
|
|
2482
2972
|
totalInput += turn.usage?.input || 0;
|
|
2483
2973
|
totalOutput += turn.usage?.output || 0;
|
|
2974
|
+
if (turn.usage) {
|
|
2975
|
+
// Display/ledger only — the stream path has no budget-enforcement check,
|
|
2976
|
+
// so leave budgetUsed's prior semantics unchanged.
|
|
2977
|
+
totalCostUsd += usageUtil.costFor(turn.usage, modelId, llm?.type || '');
|
|
2978
|
+
recordTurnUsageLedger(turn.raw?.usage || turn.usage, turnIndex);
|
|
2979
|
+
emitUsageSnapshot(turn.usage);
|
|
2980
|
+
}
|
|
2484
2981
|
streamStatus = turn.status;
|
|
2485
2982
|
streamStopReason = turn.stopReason || streamStopReason;
|
|
2486
2983
|
streamModel = turn.model || streamModel;
|
|
@@ -2489,6 +2986,10 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2489
2986
|
if (turn.text) finalOutput += turn.text;
|
|
2490
2987
|
const streamToolCalls = (turn.toolCalls || []).map(tc => ({ name: tc.name, input: tc.input }));
|
|
2491
2988
|
const streamToolResults = turn.toolResults || [];
|
|
2989
|
+
const noProgressBlocks = streamToolResults.filter((record) => {
|
|
2990
|
+
const result = record?.result || record;
|
|
2991
|
+
return result?.noProgressGuard === true;
|
|
2992
|
+
});
|
|
2492
2993
|
toolCallHistory.push(...streamToolCalls.map((tc, index) => {
|
|
2493
2994
|
const resultRecord = streamToolResults[index] || {};
|
|
2494
2995
|
return normalizeToolCallEvidence(tc, resultRecord.result || resultRecord);
|
|
@@ -2509,6 +3010,30 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2509
3010
|
});
|
|
2510
3011
|
if (turn.hadEdit) streamHadEdit = true;
|
|
2511
3012
|
|
|
3013
|
+
if (noProgressBlocks.length > 0) {
|
|
3014
|
+
noProgressGuardBlockStreak += noProgressBlocks.length;
|
|
3015
|
+
const latestBlock = noProgressBlocks[noProgressBlocks.length - 1]?.result || noProgressBlocks[noProgressBlocks.length - 1] || {};
|
|
3016
|
+
if (noProgressGuardBlockStreak >= maxNoProgressGuardBlocks) {
|
|
3017
|
+
const message = `No-progress guard stopped repeated ${latestBlock.tool || 'inspection'} loop after ${noProgressGuardBlockStreak} blocked tool calls.`;
|
|
3018
|
+
streamStatus = 'error';
|
|
3019
|
+
streamErrors.push(message);
|
|
3020
|
+
finalOutput += `\n[${message}]`;
|
|
3021
|
+
emitProgress({
|
|
3022
|
+
phase: opts.mode || 'executing',
|
|
3023
|
+
step: turnIndex,
|
|
3024
|
+
message,
|
|
3025
|
+
detail: {
|
|
3026
|
+
tool: latestBlock.tool || '',
|
|
3027
|
+
target: latestBlock.target || '',
|
|
3028
|
+
repeatCount: latestBlock.repeatCount || noProgressGuardBlockStreak,
|
|
3029
|
+
},
|
|
3030
|
+
});
|
|
3031
|
+
break;
|
|
3032
|
+
}
|
|
3033
|
+
} else if (streamToolCalls.length > 0 || turn.hadEdit) {
|
|
3034
|
+
noProgressGuardBlockStreak = 0;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
2512
3037
|
if (turn.status === 'error') break;
|
|
2513
3038
|
if ((turn.toolCalls || []).length === 0) {
|
|
2514
3039
|
const continuation = getNoActionContinuation({
|
|
@@ -2547,7 +3072,25 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2547
3072
|
}
|
|
2548
3073
|
if (turn.assistantMessage) messages.push(turn.assistantMessage);
|
|
2549
3074
|
if (turn.toolResultMessage) messages.push(turn.toolResultMessage);
|
|
2550
|
-
if (turn.verified && streamHadEdit)
|
|
3075
|
+
if (turn.verified && streamHadEdit && !completionReportRequested) {
|
|
3076
|
+
// Verification succeeded after edits — but breaking here ends the run
|
|
3077
|
+
// before the model has seen the verification result, so the persisted
|
|
3078
|
+
// reply is accumulated narration that stops mid-thought ("finished
|
|
3079
|
+
// without an answer", session c3f3af97). When a human is reading the
|
|
3080
|
+
// reply, spend ONE no-tools round on the final completion report, then
|
|
3081
|
+
// stop. Headless/benchmark runs keep the fast exit — their consumers
|
|
3082
|
+
// score outcomes, not prose.
|
|
3083
|
+
if (interactiveRun && turnIndex + 1 < turns) {
|
|
3084
|
+
completionReportRequested = true;
|
|
3085
|
+
messages.push({
|
|
3086
|
+
role: 'user',
|
|
3087
|
+
content: '[System: Verification succeeded and your edits are in place. Provide the final completion report now — what changed, how it was verified, and anything the user should know. Do not call more tools.]',
|
|
3088
|
+
});
|
|
3089
|
+
emitProgress({ phase: opts.mode || 'executing', step: turnIndex, message: 'Verified after edits — requesting final completion report' });
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
2551
3094
|
if (turn.next !== 'continue') break;
|
|
2552
3095
|
}
|
|
2553
3096
|
|
|
@@ -2586,7 +3129,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2586
3129
|
chatSessionId: opts.chatSessionId || '',
|
|
2587
3130
|
model: streamModel,
|
|
2588
3131
|
provider: llm.type || '',
|
|
2589
|
-
|
|
3132
|
+
...transcriptUsageFields(),
|
|
2590
3133
|
});
|
|
2591
3134
|
}
|
|
2592
3135
|
|
|
@@ -2604,6 +3147,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2604
3147
|
exitCode: streamStatus === 'error' ? -1 : 0,
|
|
2605
3148
|
log,
|
|
2606
3149
|
usage: { input: totalInput, output: totalOutput },
|
|
3150
|
+
costUsd: totalCostUsd,
|
|
2607
3151
|
provider: llm.type,
|
|
2608
3152
|
model: streamModel,
|
|
2609
3153
|
next: 'stop',
|
|
@@ -2635,6 +3179,9 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2635
3179
|
break;
|
|
2636
3180
|
}
|
|
2637
3181
|
|
|
3182
|
+
// Compact-and-continue: extend the budget (+ nudge to land the edit) at the cap
|
|
3183
|
+
// edge while still making progress, BEFORE turnsRemaining drives the wrap-up below.
|
|
3184
|
+
tryExtendInteractiveTurnBudget(turn);
|
|
2638
3185
|
emitProgress({
|
|
2639
3186
|
phase: opts.mode || 'executing',
|
|
2640
3187
|
step: turn,
|
|
@@ -2675,6 +3222,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2675
3222
|
reason: 'legacy_pre_turn',
|
|
2676
3223
|
opts,
|
|
2677
3224
|
});
|
|
3225
|
+
compactNoProgressState(turn, 'legacy_pre_turn');
|
|
2678
3226
|
|
|
2679
3227
|
// Graceful max-steps degradation (6n)
|
|
2680
3228
|
// Note: warnings are appended to the LAST message's content (not as separate
|
|
@@ -2705,6 +3253,8 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2705
3253
|
|
|
2706
3254
|
let response;
|
|
2707
3255
|
try {
|
|
3256
|
+
const requestKeepAlive = llmCtx.params.keepAlive ?? ollamaKeepAliveForRun(opts);
|
|
3257
|
+
const llmStartedAt = Date.now();
|
|
2708
3258
|
response = await llm.chat({
|
|
2709
3259
|
model: modelId,
|
|
2710
3260
|
system: llmCtx.system,
|
|
@@ -2716,9 +3266,21 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2716
3266
|
thinking: llmCtx.params.thinking,
|
|
2717
3267
|
reasoningEffort: llmCtx.params.reasoningEffort,
|
|
2718
3268
|
options: llmCtx.params.options,
|
|
3269
|
+
keepAlive: requestKeepAlive,
|
|
2719
3270
|
promptCache: true,
|
|
2720
3271
|
signal: ac.signal,
|
|
2721
3272
|
});
|
|
3273
|
+
emitOllamaTurnTiming({
|
|
3274
|
+
emitProgress,
|
|
3275
|
+
provider: llm,
|
|
3276
|
+
model: modelId,
|
|
3277
|
+
phase: opts.mode || 'executing',
|
|
3278
|
+
step: turn,
|
|
3279
|
+
startedAt: llmStartedAt,
|
|
3280
|
+
usage: response.usage,
|
|
3281
|
+
params: { ...llmCtx.params, keepAlive: requestKeepAlive },
|
|
3282
|
+
toolsCount: adaptedTools.length,
|
|
3283
|
+
});
|
|
2722
3284
|
} finally {
|
|
2723
3285
|
clearTimeout(timer);
|
|
2724
3286
|
unlinkExternalAbort();
|
|
@@ -2749,23 +3311,20 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2749
3311
|
totalOutput += response.usage.output || 0;
|
|
2750
3312
|
}
|
|
2751
3313
|
|
|
2752
|
-
// Per-step cost tracking (6q)
|
|
3314
|
+
// Per-step cost tracking (6q): centralized pricing (cache-discounted) via
|
|
3315
|
+
// lib/usage so coding + chat agree; also persist to the queryable ledger and
|
|
3316
|
+
// push a live token/cost snapshot to watching clients.
|
|
2753
3317
|
if (response.usage) {
|
|
2754
3318
|
const inputTokens = response.usage.input || 0;
|
|
2755
3319
|
const outputTokens = response.usage.output || 0;
|
|
2756
|
-
// Cache hits cost 0.1x input price, cache writes 1.25x (Anthropic).
|
|
2757
3320
|
const cacheRead = response.usage.cacheRead || 0;
|
|
2758
3321
|
const cacheWrite = response.usage.cacheWrite || 0;
|
|
2759
|
-
const
|
|
2760
|
-
+ cacheRead * 0.1 + cacheWrite * 1.25;
|
|
2761
|
-
// Cost estimate: rough pricing per 1M tokens
|
|
2762
|
-
const costPer1M = {
|
|
2763
|
-
input: modelId.includes('haiku') ? 0.25 : modelId.includes('sonnet') ? 3.0 : 15.0,
|
|
2764
|
-
output: modelId.includes('haiku') ? 1.25 : modelId.includes('sonnet') ? 15.0 : 75.0,
|
|
2765
|
-
};
|
|
2766
|
-
const turnCost = (effectiveInput * costPer1M.input + outputTokens * costPer1M.output) / 1_000_000;
|
|
3322
|
+
const turnCost = usageUtil.costFor(response.usage, modelId, llm?.type || '');
|
|
2767
3323
|
turnCosts.push({ turn, inputTokens, outputTokens, cacheRead, cacheWrite, cost: turnCost });
|
|
2768
3324
|
budgetUsed += turnCost;
|
|
3325
|
+
totalCostUsd += turnCost;
|
|
3326
|
+
recordTurnUsageLedger(response.raw?.usage || response.usage, turn);
|
|
3327
|
+
emitUsageSnapshot(response.usage);
|
|
2769
3328
|
if (opts.budgetUsd && budgetUsed > opts.budgetUsd) {
|
|
2770
3329
|
finalOutput += '\n[Budget exceeded]';
|
|
2771
3330
|
break;
|
|
@@ -2841,18 +3400,42 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2841
3400
|
detail: { tool: tc.name, input: tc.input },
|
|
2842
3401
|
});
|
|
2843
3402
|
|
|
2844
|
-
const
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3403
|
+
const guardBlock = noProgressGuard.check(tc, { cwd: resolvedCwd });
|
|
3404
|
+
let execution;
|
|
3405
|
+
if (guardBlock) {
|
|
3406
|
+
execution = buildBlockedToolExecution(tc, guardBlock);
|
|
3407
|
+
emitProgress({
|
|
3408
|
+
phase: opts.mode || 'executing',
|
|
3409
|
+
step: turn,
|
|
3410
|
+
message: `No-progress guard blocked repeated ${tc.name}`,
|
|
3411
|
+
detail: { tool: tc.name, target: guardBlock.target, repeatCount: guardBlock.repeatCount },
|
|
3412
|
+
});
|
|
3413
|
+
events.emit('no_progress.detected', {
|
|
3414
|
+
tool: tc.name,
|
|
3415
|
+
target: guardBlock.target,
|
|
3416
|
+
repeatCount: guardBlock.repeatCount,
|
|
3417
|
+
threshold: guardBlock.threshold,
|
|
3418
|
+
sessionId: sid,
|
|
3419
|
+
});
|
|
3420
|
+
} else {
|
|
3421
|
+
execution = await executeWithActionMemoryPolicy({
|
|
3422
|
+
call: tc,
|
|
3423
|
+
context: actionMemoryContextForCall(tc),
|
|
3424
|
+
execute: (preparedCall) => toolExecutionController.execute(preparedCall, {
|
|
3425
|
+
sessionId: sid,
|
|
3426
|
+
cwd: resolvedCwd,
|
|
3427
|
+
projectRoot: resolvedCwd,
|
|
3428
|
+
model: modelId,
|
|
3429
|
+
provider: llm.type,
|
|
3430
|
+
mode: opts.mode || '',
|
|
3431
|
+
runtimeMode: runtimeMode.id,
|
|
3432
|
+
llmCtx,
|
|
3433
|
+
interactive: opts.interactive,
|
|
3434
|
+
onTodos: (todos) => { currentTodos = todos; },
|
|
3435
|
+
}),
|
|
3436
|
+
});
|
|
3437
|
+
emitActionMemoryDecision(execution, turn);
|
|
3438
|
+
}
|
|
2856
3439
|
const result = execution.result;
|
|
2857
3440
|
const evidenceInput = execution.evidenceInput || tc.input || {};
|
|
2858
3441
|
if (!execution.ok) turnHadError = true;
|
|
@@ -2883,11 +3466,19 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2883
3466
|
detail: { tool: tc.name, resultLength: resultStr.length },
|
|
2884
3467
|
});
|
|
2885
3468
|
|
|
2886
|
-
const
|
|
3469
|
+
const evidence = normalizeToolCallEvidence({ name: tc.name, input: evidenceInput }, result);
|
|
3470
|
+
const guardRecord = noProgressGuard.record({ name: tc.name, input: evidenceInput }, {
|
|
3471
|
+
ok: execution.ok,
|
|
3472
|
+
result,
|
|
3473
|
+
blocked: execution.blocked,
|
|
3474
|
+
cwd: resolvedCwd,
|
|
3475
|
+
});
|
|
3476
|
+
const guardNudge = buildNoProgressNudge(guardRecord);
|
|
3477
|
+
let capped = truncated.content;
|
|
3478
|
+
if (guardNudge) capped += guardNudge;
|
|
2887
3479
|
toolResults.push({ type: 'tool_result', tool_use_id: tc.id, content: capped });
|
|
2888
3480
|
|
|
2889
3481
|
log[log.length - 1].toolResults = log[log.length - 1].toolResults || [];
|
|
2890
|
-
const evidence = normalizeToolCallEvidence({ name: tc.name, input: evidenceInput }, result);
|
|
2891
3482
|
log[log.length - 1].toolResults.push({
|
|
2892
3483
|
name: tc.name,
|
|
2893
3484
|
resultLength: resultStr.length,
|
|
@@ -2905,7 +3496,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2905
3496
|
if (recent.every(c => c.name === recent[0].name && c.inputHash === recent[0].inputHash)) {
|
|
2906
3497
|
// Progressive recovery (Phase 6): suggest alternative tools before forcing wrap-up
|
|
2907
3498
|
const altTools = {
|
|
2908
|
-
read_file: '
|
|
3499
|
+
read_file: 'edit_file now if the needed location is visible; do not call read_file or list_directory again for the same target',
|
|
2909
3500
|
edit_file: 'write_file or apply_patch',
|
|
2910
3501
|
run_shell: 'read the error output carefully and try a different command',
|
|
2911
3502
|
grep_files: 'glob or read_file on a specific path',
|
|
@@ -2915,6 +3506,10 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2915
3506
|
`${DOOM_LOOP_THRESHOLD} times. Try ${suggestion}, or use ask_user if you're stuck.`;
|
|
2916
3507
|
const lastResult = toolResults[toolResults.length - 1];
|
|
2917
3508
|
if (lastResult) lastResult.content += doomWarning;
|
|
3509
|
+
if (lastResult && recent[0].name === 'read_file') {
|
|
3510
|
+
const readPath = recent[0].input?.file_path || recent[0].input?.path || 'the file you just read';
|
|
3511
|
+
lastResult.content += `\n[SYSTEM] Next action contract for local models: if the requested change belongs in ${readPath}, call edit_file with a non-identical old_string/new_string. Re-reading or listing is no progress.`;
|
|
3512
|
+
}
|
|
2918
3513
|
// Force wrap-up: cap remaining turns
|
|
2919
3514
|
const turnsLeft = turns - turn;
|
|
2920
3515
|
if (turnsLeft > 3) turns = turn + 3;
|
|
@@ -2992,7 +3587,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
2992
3587
|
chatSessionId: opts.chatSessionId || '',
|
|
2993
3588
|
model: modelId,
|
|
2994
3589
|
provider: llm?.type || '',
|
|
2995
|
-
|
|
3590
|
+
...transcriptUsageFields(),
|
|
2996
3591
|
});
|
|
2997
3592
|
}
|
|
2998
3593
|
|
|
@@ -3051,6 +3646,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
3051
3646
|
exitCode: -1,
|
|
3052
3647
|
log,
|
|
3053
3648
|
usage: { input: totalInput, output: totalOutput },
|
|
3649
|
+
costUsd: totalCostUsd,
|
|
3054
3650
|
provider: llm?.type,
|
|
3055
3651
|
model: modelId,
|
|
3056
3652
|
runtimeMode: runtimeMode.id,
|
|
@@ -3102,7 +3698,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
3102
3698
|
chatSessionId: opts.chatSessionId || '',
|
|
3103
3699
|
model: modelId,
|
|
3104
3700
|
provider: llm?.type || '',
|
|
3105
|
-
|
|
3701
|
+
...transcriptUsageFields(),
|
|
3106
3702
|
});
|
|
3107
3703
|
}
|
|
3108
3704
|
|
|
@@ -3122,6 +3718,7 @@ async function runAgentLoop(prompt, opts = {}) {
|
|
|
3122
3718
|
exitCode: 0,
|
|
3123
3719
|
log,
|
|
3124
3720
|
usage: { input: totalInput, output: totalOutput },
|
|
3721
|
+
costUsd: totalCostUsd,
|
|
3125
3722
|
provider: llm?.type,
|
|
3126
3723
|
model: modelId,
|
|
3127
3724
|
runtimeMode: runtimeMode.id,
|
|
@@ -3263,6 +3860,7 @@ async function runPlannerPrompt(prompt, {
|
|
|
3263
3860
|
thinking,
|
|
3264
3861
|
reasoningEffort,
|
|
3265
3862
|
options,
|
|
3863
|
+
keepAlive,
|
|
3266
3864
|
temperature,
|
|
3267
3865
|
maxTokens,
|
|
3268
3866
|
onProgress,
|
|
@@ -3287,6 +3885,7 @@ async function runPlannerPrompt(prompt, {
|
|
|
3287
3885
|
thinking,
|
|
3288
3886
|
reasoningEffort,
|
|
3289
3887
|
options,
|
|
3888
|
+
keepAlive,
|
|
3290
3889
|
temperature: temperature ?? 0,
|
|
3291
3890
|
}, maxTokens || 4096);
|
|
3292
3891
|
const response = await llm.chat({
|
|
@@ -3300,6 +3899,7 @@ async function runPlannerPrompt(prompt, {
|
|
|
3300
3899
|
thinking: params.thinking,
|
|
3301
3900
|
reasoningEffort: params.reasoningEffort,
|
|
3302
3901
|
options: params.options,
|
|
3902
|
+
keepAlive: params.keepAlive ?? ollamaKeepAliveForRun(keepAlive !== undefined ? { keepAlive } : {}),
|
|
3303
3903
|
signal: ac.signal,
|
|
3304
3904
|
});
|
|
3305
3905
|
const output = contentToText(response?.content || '');
|
|
@@ -4439,6 +5039,7 @@ module.exports = {
|
|
|
4439
5039
|
isTimeoutOnlyOutput,
|
|
4440
5040
|
resolveRunTimeoutMs,
|
|
4441
5041
|
isInteractiveRun,
|
|
5042
|
+
shouldExtendInteractiveBudget,
|
|
4442
5043
|
isActionRequiredPrompt,
|
|
4443
5044
|
isPrematureActionResponse,
|
|
4444
5045
|
getNoActionContinuation,
|