neoagent 2.5.2-beta.8 → 3.0.0
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 +5 -1
- package/docs/integrations.md +11 -7
- package/package.json +2 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/ai/deliverables/artifact_helpers.js +108 -16
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/engine.js +2 -4724
- package/server/services/ai/loop/agent_engine_core.js +1590 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/callbacks.js +151 -0
- package/server/services/ai/loop/completion_judge.js +252 -0
- package/server/services/ai/loop/conversation_loop.js +2447 -0
- package/server/services/ai/loop/delivery_state.js +27 -0
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/iteration_budget.js +24 -0
- package/server/services/ai/loop/messaging_delivery.js +296 -0
- package/server/services/ai/loop/model_io.js +258 -0
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +81 -0
- package/server/services/ai/loop/run_state.js +356 -0
- package/server/services/ai/loop/tool_dispatch.js +231 -0
- package/server/services/ai/loopPolicy.js +15 -6
- package/server/services/ai/messagingFallback.js +23 -1
- package/server/services/ai/preModelCompaction.js +31 -0
- package/server/services/ai/repetitionGuard.js +47 -2
- package/server/services/ai/systemPrompt.js +6 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +15 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +196 -26
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/workspace/manager.js +56 -0
|
@@ -0,0 +1,2447 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const db = require('../../../db/database');
|
|
6
|
+
const { compact } = require('../compaction');
|
|
7
|
+
const { compactPayloadForModel } = require('../preModelCompaction');
|
|
8
|
+
const {
|
|
9
|
+
getConversationContext,
|
|
10
|
+
buildSummaryCarrier,
|
|
11
|
+
refreshConversationSummary,
|
|
12
|
+
sanitizeConversationMessages
|
|
13
|
+
} = require('../history');
|
|
14
|
+
const { ensureDefaultAiSettings, getAiSettings } = require('../settings');
|
|
15
|
+
const {
|
|
16
|
+
buildToolCatalog,
|
|
17
|
+
selectInitialTools,
|
|
18
|
+
selectToolsForTask,
|
|
19
|
+
} = require('../toolSelector');
|
|
20
|
+
const { compactToolResult } = require('../toolResult');
|
|
21
|
+
const { salvageTextToolCalls } = require('../toolCallSalvage');
|
|
22
|
+
const { sanitizeModelOutput } = require('../outputSanitizer');
|
|
23
|
+
const {
|
|
24
|
+
buildAnalysisPrompt,
|
|
25
|
+
buildExecutionGuidance,
|
|
26
|
+
buildPlanPrompt,
|
|
27
|
+
buildVerifierPrompt,
|
|
28
|
+
isDirectAnswerEligibleAnalysis,
|
|
29
|
+
normalizeExecutionPlan,
|
|
30
|
+
normalizeTaskAnalysis,
|
|
31
|
+
normalizeVerificationResult,
|
|
32
|
+
parseJsonObject,
|
|
33
|
+
promoteAnalysisMode,
|
|
34
|
+
shouldRunVerifier,
|
|
35
|
+
} = require('../taskAnalysis');
|
|
36
|
+
const { getCapabilityHealth, summarizeCapabilityHealth } = require('../capabilityHealth');
|
|
37
|
+
const {
|
|
38
|
+
buildPlatformFormattingGuide,
|
|
39
|
+
} = require('../../messaging/formatting_guides');
|
|
40
|
+
const {
|
|
41
|
+
buildInterimSignature,
|
|
42
|
+
normalizeInterimKind,
|
|
43
|
+
} = require('../interim');
|
|
44
|
+
const {
|
|
45
|
+
buildDeliverableWorkflowGuidance,
|
|
46
|
+
DeliverableValidationError,
|
|
47
|
+
extractArtifactsFromResult,
|
|
48
|
+
getDeliverableWorkflow,
|
|
49
|
+
selectDeliverableWorkflow,
|
|
50
|
+
validateDeliverableExecution,
|
|
51
|
+
} = require('../deliverables');
|
|
52
|
+
const { buildLoopPolicy, resolveToolResultLimits } = require('../loopPolicy');
|
|
53
|
+
const { globalHooks } = require('../hooks');
|
|
54
|
+
const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('../completion');
|
|
55
|
+
const { enforceRateLimits } = require('../rate_limits');
|
|
56
|
+
const { ToolRepetitionGuard } = require('../repetitionGuard');
|
|
57
|
+
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
58
|
+
const { IterationBudget } = require('./iteration_budget');
|
|
59
|
+
const {
|
|
60
|
+
buildBlankAfterToolFailureGuidance,
|
|
61
|
+
shouldContinueAfterBlankToolFailure,
|
|
62
|
+
} = require('./blank_recovery');
|
|
63
|
+
const {
|
|
64
|
+
shouldSendMessagingErrorFallback,
|
|
65
|
+
} = require('./error_recovery');
|
|
66
|
+
const {
|
|
67
|
+
buildCompletionDecisionPrompt,
|
|
68
|
+
buildGoalContractPrompt,
|
|
69
|
+
goalContractFromAnalysis,
|
|
70
|
+
goalContractFromPlan,
|
|
71
|
+
mergeGoalContracts,
|
|
72
|
+
normalizeCompletionDecision,
|
|
73
|
+
normalizeGoalContract,
|
|
74
|
+
resolveRunGoalContext,
|
|
75
|
+
} = require('./completion_judge');
|
|
76
|
+
const {
|
|
77
|
+
activateToolsForRun: activateToolsForRunImpl,
|
|
78
|
+
applyQueuedSteering: applyQueuedSteeringImpl,
|
|
79
|
+
applyQueuedSystemSteering: applyQueuedSystemSteeringImpl,
|
|
80
|
+
attachProcessToRun: attachProcessToRunImpl,
|
|
81
|
+
buildProgressLedgerSnapshot: buildProgressLedgerSnapshotImpl,
|
|
82
|
+
detachProcessFromRun: detachProcessFromRunImpl,
|
|
83
|
+
enqueueSteering: enqueueSteeringImpl,
|
|
84
|
+
enqueueSystemSteering: enqueueSystemSteeringImpl,
|
|
85
|
+
findActiveRunForUser: findActiveRunForUserImpl,
|
|
86
|
+
findSteerableRunForUser: findSteerableRunForUserImpl,
|
|
87
|
+
getActiveTools: getActiveToolsImpl,
|
|
88
|
+
initializeToolRuntime: initializeToolRuntimeImpl,
|
|
89
|
+
isRunStopped: isRunStoppedImpl,
|
|
90
|
+
markRunFinalDelivery: markRunFinalDeliveryImpl,
|
|
91
|
+
markRunVisibleProgress: markRunVisibleProgressImpl,
|
|
92
|
+
persistProgressLedger: persistProgressLedgerImpl,
|
|
93
|
+
persistRunMetadata: persistRunMetadataImpl,
|
|
94
|
+
recordRunEventSafe,
|
|
95
|
+
updateRunGoalContract: updateRunGoalContractImpl,
|
|
96
|
+
updateRunProgress: updateRunProgressImpl,
|
|
97
|
+
} = require('./run_state');
|
|
98
|
+
const {
|
|
99
|
+
buildInitialProgressLedger,
|
|
100
|
+
} = require('./progress_monitor');
|
|
101
|
+
const {
|
|
102
|
+
deliverMessagingFinalFallback: deliverMessagingFinalFallbackImpl,
|
|
103
|
+
requireSuccessfulMessagingDelivery,
|
|
104
|
+
sendRuntimeMessagingHeartbeat: sendRuntimeMessagingHeartbeatImpl,
|
|
105
|
+
shouldSendMessagingFinalFallback: shouldSendMessagingFinalFallbackImpl,
|
|
106
|
+
startMessagingProgressSupervisor: startMessagingProgressSupervisorImpl,
|
|
107
|
+
stopMessagingProgressSupervisor: stopMessagingProgressSupervisorImpl,
|
|
108
|
+
tickMessagingProgressSupervisor: tickMessagingProgressSupervisorImpl,
|
|
109
|
+
} = require('./messaging_delivery');
|
|
110
|
+
const {
|
|
111
|
+
createDeliveryState,
|
|
112
|
+
} = require('./delivery_state');
|
|
113
|
+
const {
|
|
114
|
+
requestModelResponse: requestModelResponseImpl,
|
|
115
|
+
requestStructuredJson: requestStructuredJsonImpl,
|
|
116
|
+
withModelCallTimeout,
|
|
117
|
+
} = require('./model_io');
|
|
118
|
+
const {
|
|
119
|
+
publishInterimUpdate: publishInterimUpdateImpl,
|
|
120
|
+
} = require('./callbacks');
|
|
121
|
+
const {
|
|
122
|
+
executeReadOnlyBatch: executeReadOnlyBatchImpl,
|
|
123
|
+
executeTool: executeToolImpl,
|
|
124
|
+
getAvailableTools: getAvailableToolsImpl,
|
|
125
|
+
isReadOnlyToolCall: isReadOnlyToolCallImpl,
|
|
126
|
+
} = require('./tool_dispatch');
|
|
127
|
+
const {
|
|
128
|
+
isProgressToolCall,
|
|
129
|
+
} = require('./progress_classification');
|
|
130
|
+
const {
|
|
131
|
+
normalizeOutgoingMessage,
|
|
132
|
+
clampRunContext,
|
|
133
|
+
joinSentMessages,
|
|
134
|
+
buildBlankMessagingReplyPrompt,
|
|
135
|
+
buildMaxIterationWrapupPrompt,
|
|
136
|
+
buildProgressUpdatePrompt,
|
|
137
|
+
buildDeterministicMessagingFallback,
|
|
138
|
+
buildMessagingFailureScenario,
|
|
139
|
+
buildDeterministicMessagingErrorReply,
|
|
140
|
+
buildModelFailureLoopPrompt,
|
|
141
|
+
} = require('../messagingFallback');
|
|
142
|
+
const {
|
|
143
|
+
classifyToolExecution,
|
|
144
|
+
summarizeToolExecutions,
|
|
145
|
+
summarizeAvailableTools,
|
|
146
|
+
inferToolFailureMessage,
|
|
147
|
+
} = require('../toolEvidence');
|
|
148
|
+
const {
|
|
149
|
+
buildMemoryConsolidationInstructions,
|
|
150
|
+
normalizeMemoryCandidates,
|
|
151
|
+
} = require('../../memory/consolidation');
|
|
152
|
+
const {
|
|
153
|
+
buildPlannerPrompt,
|
|
154
|
+
buildRerankerPrompt,
|
|
155
|
+
mergeRetrievalResults,
|
|
156
|
+
normalizeRerankResult,
|
|
157
|
+
normalizeRetrievalPlan,
|
|
158
|
+
shouldEnhanceRetrieval,
|
|
159
|
+
} = require('../../memory/retrieval_reasoning');
|
|
160
|
+
|
|
161
|
+
function generateTitle(task) {
|
|
162
|
+
if (!task || typeof task !== 'string') return 'Untitled';
|
|
163
|
+
const msgMatch = task.match(/received a (?:message|media|image|video|file|audio)[^:]*:\s*(.+)/is);
|
|
164
|
+
if (msgMatch) {
|
|
165
|
+
const body = msgMatch[1].replace(/\n[\s\S]*/s, '').trim();
|
|
166
|
+
return body.slice(0, 90) || 'Incoming message';
|
|
167
|
+
}
|
|
168
|
+
const cleaned = task.replace(/^\[.*?\]\s*/i, '').replace(/^(system|task|prompt)[:\s]+/i, '').trim();
|
|
169
|
+
return cleaned.slice(0, 90);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildInitialRunMetadata(options = {}) {
|
|
173
|
+
const metadata = {};
|
|
174
|
+
if (options.taskId != null && String(options.taskId).trim()) {
|
|
175
|
+
metadata.taskId = options.taskId;
|
|
176
|
+
}
|
|
177
|
+
if (options.widgetId != null && String(options.widgetId).trim()) {
|
|
178
|
+
metadata.widgetId = options.widgetId;
|
|
179
|
+
}
|
|
180
|
+
return metadata;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isoNow() {
|
|
184
|
+
return new Date().toISOString();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeErrorKey(errorMsg) {
|
|
188
|
+
const msg = String(errorMsg || '').toLowerCase();
|
|
189
|
+
if (/outside.*(workspace|per-user)/i.test(msg)) return 'outside_workspace';
|
|
190
|
+
if (/eisdir|illegal operation on a directory/i.test(msg)) return 'eisdir';
|
|
191
|
+
if (/enoent|no such file/i.test(msg)) return 'enoent';
|
|
192
|
+
if (/can.?t cd to|no such directory/i.test(msg)) return 'bad_cwd';
|
|
193
|
+
if (/not found/i.test(msg)) return 'not_found';
|
|
194
|
+
if (/owner_repo.*format|must be.*owner.*repo|owner.*repo.*string|owner.*repo.*combined/i.test(msg)) return 'owner_repo_format';
|
|
195
|
+
return msg.slice(0, 60);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function trackErrorPattern(errorMsg, runMeta) {
|
|
199
|
+
if (!errorMsg) return;
|
|
200
|
+
const key = normalizeErrorKey(errorMsg);
|
|
201
|
+
if (!runMeta.errorPatterns) runMeta.errorPatterns = new Map();
|
|
202
|
+
runMeta.errorPatterns.set(key, (runMeta.errorPatterns.get(key) || 0) + 1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildErrorPatternGuidance(key, count) {
|
|
206
|
+
// Immediate guidance on first occurrence for high-signal patterns that waste
|
|
207
|
+
// multiple iterations before self-correcting.
|
|
208
|
+
const immediateGuides = {
|
|
209
|
+
eisdir: 'That path is a directory or outside the workspace file-tool boundary. Use list_directory for workspace directories. Keep source files in the shared workspace before reading them with file tools.',
|
|
210
|
+
owner_repo_format: 'The parameter "owner_repo" expects a single combined string like "NeoLabs-Systems/NeoAgent" — not separate owner/repo fields. Pass the full "owner/repo" as one value.',
|
|
211
|
+
};
|
|
212
|
+
if (immediateGuides[key]) {
|
|
213
|
+
const prefix = count > 1 ? `REPEATED ERROR (${count}×): ` : 'ERROR GUIDANCE: ';
|
|
214
|
+
return `${prefix}${immediateGuides[key]}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (count < 3) return null;
|
|
218
|
+
const guides = {
|
|
219
|
+
outside_workspace: 'read_file/read_files only access the shared workspace. Put the relevant files there, then use read_files/search_files/edit_file instead of repeatedly extracting snippets through shell commands.',
|
|
220
|
+
enoent: 'That path does not exist. Use execute_command with `find . -name "..."` to locate the correct path first.',
|
|
221
|
+
bad_cwd: 'The VM home directory is not ~/. Discover the workspace root with pwd/list_directory and keep working files there so file tools can inspect them.',
|
|
222
|
+
not_found: 'This path or resource was not found. Try listing the parent directory or checking with a broader search first.',
|
|
223
|
+
};
|
|
224
|
+
const guide = guides[key];
|
|
225
|
+
if (!guide) return null;
|
|
226
|
+
return `REPEATED ERROR (${count}×): ${guide}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const OUTPUT_FINGERPRINT_TOOLS = /^(list_|search_|read_|get_|find_|github_list|github_get|github_search)/;
|
|
230
|
+
|
|
231
|
+
function fingerprintOutput(toolName, result, toolArgs = {}) {
|
|
232
|
+
const name = String(toolName || '');
|
|
233
|
+
if (
|
|
234
|
+
!name
|
|
235
|
+
|| (
|
|
236
|
+
!OUTPUT_FINGERPRINT_TOOLS.test(name)
|
|
237
|
+
&& !(name === 'execute_command' && !isProgressToolCall(name, toolArgs))
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const raw = typeof result === 'string' ? result : JSON.stringify(result ?? '');
|
|
243
|
+
if (raw.length < 200) return null;
|
|
244
|
+
// djb2 hash over first 3000 chars — fast, collision-unlikely for our sizes
|
|
245
|
+
let h = 5381;
|
|
246
|
+
const limit = Math.min(raw.length, 3000);
|
|
247
|
+
for (let i = 0; i < limit; i++) h = ((h << 5) + h) ^ raw.charCodeAt(i);
|
|
248
|
+
return h >>> 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Concise list of files/targets the run has already read or searched, so the
|
|
252
|
+
// analysis-paralysis nudge can name them and tell the model not to re-read them.
|
|
253
|
+
function summarizeReadTargets(toolExecutions = []) {
|
|
254
|
+
const targets = [];
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
for (const item of toolExecutions) {
|
|
257
|
+
if (!item || item.stateChanged) continue; // only read-only steps
|
|
258
|
+
const input = item.input || {};
|
|
259
|
+
let target = '';
|
|
260
|
+
if (Array.isArray(input.files) && input.files.length) {
|
|
261
|
+
target = input.files
|
|
262
|
+
.map((file) => (typeof file === 'string' ? file : file?.path || file?.file_path || ''))
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.slice(0, 3)
|
|
265
|
+
.join(', ');
|
|
266
|
+
} else if (typeof input.path === 'string' && input.path.trim()) {
|
|
267
|
+
target = input.path.trim();
|
|
268
|
+
} else if (typeof input.command === 'string') {
|
|
269
|
+
const files = input.command.match(/[\w./-]+\.(?:js|ts|tsx|jsx|py|dart|json|md|kt|c|h|ya?ml|sql|txt|sh)\b/g);
|
|
270
|
+
if (files && files.length) target = [...new Set(files)].slice(0, 2).join(', ');
|
|
271
|
+
} else if (typeof input.query === 'string' && input.query.trim()) {
|
|
272
|
+
target = `search:${input.query.trim().slice(0, 30)}`;
|
|
273
|
+
}
|
|
274
|
+
if (!target) continue;
|
|
275
|
+
const key = target.toLowerCase();
|
|
276
|
+
if (seen.has(key)) continue;
|
|
277
|
+
seen.add(key);
|
|
278
|
+
targets.push(target);
|
|
279
|
+
if (targets.length >= 8) break;
|
|
280
|
+
}
|
|
281
|
+
return targets.join('; ');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function cloneInterimHistory(history = []) {
|
|
285
|
+
if (!Array.isArray(history)) return [];
|
|
286
|
+
return history.map((item) => ({
|
|
287
|
+
content: String(item?.content || '').trim(),
|
|
288
|
+
kind: normalizeInterimKind(item?.kind),
|
|
289
|
+
expectsReply: item?.expectsReply === true,
|
|
290
|
+
deferFollowUp: item?.deferFollowUp === true,
|
|
291
|
+
createdAt: item?.createdAt || isoNow(),
|
|
292
|
+
})).filter((item) => item.content);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createInterimSignatureSet(history = [], platform = null) {
|
|
296
|
+
const signatures = new Set();
|
|
297
|
+
for (const item of cloneInterimHistory(history)) {
|
|
298
|
+
signatures.add(buildInterimSignature({
|
|
299
|
+
content: item.content,
|
|
300
|
+
kind: item.kind,
|
|
301
|
+
expectsReply: item.expectsReply === true,
|
|
302
|
+
platform,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
return signatures;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasVisibleInterimActivity(runMeta) {
|
|
309
|
+
return Boolean(
|
|
310
|
+
runMeta?.lastInterimMessage
|
|
311
|
+
|| (Array.isArray(runMeta?.interimMessages) && runMeta.interimMessages.length > 0)
|
|
312
|
+
|| Number(runMeta?.progressLedger?.heartbeatCount || 0) > 0
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function planningDepthForForceMode(forceMode) {
|
|
317
|
+
return forceMode === 'plan_execute' ? 'deep' : 'light';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildSkipTaskAnalysisResult(forceMode) {
|
|
321
|
+
return {
|
|
322
|
+
mode: forceMode === 'plan_execute' ? 'plan_execute' : 'execute',
|
|
323
|
+
reply_mode: 'task',
|
|
324
|
+
freshness_risk: 'none',
|
|
325
|
+
verification_need: 'none',
|
|
326
|
+
planning_depth: planningDepthForForceMode(forceMode),
|
|
327
|
+
confidence: 0.5,
|
|
328
|
+
suggested_tools: [],
|
|
329
|
+
needs_subagents: false,
|
|
330
|
+
draft_reply: '',
|
|
331
|
+
goal: 'Complete the user request accurately.',
|
|
332
|
+
success_criteria: [],
|
|
333
|
+
complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
|
|
334
|
+
autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
|
|
335
|
+
progress_update_policy: 'optional',
|
|
336
|
+
parallel_work: false,
|
|
337
|
+
completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
|
|
342
|
+
return {
|
|
343
|
+
mode: forceMode || 'execute',
|
|
344
|
+
verification_need: 'light',
|
|
345
|
+
planning_depth: planningDepthForForceMode(forceMode),
|
|
346
|
+
goal: userMessage ? String(userMessage).trim().slice(0, 300) : '',
|
|
347
|
+
complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
|
|
348
|
+
autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
|
|
349
|
+
progress_update_policy: 'optional',
|
|
350
|
+
parallel_work: false,
|
|
351
|
+
completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function applyForcedAnalysisMode(analysis, forceMode) {
|
|
356
|
+
if (!analysis || typeof analysis !== 'object') return analysis;
|
|
357
|
+
if (forceMode !== 'plan_execute') return analysis;
|
|
358
|
+
return {
|
|
359
|
+
...analysis,
|
|
360
|
+
mode: 'plan_execute',
|
|
361
|
+
planning_depth: 'deep',
|
|
362
|
+
complexity: 'complex',
|
|
363
|
+
autonomy_level: 'high',
|
|
364
|
+
completion_confidence_required: analysis.completion_confidence_required || 'high',
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildAutonomyPolicyFromAnalysis(analysis = {}) {
|
|
369
|
+
return {
|
|
370
|
+
complexity: analysis.complexity || 'standard',
|
|
371
|
+
autonomy_level: analysis.autonomy_level || 'normal',
|
|
372
|
+
progress_update_policy: analysis.progress_update_policy || 'optional',
|
|
373
|
+
parallel_work: analysis.parallel_work === true,
|
|
374
|
+
completion_confidence_required: analysis.completion_confidence_required || 'medium',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
|
|
379
|
+
const { getSupportedModels, createProviderInstance } = require('../models');
|
|
380
|
+
const agentId = providerConfig.agentId || null;
|
|
381
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
382
|
+
const models = await getSupportedModels(userId, agentId);
|
|
383
|
+
|
|
384
|
+
let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
|
|
385
|
+
const defaultChatModel = aiSettings.default_chat_model || 'auto';
|
|
386
|
+
const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
|
|
387
|
+
const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
|
|
388
|
+
|
|
389
|
+
const knownModelIds = new Set(models.map((m) => m.id));
|
|
390
|
+
const selectableModels = models.filter((m) => m.available !== false);
|
|
391
|
+
|
|
392
|
+
enabledIds = Array.isArray(enabledIds)
|
|
393
|
+
? enabledIds
|
|
394
|
+
.map((id) => String(id))
|
|
395
|
+
.filter((id) => knownModelIds.has(id))
|
|
396
|
+
: [];
|
|
397
|
+
|
|
398
|
+
let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
|
|
399
|
+
if (availableModels.length === 0) {
|
|
400
|
+
enabledIds = selectableModels.map((m) => m.id);
|
|
401
|
+
availableModels = [...selectableModels];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
|
|
405
|
+
|
|
406
|
+
if (!fallbackModel) {
|
|
407
|
+
throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let selectedModelDef = fallbackModel;
|
|
411
|
+
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
412
|
+
|
|
413
|
+
if (modelOverride && typeof modelOverride === 'string') {
|
|
414
|
+
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
415
|
+
if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
|
|
416
|
+
selectedModelDef = requested;
|
|
417
|
+
return {
|
|
418
|
+
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
419
|
+
model: selectedModelDef.id,
|
|
420
|
+
providerName: selectedModelDef.provider
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
426
|
+
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
427
|
+
} else {
|
|
428
|
+
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
429
|
+
? providerConfig.selectionHint
|
|
430
|
+
: {};
|
|
431
|
+
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
432
|
+
const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
|
|
433
|
+
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
434
|
+
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
|
|
435
|
+
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
436
|
+
? preferredPurpose
|
|
437
|
+
: '';
|
|
438
|
+
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
439
|
+
const chooseForPurpose = (purpose) => {
|
|
440
|
+
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
441
|
+
if (candidates.length === 0) return null;
|
|
442
|
+
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
443
|
+
return [...candidates].sort((left, right) => (
|
|
444
|
+
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
445
|
+
))[0];
|
|
446
|
+
}
|
|
447
|
+
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
448
|
+
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
|
|
449
|
+
}
|
|
450
|
+
return candidates[0];
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
if (smarterSelection && requestedPurpose) {
|
|
454
|
+
selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
455
|
+
} else if (smarterSelection && highAutonomy) {
|
|
456
|
+
selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
457
|
+
} else if (isSubagent) {
|
|
458
|
+
selectedModelDef = chooseForPurpose('fast') || fallbackModel;
|
|
459
|
+
} else {
|
|
460
|
+
selectedModelDef = chooseForPurpose('general') || fallbackModel;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
466
|
+
model: selectedModelDef.id,
|
|
467
|
+
providerName: selectedModelDef.provider
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null, failureError = null) {
|
|
472
|
+
const { getSupportedModels } = require('../models');
|
|
473
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
474
|
+
const models = await getSupportedModels(userId, agentId);
|
|
475
|
+
const availableModels = models.filter((model) => model.available !== false);
|
|
476
|
+
const knownIds = new Set(availableModels.map((model) => model.id));
|
|
477
|
+
const enabledIds = Array.isArray(aiSettings.enabled_models)
|
|
478
|
+
? aiSettings.enabled_models.map((id) => String(id)).filter((id) => knownIds.has(id))
|
|
479
|
+
: [];
|
|
480
|
+
const pool = enabledIds.length > 0
|
|
481
|
+
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
482
|
+
: availableModels;
|
|
483
|
+
const currentModel = pool.find((model) => model.id === currentModelId)
|
|
484
|
+
|| availableModels.find((model) => model.id === currentModelId)
|
|
485
|
+
|| null;
|
|
486
|
+
|
|
487
|
+
// When the failure is a provider-level rate limit, the preferred fallback is
|
|
488
|
+
// likely on the same provider and will hit the same limit. Skip it and prefer
|
|
489
|
+
// a fallback from a different provider instead.
|
|
490
|
+
const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
|
|
491
|
+
|
|
492
|
+
if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
|
|
493
|
+
const preferred = pool.find((model) => model.id === preferredFallbackId)
|
|
494
|
+
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
495
|
+
if (preferred) return preferred.id;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (currentModel?.provider) {
|
|
499
|
+
const differentProvider = pool.find((model) => model.id !== currentModelId && model.provider !== currentModel.provider)
|
|
500
|
+
|| availableModels.find((model) => model.id !== currentModelId && model.provider !== currentModel.provider);
|
|
501
|
+
if (differentProvider) return differentProvider.id;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// If no different-provider model exists, still try the preferred fallback
|
|
505
|
+
// even on rate limits (it's better than nothing).
|
|
506
|
+
if (preferredFallbackId && preferredFallbackId !== currentModelId) {
|
|
507
|
+
const preferred = pool.find((model) => model.id === preferredFallbackId)
|
|
508
|
+
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
509
|
+
if (preferred) return preferred.id;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const differentModel = pool.find((model) => model.id !== currentModelId)
|
|
513
|
+
|| availableModels.find((model) => model.id !== currentModelId);
|
|
514
|
+
return differentModel?.id || null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function estimateTokenValue(value) {
|
|
518
|
+
if (!value) return 0;
|
|
519
|
+
if (typeof value === 'string') return Math.ceil(value.length / 4);
|
|
520
|
+
return Math.ceil(JSON.stringify(value).length / 4);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function runConversation(engine, userId, userMessage, options = {}, _modelOverride = null) {
|
|
524
|
+
const triggerType = options.triggerType || 'user';
|
|
525
|
+
const { resolveAgentId } = require('../../agents/manager');
|
|
526
|
+
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
527
|
+
ensureDefaultAiSettings(userId, agentId);
|
|
528
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
529
|
+
|
|
530
|
+
enforceRateLimits(userId);
|
|
531
|
+
|
|
532
|
+
const runId = options.runId || uuidv4();
|
|
533
|
+
const conversationId = options.conversationId;
|
|
534
|
+
const app = options.app || engine.app;
|
|
535
|
+
const triggerSource = options.triggerSource || 'web';
|
|
536
|
+
const historyWindow = Math.max(
|
|
537
|
+
1,
|
|
538
|
+
Number(options.historyWindow || aiSettings.chat_history_window) || aiSettings.chat_history_window,
|
|
539
|
+
);
|
|
540
|
+
// loopPolicy is built after task analysis so analysisMode can be passed in;
|
|
541
|
+
// we build a provisional policy now (with default mode) and rebuild after
|
|
542
|
+
// analysis when the mode is known. See the post-analysis policy rebuild below.
|
|
543
|
+
let loopPolicy = buildLoopPolicy(aiSettings, triggerType, 'execute', options);
|
|
544
|
+
let maxIterations = loopPolicy.maxIterations;
|
|
545
|
+
const providerStatusConfig = {
|
|
546
|
+
agentId,
|
|
547
|
+
onStatus: (status) => {
|
|
548
|
+
if (!status?.message) return;
|
|
549
|
+
engine.emit(userId, 'run:interim', {
|
|
550
|
+
runId,
|
|
551
|
+
message: status.message,
|
|
552
|
+
phase: status.phase
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const selectedProvider = await getProviderForUser(
|
|
557
|
+
userId,
|
|
558
|
+
userMessage,
|
|
559
|
+
triggerType === 'subagent',
|
|
560
|
+
_modelOverride,
|
|
561
|
+
providerStatusConfig
|
|
562
|
+
);
|
|
563
|
+
let provider = selectedProvider.provider;
|
|
564
|
+
let model = selectedProvider.model;
|
|
565
|
+
let providerName = selectedProvider.providerName;
|
|
566
|
+
const switchToFallbackModel = async (failedModel, error, phase) => {
|
|
567
|
+
const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
|
|
568
|
+
if (!fallbackModelId || fallbackModelId === failedModel) return false;
|
|
569
|
+
console.log(`[Engine] ${phase} failed on ${failedModel}; attempting fallback to: ${fallbackModelId}`);
|
|
570
|
+
engine.emit(userId, 'run:interim', {
|
|
571
|
+
runId,
|
|
572
|
+
message: `Model service failed on ${failedModel}; retrying with ${fallbackModelId}.`,
|
|
573
|
+
phase: 'model_fallback'
|
|
574
|
+
});
|
|
575
|
+
const fallback = await getProviderForUser(
|
|
576
|
+
userId,
|
|
577
|
+
userMessage,
|
|
578
|
+
triggerType === 'subagent',
|
|
579
|
+
fallbackModelId,
|
|
580
|
+
providerStatusConfig
|
|
581
|
+
);
|
|
582
|
+
provider = fallback.provider;
|
|
583
|
+
model = fallback.model;
|
|
584
|
+
providerName = fallback.providerName;
|
|
585
|
+
return true;
|
|
586
|
+
};
|
|
587
|
+
const runWithModelFallback = async (phase, fn) => {
|
|
588
|
+
try {
|
|
589
|
+
return await fn();
|
|
590
|
+
} catch (err) {
|
|
591
|
+
const failedModel = model;
|
|
592
|
+
const switched = await switchToFallbackModel(failedModel, err, phase);
|
|
593
|
+
if (!switched) throw err;
|
|
594
|
+
return await fn();
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const runTitle = generateTitle(userMessage);
|
|
599
|
+
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
600
|
+
db.prepare(`INSERT INTO agent_runs(
|
|
601
|
+
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
602
|
+
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
603
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
604
|
+
status = 'running',
|
|
605
|
+
model = excluded.model,
|
|
606
|
+
updated_at = datetime('now'),
|
|
607
|
+
completed_at = NULL,
|
|
608
|
+
error = NULL,
|
|
609
|
+
metadata_json = COALESCE(agent_runs.metadata_json, excluded.metadata_json)`).run(
|
|
610
|
+
runId,
|
|
611
|
+
userId,
|
|
612
|
+
agentId,
|
|
613
|
+
runTitle,
|
|
614
|
+
triggerType,
|
|
615
|
+
triggerSource,
|
|
616
|
+
model,
|
|
617
|
+
Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const retryMessagingState = options.messagingRetryState || {};
|
|
621
|
+
const carriedFinalMessage = String(retryMessagingState.lastFinalMessage || '').trim();
|
|
622
|
+
const carriedExplicitMessageSent = retryMessagingState.explicitMessageSent === true;
|
|
623
|
+
const carriedInterimHistory = cloneInterimHistory(retryMessagingState.interimHistory);
|
|
624
|
+
const carriedLastInterimMessage = carriedInterimHistory[carriedInterimHistory.length - 1]?.content || '';
|
|
625
|
+
const carriedGoalContract = mergeGoalContracts(
|
|
626
|
+
normalizeGoalContract({
|
|
627
|
+
goal: clampRunContext(userMessage, 1200),
|
|
628
|
+
}),
|
|
629
|
+
retryMessagingState.goalContract,
|
|
630
|
+
);
|
|
631
|
+
const startedAtIso = isoNow();
|
|
632
|
+
const progressLedger = buildInitialProgressLedger({
|
|
633
|
+
startedAt: startedAtIso,
|
|
634
|
+
retryState: retryMessagingState,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
engine.activeRuns.set(runId, {
|
|
638
|
+
userId,
|
|
639
|
+
agentId,
|
|
640
|
+
title: runTitle,
|
|
641
|
+
status: 'running',
|
|
642
|
+
aborted: false,
|
|
643
|
+
messagingSent: false,
|
|
644
|
+
noResponse: false,
|
|
645
|
+
explicitMessageSent: carriedExplicitMessageSent,
|
|
646
|
+
finalDeliverySent: carriedExplicitMessageSent,
|
|
647
|
+
lastSentMessage: carriedExplicitMessageSent ? carriedFinalMessage : '',
|
|
648
|
+
sentMessages: [],
|
|
649
|
+
widgetSnapshotSaved: false,
|
|
650
|
+
triggerType,
|
|
651
|
+
triggerSource,
|
|
652
|
+
startedAt: Date.now(),
|
|
653
|
+
startedAtIso,
|
|
654
|
+
lastToolName: null,
|
|
655
|
+
lastToolTarget: null,
|
|
656
|
+
lastInterimMessage: carriedExplicitMessageSent ? '' : carriedLastInterimMessage,
|
|
657
|
+
interimMessages: carriedExplicitMessageSent ? [] : carriedInterimHistory,
|
|
658
|
+
interimSignatures: carriedExplicitMessageSent
|
|
659
|
+
? new Set()
|
|
660
|
+
: createInterimSignatureSet(carriedInterimHistory, options.source || null),
|
|
661
|
+
terminalInterim: null,
|
|
662
|
+
voiceSessionId: options.voiceSessionId || null,
|
|
663
|
+
steeringQueue: [],
|
|
664
|
+
systemSteeringQueue: [],
|
|
665
|
+
toolPids: new Set(),
|
|
666
|
+
repetitionGuard: new ToolRepetitionGuard(),
|
|
667
|
+
seenOutputHashes: new Map(),
|
|
668
|
+
consecutiveReadOnlyIterations: 0,
|
|
669
|
+
messagingContext: triggerSource === 'messaging'
|
|
670
|
+
? {
|
|
671
|
+
platform: options.source || null,
|
|
672
|
+
chatId: options.chatId || null,
|
|
673
|
+
}
|
|
674
|
+
: null,
|
|
675
|
+
goalContract: carriedGoalContract,
|
|
676
|
+
progressLedger,
|
|
677
|
+
deliveryState: createDeliveryState({
|
|
678
|
+
alreadySent: carriedInterimHistory.length > 0 || carriedExplicitMessageSent,
|
|
679
|
+
finalResponseSent: carriedExplicitMessageSent,
|
|
680
|
+
finalContentDelivered: carriedExplicitMessageSent,
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
engine.persistRunMetadata(runId, {
|
|
684
|
+
progressLedger,
|
|
685
|
+
goalContract: carriedGoalContract,
|
|
686
|
+
});
|
|
687
|
+
engine.startMessagingProgressSupervisor(runId);
|
|
688
|
+
engine.emit(userId, 'run:start', { runId, agentId, title: runTitle, model, triggerType, triggerSource });
|
|
689
|
+
engine.recordRunEvent(userId, runId, 'run_started', {
|
|
690
|
+
title: runTitle,
|
|
691
|
+
model,
|
|
692
|
+
triggerType,
|
|
693
|
+
triggerSource,
|
|
694
|
+
}, { agentId });
|
|
695
|
+
console.info(
|
|
696
|
+
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${model} title=${summarizeForLog(runTitle, 120)}`
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const systemPrompt = await engine.buildSystemPrompt(userId, {
|
|
700
|
+
...(options.context || {}),
|
|
701
|
+
userMessage,
|
|
702
|
+
agentId,
|
|
703
|
+
triggerSource,
|
|
704
|
+
});
|
|
705
|
+
// Pass short descriptions so the model always knows every available tool.
|
|
706
|
+
// compactToolDefinition caps tool desc at 120 chars, param desc at 70 chars.
|
|
707
|
+
const builtInTools = engine.getAvailableTools(app, {
|
|
708
|
+
includeDescriptions: true,
|
|
709
|
+
userId,
|
|
710
|
+
agentId,
|
|
711
|
+
triggerType,
|
|
712
|
+
triggerSource,
|
|
713
|
+
widgetId: options.widgetId || null,
|
|
714
|
+
});
|
|
715
|
+
const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || engine.mcpManager;
|
|
716
|
+
const integrationManager = app?.locals?.integrationManager || null;
|
|
717
|
+
const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
|
|
718
|
+
const allTools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
|
|
719
|
+
let tools = allTools;
|
|
720
|
+
const toolNames = allTools.map((tool) => tool.name).filter(Boolean);
|
|
721
|
+
const coreToolStatus = {
|
|
722
|
+
send_message: toolNames.includes('send_message'),
|
|
723
|
+
create_task: toolNames.includes('create_task'),
|
|
724
|
+
list_tasks: toolNames.includes('list_tasks'),
|
|
725
|
+
update_task: toolNames.includes('update_task'),
|
|
726
|
+
delete_task: toolNames.includes('delete_task'),
|
|
727
|
+
};
|
|
728
|
+
engine.recordRunEvent(userId, runId, 'tool_inventory', {
|
|
729
|
+
total: toolNames.length,
|
|
730
|
+
builtInTotal: builtInTools.length,
|
|
731
|
+
mcpTotal: mcpTools.length,
|
|
732
|
+
core: coreToolStatus,
|
|
733
|
+
}, { agentId });
|
|
734
|
+
console.info(
|
|
735
|
+
`[Run ${shortenRunId(runId)}] tools total=${toolNames.length} builtIns=${builtInTools.length} mcp=${mcpTools.length} core=${JSON.stringify(coreToolStatus)}`
|
|
736
|
+
);
|
|
737
|
+
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine });
|
|
738
|
+
const capabilitySummary = summarizeCapabilityHealth(capabilityHealth);
|
|
739
|
+
const integrationSummary = integrationManager?.summarizeConnectedProviders?.(userId, agentId) || '';
|
|
740
|
+
|
|
741
|
+
const { MemoryManager } = require('../../memory/manager');
|
|
742
|
+
const memoryManager = engine.memoryManager || new MemoryManager();
|
|
743
|
+
const recallQuery = options.context?.rawUserMessage || userMessage;
|
|
744
|
+
const recallMsg = options.skipGlobalRecall === true
|
|
745
|
+
? null
|
|
746
|
+
: await engine.buildMemoryRecall({
|
|
747
|
+
memoryManager,
|
|
748
|
+
userId,
|
|
749
|
+
agentId,
|
|
750
|
+
query: recallQuery,
|
|
751
|
+
provider,
|
|
752
|
+
providerName,
|
|
753
|
+
model,
|
|
754
|
+
runId,
|
|
755
|
+
options,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
let summaryMessage = null;
|
|
759
|
+
let historyMessages = [];
|
|
760
|
+
|
|
761
|
+
if (conversationId && options.skipConversationHistory !== true) {
|
|
762
|
+
const conversationContext = getConversationContext(conversationId, historyWindow);
|
|
763
|
+
summaryMessage = buildSummaryCarrier(conversationContext.summary || options.priorSummary || '');
|
|
764
|
+
historyMessages = conversationContext.recentMessages.length > 0
|
|
765
|
+
? conversationContext.recentMessages
|
|
766
|
+
: (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
767
|
+
} else {
|
|
768
|
+
summaryMessage = buildSummaryCarrier(options.priorSummary || '');
|
|
769
|
+
historyMessages = (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let messages = engine.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
|
|
773
|
+
if (capabilitySummary) {
|
|
774
|
+
messages.push({ role: 'system', content: `[Capability health]\n${capabilitySummary}` });
|
|
775
|
+
}
|
|
776
|
+
if (integrationSummary) {
|
|
777
|
+
messages.push({ role: 'system', content: `[Official integrations]\n${integrationSummary}` });
|
|
778
|
+
}
|
|
779
|
+
const threadStateMessage = conversationId ? memoryManager.buildConversationStateMessage(conversationId) : null;
|
|
780
|
+
if (threadStateMessage) {
|
|
781
|
+
messages.push({ role: 'system', content: threadStateMessage });
|
|
782
|
+
}
|
|
783
|
+
if (carriedGoalContract) {
|
|
784
|
+
messages.push({
|
|
785
|
+
role: 'system',
|
|
786
|
+
content: buildGoalContractPrompt(carriedGoalContract, 'Persisted run goal'),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
engine.recordRunEvent(userId, runId, 'memory_injected', {
|
|
790
|
+
hasRecallContext: Boolean(recallMsg),
|
|
791
|
+
hasThreadState: Boolean(threadStateMessage),
|
|
792
|
+
recallPreview: recallMsg ? String(recallMsg).slice(0, 240) : '',
|
|
793
|
+
}, { agentId });
|
|
794
|
+
messages.push(engine.buildUserMessage(userMessage, options));
|
|
795
|
+
messages = sanitizeConversationMessages(messages);
|
|
796
|
+
|
|
797
|
+
if (conversationId) {
|
|
798
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
799
|
+
.run(conversationId, 'user', userMessage);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
let iteration = 0;
|
|
803
|
+
let totalTokens = 0;
|
|
804
|
+
let lastContent = '';
|
|
805
|
+
let stepIndex = 0;
|
|
806
|
+
let failedStepCount = 0;
|
|
807
|
+
let modelFailureRecoveries = 0;
|
|
808
|
+
let promptMetrics = {};
|
|
809
|
+
let toolExecutions = [];
|
|
810
|
+
let compactionMetrics = [];
|
|
811
|
+
let analysis = null;
|
|
812
|
+
let plan = null;
|
|
813
|
+
|
|
814
|
+
// Model-authored progress updates: the messaging supervisor calls this to get a
|
|
815
|
+
// dynamic "what I'm doing right now" line instead of a hard-coded string. It runs
|
|
816
|
+
// a tiny tool-less model call over the current run activity. `provider`/`model` are
|
|
817
|
+
// `let` (closure tracks fallbacks) and `toolExecutions` is push-only.
|
|
818
|
+
if (triggerSource === 'messaging') {
|
|
819
|
+
const runMetaForCompose = engine.getRunMeta(runId);
|
|
820
|
+
if (runMetaForCompose) {
|
|
821
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
822
|
+
try {
|
|
823
|
+
const rm = engine.getRunMeta(runId);
|
|
824
|
+
const ledger = rm?.progressLedger || {};
|
|
825
|
+
// Real, specific evidence (actual commands + their output) so the update is
|
|
826
|
+
// grounded in what happened, not invented. Bare tool names make a weak model
|
|
827
|
+
// confabulate generic activity ("running the build", "training").
|
|
828
|
+
const recent = summarizeToolExecutions(toolExecutions, 5) || '(no tool activity yet)';
|
|
829
|
+
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
830
|
+
const contextBlock = [
|
|
831
|
+
buildProgressUpdatePrompt(),
|
|
832
|
+
stalled ? 'This step has been running a while with no new progress; reassure the user you are still on it.' : '',
|
|
833
|
+
'',
|
|
834
|
+
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
835
|
+
`Doing now: ${ledger.currentTool ? `using ${ledger.currentTool}` : (ledger.currentPhase || 'thinking')}`,
|
|
836
|
+
`Actual recent tool activity (newest last) — describe ONLY this, do not extrapolate:\n${recent}`,
|
|
837
|
+
priorUpdate ? `Your previous update (say something different): ${summarizeForLog(priorUpdate, 160)}` : '',
|
|
838
|
+
].filter(Boolean).join('\n');
|
|
839
|
+
// Reuse the run's real system prompt so the update follows the same voice and
|
|
840
|
+
// formatting guidelines as every other message (single source of truth).
|
|
841
|
+
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
842
|
+
|| 'You are a helpful assistant.';
|
|
843
|
+
const resp = await withModelCallTimeout(
|
|
844
|
+
provider.chat(
|
|
845
|
+
[
|
|
846
|
+
{ role: 'system', content: sysContent },
|
|
847
|
+
{ role: 'user', content: contextBlock },
|
|
848
|
+
],
|
|
849
|
+
[],
|
|
850
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
851
|
+
),
|
|
852
|
+
options,
|
|
853
|
+
'Progress update compose',
|
|
854
|
+
);
|
|
855
|
+
return sanitizeModelOutput(resp.content || '', { model });
|
|
856
|
+
} catch (composeErr) {
|
|
857
|
+
console.warn(`[Run ${shortenRunId(runId)}] progress_update_compose failed: ${summarizeForLog(composeErr?.message || composeErr, 140)}`);
|
|
858
|
+
return '';
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
let verification = null;
|
|
864
|
+
let deliverableWorkflow = null;
|
|
865
|
+
let deliverablePlan = null;
|
|
866
|
+
let deliverableArtifacts = [];
|
|
867
|
+
let deliverableValidation = null;
|
|
868
|
+
let directAnswerEligible = false;
|
|
869
|
+
let analysisUsage = 0;
|
|
870
|
+
|
|
871
|
+
try {
|
|
872
|
+
if (options.skipTaskAnalysis === true) {
|
|
873
|
+
analysis = buildSkipTaskAnalysisResult(options.forceMode);
|
|
874
|
+
} else {
|
|
875
|
+
const analysisResult = await runWithModelFallback('task analysis', () => engine.analyzeTask({
|
|
876
|
+
provider,
|
|
877
|
+
providerName,
|
|
878
|
+
model,
|
|
879
|
+
messages,
|
|
880
|
+
tools,
|
|
881
|
+
capabilityHealth,
|
|
882
|
+
forceMode: options.forceMode || null,
|
|
883
|
+
userMessage,
|
|
884
|
+
options: { ...options, triggerSource, runId, userId, agentId },
|
|
885
|
+
}));
|
|
886
|
+
analysisUsage = analysisResult.usage || 0;
|
|
887
|
+
totalTokens += analysisUsage;
|
|
888
|
+
analysis = applyForcedAnalysisMode({ ...analysisResult.analysis }, options.forceMode);
|
|
889
|
+
if (!analysis.goal && userMessage) {
|
|
890
|
+
analysis.goal = String(userMessage).trim().slice(0, 300);
|
|
891
|
+
}
|
|
892
|
+
analysis.mode = promoteAnalysisMode(analysis.mode, {
|
|
893
|
+
verificationNeed: analysis.verification_need,
|
|
894
|
+
freshnessRisk: analysis.freshness_risk,
|
|
895
|
+
draftReply: analysis.draft_reply,
|
|
896
|
+
planningDepth: analysis.planning_depth,
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
stepIndex += 1;
|
|
900
|
+
const analysisStepId = uuidv4();
|
|
901
|
+
db.prepare(`INSERT INTO agent_steps
|
|
902
|
+
(id, run_id, step_index, type, description, status, result, started_at, completed_at)
|
|
903
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
904
|
+
.run(
|
|
905
|
+
analysisStepId,
|
|
906
|
+
runId,
|
|
907
|
+
stepIndex,
|
|
908
|
+
'analysis',
|
|
909
|
+
'Task analysis contract',
|
|
910
|
+
'completed',
|
|
911
|
+
JSON.stringify(analysis).slice(0, 20000)
|
|
912
|
+
);
|
|
913
|
+
engine.persistRunMetadata(runId, {
|
|
914
|
+
taskAnalysis: analysis,
|
|
915
|
+
capabilityHealth,
|
|
916
|
+
});
|
|
917
|
+
engine.updateRunGoalContract(runId, goalContractFromAnalysis(analysis));
|
|
918
|
+
engine.emit(userId, 'run:analysis', {
|
|
919
|
+
runId,
|
|
920
|
+
...analysis,
|
|
921
|
+
capabilitySummary,
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
tools = selectInitialTools(allTools, analysis.suggested_tools, {
|
|
927
|
+
widgetId: options.widgetId || null,
|
|
928
|
+
includeCoreFileTools: analysis.mode === 'execute' || analysis.mode === 'plan_execute',
|
|
929
|
+
});
|
|
930
|
+
engine.initializeToolRuntime(runId, allTools, tools, options);
|
|
931
|
+
messages.push({
|
|
932
|
+
role: 'system',
|
|
933
|
+
content: [
|
|
934
|
+
'[Available tool catalog]',
|
|
935
|
+
buildToolCatalog(allTools),
|
|
936
|
+
'',
|
|
937
|
+
`Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
|
|
938
|
+
'For workspace file inspection/editing, prefer read_files, read_file, search_files, list_directory, edit_file, replace_file_range, and write_file over shell cat/sed/python snippets. Use execute_command for git, tests, package managers, builds, and other shell-native actions.',
|
|
939
|
+
'Use activate_tools with exact catalog names when another schema is required.',
|
|
940
|
+
].join('\n'),
|
|
941
|
+
});
|
|
942
|
+
engine.recordRunEvent(userId, runId, 'tool_selection_applied', {
|
|
943
|
+
activeToolNames: tools.map((tool) => tool.name),
|
|
944
|
+
catalogSize: allTools.length,
|
|
945
|
+
}, { agentId });
|
|
946
|
+
|
|
947
|
+
const activeDefaultModelSetting = triggerType === 'subagent'
|
|
948
|
+
? aiSettings.default_subagent_model
|
|
949
|
+
: aiSettings.default_chat_model;
|
|
950
|
+
if (!_modelOverride && activeDefaultModelSetting === 'auto' && aiSettings.smarter_model_selector !== false) {
|
|
951
|
+
const requestedPurpose = analysis?.mode === 'plan_execute' || analysis?.complexity === 'complex' || analysis?.autonomy_level === 'high'
|
|
952
|
+
? 'planning'
|
|
953
|
+
: triggerType === 'subagent'
|
|
954
|
+
? 'fast'
|
|
955
|
+
: '';
|
|
956
|
+
if (requestedPurpose) {
|
|
957
|
+
const selectedAfterAnalysis = await getProviderForUser(
|
|
958
|
+
userId,
|
|
959
|
+
userMessage,
|
|
960
|
+
triggerType === 'subagent',
|
|
961
|
+
null,
|
|
962
|
+
{
|
|
963
|
+
...providerStatusConfig,
|
|
964
|
+
selectionHint: {
|
|
965
|
+
purpose: requestedPurpose,
|
|
966
|
+
complexity: analysis?.complexity,
|
|
967
|
+
autonomyLevel: analysis?.autonomy_level,
|
|
968
|
+
requiredConfidence: analysis?.completion_confidence_required,
|
|
969
|
+
costMode: aiSettings.cost_mode,
|
|
970
|
+
},
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
if (selectedAfterAnalysis.model !== model) {
|
|
974
|
+
provider = selectedAfterAnalysis.provider;
|
|
975
|
+
model = selectedAfterAnalysis.model;
|
|
976
|
+
providerName = selectedAfterAnalysis.providerName;
|
|
977
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
978
|
+
.run(model, runId);
|
|
979
|
+
engine.emit(userId, 'run:interim', {
|
|
980
|
+
runId,
|
|
981
|
+
message: `Switched to ${model} for this run after task analysis.`,
|
|
982
|
+
phase: 'model_selection'
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Rebuild loop policy with the resolved analysis mode. Runs in both the
|
|
989
|
+
// normal path and the skipTaskAnalysis path so that forceMode='plan_execute'
|
|
990
|
+
// (or any mode set by buildSkipTaskAnalysisResult) raises the iteration
|
|
991
|
+
// ceiling correctly.
|
|
992
|
+
loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute', {
|
|
993
|
+
...options,
|
|
994
|
+
autonomyPolicy: buildAutonomyPolicyFromAnalysis(analysis),
|
|
995
|
+
});
|
|
996
|
+
maxIterations = loopPolicy.maxIterations;
|
|
997
|
+
|
|
998
|
+
if (options.skipDeliverableWorkflow !== true) {
|
|
999
|
+
try {
|
|
1000
|
+
const deliverableSelectionResult = await selectDeliverableWorkflow({
|
|
1001
|
+
engine,
|
|
1002
|
+
provider,
|
|
1003
|
+
providerName,
|
|
1004
|
+
model,
|
|
1005
|
+
messages,
|
|
1006
|
+
tools,
|
|
1007
|
+
options: { ...options, runId, userId, agentId },
|
|
1008
|
+
});
|
|
1009
|
+
totalTokens += deliverableSelectionResult.usage || 0;
|
|
1010
|
+
const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
|
|
1011
|
+
if (selectedWorkflow?.canHandle(deliverableSelectionResult.selection)) {
|
|
1012
|
+
deliverableWorkflow = {
|
|
1013
|
+
workflow: selectedWorkflow,
|
|
1014
|
+
selection: deliverableSelectionResult.selection,
|
|
1015
|
+
request: selectedWorkflow.normalizeRequest({
|
|
1016
|
+
...deliverableSelectionResult.selection,
|
|
1017
|
+
userMessage,
|
|
1018
|
+
}),
|
|
1019
|
+
};
|
|
1020
|
+
deliverablePlan = selectedWorkflow.buildExecutionPlan(deliverableWorkflow.request, {
|
|
1021
|
+
analysis,
|
|
1022
|
+
tools,
|
|
1023
|
+
options,
|
|
1024
|
+
});
|
|
1025
|
+
await selectedWorkflow.run(deliverablePlan, {
|
|
1026
|
+
engine,
|
|
1027
|
+
userId,
|
|
1028
|
+
agentId,
|
|
1029
|
+
runId,
|
|
1030
|
+
app,
|
|
1031
|
+
});
|
|
1032
|
+
engine.persistRunMetadata(runId, {
|
|
1033
|
+
deliverableWorkflow: {
|
|
1034
|
+
...deliverableWorkflow.selection,
|
|
1035
|
+
plan: deliverablePlan,
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
engine.updateRunGoalContract(runId, {
|
|
1039
|
+
goal: deliverableWorkflow.selection.goal,
|
|
1040
|
+
});
|
|
1041
|
+
engine.recordRunEvent(userId, runId, 'deliverable_workflow_selected', {
|
|
1042
|
+
type: deliverableWorkflow.selection.type,
|
|
1043
|
+
confidence: deliverableWorkflow.selection.confidence,
|
|
1044
|
+
goal: deliverableWorkflow.selection.goal,
|
|
1045
|
+
requestedOutputs: deliverableWorkflow.selection.requestedOutputs,
|
|
1046
|
+
}, { agentId });
|
|
1047
|
+
}
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
engine.recordRunEvent(userId, runId, 'deliverable_workflow_skipped', {
|
|
1050
|
+
reason: summarizeForLog(error?.message || error, 240),
|
|
1051
|
+
}, { agentId });
|
|
1052
|
+
messages.push({
|
|
1053
|
+
role: 'system',
|
|
1054
|
+
content: 'The optional deliverable workflow classifier failed. Continue with the normal agent loop; do not stop or retry the whole run just because this classifier failed.',
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (analysis.mode === 'plan_execute') {
|
|
1060
|
+
const planResult = await runWithModelFallback('execution planning', () => engine.createExecutionPlan({
|
|
1061
|
+
provider,
|
|
1062
|
+
providerName,
|
|
1063
|
+
model,
|
|
1064
|
+
messages,
|
|
1065
|
+
analysis,
|
|
1066
|
+
capabilitySummary,
|
|
1067
|
+
options: { ...options, runId, userId, agentId },
|
|
1068
|
+
}));
|
|
1069
|
+
totalTokens += planResult.usage || 0;
|
|
1070
|
+
plan = planResult.plan;
|
|
1071
|
+
stepIndex += 1;
|
|
1072
|
+
const planStepId = uuidv4();
|
|
1073
|
+
db.prepare(`INSERT INTO agent_steps
|
|
1074
|
+
(id, run_id, step_index, type, description, status, result, started_at, completed_at)
|
|
1075
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
1076
|
+
.run(
|
|
1077
|
+
planStepId,
|
|
1078
|
+
runId,
|
|
1079
|
+
stepIndex,
|
|
1080
|
+
'planning',
|
|
1081
|
+
'Execution plan',
|
|
1082
|
+
'completed',
|
|
1083
|
+
JSON.stringify(plan).slice(0, 20000)
|
|
1084
|
+
);
|
|
1085
|
+
engine.persistRunMetadata(runId, { executionPlan: plan });
|
|
1086
|
+
engine.updateRunGoalContract(runId, goalContractFromPlan(plan));
|
|
1087
|
+
engine.emit(userId, 'run:plan', {
|
|
1088
|
+
runId,
|
|
1089
|
+
steps: plan.steps,
|
|
1090
|
+
successCriteria: plan.success_criteria,
|
|
1091
|
+
verificationFocus: plan.verification_focus,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const runGoalContract = engine.getRunMeta(runId)?.goalContract || null;
|
|
1096
|
+
if (runGoalContract) {
|
|
1097
|
+
messages.push({
|
|
1098
|
+
role: 'system',
|
|
1099
|
+
content: buildGoalContractPrompt(runGoalContract, 'Run goal contract'),
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
messages.push({
|
|
1103
|
+
role: 'system',
|
|
1104
|
+
content: buildExecutionGuidance({
|
|
1105
|
+
analysis,
|
|
1106
|
+
plan,
|
|
1107
|
+
capabilityHealth: capabilitySummary,
|
|
1108
|
+
}),
|
|
1109
|
+
});
|
|
1110
|
+
if (deliverablePlan) {
|
|
1111
|
+
messages.push({
|
|
1112
|
+
role: 'system',
|
|
1113
|
+
content: buildDeliverableWorkflowGuidance(deliverablePlan),
|
|
1114
|
+
});
|
|
1115
|
+
engine.recordRunEvent(userId, runId, 'deliverable_execution_started', {
|
|
1116
|
+
type: deliverableWorkflow?.selection?.type,
|
|
1117
|
+
preferredTools: deliverablePlan.preferredTools || [],
|
|
1118
|
+
expectedOutputs: deliverablePlan.expectedOutputs || [],
|
|
1119
|
+
}, { agentId });
|
|
1120
|
+
}
|
|
1121
|
+
messages = sanitizeConversationMessages(messages);
|
|
1122
|
+
|
|
1123
|
+
directAnswerEligible = isDirectAnswerEligibleAnalysis(analysis)
|
|
1124
|
+
&& Boolean(normalizeOutgoingMessage(analysis.draft_reply));
|
|
1125
|
+
|
|
1126
|
+
if (directAnswerEligible) {
|
|
1127
|
+
iteration = 1;
|
|
1128
|
+
lastContent = analysis.draft_reply.trim();
|
|
1129
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1130
|
+
if (conversationId) {
|
|
1131
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1132
|
+
.run(conversationId, 'assistant', lastContent, analysisUsage);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// BUG FIX: consecutiveToolFailures was previously declared INSIDE the
|
|
1137
|
+
// while loop (resetting each iteration). It is now tracked across the
|
|
1138
|
+
// full run so the failure guard fires correctly after 5 consecutive failures
|
|
1139
|
+
// regardless of which iteration they fall in.
|
|
1140
|
+
let consecutiveToolFailures = 0;
|
|
1141
|
+
const iterationBudget = new IterationBudget(maxIterations);
|
|
1142
|
+
|
|
1143
|
+
while (!directAnswerEligible && iterationBudget.consume()) {
|
|
1144
|
+
if (engine.isRunStopped(runId)) break;
|
|
1145
|
+
iteration = iterationBudget.used;
|
|
1146
|
+
|
|
1147
|
+
const systemSteeringAtLoopStart = engine.applyQueuedSystemSteering(runId, messages);
|
|
1148
|
+
messages = systemSteeringAtLoopStart.messages;
|
|
1149
|
+
const steeringAtLoopStart = engine.applyQueuedSteering(runId, messages, {
|
|
1150
|
+
userId,
|
|
1151
|
+
conversationId
|
|
1152
|
+
});
|
|
1153
|
+
messages = steeringAtLoopStart.messages;
|
|
1154
|
+
messages = sanitizeConversationMessages(messages);
|
|
1155
|
+
|
|
1156
|
+
// Analysis-paralysis gate: fire at the start of every iteration where
|
|
1157
|
+
// the agent has spent N turns only reading/listing/searching without
|
|
1158
|
+
// taking any concrete action. Escalates in urgency each turn.
|
|
1159
|
+
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1160
|
+
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1161
|
+
if (readOnlyCount >= 3) {
|
|
1162
|
+
const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
|
|
1163
|
+
const alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1164
|
+
messages.push({
|
|
1165
|
+
role: 'system',
|
|
1166
|
+
content: [
|
|
1167
|
+
`${urgency} — ${readOnlyCount} consecutive read-only turns with no concrete action.`,
|
|
1168
|
+
alreadyRead
|
|
1169
|
+
? `You have ALREADY read/searched: ${alreadyRead}. Their output is in this conversation above — do not read or search them again; re-reading what you already have is the main way runs stall.`
|
|
1170
|
+
: 'Do not re-read or re-search anything already in this conversation.',
|
|
1171
|
+
'If you are repeatedly extracting evidence through low-level commands, switch to the available file/search/edit tools over the shared workspace.',
|
|
1172
|
+
'Act on what you already know now: edit files, run a state-changing command, create the branch/PR, or send the result.',
|
|
1173
|
+
'If you genuinely cannot proceed, call task_complete with your best answer or a concrete blocker. Deciding to finish is a valid action; continuing to gather is not.',
|
|
1174
|
+
].join(' '),
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
engine.updateRunProgress(runId, {
|
|
1180
|
+
currentPhase: 'model',
|
|
1181
|
+
currentStep: `model:${iteration}`,
|
|
1182
|
+
currentTool: null,
|
|
1183
|
+
currentStepStartedAt: isoNow(),
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
let metrics = engine.estimatePromptMetrics(messages, tools);
|
|
1187
|
+
const contextWindow = provider.getContextWindow(model);
|
|
1188
|
+
if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
|
|
1189
|
+
messages = await withModelCallTimeout(
|
|
1190
|
+
compact(messages, provider, model, contextWindow),
|
|
1191
|
+
options,
|
|
1192
|
+
`Context compaction before iteration ${iteration}`,
|
|
1193
|
+
);
|
|
1194
|
+
messages = sanitizeConversationMessages(messages);
|
|
1195
|
+
engine.emit(userId, 'run:compaction', { runId, iteration });
|
|
1196
|
+
metrics = engine.estimatePromptMetrics(messages, tools);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
promptMetrics = engine.mergePromptMetrics(promptMetrics, metrics, iteration, tools.length);
|
|
1200
|
+
engine.persistPromptMetrics(runId, promptMetrics).catch(() => { });
|
|
1201
|
+
engine.emit(userId, 'run:thinking', { runId, iteration });
|
|
1202
|
+
engine.recordRunEvent(userId, runId, 'model_turn_started', {
|
|
1203
|
+
iteration,
|
|
1204
|
+
toolCount: tools.length,
|
|
1205
|
+
}, { agentId });
|
|
1206
|
+
|
|
1207
|
+
let response;
|
|
1208
|
+
let responseModel = model;
|
|
1209
|
+
let streamContent = '';
|
|
1210
|
+
|
|
1211
|
+
const tryModelCall = async (retryForFallback = true) => {
|
|
1212
|
+
try {
|
|
1213
|
+
const modelCall = await engine.requestModelResponse({
|
|
1214
|
+
provider,
|
|
1215
|
+
providerName,
|
|
1216
|
+
model,
|
|
1217
|
+
messages,
|
|
1218
|
+
tools,
|
|
1219
|
+
options: { ...options, userId, agentId, runId, phase: 'model_turn' },
|
|
1220
|
+
runId,
|
|
1221
|
+
iteration,
|
|
1222
|
+
});
|
|
1223
|
+
response = modelCall.response;
|
|
1224
|
+
responseModel = modelCall.responseModel;
|
|
1225
|
+
streamContent = modelCall.streamContent;
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1228
|
+
const fallbackModelId = retryForFallback
|
|
1229
|
+
? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id, err)
|
|
1230
|
+
: null;
|
|
1231
|
+
if (fallbackModelId) {
|
|
1232
|
+
const failedModel = model;
|
|
1233
|
+
console.log(`[Engine] Attempting fallback to: ${fallbackModelId}`);
|
|
1234
|
+
const fallback = await getProviderForUser(
|
|
1235
|
+
userId,
|
|
1236
|
+
userMessage,
|
|
1237
|
+
triggerType === 'subagent',
|
|
1238
|
+
fallbackModelId,
|
|
1239
|
+
providerStatusConfig
|
|
1240
|
+
);
|
|
1241
|
+
provider = fallback.provider;
|
|
1242
|
+
model = fallback.model;
|
|
1243
|
+
providerName = fallback.providerName;
|
|
1244
|
+
|
|
1245
|
+
const retryMessages = sanitizeConversationMessages([
|
|
1246
|
+
...messages,
|
|
1247
|
+
{
|
|
1248
|
+
role: 'system',
|
|
1249
|
+
content: buildModelFailureLoopPrompt({
|
|
1250
|
+
failedModel,
|
|
1251
|
+
nextModel: model,
|
|
1252
|
+
errorMessage: err.message
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1255
|
+
]);
|
|
1256
|
+
|
|
1257
|
+
const fallbackCall = await engine.requestModelResponse({
|
|
1258
|
+
provider,
|
|
1259
|
+
providerName,
|
|
1260
|
+
model,
|
|
1261
|
+
messages: retryMessages,
|
|
1262
|
+
tools,
|
|
1263
|
+
options: { ...options, userId },
|
|
1264
|
+
runId,
|
|
1265
|
+
iteration,
|
|
1266
|
+
});
|
|
1267
|
+
response = fallbackCall.response;
|
|
1268
|
+
responseModel = fallbackCall.responseModel;
|
|
1269
|
+
streamContent = fallbackCall.streamContent;
|
|
1270
|
+
} else {
|
|
1271
|
+
throw err;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
try {
|
|
1277
|
+
await tryModelCall();
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
const modelError = String(err?.message || 'Model call failed');
|
|
1280
|
+
|
|
1281
|
+
if (modelFailureRecoveries < loopPolicy.maxModelFailureRecoveries) {
|
|
1282
|
+
const failedModel = model;
|
|
1283
|
+
const switched = await switchToFallbackModel(failedModel, err, 'model turn');
|
|
1284
|
+
if (!switched) throw err;
|
|
1285
|
+
modelFailureRecoveries += 1;
|
|
1286
|
+
failedStepCount += 1;
|
|
1287
|
+
messages.push({
|
|
1288
|
+
role: 'system',
|
|
1289
|
+
content: buildModelFailureLoopPrompt({
|
|
1290
|
+
failedModel,
|
|
1291
|
+
nextModel: model,
|
|
1292
|
+
errorMessage: modelError
|
|
1293
|
+
})
|
|
1294
|
+
});
|
|
1295
|
+
engine.emit(userId, 'run:interim', {
|
|
1296
|
+
runId,
|
|
1297
|
+
message: 'Model call failed; adapting and retrying autonomously.',
|
|
1298
|
+
phase: 'recovering'
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
throw err;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (!response) {
|
|
1307
|
+
response = { content: streamContent, toolCalls: [], finishReason: 'stop', usage: null };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (response.usage) {
|
|
1311
|
+
totalTokens += response.usage.totalTokens || 0;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
lastContent = sanitizeModelOutput(response.content || streamContent || '', { model: responseModel });
|
|
1315
|
+
|
|
1316
|
+
if ((!response.toolCalls || response.toolCalls.length === 0) && lastContent) {
|
|
1317
|
+
const salvaged = salvageTextToolCalls(lastContent, tools);
|
|
1318
|
+
if (salvaged.toolCalls.length > 0) {
|
|
1319
|
+
response.toolCalls = salvaged.toolCalls;
|
|
1320
|
+
response.finishReason = 'tool_calls';
|
|
1321
|
+
response.content = salvaged.content;
|
|
1322
|
+
lastContent = salvaged.content;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
engine.recordRunEvent(userId, runId, 'model_turn_completed', {
|
|
1327
|
+
iteration,
|
|
1328
|
+
toolCallCount: response.toolCalls?.length || 0,
|
|
1329
|
+
contentPreview: String(lastContent || streamContent || '').slice(0, 240),
|
|
1330
|
+
}, { agentId });
|
|
1331
|
+
engine.updateRunProgress(runId, {}, {
|
|
1332
|
+
verified: true,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
1336
|
+
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
1337
|
+
if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
|
|
1338
|
+
messages.push(assistantMessage);
|
|
1339
|
+
|
|
1340
|
+
if (conversationId) {
|
|
1341
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_calls, tokens) VALUES (?, ?, ?, ?, ?)')
|
|
1342
|
+
.run(
|
|
1343
|
+
conversationId,
|
|
1344
|
+
'assistant',
|
|
1345
|
+
lastContent,
|
|
1346
|
+
response.toolCalls?.length ? JSON.stringify(response.toolCalls) : null,
|
|
1347
|
+
response.usage?.totalTokens || 0
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1352
|
+
engine.updateRunProgress(runId, {
|
|
1353
|
+
currentPhase: 'idle',
|
|
1354
|
+
currentStep: null,
|
|
1355
|
+
currentTool: null,
|
|
1356
|
+
currentStepStartedAt: null,
|
|
1357
|
+
});
|
|
1358
|
+
// Check for queued steering first — if something was injected while the
|
|
1359
|
+
// model was responding (e.g. a heartbeat nudge), give the model a chance
|
|
1360
|
+
// to act on it before we treat this as a final answer.
|
|
1361
|
+
const systemSteeringAfterResponse = engine.applyQueuedSystemSteering(runId, messages);
|
|
1362
|
+
messages = systemSteeringAfterResponse.messages;
|
|
1363
|
+
if (systemSteeringAfterResponse.appliedCount > 0) {
|
|
1364
|
+
iterationBudget.refund();
|
|
1365
|
+
iteration = iterationBudget.used;
|
|
1366
|
+
lastContent = '';
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const steeringAfterResponse = engine.applyQueuedSteering(runId, messages, {
|
|
1370
|
+
userId,
|
|
1371
|
+
conversationId
|
|
1372
|
+
});
|
|
1373
|
+
messages = steeringAfterResponse.messages;
|
|
1374
|
+
if (steeringAfterResponse.appliedCount > 0) {
|
|
1375
|
+
iterationBudget.refund();
|
|
1376
|
+
iteration = iterationBudget.used;
|
|
1377
|
+
lastContent = '';
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (engine.shouldFastCompleteVoiceReply({
|
|
1381
|
+
options,
|
|
1382
|
+
toolExecutions,
|
|
1383
|
+
failedStepCount,
|
|
1384
|
+
messagingSent: engine.activeRuns.get(runId)?.messagingSent || false,
|
|
1385
|
+
lastReply: lastContent,
|
|
1386
|
+
})) {
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
if (shouldContinueAfterBlankToolFailure({
|
|
1390
|
+
lastContent,
|
|
1391
|
+
failedStepCount,
|
|
1392
|
+
remainingIterations: iterationBudget.remaining,
|
|
1393
|
+
toolExecutions,
|
|
1394
|
+
})) {
|
|
1395
|
+
engine.recordRunEvent(userId, runId, 'blank_after_tool_failure_continued', {
|
|
1396
|
+
iteration,
|
|
1397
|
+
remainingIterations: iterationBudget.remaining,
|
|
1398
|
+
}, { agentId });
|
|
1399
|
+
messages.push({
|
|
1400
|
+
role: 'system',
|
|
1401
|
+
content: buildBlankAfterToolFailureGuidance(toolExecutions),
|
|
1402
|
+
});
|
|
1403
|
+
lastContent = '';
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const loopDecision = await engine.decideLoopState({
|
|
1407
|
+
provider,
|
|
1408
|
+
providerName,
|
|
1409
|
+
model,
|
|
1410
|
+
messages,
|
|
1411
|
+
analysis,
|
|
1412
|
+
plan,
|
|
1413
|
+
tools,
|
|
1414
|
+
toolExecutions,
|
|
1415
|
+
lastReply: lastContent,
|
|
1416
|
+
iteration,
|
|
1417
|
+
maxIterations,
|
|
1418
|
+
options: { ...options, triggerSource, runId, userId, agentId },
|
|
1419
|
+
messagingSent: engine.activeRuns.get(runId)?.messagingSent || false,
|
|
1420
|
+
});
|
|
1421
|
+
totalTokens += loopDecision.usage || 0;
|
|
1422
|
+
engine.recordRunEvent(userId, runId, 'loop_completion_checked', {
|
|
1423
|
+
status: loopDecision.decision.status,
|
|
1424
|
+
reason: loopDecision.decision.reason,
|
|
1425
|
+
iteration,
|
|
1426
|
+
}, { agentId });
|
|
1427
|
+
if (loopDecision.decision.status === 'continue') {
|
|
1428
|
+
messages.push({
|
|
1429
|
+
role: 'system',
|
|
1430
|
+
content: [
|
|
1431
|
+
'The run self-check determined the latest assistant text is not terminal.',
|
|
1432
|
+
'Continue with the next safe tool/model step.',
|
|
1433
|
+
'If the text was only a progress note, do not repeat it; either make progress or provide a real final/blocker reply.',
|
|
1434
|
+
].join(' '),
|
|
1435
|
+
});
|
|
1436
|
+
lastContent = '';
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
directAnswerEligible = true;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const canRunParallelBatch = (
|
|
1444
|
+
response.toolCalls.length > 1
|
|
1445
|
+
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(toolCall))
|
|
1446
|
+
);
|
|
1447
|
+
if (canRunParallelBatch) {
|
|
1448
|
+
const parallelToolNames = response.toolCalls
|
|
1449
|
+
.map((toolCall) => toolCall.function?.name)
|
|
1450
|
+
.filter(Boolean);
|
|
1451
|
+
engine.updateRunProgress(runId, {
|
|
1452
|
+
currentPhase: 'tool',
|
|
1453
|
+
currentStep: `parallel:${iteration}`,
|
|
1454
|
+
currentTool: parallelToolNames.join(', ') || 'parallel tools',
|
|
1455
|
+
currentStepStartedAt: isoNow(),
|
|
1456
|
+
});
|
|
1457
|
+
const batch = await engine.executeReadOnlyBatch(response.toolCalls, {
|
|
1458
|
+
userId,
|
|
1459
|
+
runId,
|
|
1460
|
+
agentId,
|
|
1461
|
+
app,
|
|
1462
|
+
triggerType,
|
|
1463
|
+
triggerSource,
|
|
1464
|
+
conversationId,
|
|
1465
|
+
startingStepIndex: stepIndex,
|
|
1466
|
+
iteration,
|
|
1467
|
+
options,
|
|
1468
|
+
});
|
|
1469
|
+
stepIndex = batch.endingStepIndex;
|
|
1470
|
+
for (const item of batch.results) {
|
|
1471
|
+
const execution = classifyToolExecution(
|
|
1472
|
+
item.toolName,
|
|
1473
|
+
item.toolArgs,
|
|
1474
|
+
item.result,
|
|
1475
|
+
item.error || '',
|
|
1476
|
+
);
|
|
1477
|
+
execution.input = item.toolArgs;
|
|
1478
|
+
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
1479
|
+
toolExecutions.push(execution);
|
|
1480
|
+
engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
|
|
1481
|
+
if (item.error) failedStepCount += 1;
|
|
1482
|
+
const modelPayload = compactPayloadForModel(item.toolName, item.result);
|
|
1483
|
+
const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
|
|
1484
|
+
const toolMessage = {
|
|
1485
|
+
role: 'tool',
|
|
1486
|
+
name: item.toolName,
|
|
1487
|
+
tool_call_id: item.toolCall.id,
|
|
1488
|
+
content: compactToolResult(item.toolName, item.toolArgs, modelPayload.result, {
|
|
1489
|
+
softLimit: toolResultLimits.softLimit,
|
|
1490
|
+
hardLimit: toolResultLimits.hardLimit,
|
|
1491
|
+
}),
|
|
1492
|
+
};
|
|
1493
|
+
messages.push(toolMessage);
|
|
1494
|
+
if (conversationId) {
|
|
1495
|
+
db.prepare(
|
|
1496
|
+
`INSERT INTO conversation_messages (
|
|
1497
|
+
conversation_id, role, content, tool_call_id, name
|
|
1498
|
+
) VALUES (?, 'tool', ?, ?, ?)`
|
|
1499
|
+
).run(conversationId, toolMessage.content, item.toolCall.id, item.toolName);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
engine.persistRunMetadata(runId, {
|
|
1503
|
+
evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
|
|
1504
|
+
subagentState: engine.listSubagents(runId),
|
|
1505
|
+
deliverableArtifacts,
|
|
1506
|
+
compactionMetrics: compactionMetrics.slice(-20),
|
|
1507
|
+
});
|
|
1508
|
+
engine.updateRunProgress(runId, {
|
|
1509
|
+
currentPhase: 'idle',
|
|
1510
|
+
currentStep: null,
|
|
1511
|
+
currentTool: null,
|
|
1512
|
+
currentStepStartedAt: null,
|
|
1513
|
+
}, {
|
|
1514
|
+
verified: true,
|
|
1515
|
+
});
|
|
1516
|
+
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1517
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1518
|
+
if (iterMeta) {
|
|
1519
|
+
iterMeta.consecutiveReadOnlyIterations = (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
let iterationConcreteProgress = false;
|
|
1526
|
+
for (const toolCall of response.toolCalls) {
|
|
1527
|
+
if (engine.isRunStopped(runId)) break;
|
|
1528
|
+
stepIndex++;
|
|
1529
|
+
const stepId = uuidv4();
|
|
1530
|
+
const toolName = toolCall.function.name;
|
|
1531
|
+
const stepStartedAt = Date.now();
|
|
1532
|
+
let toolArgs;
|
|
1533
|
+
try {
|
|
1534
|
+
toolArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
1535
|
+
} catch {
|
|
1536
|
+
toolArgs = {};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// ── task_complete: AI explicitly signals the task is fully done ──
|
|
1540
|
+
if (toolName === 'task_complete') {
|
|
1541
|
+
const finalMessage = String(toolArgs.message || '').trim();
|
|
1542
|
+
const completionDecision = await engine.evaluateTaskCompleteSignal({
|
|
1543
|
+
provider,
|
|
1544
|
+
providerName,
|
|
1545
|
+
model,
|
|
1546
|
+
messages,
|
|
1547
|
+
analysis,
|
|
1548
|
+
plan,
|
|
1549
|
+
tools,
|
|
1550
|
+
toolExecutions,
|
|
1551
|
+
finalMessage,
|
|
1552
|
+
confidence: toolArgs.confidence,
|
|
1553
|
+
iteration,
|
|
1554
|
+
maxIterations,
|
|
1555
|
+
options: { ...options, triggerSource, runId, userId, agentId },
|
|
1556
|
+
messagingSent: engine.activeRuns.get(runId)?.messagingSent || false,
|
|
1557
|
+
});
|
|
1558
|
+
totalTokens += completionDecision.usage || 0;
|
|
1559
|
+
engine.recordRunEvent(userId, runId, 'task_complete_signaled', {
|
|
1560
|
+
accepted: completionDecision.accepted,
|
|
1561
|
+
status: completionDecision.status,
|
|
1562
|
+
reason: completionDecision.reason,
|
|
1563
|
+
iteration,
|
|
1564
|
+
messageLength: finalMessage.length,
|
|
1565
|
+
}, { agentId });
|
|
1566
|
+
if (!completionDecision.accepted) {
|
|
1567
|
+
const rejectedResult = {
|
|
1568
|
+
tool: 'task_complete',
|
|
1569
|
+
status: 'continue',
|
|
1570
|
+
reason: completionDecision.reason || 'The completion self-check determined more work is still required.',
|
|
1571
|
+
};
|
|
1572
|
+
messages.push({
|
|
1573
|
+
role: 'tool',
|
|
1574
|
+
name: toolName,
|
|
1575
|
+
tool_call_id: toolCall.id,
|
|
1576
|
+
content: JSON.stringify(rejectedResult),
|
|
1577
|
+
});
|
|
1578
|
+
messages.push({
|
|
1579
|
+
role: 'system',
|
|
1580
|
+
content: 'The task_complete signal was rejected by the run self-check. Continue autonomously with the next safe step, or provide a truthful blocker only if no safe next step remains.',
|
|
1581
|
+
});
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
console.info(
|
|
1585
|
+
`[Run ${shortenRunId(runId)}] task_complete accepted status=${completionDecision.status} iteration=${iteration}`
|
|
1586
|
+
);
|
|
1587
|
+
lastContent = finalMessage;
|
|
1588
|
+
directAnswerEligible = true;
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const repetitionGuard = engine.getRunMeta(runId)?.repetitionGuard;
|
|
1593
|
+
if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
|
|
1594
|
+
const blockedResult = {
|
|
1595
|
+
tool: toolName,
|
|
1596
|
+
status: 'blocked',
|
|
1597
|
+
reason: 'The same tool call already returned an unchanged result twice. Change the approach or complete with the available evidence.',
|
|
1598
|
+
};
|
|
1599
|
+
messages.push({
|
|
1600
|
+
role: 'tool',
|
|
1601
|
+
name: toolName,
|
|
1602
|
+
tool_call_id: toolCall.id,
|
|
1603
|
+
content: JSON.stringify(blockedResult),
|
|
1604
|
+
});
|
|
1605
|
+
engine.recordRunEvent(userId, runId, 'repetition_blocked', {
|
|
1606
|
+
toolName,
|
|
1607
|
+
toolArgs,
|
|
1608
|
+
}, { agentId });
|
|
1609
|
+
engine.emit(userId, 'run:tool_end', {
|
|
1610
|
+
runId,
|
|
1611
|
+
toolName,
|
|
1612
|
+
status: 'blocked',
|
|
1613
|
+
result: blockedResult,
|
|
1614
|
+
});
|
|
1615
|
+
messages.push({
|
|
1616
|
+
role: 'system',
|
|
1617
|
+
content: 'The repeated call was blocked because it made no progress. Use a different tool, change the arguments, or finish with a truthful result.',
|
|
1618
|
+
});
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// ── before_tool_call hook ──
|
|
1623
|
+
// Plugins can block a tool call (e.g. security policy) or mutate args.
|
|
1624
|
+
if (globalHooks.has('before_tool_call')) {
|
|
1625
|
+
const hookCtx = { toolName, toolArgs, runId, userId, agentId, iteration };
|
|
1626
|
+
const hookResult = await globalHooks.run('before_tool_call', hookCtx);
|
|
1627
|
+
if (hookResult.block) {
|
|
1628
|
+
const blockReason = hookResult.reason || 'Blocked by policy.';
|
|
1629
|
+
const blockedBy = hookResult.blocked_by || 'policy';
|
|
1630
|
+
console.warn(`[Run ${shortenRunId(runId)}] before_tool_call hook blocked tool=${toolName} reason="${blockReason}"`);
|
|
1631
|
+
messages.push({
|
|
1632
|
+
role: 'tool',
|
|
1633
|
+
name: toolName,
|
|
1634
|
+
tool_call_id: toolCall.id,
|
|
1635
|
+
content: JSON.stringify({ tool: toolName, status: 'blocked', reason: blockReason, blocked_by: blockedBy }),
|
|
1636
|
+
});
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
db.prepare('INSERT INTO agent_steps (id, run_id, step_index, type, description, status, tool_name, tool_input, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime(\'now\'))')
|
|
1643
|
+
.run(stepId, runId, stepIndex, engine.getStepType(toolName), `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)} `, 'running', toolName, JSON.stringify(toolArgs));
|
|
1644
|
+
engine.updateRunProgress(runId, {
|
|
1645
|
+
currentPhase: 'tool',
|
|
1646
|
+
currentStep: stepId,
|
|
1647
|
+
currentTool: toolName,
|
|
1648
|
+
currentStepStartedAt: isoNow(),
|
|
1649
|
+
}, {
|
|
1650
|
+
stepId,
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
engine.emit(userId, 'run:tool_start', {
|
|
1654
|
+
runId, stepId, stepIndex, toolName, toolArgs,
|
|
1655
|
+
type: engine.getStepType(toolName)
|
|
1656
|
+
});
|
|
1657
|
+
engine.recordRunEvent(userId, runId, 'tool_started', {
|
|
1658
|
+
stepIndex,
|
|
1659
|
+
toolName,
|
|
1660
|
+
toolArgs,
|
|
1661
|
+
type: engine.getStepType(toolName),
|
|
1662
|
+
}, { agentId, stepId });
|
|
1663
|
+
console.info(
|
|
1664
|
+
`[Run ${shortenRunId(runId)}] step=${stepIndex} start tool=${toolName} args=${summarizeForLog(toolArgs)}`
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
let toolResult;
|
|
1668
|
+
let toolErrorMessage = '';
|
|
1669
|
+
try {
|
|
1670
|
+
toolResult = await engine.executeTool(toolName, toolArgs, {
|
|
1671
|
+
userId,
|
|
1672
|
+
runId,
|
|
1673
|
+
agentId,
|
|
1674
|
+
app,
|
|
1675
|
+
triggerType,
|
|
1676
|
+
triggerSource,
|
|
1677
|
+
conversationId,
|
|
1678
|
+
source: options.source || null,
|
|
1679
|
+
chatId: options.chatId || null,
|
|
1680
|
+
taskId: options.taskId || null,
|
|
1681
|
+
widgetId: options.widgetId || null,
|
|
1682
|
+
deliveryState: options.deliveryState || null,
|
|
1683
|
+
allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
|
|
1684
|
+
allowExternalSideEffects: options.allowExternalSideEffects === true,
|
|
1685
|
+
});
|
|
1686
|
+
engine.detachProcessFromRun(runId, toolResult?.pid);
|
|
1687
|
+
toolErrorMessage = inferToolFailureMessage(toolName, toolResult);
|
|
1688
|
+
if (toolErrorMessage) {
|
|
1689
|
+
failedStepCount++;
|
|
1690
|
+
}
|
|
1691
|
+
const screenshotPath = toolResult?.screenshotPath || null;
|
|
1692
|
+
const stepStatus = engine.isRunStopped(runId) ? 'stopped' : (toolErrorMessage ? 'failed' : 'completed');
|
|
1693
|
+
db.prepare('UPDATE agent_steps SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
1694
|
+
.run(stepStatus, JSON.stringify(toolResult).slice(0, 20000), toolErrorMessage || null, screenshotPath, stepId);
|
|
1695
|
+
if (toolErrorMessage) {
|
|
1696
|
+
engine.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: toolErrorMessage, result: toolResult, screenshotPath, status: stepStatus });
|
|
1697
|
+
engine.recordRunEvent(userId, runId, 'tool_failed', {
|
|
1698
|
+
toolName,
|
|
1699
|
+
status: stepStatus,
|
|
1700
|
+
error: toolErrorMessage,
|
|
1701
|
+
durationMs: Date.now() - stepStartedAt,
|
|
1702
|
+
resultPreview: summarizeForLog(toolResult),
|
|
1703
|
+
}, { agentId, stepId });
|
|
1704
|
+
console.warn(
|
|
1705
|
+
`[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(toolErrorMessage, 160)}`
|
|
1706
|
+
);
|
|
1707
|
+
} else {
|
|
1708
|
+
engine.emit(userId, 'run:tool_end', { runId, stepId, toolName, result: toolResult, screenshotPath, status: stepStatus });
|
|
1709
|
+
engine.recordRunEvent(userId, runId, 'tool_completed', {
|
|
1710
|
+
toolName,
|
|
1711
|
+
status: stepStatus,
|
|
1712
|
+
durationMs: Date.now() - stepStartedAt,
|
|
1713
|
+
resultPreview: summarizeForLog(toolResult),
|
|
1714
|
+
}, { agentId, stepId });
|
|
1715
|
+
console.info(
|
|
1716
|
+
`[Run ${shortenRunId(runId)}] step=${stepIndex} done tool=${toolName} status=${stepStatus} durationMs=${Date.now() - stepStartedAt} result=${summarizeForLog(toolResult)}`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
toolResult = { error: err.message };
|
|
1721
|
+
toolErrorMessage = String(err.message || 'Tool execution failed');
|
|
1722
|
+
failedStepCount++;
|
|
1723
|
+
engine.detachProcessFromRun(runId, toolResult?.pid);
|
|
1724
|
+
db.prepare('UPDATE agent_steps SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
1725
|
+
.run('failed', err.message, stepId);
|
|
1726
|
+
engine.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: err.message, status: 'failed' });
|
|
1727
|
+
engine.recordRunEvent(userId, runId, 'tool_failed', {
|
|
1728
|
+
toolName,
|
|
1729
|
+
status: 'failed',
|
|
1730
|
+
error: err.message,
|
|
1731
|
+
durationMs: Date.now() - stepStartedAt,
|
|
1732
|
+
}, { agentId, stepId });
|
|
1733
|
+
console.warn(
|
|
1734
|
+
`[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(err.message, 160)}`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
|
|
1739
|
+
execution.input = toolArgs;
|
|
1740
|
+
if (execution.stateChanged && isProgressToolCall(toolName, toolArgs)) {
|
|
1741
|
+
iterationConcreteProgress = true;
|
|
1742
|
+
}
|
|
1743
|
+
repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1744
|
+
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
1745
|
+
toolExecutions.push(execution);
|
|
1746
|
+
if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
|
|
1747
|
+
for (const artifact of execution.artifacts) {
|
|
1748
|
+
if (!deliverableArtifacts.some((existing) => (
|
|
1749
|
+
(existing.path && artifact.path && existing.path === artifact.path)
|
|
1750
|
+
|| (existing.uri && artifact.uri && existing.uri === artifact.uri)
|
|
1751
|
+
))) {
|
|
1752
|
+
deliverableArtifacts.push(artifact);
|
|
1753
|
+
engine.recordRunEvent(userId, runId, 'deliverable_artifact_produced', {
|
|
1754
|
+
type: deliverableWorkflow.selection.type,
|
|
1755
|
+
toolName,
|
|
1756
|
+
artifact,
|
|
1757
|
+
}, { agentId, stepId });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
engine.persistRunMetadata(runId, {
|
|
1762
|
+
evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
|
|
1763
|
+
subagentState: engine.listSubagents(runId),
|
|
1764
|
+
deliverableArtifacts,
|
|
1765
|
+
compactionMetrics: compactionMetrics.slice(-20),
|
|
1766
|
+
});
|
|
1767
|
+
const modelPayload = compactPayloadForModel(toolName, toolResult);
|
|
1768
|
+
if (modelPayload.metrics?.applied) {
|
|
1769
|
+
const metric = {
|
|
1770
|
+
toolName,
|
|
1771
|
+
stepId,
|
|
1772
|
+
...modelPayload.metrics,
|
|
1773
|
+
createdAt: new Date().toISOString(),
|
|
1774
|
+
};
|
|
1775
|
+
compactionMetrics.push(metric);
|
|
1776
|
+
engine.persistRunMetadata(runId, {
|
|
1777
|
+
compactionMetrics: compactionMetrics.slice(-20),
|
|
1778
|
+
});
|
|
1779
|
+
engine.recordRunEvent(userId, runId, 'pre_model_compaction_applied', {
|
|
1780
|
+
toolName,
|
|
1781
|
+
metrics: modelPayload.metrics,
|
|
1782
|
+
}, { agentId, stepId });
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const toolResultLimits = resolveToolResultLimits(toolName, loopPolicy);
|
|
1786
|
+
const toolMessage = {
|
|
1787
|
+
role: 'tool',
|
|
1788
|
+
name: toolName,
|
|
1789
|
+
tool_call_id: toolCall.id,
|
|
1790
|
+
content: compactToolResult(toolName, toolArgs, modelPayload.result, {
|
|
1791
|
+
softLimit: toolResultLimits.softLimit,
|
|
1792
|
+
hardLimit: toolResultLimits.hardLimit,
|
|
1793
|
+
})
|
|
1794
|
+
};
|
|
1795
|
+
messages.push(toolMessage);
|
|
1796
|
+
if (toolName === 'activate_tools' && !toolErrorMessage) {
|
|
1797
|
+
tools = engine.getActiveTools(runId);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (toolErrorMessage) {
|
|
1801
|
+
consecutiveToolFailures += 1;
|
|
1802
|
+
const currentRunMeta = engine.getRunMeta(runId);
|
|
1803
|
+
trackErrorPattern(toolErrorMessage, currentRunMeta);
|
|
1804
|
+
const errorKey = normalizeErrorKey(toolErrorMessage);
|
|
1805
|
+
const errorCount = currentRunMeta?.errorPatterns?.get(errorKey) || 0;
|
|
1806
|
+
const patternGuide = buildErrorPatternGuidance(errorKey, errorCount);
|
|
1807
|
+
const alternativeTools = summarizeAvailableTools(tools, { exclude: toolName });
|
|
1808
|
+
messages.push({
|
|
1809
|
+
role: 'system',
|
|
1810
|
+
content: [
|
|
1811
|
+
`Tool "${toolName}" failed with error: ${summarizeForLog(toolErrorMessage, 240)}.`,
|
|
1812
|
+
'This tool failure is not, by itself, a user-facing blocker.',
|
|
1813
|
+
'Continue autonomously: retry with corrected arguments, try an alternative tool/path, or verify the outcome using other available tools.',
|
|
1814
|
+
alternativeTools ? `Other available tools in this run: ${alternativeTools}.` : '',
|
|
1815
|
+
patternGuide || '',
|
|
1816
|
+
'Only stop and tell the user you are blocked if the remaining issue truly requires an external dependency or user action outside this run.'
|
|
1817
|
+
].filter(Boolean).join(' ')
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
if (consecutiveToolFailures >= loopPolicy.maxConsecutiveToolFailures) {
|
|
1821
|
+
messages.push({
|
|
1822
|
+
role: 'system',
|
|
1823
|
+
content: `There were ${consecutiveToolFailures} consecutive tool failures. Stop calling tools now and return a clear blocker response that summarizes attempted actions and concrete errors.`
|
|
1824
|
+
});
|
|
1825
|
+
break;
|
|
1826
|
+
}
|
|
1827
|
+
} else {
|
|
1828
|
+
consecutiveToolFailures = 0;
|
|
1829
|
+
// Output fingerprint guard: steer away from re-fetching data already seen.
|
|
1830
|
+
if (!toolErrorMessage) {
|
|
1831
|
+
const currentRunMeta = engine.getRunMeta(runId);
|
|
1832
|
+
const fp = fingerprintOutput(toolName, toolResult, toolArgs);
|
|
1833
|
+
if (fp !== null && currentRunMeta?.seenOutputHashes) {
|
|
1834
|
+
const prior = currentRunMeta.seenOutputHashes.get(fp);
|
|
1835
|
+
if (prior) {
|
|
1836
|
+
messages.push({
|
|
1837
|
+
role: 'system',
|
|
1838
|
+
content: `DUPLICATE DATA: This response is identical to what "${prior.toolName}" returned in iteration ${prior.iteration}. You already have this information. Stop fetching and use what you have — proceed to the next concrete action.`,
|
|
1839
|
+
});
|
|
1840
|
+
} else {
|
|
1841
|
+
currentRunMeta.seenOutputHashes.set(fp, { toolName, iteration });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (toolName === 'send_interim_update') {
|
|
1848
|
+
messages.push({
|
|
1849
|
+
role: 'system',
|
|
1850
|
+
content: 'An interim user-visible update was already sent. Do not later output meta commentary about having already replied. When you have the final answer, give the answer itself. If you need to deliver that final answer to the user in messaging, use send_message.'
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (toolName === 'execute_command' && (toolResult?.timedOut || toolResult?.killed)) {
|
|
1855
|
+
messages.push({
|
|
1856
|
+
role: 'system',
|
|
1857
|
+
content: 'The previous shell command did not finish cleanly. Keep working until you rerun it with enough time or verify the requested outcome with follow-up commands.'
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (
|
|
1862
|
+
toolName === 'execute_command'
|
|
1863
|
+
&& toolResult?.exitCode !== undefined
|
|
1864
|
+
&& toolResult.exitCode !== 0
|
|
1865
|
+
) {
|
|
1866
|
+
messages.push({
|
|
1867
|
+
role: 'system',
|
|
1868
|
+
content: 'The previous shell command exited non-zero. Treat its output as partial evidence only. If it chained multiple shell segments, later segments may not have run. Do not summarize missing sections as observed facts; rerun or verify them separately first.'
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (conversationId) {
|
|
1873
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_call_id, name) VALUES (?, ?, ?, ?, ?)')
|
|
1874
|
+
.run(conversationId, 'tool', toolMessage.content, toolCall.id, toolName);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
engine.updateRunProgress(runId, {
|
|
1878
|
+
currentPhase: 'idle',
|
|
1879
|
+
currentStep: null,
|
|
1880
|
+
currentTool: null,
|
|
1881
|
+
currentStepStartedAt: null,
|
|
1882
|
+
}, {
|
|
1883
|
+
verified: true,
|
|
1884
|
+
stepId,
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
const runMeta = engine.activeRuns.get(runId);
|
|
1888
|
+
if (runMeta) {
|
|
1889
|
+
runMeta.lastToolName = toolName;
|
|
1890
|
+
runMeta.lastToolTarget = toolName === 'send_message' ? toolArgs.to : null;
|
|
1891
|
+
if (toolName === 'save_widget_snapshot' && !toolErrorMessage) {
|
|
1892
|
+
runMeta.widgetSnapshotSaved = true;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
if (toolName === 'save_widget_snapshot' && !toolErrorMessage) {
|
|
1897
|
+
lastContent = 'Widget snapshot updated.';
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (runMeta?.terminalInterim) {
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// Update analysis-paralysis counter after each iteration's tool calls.
|
|
1907
|
+
// Resets to 0 when any progress tool was called; otherwise increments.
|
|
1908
|
+
if (!directAnswerEligible && response?.toolCalls?.length > 0
|
|
1909
|
+
&& (analysis.mode === 'execute' || analysis.mode === 'plan_execute')) {
|
|
1910
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1911
|
+
if (iterMeta) {
|
|
1912
|
+
iterMeta.consecutiveReadOnlyIterations = iterationConcreteProgress
|
|
1913
|
+
? 0
|
|
1914
|
+
: (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (engine.isRunStopped(runId)) break;
|
|
1919
|
+
if (engine.getRunMeta(runId)?.terminalInterim) break;
|
|
1920
|
+
if (engine.getRunMeta(runId)?.widgetSnapshotSaved) break;
|
|
1921
|
+
if (!engine.activeRuns.has(runId)) break;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (engine.isRunStopped(runId)) {
|
|
1925
|
+
db.prepare('UPDATE agent_runs SET status = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
1926
|
+
.run('stopped', runId);
|
|
1927
|
+
console.warn(
|
|
1928
|
+
`[Run ${shortenRunId(runId)}] stopped trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
1929
|
+
);
|
|
1930
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
1931
|
+
engine.activeRuns.delete(runId);
|
|
1932
|
+
engine.emit(userId, 'run:stopped', { runId, triggerSource });
|
|
1933
|
+
engine.recordRunEvent(userId, runId, 'run_stopped', {
|
|
1934
|
+
triggerSource,
|
|
1935
|
+
totalTokens,
|
|
1936
|
+
iterations: iteration,
|
|
1937
|
+
}, { agentId });
|
|
1938
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const runMeta = engine.activeRuns.get(runId);
|
|
1942
|
+
if (runMeta?.terminalInterim) {
|
|
1943
|
+
lastContent = '';
|
|
1944
|
+
}
|
|
1945
|
+
if (runMeta?.widgetSnapshotSaved && !lastContent) {
|
|
1946
|
+
lastContent = 'Widget snapshot updated.';
|
|
1947
|
+
}
|
|
1948
|
+
const messagingSent = runMeta?.messagingSent || false;
|
|
1949
|
+
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
1950
|
+
|
|
1951
|
+
// Hermes _handle_max_iterations: if the run exhausted its step budget without a
|
|
1952
|
+
// judged completion, the model's last text is usually a mid-thought fragment
|
|
1953
|
+
// ("let me inline everything:"). Do one tool-less wrap-up call so the user gets a
|
|
1954
|
+
// real final answer instead of that fragment.
|
|
1955
|
+
const budgetExhaustedWithoutCompletion = triggerSource === 'messaging'
|
|
1956
|
+
&& !directAnswerEligible
|
|
1957
|
+
&& !messagingSent
|
|
1958
|
+
&& !runMeta?.terminalInterim
|
|
1959
|
+
&& iteration >= maxIterations;
|
|
1960
|
+
if (budgetExhaustedWithoutCompletion) {
|
|
1961
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
1962
|
+
try {
|
|
1963
|
+
const wrapResponse = await withModelCallTimeout(
|
|
1964
|
+
provider.chat(
|
|
1965
|
+
sanitizeConversationMessages([
|
|
1966
|
+
...messages,
|
|
1967
|
+
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
1968
|
+
]),
|
|
1969
|
+
[],
|
|
1970
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
1971
|
+
),
|
|
1972
|
+
options,
|
|
1973
|
+
'Max-iteration wrap-up',
|
|
1974
|
+
);
|
|
1975
|
+
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
1976
|
+
const wrapText = sanitizeModelOutput(wrapResponse.content || '', { model });
|
|
1977
|
+
// On budget exhaustion the model's last text is an untrustworthy mid-thought
|
|
1978
|
+
// fragment. Replace it with the wrap-up answer, or a clean deterministic
|
|
1979
|
+
// fallback if the wrap-up came back empty — never deliver the fragment.
|
|
1980
|
+
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null);
|
|
1981
|
+
lastContent = usableWrap
|
|
1982
|
+
? wrapText
|
|
1983
|
+
: buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1984
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1985
|
+
if (conversationId) {
|
|
1986
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1987
|
+
.run(conversationId, 'assistant', lastContent, usableWrap ? (wrapResponse.usage?.totalTokens || 0) : 0);
|
|
1988
|
+
}
|
|
1989
|
+
engine.recordRunEvent(userId, runId, 'max_iteration_wrapup_delivered', {
|
|
1990
|
+
iteration, maxIterations, stepIndex, source: usableWrap ? 'model' : 'deterministic',
|
|
1991
|
+
}, { agentId });
|
|
1992
|
+
} catch (wrapErr) {
|
|
1993
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1994
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1995
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
|
|
2000
|
+
// Simplified blank reply recovery: one model call with direct instruction,
|
|
2001
|
+
// then fall back to a deterministic message. No multi-attempt LLM loop.
|
|
2002
|
+
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery model=${model}`);
|
|
2003
|
+
let recoveredTokens = 0;
|
|
2004
|
+
try {
|
|
2005
|
+
const recoveryResponse = await withModelCallTimeout(
|
|
2006
|
+
provider.chat(
|
|
2007
|
+
sanitizeConversationMessages([
|
|
2008
|
+
...messages,
|
|
2009
|
+
{
|
|
2010
|
+
role: 'system',
|
|
2011
|
+
content: buildBlankMessagingReplyPrompt(1, options?.source || null)
|
|
2012
|
+
}
|
|
2013
|
+
]),
|
|
2014
|
+
[],
|
|
2015
|
+
{
|
|
2016
|
+
model,
|
|
2017
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2018
|
+
}
|
|
2019
|
+
),
|
|
2020
|
+
options,
|
|
2021
|
+
'Blank messaging reply recovery',
|
|
2022
|
+
);
|
|
2023
|
+
recoveredTokens = recoveryResponse.usage?.totalTokens || 0;
|
|
2024
|
+
lastContent = sanitizeModelOutput(recoveryResponse.content || '', { model });
|
|
2025
|
+
} catch (recoverErr) {
|
|
2026
|
+
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`);
|
|
2027
|
+
}
|
|
2028
|
+
totalTokens += recoveredTokens;
|
|
2029
|
+
// The loop has already exited, so we cannot keep working: deliver the model's
|
|
2030
|
+
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2031
|
+
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2032
|
+
// a deterministic message when the model returned nothing usable.
|
|
2033
|
+
const recoveredVisible = Boolean(normalizeOutgoingMessage(lastContent, options?.source || null));
|
|
2034
|
+
if (!recoveredVisible) {
|
|
2035
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2036
|
+
}
|
|
2037
|
+
if (normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
2038
|
+
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_delivered', {
|
|
2039
|
+
source: recoveredVisible ? 'model' : 'deterministic',
|
|
2040
|
+
stepIndex,
|
|
2041
|
+
failedStepCount,
|
|
2042
|
+
}, { agentId });
|
|
2043
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2044
|
+
if (conversationId) {
|
|
2045
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2046
|
+
.run(conversationId, 'assistant', lastContent, recoveredVisible ? recoveredTokens : 0);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (
|
|
2052
|
+
!normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
2053
|
+
&& !messagingSent
|
|
2054
|
+
&& runMeta?.widgetSnapshotSaved !== true
|
|
2055
|
+
) {
|
|
2056
|
+
const explicitNoResponse = (
|
|
2057
|
+
runMeta?.noResponse === true
|
|
2058
|
+
|| options.deliveryState?.noResponse === true
|
|
2059
|
+
);
|
|
2060
|
+
if (
|
|
2061
|
+
(triggerSource === 'schedule' || triggerSource === 'tasks')
|
|
2062
|
+
&& !explicitNoResponse
|
|
2063
|
+
) {
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
'Background run ended without producing a result or an explicit no-response decision.',
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
if (iteration >= maxIterations) {
|
|
2069
|
+
engine.recordRunEvent(userId, runId, 'loop_budget_exhausted', {
|
|
2070
|
+
maxIterations,
|
|
2071
|
+
stepIndex,
|
|
2072
|
+
failedStepCount,
|
|
2073
|
+
}, { agentId });
|
|
2074
|
+
if (triggerSource === 'messaging') {
|
|
2075
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2076
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2077
|
+
if (conversationId) {
|
|
2078
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2079
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
2080
|
+
}
|
|
2081
|
+
} else {
|
|
2082
|
+
throw new Error(`Iteration budget exhausted before judged completion after ${maxIterations} iterations.`);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
if (stepIndex > 0 && !lastToolWasMessaging && iteration < maxIterations) {
|
|
2086
|
+
if (triggerSource === 'messaging') {
|
|
2087
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2088
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2089
|
+
if (conversationId) {
|
|
2090
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2091
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
throw new Error('Run ended without an explicit completion or blocker reply.');
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
const sentMessageText = joinSentMessages(runMeta?.sentMessages);
|
|
2100
|
+
const normalizedLastContent = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
2101
|
+
let finalResponseText = messagingSent
|
|
2102
|
+
? (sentMessageText || (normalizedLastContent ? lastContent.trim() : ''))
|
|
2103
|
+
: (normalizedLastContent ? lastContent.trim() : sentMessageText);
|
|
2104
|
+
const lastFinalDeliveryMessage = normalizeOutgoingMessage(
|
|
2105
|
+
runMeta?.lastSentMessage
|
|
2106
|
+
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
2107
|
+
|| '',
|
|
2108
|
+
options?.source || null
|
|
2109
|
+
);
|
|
2110
|
+
|
|
2111
|
+
if (
|
|
2112
|
+
options.skipVerifier !== true
|
|
2113
|
+
&& shouldRunVerifier({
|
|
2114
|
+
analysis,
|
|
2115
|
+
toolExecutions,
|
|
2116
|
+
finalReply: finalResponseText,
|
|
2117
|
+
})) {
|
|
2118
|
+
const verificationResult = await runWithModelFallback('final verification', () => engine.verifyFinalResponse({
|
|
2119
|
+
provider,
|
|
2120
|
+
providerName,
|
|
2121
|
+
model,
|
|
2122
|
+
messages,
|
|
2123
|
+
analysis,
|
|
2124
|
+
tools,
|
|
2125
|
+
toolExecutions,
|
|
2126
|
+
finalReply: finalResponseText,
|
|
2127
|
+
options: { ...options, runId, userId, agentId },
|
|
2128
|
+
}));
|
|
2129
|
+
totalTokens += verificationResult.usage || 0;
|
|
2130
|
+
verification = verificationResult.verification;
|
|
2131
|
+
if (verification.final_reply) {
|
|
2132
|
+
finalResponseText = verification.final_reply;
|
|
2133
|
+
lastContent = verification.final_reply;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
stepIndex += 1;
|
|
2137
|
+
const verificationStepId = uuidv4();
|
|
2138
|
+
db.prepare(`INSERT INTO agent_steps
|
|
2139
|
+
(id, run_id, step_index, type, description, status, result, started_at, completed_at)
|
|
2140
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
2141
|
+
.run(
|
|
2142
|
+
verificationStepId,
|
|
2143
|
+
runId,
|
|
2144
|
+
stepIndex,
|
|
2145
|
+
'verification',
|
|
2146
|
+
'Evidence verification',
|
|
2147
|
+
verification.status === 'verified' ? 'completed' : 'failed',
|
|
2148
|
+
JSON.stringify(verification).slice(0, 20000)
|
|
2149
|
+
);
|
|
2150
|
+
engine.persistRunMetadata(runId, {
|
|
2151
|
+
verification,
|
|
2152
|
+
evidenceSources: verificationResult.evidenceSources,
|
|
2153
|
+
});
|
|
2154
|
+
engine.emit(userId, 'run:verification', {
|
|
2155
|
+
runId,
|
|
2156
|
+
...verification,
|
|
2157
|
+
evidenceSources: verificationResult.evidenceSources,
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (deliverableWorkflow && deliverablePlan) {
|
|
2162
|
+
engine.recordRunEvent(userId, runId, 'deliverable_validation_started', {
|
|
2163
|
+
type: deliverableWorkflow.selection.type,
|
|
2164
|
+
artifactCount: deliverableArtifacts.length,
|
|
2165
|
+
}, { agentId });
|
|
2166
|
+
const validationResult = await validateDeliverableExecution({
|
|
2167
|
+
workflow: deliverableWorkflow.workflow,
|
|
2168
|
+
request: deliverableWorkflow.request,
|
|
2169
|
+
plan: deliverablePlan,
|
|
2170
|
+
finalReply: finalResponseText,
|
|
2171
|
+
artifacts: deliverableArtifacts,
|
|
2172
|
+
toolExecutions,
|
|
2173
|
+
runId,
|
|
2174
|
+
});
|
|
2175
|
+
deliverableValidation = validationResult.validation;
|
|
2176
|
+
engine.persistRunMetadata(runId, {
|
|
2177
|
+
deliverable: validationResult.result,
|
|
2178
|
+
});
|
|
2179
|
+
if (deliverableValidation.status !== 'passed') {
|
|
2180
|
+
engine.recordRunEvent(userId, runId, 'deliverable_validation_failed', {
|
|
2181
|
+
type: deliverableWorkflow.selection.type,
|
|
2182
|
+
errors: deliverableValidation.errors,
|
|
2183
|
+
warnings: deliverableValidation.warnings,
|
|
2184
|
+
}, { agentId });
|
|
2185
|
+
throw new DeliverableValidationError(
|
|
2186
|
+
deliverableValidation.summary || `Deliverable validation failed for ${deliverableWorkflow.selection.type}.`,
|
|
2187
|
+
{
|
|
2188
|
+
validation: deliverableValidation,
|
|
2189
|
+
result: validationResult.result,
|
|
2190
|
+
},
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
await engine.persistDeliverableMemory(userId, runId, agentId, validationResult.result);
|
|
2194
|
+
engine.recordRunEvent(userId, runId, 'deliverable_completed', {
|
|
2195
|
+
type: deliverableWorkflow.selection.type,
|
|
2196
|
+
artifactCount: validationResult.result.artifacts.length,
|
|
2197
|
+
summary: validationResult.result.summary,
|
|
2198
|
+
}, { agentId });
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (conversationId) {
|
|
2202
|
+
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2203
|
+
.run(totalTokens, conversationId);
|
|
2204
|
+
if (options.skipConversationMaintenance !== true) {
|
|
2205
|
+
refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
|
|
2206
|
+
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
await engine.persistPromptMetrics(runId, {
|
|
2212
|
+
...promptMetrics,
|
|
2213
|
+
finalTotalTokens: totalTokens
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
await engine.persistRunContext(userId, {
|
|
2217
|
+
triggerSource,
|
|
2218
|
+
runTitle,
|
|
2219
|
+
userMessage,
|
|
2220
|
+
lastContent: finalResponseText,
|
|
2221
|
+
stepIndex,
|
|
2222
|
+
skipPersistence: options.skipRunContextPersistence === true
|
|
2223
|
+
});
|
|
2224
|
+
|
|
2225
|
+
// Fallback: if this was a messaging-triggered run and no final delivery
|
|
2226
|
+
// was already sent in this run, auto-send the final assistant text.
|
|
2227
|
+
// Interim progress updates do not suppress this final delivery.
|
|
2228
|
+
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
2229
|
+
if (engine.shouldSendMessagingFinalFallback(runMeta, lastContent || '', options.source) && !lastFinalDeliveryMessage) {
|
|
2230
|
+
await engine.deliverMessagingFinalFallback({
|
|
2231
|
+
runId,
|
|
2232
|
+
userId,
|
|
2233
|
+
agentId,
|
|
2234
|
+
platform: options.source,
|
|
2235
|
+
chatId: options.chatId,
|
|
2236
|
+
content: lastContent || '',
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
db.prepare('UPDATE agent_runs SET status = ?, total_tokens = ?, final_response = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
2242
|
+
.run('completed', totalTokens, finalResponseText || null, runId);
|
|
2243
|
+
|
|
2244
|
+
if (conversationId && options.skipConversationMaintenance !== true) {
|
|
2245
|
+
await engine.refreshConversationState({
|
|
2246
|
+
conversationId,
|
|
2247
|
+
runId,
|
|
2248
|
+
provider,
|
|
2249
|
+
providerName,
|
|
2250
|
+
model,
|
|
2251
|
+
finalReply: finalResponseText,
|
|
2252
|
+
analysis,
|
|
2253
|
+
verification,
|
|
2254
|
+
historyWindow,
|
|
2255
|
+
options: { ...options, userId, agentId },
|
|
2256
|
+
}).catch((err) => {
|
|
2257
|
+
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
console.info(
|
|
2262
|
+
`[Run ${shortenRunId(runId)}] completed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} durationMs=${runMeta?.startedAt ? Date.now() - runMeta.startedAt : 0} finalResponse=${finalResponseText ? 'yes' : 'no'} sentMessages=${runMeta?.sentMessages?.length || 0}`
|
|
2263
|
+
);
|
|
2264
|
+
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2265
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
2266
|
+
engine.activeRuns.delete(runId);
|
|
2267
|
+
engine.emit(userId, 'run:complete', {
|
|
2268
|
+
runId,
|
|
2269
|
+
content: lastContent,
|
|
2270
|
+
totalTokens,
|
|
2271
|
+
iterations: iteration,
|
|
2272
|
+
triggerSource,
|
|
2273
|
+
executionMode: analysis?.mode || 'execute',
|
|
2274
|
+
verificationStatus: verification?.status || 'skipped',
|
|
2275
|
+
});
|
|
2276
|
+
engine.recordRunEvent(userId, runId, 'run_completed', {
|
|
2277
|
+
contentPreview: String(finalResponseText || lastContent || '').slice(0, 240),
|
|
2278
|
+
totalTokens,
|
|
2279
|
+
iterations: iteration,
|
|
2280
|
+
triggerSource,
|
|
2281
|
+
executionMode: analysis?.mode || 'execute',
|
|
2282
|
+
verificationStatus: verification?.status || 'skipped',
|
|
2283
|
+
}, { agentId });
|
|
2284
|
+
// ── on_loop_end hook ──
|
|
2285
|
+
// Fire-and-forget: plugins can use this for self-improvement, memory
|
|
2286
|
+
// consolidation, analytics, or other post-run housekeeping.
|
|
2287
|
+
if (globalHooks.has('on_loop_end')) {
|
|
2288
|
+
globalHooks.run('on_loop_end', {
|
|
2289
|
+
userId, runId, agentId, status: 'completed',
|
|
2290
|
+
iterations: iteration, totalTokens,
|
|
2291
|
+
taskAnalysis: analysis,
|
|
2292
|
+
finalContent: finalResponseText,
|
|
2293
|
+
}).catch(() => {});
|
|
2294
|
+
}
|
|
2295
|
+
if (engine.learningManager) {
|
|
2296
|
+
try {
|
|
2297
|
+
const learningSteps = db.prepare(
|
|
2298
|
+
`SELECT tool_name, tool_input, result, status
|
|
2299
|
+
FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC`
|
|
2300
|
+
).all(runId);
|
|
2301
|
+
engine.learningManager.maybeCaptureDraft({
|
|
2302
|
+
userId,
|
|
2303
|
+
agentId,
|
|
2304
|
+
runId,
|
|
2305
|
+
triggerSource,
|
|
2306
|
+
triggerType,
|
|
2307
|
+
task: userMessage,
|
|
2308
|
+
title: runTitle,
|
|
2309
|
+
finalContent: finalResponseText || lastContent,
|
|
2310
|
+
steps: learningSteps,
|
|
2311
|
+
});
|
|
2312
|
+
} catch (learningError) {
|
|
2313
|
+
console.warn('[Engine] Skill reflection failed:', learningError.message);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
|
|
2318
|
+
} catch (err) {
|
|
2319
|
+
if (engine.isRunStopped(runId)) {
|
|
2320
|
+
db.prepare('UPDATE agent_runs SET status = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
2321
|
+
.run('stopped', runId);
|
|
2322
|
+
console.warn(
|
|
2323
|
+
`[Run ${shortenRunId(runId)}] stopped trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2324
|
+
);
|
|
2325
|
+
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2326
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
2327
|
+
engine.activeRuns.delete(runId);
|
|
2328
|
+
engine.emit(userId, 'run:stopped', { runId, triggerSource });
|
|
2329
|
+
engine.recordRunEvent(userId, runId, 'run_stopped', {
|
|
2330
|
+
triggerSource,
|
|
2331
|
+
totalTokens,
|
|
2332
|
+
iterations: iteration,
|
|
2333
|
+
}, { agentId });
|
|
2334
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
const runMeta = engine.activeRuns.get(runId);
|
|
2338
|
+
|
|
2339
|
+
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2340
|
+
|| err?.deliverableValidation?.summary
|
|
2341
|
+
|| '';
|
|
2342
|
+
let messagingFailureContent = '';
|
|
2343
|
+
let sendSucceeded = false;
|
|
2344
|
+
if (shouldSendMessagingErrorFallback({ triggerSource, options, runMeta })) {
|
|
2345
|
+
const manager = engine.messagingManager;
|
|
2346
|
+
if (manager) {
|
|
2347
|
+
const failureScenario = buildMessagingFailureScenario({
|
|
2348
|
+
err,
|
|
2349
|
+
failedStepCount,
|
|
2350
|
+
stepIndex,
|
|
2351
|
+
toolExecutions,
|
|
2352
|
+
});
|
|
2353
|
+
try {
|
|
2354
|
+
const failedMessage = sanitizeConversationMessages([
|
|
2355
|
+
...messages,
|
|
2356
|
+
{
|
|
2357
|
+
role: 'system',
|
|
2358
|
+
content: `The run encountered a runtime error and cannot continue reliably. Use the actual run scenario below to explain the blocker naturally.\n\nScenario:\n${failureScenario || 'No additional scenario details were captured.'}\n\nDo not call tools. Write exactly one short user message. Do not ask the user to resend or restate the same task. Only ask the user for something if a specific external input, permission, or configuration change is actually required. Do not promise future work unless it will happen automatically before this reply is sent.\n\n${buildPlatformFormattingGuide(options?.source || null)}`
|
|
2359
|
+
}
|
|
2360
|
+
]);
|
|
2361
|
+
const modelReply = await withModelCallTimeout(
|
|
2362
|
+
provider.chat(failedMessage, [], {
|
|
2363
|
+
model,
|
|
2364
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2365
|
+
}),
|
|
2366
|
+
options,
|
|
2367
|
+
'Messaging failure reply',
|
|
2368
|
+
);
|
|
2369
|
+
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2370
|
+
if (normalizeOutgoingMessage(drafted, options?.source || null)) {
|
|
2371
|
+
messagingFailureContent = drafted.trim();
|
|
2372
|
+
}
|
|
2373
|
+
} catch {
|
|
2374
|
+
// Fall back to deterministic text below.
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (!messagingFailureContent) {
|
|
2378
|
+
messagingFailureContent = buildDeterministicMessagingErrorReply({
|
|
2379
|
+
err,
|
|
2380
|
+
failedStepCount,
|
|
2381
|
+
stepIndex,
|
|
2382
|
+
toolExecutions,
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
try {
|
|
2387
|
+
const deliveryResult = await manager.sendMessage(
|
|
2388
|
+
userId,
|
|
2389
|
+
options.source,
|
|
2390
|
+
options.chatId,
|
|
2391
|
+
messagingFailureContent,
|
|
2392
|
+
{ runId, agentId },
|
|
2393
|
+
);
|
|
2394
|
+
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
2395
|
+
sendSucceeded = true;
|
|
2396
|
+
if (runMeta) {
|
|
2397
|
+
runMeta.lastSentMessage = messagingFailureContent;
|
|
2398
|
+
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
2399
|
+
runMeta.sentMessages.push(messagingFailureContent);
|
|
2400
|
+
}
|
|
2401
|
+
engine.markRunFinalDelivery(runId, messagingFailureContent);
|
|
2402
|
+
} catch (sendErr) {
|
|
2403
|
+
console.error('[Engine] Messaging error fallback failed:', sendErr.message);
|
|
2404
|
+
messagingFailureContent = '';
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
db.prepare('UPDATE agent_runs SET status = ?, error = ?, final_response = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2410
|
+
.run(
|
|
2411
|
+
'failed',
|
|
2412
|
+
err.message,
|
|
2413
|
+
sendSucceeded
|
|
2414
|
+
? (messagingFailureContent || null)
|
|
2415
|
+
: (deliverableFailureResponse || null),
|
|
2416
|
+
runId,
|
|
2417
|
+
);
|
|
2418
|
+
console.error(
|
|
2419
|
+
`[Run ${shortenRunId(runId)}] failed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} error=${summarizeForLog(err.message, 180)}`
|
|
2420
|
+
);
|
|
2421
|
+
|
|
2422
|
+
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2423
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
2424
|
+
engine.activeRuns.delete(runId);
|
|
2425
|
+
engine.emit(userId, 'run:error', { runId, error: err.message });
|
|
2426
|
+
engine.recordRunEvent(userId, runId, 'run_failed', {
|
|
2427
|
+
error: err.message,
|
|
2428
|
+
totalTokens,
|
|
2429
|
+
iterations: iteration,
|
|
2430
|
+
deliverableType: deliverableWorkflow?.selection?.type || null,
|
|
2431
|
+
}, { agentId });
|
|
2432
|
+
|
|
2433
|
+
if (messagingFailureContent) {
|
|
2434
|
+
return {
|
|
2435
|
+
runId,
|
|
2436
|
+
content: messagingFailureContent,
|
|
2437
|
+
totalTokens,
|
|
2438
|
+
iterations: iteration,
|
|
2439
|
+
status: 'failed'
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
throw err;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
module.exports = { runConversation };
|