neoagent 2.5.2-beta.3 → 2.5.2-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/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 +1 -0
- package/server/services/ai/engine.js +343 -599
- package/server/services/ai/tools.js +42 -2
- package/server/services/messaging/manager.js +7 -0
- package/server/services/runtime/backends/local-vm.js +7 -7
|
@@ -117,6 +117,7 @@ const MESSAGING_PROGRESS_REPEAT_MS = 90 * 1000;
|
|
|
117
117
|
const MESSAGING_PROGRESS_STALL_MS = 240 * 1000;
|
|
118
118
|
const MESSAGING_PROGRESS_TICK_MS = 15 * 1000;
|
|
119
119
|
const GOAL_CONTRACT_SUCCESS_CRITERIA_LIMIT = 12;
|
|
120
|
+
const MODEL_CALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
120
121
|
|
|
121
122
|
function isoNow() {
|
|
122
123
|
return new Date().toISOString();
|
|
@@ -136,6 +137,62 @@ function formatElapsedDuration(durationMs) {
|
|
|
136
137
|
return `${minutes}m ${seconds}s`;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
function normalizeErrorKey(errorMsg) {
|
|
141
|
+
const msg = String(errorMsg || '').toLowerCase();
|
|
142
|
+
if (/outside.*(workspace|per-user)/i.test(msg)) return 'outside_workspace';
|
|
143
|
+
if (/eisdir|illegal operation on a directory/i.test(msg)) return 'eisdir';
|
|
144
|
+
if (/enoent|no such file/i.test(msg)) return 'enoent';
|
|
145
|
+
if (/can.?t cd to|no such directory/i.test(msg)) return 'bad_cwd';
|
|
146
|
+
if (/not found/i.test(msg)) return 'not_found';
|
|
147
|
+
return msg.slice(0, 60);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function trackErrorPattern(errorMsg, runMeta) {
|
|
151
|
+
if (!errorMsg) return;
|
|
152
|
+
const key = normalizeErrorKey(errorMsg);
|
|
153
|
+
if (!runMeta.errorPatterns) runMeta.errorPatterns = new Map();
|
|
154
|
+
runMeta.errorPatterns.set(key, (runMeta.errorPatterns.get(key) || 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildErrorPatternGuidance(key, count) {
|
|
158
|
+
if (count < 3) return null;
|
|
159
|
+
const guides = {
|
|
160
|
+
outside_workspace: 'read_file cannot access /tmp paths. Use execute_command with `cat <path>` instead.',
|
|
161
|
+
eisdir: 'That path is a directory, not a file. Use list_directory or execute_command with `ls` to inspect it.',
|
|
162
|
+
enoent: 'That path does not exist. Use execute_command with `find . -name "..."` to locate the correct path first.',
|
|
163
|
+
bad_cwd: 'The VM home directory is not ~/. Use absolute paths starting from /tmp or discover the workspace root first.',
|
|
164
|
+
not_found: 'This path or resource was not found. Try listing the parent directory or checking with a broader search first.',
|
|
165
|
+
};
|
|
166
|
+
const guide = guides[key];
|
|
167
|
+
if (!guide) return null;
|
|
168
|
+
return `REPEATED ERROR (${count}×): ${guide}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveModelCallTimeoutMs(options = {}) {
|
|
172
|
+
const requested = Number(options?.modelCallTimeoutMs);
|
|
173
|
+
if (Number.isFinite(requested) && requested > 0) {
|
|
174
|
+
return Math.max(10, requested);
|
|
175
|
+
}
|
|
176
|
+
return MODEL_CALL_TIMEOUT_MS;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function withModelCallTimeout(promise, options = {}, label = 'Model call') {
|
|
180
|
+
const timeoutMs = resolveModelCallTimeoutMs(options);
|
|
181
|
+
let timer = null;
|
|
182
|
+
const timeout = new Promise((_, reject) => {
|
|
183
|
+
timer = setTimeout(() => {
|
|
184
|
+
const error = new Error(`${label} timed out after ${formatElapsedDuration(timeoutMs)}.`);
|
|
185
|
+
error.code = 'MODEL_CALL_TIMEOUT';
|
|
186
|
+
reject(error);
|
|
187
|
+
}, timeoutMs);
|
|
188
|
+
});
|
|
189
|
+
try {
|
|
190
|
+
return await Promise.race([Promise.resolve(promise), timeout]);
|
|
191
|
+
} finally {
|
|
192
|
+
if (timer) clearTimeout(timer);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
139
196
|
function cloneInterimHistory(history = []) {
|
|
140
197
|
if (!Array.isArray(history)) return [];
|
|
141
198
|
return history.map((item) => ({
|
|
@@ -187,6 +244,23 @@ function hasVisibleInterimActivity(runMeta) {
|
|
|
187
244
|
);
|
|
188
245
|
}
|
|
189
246
|
|
|
247
|
+
function requireSuccessfulMessagingDelivery(result, label = 'Messaging delivery') {
|
|
248
|
+
if (result?.success === true && result?.suppressed !== true) {
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
const reason = String(
|
|
252
|
+
result?.error
|
|
253
|
+
|| result?.reason
|
|
254
|
+
|| result?.result?.error
|
|
255
|
+
|| result?.result?.reason
|
|
256
|
+
|| 'the platform did not confirm delivery',
|
|
257
|
+
).trim();
|
|
258
|
+
const error = new Error(`${label} failed: ${reason}`);
|
|
259
|
+
error.code = 'MESSAGING_DELIVERY_FAILED';
|
|
260
|
+
error.deliveryResult = result || null;
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
190
264
|
function normalizeGoalCriteria(value) {
|
|
191
265
|
if (!Array.isArray(value)) return [];
|
|
192
266
|
const seen = new Set();
|
|
@@ -257,7 +331,7 @@ function mergeGoalContracts(existing = null, patch = null) {
|
|
|
257
331
|
const nextPatch = normalizeGoalContract(patch) || null;
|
|
258
332
|
if (!current && !nextPatch) return null;
|
|
259
333
|
|
|
260
|
-
const goal = String(
|
|
334
|
+
const goal = String(current?.goal || nextPatch?.goal || '').trim();
|
|
261
335
|
const successCriteria = normalizeGoalCriteria([
|
|
262
336
|
...(current?.successCriteria || []),
|
|
263
337
|
...(nextPatch?.successCriteria || []),
|
|
@@ -363,7 +437,6 @@ function resolveRunGoalContext(runMeta, analysis = null, plan = null) {
|
|
|
363
437
|
}
|
|
364
438
|
|
|
365
439
|
function buildCompletionDecisionPrompt({
|
|
366
|
-
mode,
|
|
367
440
|
triggerSource,
|
|
368
441
|
messagingSent = false,
|
|
369
442
|
goalContext,
|
|
@@ -373,52 +446,28 @@ function buildCompletionDecisionPrompt({
|
|
|
373
446
|
lastReply,
|
|
374
447
|
iteration,
|
|
375
448
|
maxIterations,
|
|
376
|
-
progressSummary = '',
|
|
377
|
-
platform = null,
|
|
378
449
|
}) {
|
|
379
|
-
const draftReply =
|
|
380
|
-
? (normalizeOutgoingMessage(lastReply || '', platform, { collapseWhitespace: false })
|
|
381
|
-
? String(lastReply || '').trim()
|
|
382
|
-
: '')
|
|
383
|
-
: normalizeOutgoingMessage(lastReply) || '';
|
|
450
|
+
const draftReply = normalizeOutgoingMessage(lastReply) || '';
|
|
384
451
|
const lines = [
|
|
385
452
|
'Return JSON only.',
|
|
453
|
+
'Decide whether this run should continue autonomously or stop now.',
|
|
454
|
+
'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason"}',
|
|
455
|
+
'Rules:',
|
|
456
|
+
'- Use "continue" whenever any safe next step remains in this same run.',
|
|
457
|
+
'- Use "complete" only when the requested outcome is actually achieved and the latest draft is the finished user-facing answer.',
|
|
458
|
+
'- Use "blocked" only when a specific external dependency, missing user input, or permission outside this run is required and the latest draft is the blocker reply.',
|
|
459
|
+
'- If the latest draft asks the user for a missing required value, confirmation, or choice needed to proceed, use "blocked" so the run waits instead of repeating the same ask.',
|
|
460
|
+
'- A progress note, next-step note, apology, plan, or promise to investigate is "continue", not "complete".',
|
|
461
|
+
'- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
|
|
462
|
+
'- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
|
|
463
|
+
`- If completion_confidence_required is ${goalContext.effectiveCompletionConfidence} and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.`,
|
|
464
|
+
triggerSource === 'messaging' && messagingSent
|
|
465
|
+
? '- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.'
|
|
466
|
+
: triggerSource === 'messaging'
|
|
467
|
+
? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.'
|
|
468
|
+
: '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
|
|
386
469
|
];
|
|
387
470
|
|
|
388
|
-
if (mode === 'messaging') {
|
|
389
|
-
lines.push(
|
|
390
|
-
'A messaging run is about to stop after sending user-visible progress, but no final delivery has happened yet.',
|
|
391
|
-
'Decide whether the run should keep working, finish with the completed result now, or stop with one blocker reply now.',
|
|
392
|
-
'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason","final_reply":"string"}',
|
|
393
|
-
'Rules:',
|
|
394
|
-
'- Use "continue" whenever any safe next step remains in this same run.',
|
|
395
|
-
'- Use "complete" only when the requested outcome is actually achieved and final_reply is the finished user-facing answer to send now.',
|
|
396
|
-
'- Use "blocked" only when a specific external dependency, missing user input, or permission outside this run is required and final_reply is the concise blocker reply to send now.',
|
|
397
|
-
'- A progress note, next-step note, apology, plan, or "I will investigate" draft is "continue", not "complete" and not "blocked".',
|
|
398
|
-
'- If user-visible progress was already sent and no final delivery exists yet, do not stop silently and do not stop on a status-only draft.',
|
|
399
|
-
'- final_reply must be empty when status is "continue".',
|
|
400
|
-
);
|
|
401
|
-
} else {
|
|
402
|
-
lines.push(
|
|
403
|
-
'Decide whether this run should continue autonomously or stop now.',
|
|
404
|
-
'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason"}',
|
|
405
|
-
'Rules:',
|
|
406
|
-
'- Use "continue" whenever any safe next step remains in this same run.',
|
|
407
|
-
'- Use "complete" only when the requested outcome is actually achieved or a truthful final user reply is already ready now.',
|
|
408
|
-
'- Use "blocked" only when a specific external dependency outside this run is required.',
|
|
409
|
-
'- If the latest draft asks the user for a missing required value, confirmation, or choice needed to proceed, use "blocked" so the run waits instead of repeating the same ask.',
|
|
410
|
-
'- A progress update is not complete.',
|
|
411
|
-
'- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
|
|
412
|
-
'- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
|
|
413
|
-
`- If completion_confidence_required is ${goalContext.effectiveCompletionConfidence} and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.`,
|
|
414
|
-
triggerSource === 'messaging' && messagingSent
|
|
415
|
-
? '- A reply was already delivered to the user via send_message. Use "complete" unless there is concrete remaining work (e.g., a tool call you still need to make) before the task is truly done. Do not send follow-up elaborations or re-introductions.'
|
|
416
|
-
: triggerSource === 'messaging'
|
|
417
|
-
? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked. If you already asked for missing user input, choose "blocked" and wait.'
|
|
418
|
-
: '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
471
|
lines.push(
|
|
423
472
|
goalContext.effectiveGoal ? `Goal: ${goalContext.effectiveGoal}` : '',
|
|
424
473
|
goalContext.persistedGoalPrompt,
|
|
@@ -428,44 +477,14 @@ function buildCompletionDecisionPrompt({
|
|
|
428
477
|
: '',
|
|
429
478
|
`Current iteration: ${iteration} of ${maxIterations}.`,
|
|
430
479
|
`Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
|
|
431
|
-
mode === 'messaging' && progressSummary ? `Progress ledger: ${progressSummary}` : '',
|
|
432
480
|
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 8) || 'none'}`,
|
|
433
481
|
`Latest draft reply:\n${draftReply || '(empty)'}`,
|
|
434
|
-
mode === 'messaging' ? buildPlatformFormattingGuide(platform) : '',
|
|
435
482
|
);
|
|
436
483
|
return lines.filter(Boolean).join('\n');
|
|
437
484
|
}
|
|
438
485
|
|
|
439
|
-
function normalizeCompletionDecision(raw, {
|
|
440
|
-
mode,
|
|
441
|
-
fallbackStatus = 'continue',
|
|
442
|
-
platform = null,
|
|
443
|
-
draftReply = '',
|
|
444
|
-
}) {
|
|
486
|
+
function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
|
|
445
487
|
const allowed = new Set(['continue', 'complete', 'blocked']);
|
|
446
|
-
if (mode === 'messaging') {
|
|
447
|
-
let status = allowed.has(String(raw.status || '').trim().toLowerCase())
|
|
448
|
-
? String(raw.status || '').trim().toLowerCase()
|
|
449
|
-
: 'continue';
|
|
450
|
-
let finalReply = normalizeOutgoingMessage(raw.final_reply || '', platform, {
|
|
451
|
-
collapseWhitespace: false,
|
|
452
|
-
})
|
|
453
|
-
? String(raw.final_reply || '').trim()
|
|
454
|
-
: '';
|
|
455
|
-
if (status === 'continue') {
|
|
456
|
-
finalReply = '';
|
|
457
|
-
} else if (!finalReply && draftReply) {
|
|
458
|
-
finalReply = draftReply;
|
|
459
|
-
} else if (!finalReply) {
|
|
460
|
-
status = 'continue';
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
status,
|
|
464
|
-
reason: String(raw.reason || '').trim().slice(0, 400),
|
|
465
|
-
final_reply: finalReply,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
488
|
const requestedStatus = String(raw.status || '').trim().toLowerCase();
|
|
470
489
|
return {
|
|
471
490
|
status: allowed.has(requestedStatus) ? requestedStatus : fallbackStatus,
|
|
@@ -473,16 +492,6 @@ function normalizeCompletionDecision(raw, {
|
|
|
473
492
|
};
|
|
474
493
|
}
|
|
475
494
|
|
|
476
|
-
function shouldRequireMessagingFinalityCheck(runMeta) {
|
|
477
|
-
return Boolean(
|
|
478
|
-
runMeta
|
|
479
|
-
&& runMeta.triggerSource === 'messaging'
|
|
480
|
-
&& runMeta.finalDeliverySent !== true
|
|
481
|
-
&& !runMeta.terminalInterim
|
|
482
|
-
&& hasVisibleInterimActivity(runMeta)
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
495
|
function planningDepthForForceMode(forceMode) {
|
|
487
496
|
return forceMode === 'plan_execute' ? 'deep' : 'light';
|
|
488
497
|
}
|
|
@@ -706,6 +715,7 @@ class AgentEngine {
|
|
|
706
715
|
this.taskRuntime = services.taskRuntime || null;
|
|
707
716
|
this.memoryManager = services.memoryManager || null;
|
|
708
717
|
this.voiceRuntimeManager = services.voiceRuntimeManager || null;
|
|
718
|
+
this.messagingDeliveryRetry = services.messagingDeliveryRetry || {};
|
|
709
719
|
}
|
|
710
720
|
|
|
711
721
|
async buildSystemPrompt(userId, context = {}) {
|
|
@@ -926,21 +936,6 @@ class AgentEngine {
|
|
|
926
936
|
.run(JSON.stringify(next), runId);
|
|
927
937
|
}
|
|
928
938
|
|
|
929
|
-
replaceLatestConversationAssistantMessage(conversationId, content) {
|
|
930
|
-
if (!conversationId) return false;
|
|
931
|
-
const messageId = db.prepare(
|
|
932
|
-
`SELECT id
|
|
933
|
-
FROM conversation_messages
|
|
934
|
-
WHERE conversation_id = ? AND role = 'assistant'
|
|
935
|
-
ORDER BY id DESC
|
|
936
|
-
LIMIT 1`
|
|
937
|
-
).get(conversationId)?.id;
|
|
938
|
-
if (!messageId) return false;
|
|
939
|
-
db.prepare('UPDATE conversation_messages SET content = ? WHERE id = ?')
|
|
940
|
-
.run(content, messageId);
|
|
941
|
-
return true;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
939
|
updateRunGoalContract(runId, patch = {}, options = {}) {
|
|
945
940
|
const runMeta = this.getRunMeta(runId);
|
|
946
941
|
if (!runMeta) return null;
|
|
@@ -1031,6 +1026,7 @@ class AgentEngine {
|
|
|
1031
1026
|
markRunFinalDelivery(runId, content = '', timestamp = isoNow()) {
|
|
1032
1027
|
const runMeta = this.getRunMeta(runId);
|
|
1033
1028
|
if (!runMeta) return null;
|
|
1029
|
+
runMeta.messagingSent = true;
|
|
1034
1030
|
runMeta.finalDeliverySent = true;
|
|
1035
1031
|
runMeta.lastSentMessage = String(content || '').trim() || runMeta.lastSentMessage || '';
|
|
1036
1032
|
const ledger = this.updateRunProgress(runId, {
|
|
@@ -1142,13 +1138,14 @@ class AgentEngine {
|
|
|
1142
1138
|
if (!platform || !chatId || !this.messagingManager) {
|
|
1143
1139
|
return { sent: false, skipped: true, reason: 'Messaging context is not available.' };
|
|
1144
1140
|
}
|
|
1145
|
-
await this.messagingManager.sendMessage(userId, platform, chatId, normalizedContent, {
|
|
1141
|
+
const deliveryResult = await this.messagingManager.sendMessage(userId, platform, chatId, normalizedContent, {
|
|
1146
1142
|
agentId,
|
|
1147
1143
|
runId,
|
|
1148
1144
|
persistConversation: true,
|
|
1149
1145
|
metadata,
|
|
1150
1146
|
deliveryKind: 'interim',
|
|
1151
1147
|
});
|
|
1148
|
+
requireSuccessfulMessagingDelivery(deliveryResult, 'Interim messaging delivery');
|
|
1152
1149
|
} else if (triggerSource === 'voice_live') {
|
|
1153
1150
|
const voiceSessionId = runMeta.voiceSessionId || null;
|
|
1154
1151
|
const manager = this.voiceRuntimeManager || this.app?.locals?.voiceRuntimeManager || null;
|
|
@@ -1242,42 +1239,72 @@ class AgentEngine {
|
|
|
1242
1239
|
phase = 'structured',
|
|
1243
1240
|
}) {
|
|
1244
1241
|
const startedAt = Date.now();
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
{
|
|
1253
|
-
model,
|
|
1254
|
-
maxTokens,
|
|
1255
|
-
reasoningEffort: reasoningEffort || this.getReasoningEffort(providerName, {}),
|
|
1256
|
-
}
|
|
1257
|
-
),
|
|
1258
|
-
{ label: `Engine ${model} (structured)` }
|
|
1259
|
-
);
|
|
1260
|
-
if (telemetry?.runId && telemetry?.userId) {
|
|
1261
|
-
recordModelUsage({
|
|
1262
|
-
runId: telemetry.runId,
|
|
1263
|
-
stepId: telemetry.stepId || null,
|
|
1264
|
-
userId: telemetry.userId,
|
|
1265
|
-
agentId: telemetry.agentId || null,
|
|
1266
|
-
provider: providerName,
|
|
1267
|
-
model,
|
|
1268
|
-
phase,
|
|
1269
|
-
usage: response.usage,
|
|
1270
|
-
latencyMs: Date.now() - startedAt,
|
|
1242
|
+
const structuredStep = `model:${phase}`;
|
|
1243
|
+
if (telemetry?.runId) {
|
|
1244
|
+
this.updateRunProgress(telemetry.runId, {
|
|
1245
|
+
currentPhase: 'model',
|
|
1246
|
+
currentStep: structuredStep,
|
|
1247
|
+
currentTool: null,
|
|
1248
|
+
currentStepStartedAt: isoNow(),
|
|
1271
1249
|
});
|
|
1272
1250
|
}
|
|
1273
1251
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1252
|
+
let completed = false;
|
|
1253
|
+
try {
|
|
1254
|
+
const response = await withProviderRetry(
|
|
1255
|
+
() => withModelCallTimeout(
|
|
1256
|
+
provider.chat(
|
|
1257
|
+
sanitizeConversationMessages([
|
|
1258
|
+
...messages,
|
|
1259
|
+
{ role: 'system', content: prompt },
|
|
1260
|
+
]),
|
|
1261
|
+
[],
|
|
1262
|
+
{
|
|
1263
|
+
model,
|
|
1264
|
+
maxTokens,
|
|
1265
|
+
reasoningEffort: reasoningEffort || this.getReasoningEffort(providerName, {}),
|
|
1266
|
+
}
|
|
1267
|
+
),
|
|
1268
|
+
telemetry || {},
|
|
1269
|
+
`${phase} model call`,
|
|
1270
|
+
),
|
|
1271
|
+
{ label: `Engine ${model} (structured)` }
|
|
1272
|
+
);
|
|
1273
|
+
completed = true;
|
|
1274
|
+
if (telemetry?.runId && telemetry?.userId) {
|
|
1275
|
+
recordModelUsage({
|
|
1276
|
+
runId: telemetry.runId,
|
|
1277
|
+
stepId: telemetry.stepId || null,
|
|
1278
|
+
userId: telemetry.userId,
|
|
1279
|
+
agentId: telemetry.agentId || null,
|
|
1280
|
+
provider: providerName,
|
|
1281
|
+
model,
|
|
1282
|
+
phase,
|
|
1283
|
+
usage: response.usage,
|
|
1284
|
+
latencyMs: Date.now() - startedAt,
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const parsed = parseJsonObject(response.content || '');
|
|
1289
|
+
const normalizedUsage = normalizeUsage(response.usage);
|
|
1290
|
+
return {
|
|
1291
|
+
value: normalize(parsed || {}, fallback),
|
|
1292
|
+
raw: response.content || '',
|
|
1293
|
+
usage: normalizedUsage?.totalTokens || 0,
|
|
1294
|
+
};
|
|
1295
|
+
} finally {
|
|
1296
|
+
const runMeta = telemetry?.runId ? this.getRunMeta(telemetry.runId) : null;
|
|
1297
|
+
if (runMeta?.progressLedger?.currentStep === structuredStep) {
|
|
1298
|
+
this.updateRunProgress(telemetry.runId, {
|
|
1299
|
+
currentPhase: 'idle',
|
|
1300
|
+
currentStep: null,
|
|
1301
|
+
currentTool: null,
|
|
1302
|
+
currentStepStartedAt: null,
|
|
1303
|
+
}, {
|
|
1304
|
+
verified: completed,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1281
1308
|
}
|
|
1282
1309
|
|
|
1283
1310
|
async requestModelResponse({
|
|
@@ -1304,8 +1331,16 @@ class AgentEngine {
|
|
|
1304
1331
|
if (options.stream !== false) {
|
|
1305
1332
|
let emittedContent = false;
|
|
1306
1333
|
const stream = provider.stream(requestMessages, tools, callOptions);
|
|
1334
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
1307
1335
|
try {
|
|
1308
|
-
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const next = await withModelCallTimeout(
|
|
1338
|
+
iterator.next(),
|
|
1339
|
+
options,
|
|
1340
|
+
`Model stream iteration ${iteration}`,
|
|
1341
|
+
);
|
|
1342
|
+
if (next.done) break;
|
|
1343
|
+
const chunk = next.value;
|
|
1309
1344
|
if (chunk.type === 'content') {
|
|
1310
1345
|
emittedContent = true;
|
|
1311
1346
|
streamContent += chunk.content;
|
|
@@ -1329,13 +1364,18 @@ class AgentEngine {
|
|
|
1329
1364
|
}
|
|
1330
1365
|
}
|
|
1331
1366
|
} catch (err) {
|
|
1367
|
+
Promise.resolve(iterator.return?.()).catch(() => {});
|
|
1332
1368
|
// Once tokens have streamed to the client a retry would duplicate
|
|
1333
1369
|
// output, so only the pre-stream window is safe to replay.
|
|
1334
1370
|
if (emittedContent) err.__providerRetryUnsafe = true;
|
|
1335
1371
|
throw err;
|
|
1336
1372
|
}
|
|
1337
1373
|
} else {
|
|
1338
|
-
response = await
|
|
1374
|
+
response = await withModelCallTimeout(
|
|
1375
|
+
provider.chat(requestMessages, tools, callOptions),
|
|
1376
|
+
options,
|
|
1377
|
+
`Model iteration ${iteration}`,
|
|
1378
|
+
);
|
|
1339
1379
|
}
|
|
1340
1380
|
|
|
1341
1381
|
return { response, streamContent };
|
|
@@ -1459,60 +1499,6 @@ class AgentEngine {
|
|
|
1459
1499
|
};
|
|
1460
1500
|
}
|
|
1461
1501
|
|
|
1462
|
-
async decideLoopState({
|
|
1463
|
-
provider,
|
|
1464
|
-
providerName,
|
|
1465
|
-
model,
|
|
1466
|
-
messages,
|
|
1467
|
-
tools,
|
|
1468
|
-
analysis,
|
|
1469
|
-
plan,
|
|
1470
|
-
toolExecutions,
|
|
1471
|
-
lastReply,
|
|
1472
|
-
triggerSource,
|
|
1473
|
-
messagingSent,
|
|
1474
|
-
iteration,
|
|
1475
|
-
maxIterations,
|
|
1476
|
-
options,
|
|
1477
|
-
fallbackStatus,
|
|
1478
|
-
}) {
|
|
1479
|
-
const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
|
|
1480
|
-
const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
|
|
1481
|
-
|
|
1482
|
-
const response = await this.requestStructuredJson({
|
|
1483
|
-
provider,
|
|
1484
|
-
providerName,
|
|
1485
|
-
model,
|
|
1486
|
-
messages,
|
|
1487
|
-
prompt: buildCompletionDecisionPrompt({
|
|
1488
|
-
mode: 'loop',
|
|
1489
|
-
triggerSource,
|
|
1490
|
-
messagingSent,
|
|
1491
|
-
goalContext,
|
|
1492
|
-
parallelWork: analysis?.parallel_work === true,
|
|
1493
|
-
tools,
|
|
1494
|
-
toolExecutions,
|
|
1495
|
-
lastReply,
|
|
1496
|
-
iteration,
|
|
1497
|
-
maxIterations,
|
|
1498
|
-
}),
|
|
1499
|
-
maxTokens: 320,
|
|
1500
|
-
normalize: (raw) => normalizeCompletionDecision(raw, {
|
|
1501
|
-
mode: 'loop',
|
|
1502
|
-
fallbackStatus,
|
|
1503
|
-
}),
|
|
1504
|
-
fallback: { status: fallbackStatus },
|
|
1505
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1506
|
-
telemetry: options,
|
|
1507
|
-
phase: 'loop_decision',
|
|
1508
|
-
});
|
|
1509
|
-
|
|
1510
|
-
return {
|
|
1511
|
-
decision: response.value,
|
|
1512
|
-
usage: response.usage,
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
1502
|
async verifyFinalResponse({
|
|
1517
1503
|
provider,
|
|
1518
1504
|
providerName,
|
|
@@ -1623,11 +1609,15 @@ class AgentEngine {
|
|
|
1623
1609
|
}
|
|
1624
1610
|
];
|
|
1625
1611
|
|
|
1626
|
-
const response = await
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1612
|
+
const response = await withModelCallTimeout(
|
|
1613
|
+
provider.chat(promptMessages, [], {
|
|
1614
|
+
model,
|
|
1615
|
+
maxTokens: 800,
|
|
1616
|
+
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1617
|
+
}),
|
|
1618
|
+
options,
|
|
1619
|
+
'Conversation state refresh',
|
|
1620
|
+
);
|
|
1631
1621
|
const parsed = parseJsonObject(response.content || '') || {};
|
|
1632
1622
|
const nextState = {
|
|
1633
1623
|
summary: String(parsed.summary || existingState?.summary || '').trim(),
|
|
@@ -1662,69 +1652,6 @@ class AgentEngine {
|
|
|
1662
1652
|
return nextState;
|
|
1663
1653
|
}
|
|
1664
1654
|
|
|
1665
|
-
async recoverBlankMessagingReply({
|
|
1666
|
-
userId,
|
|
1667
|
-
runId,
|
|
1668
|
-
messages,
|
|
1669
|
-
provider,
|
|
1670
|
-
model,
|
|
1671
|
-
providerName,
|
|
1672
|
-
options,
|
|
1673
|
-
stepIndex,
|
|
1674
|
-
failedStepCount,
|
|
1675
|
-
toolExecutions = [],
|
|
1676
|
-
tools = []
|
|
1677
|
-
}) {
|
|
1678
|
-
const attempts = 3;
|
|
1679
|
-
let recoveredContent = '';
|
|
1680
|
-
let totalTokens = 0;
|
|
1681
|
-
|
|
1682
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1683
|
-
console.warn(
|
|
1684
|
-
`[Run ${shortenRunId(runId)}] blank_reply_recovery attempt=${attempt} model=${model}`
|
|
1685
|
-
);
|
|
1686
|
-
try {
|
|
1687
|
-
const response = await provider.chat(
|
|
1688
|
-
sanitizeConversationMessages([
|
|
1689
|
-
...messages,
|
|
1690
|
-
{
|
|
1691
|
-
role: 'system',
|
|
1692
|
-
content: buildBlankMessagingReplyPrompt(attempt, options?.source || null)
|
|
1693
|
-
}
|
|
1694
|
-
]),
|
|
1695
|
-
[],
|
|
1696
|
-
{
|
|
1697
|
-
model,
|
|
1698
|
-
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
1699
|
-
}
|
|
1700
|
-
);
|
|
1701
|
-
totalTokens += response.usage?.totalTokens || 0;
|
|
1702
|
-
recoveredContent = sanitizeModelOutput(response.content || '', { model });
|
|
1703
|
-
if (normalizeOutgoingMessage(recoveredContent)) {
|
|
1704
|
-
console.info(
|
|
1705
|
-
`[Run ${shortenRunId(runId)}] blank_reply_recovery succeeded attempt=${attempt}`
|
|
1706
|
-
);
|
|
1707
|
-
return { content: recoveredContent, tokens: totalTokens, recovered: true };
|
|
1708
|
-
}
|
|
1709
|
-
} catch (recoverErr) {
|
|
1710
|
-
console.warn(
|
|
1711
|
-
`[Run ${shortenRunId(runId)}] blank_reply_recovery attempt=${attempt} failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`
|
|
1712
|
-
);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
const error = new Error(
|
|
1717
|
-
buildDeterministicMessagingFallback({
|
|
1718
|
-
failedStepCount,
|
|
1719
|
-
stepIndex,
|
|
1720
|
-
toolExecutions,
|
|
1721
|
-
})
|
|
1722
|
-
);
|
|
1723
|
-
error.code = 'BLANK_MESSAGING_REPLY';
|
|
1724
|
-
error.recoveryTokens = totalTokens;
|
|
1725
|
-
throw error;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
1655
|
getAvailableTools(app, options = {}) {
|
|
1729
1656
|
const { getAvailableTools } = require('./tools');
|
|
1730
1657
|
return getAvailableTools(app, options);
|
|
@@ -2127,169 +2054,6 @@ class AgentEngine {
|
|
|
2127
2054
|
return { messages, appliedCount: queued.length };
|
|
2128
2055
|
}
|
|
2129
2056
|
|
|
2130
|
-
async decideMessagingCompletionState({
|
|
2131
|
-
provider,
|
|
2132
|
-
providerName,
|
|
2133
|
-
model,
|
|
2134
|
-
messages,
|
|
2135
|
-
analysis,
|
|
2136
|
-
plan,
|
|
2137
|
-
tools,
|
|
2138
|
-
toolExecutions,
|
|
2139
|
-
lastReply,
|
|
2140
|
-
iteration,
|
|
2141
|
-
maxIterations,
|
|
2142
|
-
runId,
|
|
2143
|
-
options,
|
|
2144
|
-
}) {
|
|
2145
|
-
const runMeta = this.getRunMeta(runId);
|
|
2146
|
-
const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
|
|
2147
|
-
const platform = options?.source || null;
|
|
2148
|
-
const normalizedDraft = normalizeOutgoingMessage(lastReply || '', platform, {
|
|
2149
|
-
collapseWhitespace: false,
|
|
2150
|
-
});
|
|
2151
|
-
const draftReply = normalizedDraft ? String(lastReply || '').trim() : '';
|
|
2152
|
-
const ledger = runMeta?.progressLedger || null;
|
|
2153
|
-
const progressSummary = [
|
|
2154
|
-
`progress_state=${ledger?.progressState || 'active'}`,
|
|
2155
|
-
`current_phase=${ledger?.currentPhase || 'idle'}`,
|
|
2156
|
-
`current_tool=${ledger?.currentTool || 'none'}`,
|
|
2157
|
-
`heartbeat_count=${Number(ledger?.heartbeatCount || 0)}`,
|
|
2158
|
-
`last_visible_update=${ledger?.lastUserVisibleUpdateAt || 'none'}`,
|
|
2159
|
-
`last_verified_progress=${ledger?.lastVerifiedProgressAt || 'none'}`,
|
|
2160
|
-
`last_final_delivery=${ledger?.lastFinalDeliveryAt || 'none'}`,
|
|
2161
|
-
].join('; ');
|
|
2162
|
-
|
|
2163
|
-
const response = await this.requestStructuredJson({
|
|
2164
|
-
provider,
|
|
2165
|
-
providerName,
|
|
2166
|
-
model,
|
|
2167
|
-
messages,
|
|
2168
|
-
prompt: buildCompletionDecisionPrompt({
|
|
2169
|
-
mode: 'messaging',
|
|
2170
|
-
goalContext,
|
|
2171
|
-
parallelWork: analysis?.parallel_work === true,
|
|
2172
|
-
tools,
|
|
2173
|
-
toolExecutions,
|
|
2174
|
-
lastReply: draftReply,
|
|
2175
|
-
iteration,
|
|
2176
|
-
maxIterations,
|
|
2177
|
-
progressSummary,
|
|
2178
|
-
platform,
|
|
2179
|
-
}),
|
|
2180
|
-
maxTokens: 480,
|
|
2181
|
-
normalize: (raw) => normalizeCompletionDecision(raw, {
|
|
2182
|
-
mode: 'messaging',
|
|
2183
|
-
platform,
|
|
2184
|
-
draftReply,
|
|
2185
|
-
}),
|
|
2186
|
-
fallback: {
|
|
2187
|
-
status: 'continue',
|
|
2188
|
-
reason: '',
|
|
2189
|
-
final_reply: '',
|
|
2190
|
-
},
|
|
2191
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
2192
|
-
telemetry: options,
|
|
2193
|
-
phase: 'messaging_completion',
|
|
2194
|
-
});
|
|
2195
|
-
|
|
2196
|
-
return {
|
|
2197
|
-
decision: response.value,
|
|
2198
|
-
usage: response.usage,
|
|
2199
|
-
};
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
async resolveMessagingCompletionDecision({
|
|
2203
|
-
provider,
|
|
2204
|
-
providerName,
|
|
2205
|
-
model,
|
|
2206
|
-
messages,
|
|
2207
|
-
analysis,
|
|
2208
|
-
plan,
|
|
2209
|
-
tools,
|
|
2210
|
-
toolExecutions,
|
|
2211
|
-
lastReply,
|
|
2212
|
-
iteration,
|
|
2213
|
-
maxIterations,
|
|
2214
|
-
runId,
|
|
2215
|
-
conversationId,
|
|
2216
|
-
options,
|
|
2217
|
-
}) {
|
|
2218
|
-
const runMeta = this.getRunMeta(runId);
|
|
2219
|
-
if (!shouldRequireMessagingFinalityCheck(runMeta)) {
|
|
2220
|
-
return {
|
|
2221
|
-
action: 'none',
|
|
2222
|
-
content: lastReply,
|
|
2223
|
-
reason: '',
|
|
2224
|
-
usage: 0,
|
|
2225
|
-
};
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
let completionDecision;
|
|
2229
|
-
try {
|
|
2230
|
-
completionDecision = await this.decideMessagingCompletionState({
|
|
2231
|
-
provider,
|
|
2232
|
-
providerName,
|
|
2233
|
-
model,
|
|
2234
|
-
messages,
|
|
2235
|
-
analysis,
|
|
2236
|
-
plan,
|
|
2237
|
-
tools,
|
|
2238
|
-
toolExecutions,
|
|
2239
|
-
lastReply,
|
|
2240
|
-
iteration,
|
|
2241
|
-
maxIterations,
|
|
2242
|
-
runId,
|
|
2243
|
-
options,
|
|
2244
|
-
});
|
|
2245
|
-
} catch (error) {
|
|
2246
|
-
if (iteration >= maxIterations) {
|
|
2247
|
-
const wrapped = new Error(
|
|
2248
|
-
`Messaging completion check failed after visible progress: ${error?.message || error}`,
|
|
2249
|
-
);
|
|
2250
|
-
wrapped.disableAutonomousRetry = error?.disableAutonomousRetry === true;
|
|
2251
|
-
throw wrapped;
|
|
2252
|
-
}
|
|
2253
|
-
return {
|
|
2254
|
-
action: 'continue',
|
|
2255
|
-
content: '',
|
|
2256
|
-
reason: 'The run still needs an explicit final result or blocker decision.',
|
|
2257
|
-
usage: 0,
|
|
2258
|
-
};
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
const decision = completionDecision.decision || { status: 'continue', reason: '' };
|
|
2262
|
-
if (decision.status === 'continue') {
|
|
2263
|
-
if (iteration >= maxIterations) {
|
|
2264
|
-
throw new Error(
|
|
2265
|
-
'Messaging run reached the iteration limit before producing a final answer or blocker after visible progress.',
|
|
2266
|
-
);
|
|
2267
|
-
}
|
|
2268
|
-
return {
|
|
2269
|
-
action: 'continue',
|
|
2270
|
-
content: '',
|
|
2271
|
-
reason: decision.reason || 'The current draft is still only progress.',
|
|
2272
|
-
usage: completionDecision.usage || 0,
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
const finalContent = String(decision.final_reply || lastReply || '').trim();
|
|
2277
|
-
if (finalContent && messages[messages.length - 1]?.role === 'assistant') {
|
|
2278
|
-
messages[messages.length - 1] = {
|
|
2279
|
-
...messages[messages.length - 1],
|
|
2280
|
-
content: finalContent,
|
|
2281
|
-
};
|
|
2282
|
-
this.replaceLatestConversationAssistantMessage(conversationId, finalContent);
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
return {
|
|
2286
|
-
action: decision.status === 'blocked' ? 'blocked' : 'complete',
|
|
2287
|
-
content: finalContent,
|
|
2288
|
-
reason: decision.reason || '',
|
|
2289
|
-
usage: completionDecision.usage || 0,
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
2057
|
buildMessagingHeartbeatText(runMeta, options = {}) {
|
|
2294
2058
|
const stalled = options.stalled === true;
|
|
2295
2059
|
const now = Date.now();
|
|
@@ -2305,14 +2069,16 @@ class AgentEngine {
|
|
|
2305
2069
|
runStartedAtMs,
|
|
2306
2070
|
));
|
|
2307
2071
|
const currentTool = String(runMeta?.progressLedger?.currentTool || '').trim();
|
|
2072
|
+
const runTitle = String(runMeta?.title || '').trim().slice(0, 60);
|
|
2073
|
+
const titlePrefix = runTitle ? `[${runTitle}] ` : '';
|
|
2308
2074
|
if (currentTool) {
|
|
2309
2075
|
return stalled
|
|
2310
|
-
?
|
|
2311
|
-
:
|
|
2076
|
+
? `${titlePrefix}Still working on ${currentTool}. Run active ${runElapsed}; no verified progress for ${unverifiedElapsed}.`
|
|
2077
|
+
: `${titlePrefix}Still working on ${currentTool}. Run active ${runElapsed}; current step ${stepElapsed} so far.`;
|
|
2312
2078
|
}
|
|
2313
2079
|
return stalled
|
|
2314
|
-
?
|
|
2315
|
-
:
|
|
2080
|
+
? `${titlePrefix}Still working on this. Run active ${runElapsed}; no verified progress for ${unverifiedElapsed}.`
|
|
2081
|
+
: `${titlePrefix}Still working on this. Run active ${runElapsed}.`;
|
|
2316
2082
|
}
|
|
2317
2083
|
|
|
2318
2084
|
async sendRuntimeMessagingHeartbeat(runId, options = {}) {
|
|
@@ -2327,7 +2093,7 @@ class AgentEngine {
|
|
|
2327
2093
|
|
|
2328
2094
|
const createdAt = isoNow();
|
|
2329
2095
|
const content = this.buildMessagingHeartbeatText(runMeta, options);
|
|
2330
|
-
await this.messagingManager.sendMessage(
|
|
2096
|
+
const deliveryResult = await this.messagingManager.sendMessage(
|
|
2331
2097
|
runMeta.userId,
|
|
2332
2098
|
runMeta.messagingContext.platform,
|
|
2333
2099
|
runMeta.messagingContext.chatId,
|
|
@@ -2345,6 +2111,7 @@ class AgentEngine {
|
|
|
2345
2111
|
deliveryKind: 'interim',
|
|
2346
2112
|
},
|
|
2347
2113
|
);
|
|
2114
|
+
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging heartbeat delivery');
|
|
2348
2115
|
|
|
2349
2116
|
runMeta.lastInterimMessage = content;
|
|
2350
2117
|
if (!Array.isArray(runMeta.interimMessages)) {
|
|
@@ -2369,8 +2136,8 @@ class AgentEngine {
|
|
|
2369
2136
|
}, { agentId: runMeta.agentId });
|
|
2370
2137
|
this.enqueueSystemSteering(
|
|
2371
2138
|
runId,
|
|
2372
|
-
'A runtime
|
|
2373
|
-
{ reason: '
|
|
2139
|
+
'A runtime progress update was just sent on your behalf because you were blocked in a tool. On your NEXT free turn: use send_interim_update to write 1-2 sentences in your own words describing what you are doing and why. Keep it short and concrete. Then continue toward the final answer.',
|
|
2140
|
+
{ reason: 'heartbeat_ai_followup' },
|
|
2374
2141
|
);
|
|
2375
2142
|
return { sent: true, content };
|
|
2376
2143
|
}
|
|
@@ -2421,9 +2188,31 @@ class AgentEngine {
|
|
|
2421
2188
|
await this.messagingManager.sendTyping(userId, platform, chatId, true, { agentId }).catch(() => {});
|
|
2422
2189
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2423
2190
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2191
|
+
try {
|
|
2192
|
+
await withProviderRetry(async () => {
|
|
2193
|
+
const deliveryResult = await this.messagingManager.sendMessage(
|
|
2194
|
+
userId,
|
|
2195
|
+
platform,
|
|
2196
|
+
chatId,
|
|
2197
|
+
chunks[i],
|
|
2198
|
+
{ runId, agentId },
|
|
2199
|
+
);
|
|
2200
|
+
return requireSuccessfulMessagingDelivery(deliveryResult, 'Final messaging delivery');
|
|
2201
|
+
}, {
|
|
2202
|
+
...this.messagingDeliveryRetry,
|
|
2203
|
+
label: `MessagingDelivery ${platform}`,
|
|
2204
|
+
isRetryable: (error) => (
|
|
2205
|
+
error?.retryable !== false
|
|
2206
|
+
&& (
|
|
2207
|
+
error?.code === 'MESSAGING_DELIVERY_FAILED'
|
|
2208
|
+
|| isTransientError(error)
|
|
2209
|
+
)
|
|
2210
|
+
),
|
|
2211
|
+
});
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
error.disableAutonomousRetry = true;
|
|
2214
|
+
throw error;
|
|
2215
|
+
}
|
|
2427
2216
|
}
|
|
2428
2217
|
|
|
2429
2218
|
runMeta.lastSentMessage = chunks[chunks.length - 1] || cleanedContent;
|
|
@@ -2474,7 +2263,10 @@ class AgentEngine {
|
|
|
2474
2263
|
return { sent: false, skipped: true };
|
|
2475
2264
|
}
|
|
2476
2265
|
|
|
2477
|
-
if (
|
|
2266
|
+
if (
|
|
2267
|
+
(ledger.currentPhase === 'tool' || ledger.currentPhase === 'model')
|
|
2268
|
+
&& ledger.currentStepStartedAt
|
|
2269
|
+
) {
|
|
2478
2270
|
return this.sendRuntimeMessagingHeartbeat(runId, { stalled });
|
|
2479
2271
|
}
|
|
2480
2272
|
|
|
@@ -2487,9 +2279,10 @@ class AgentEngine {
|
|
|
2487
2279
|
return { sent: false, skipped: true };
|
|
2488
2280
|
}
|
|
2489
2281
|
|
|
2282
|
+
const elapsed = formatElapsedDuration(now - startedAtMs);
|
|
2490
2283
|
const nudge = stalled
|
|
2491
|
-
?
|
|
2492
|
-
:
|
|
2284
|
+
? `You have been running for ${elapsed} and appear stalled. Use send_interim_update RIGHT NOW to write 1-2 sentences explaining the blocker in your own words, then either resolve it or call task_complete with what you have. Do not leave the user without an answer.`
|
|
2285
|
+
: `You have been running for ${elapsed} without sending an update to the user. Use send_interim_update RIGHT NOW to write 1-2 sentences explaining what you are currently doing. Keep it short and concrete. Then continue working toward the final answer.`;
|
|
2493
2286
|
const queued = this.enqueueSystemSteering(runId, nudge, {
|
|
2494
2287
|
reason: stalled ? 'stalled_progress_check' : 'progress_check',
|
|
2495
2288
|
});
|
|
@@ -2788,7 +2581,12 @@ class AgentEngine {
|
|
|
2788
2581
|
const carriedExplicitMessageSent = retryMessagingState.explicitMessageSent === true;
|
|
2789
2582
|
const carriedInterimHistory = cloneInterimHistory(retryMessagingState.interimHistory);
|
|
2790
2583
|
const carriedLastInterimMessage = carriedInterimHistory[carriedInterimHistory.length - 1]?.content || '';
|
|
2791
|
-
const carriedGoalContract =
|
|
2584
|
+
const carriedGoalContract = mergeGoalContracts(
|
|
2585
|
+
normalizeGoalContract({
|
|
2586
|
+
goal: clampRunContext(userMessage, 1200),
|
|
2587
|
+
}),
|
|
2588
|
+
retryMessagingState.goalContract,
|
|
2589
|
+
);
|
|
2792
2590
|
const startedAtIso = isoNow();
|
|
2793
2591
|
const progressLedger = buildInitialProgressLedger({
|
|
2794
2592
|
startedAt: startedAtIso,
|
|
@@ -3248,14 +3046,16 @@ class AgentEngine {
|
|
|
3248
3046
|
currentStep: `model:${iteration}`,
|
|
3249
3047
|
currentTool: null,
|
|
3250
3048
|
currentStepStartedAt: isoNow(),
|
|
3251
|
-
}, {
|
|
3252
|
-
verified: true,
|
|
3253
3049
|
});
|
|
3254
3050
|
|
|
3255
3051
|
let metrics = this.estimatePromptMetrics(messages, tools);
|
|
3256
3052
|
const contextWindow = provider.getContextWindow(model);
|
|
3257
3053
|
if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
|
|
3258
|
-
messages = await
|
|
3054
|
+
messages = await withModelCallTimeout(
|
|
3055
|
+
compact(messages, provider, model, contextWindow),
|
|
3056
|
+
options,
|
|
3057
|
+
`Context compaction before iteration ${iteration}`,
|
|
3058
|
+
);
|
|
3259
3059
|
messages = sanitizeConversationMessages(messages);
|
|
3260
3060
|
this.emit(userId, 'run:compaction', { runId, iteration });
|
|
3261
3061
|
metrics = this.estimatePromptMetrics(messages, tools);
|
|
@@ -3393,6 +3193,9 @@ class AgentEngine {
|
|
|
3393
3193
|
toolCallCount: response.toolCalls?.length || 0,
|
|
3394
3194
|
contentPreview: String(lastContent || streamContent || '').slice(0, 240),
|
|
3395
3195
|
}, { agentId });
|
|
3196
|
+
this.updateRunProgress(runId, {}, {
|
|
3197
|
+
verified: true,
|
|
3198
|
+
});
|
|
3396
3199
|
|
|
3397
3200
|
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
3398
3201
|
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
@@ -3416,9 +3219,10 @@ class AgentEngine {
|
|
|
3416
3219
|
currentStep: null,
|
|
3417
3220
|
currentTool: null,
|
|
3418
3221
|
currentStepStartedAt: null,
|
|
3419
|
-
}, {
|
|
3420
|
-
verified: true,
|
|
3421
3222
|
});
|
|
3223
|
+
// Check for queued steering first — if something was injected while the
|
|
3224
|
+
// model was responding (e.g. a heartbeat nudge), give the model a chance
|
|
3225
|
+
// to act on it before we treat this as a final answer.
|
|
3422
3226
|
const systemSteeringAfterResponse = this.applyQueuedSystemSteering(runId, messages);
|
|
3423
3227
|
messages = systemSteeringAfterResponse.messages;
|
|
3424
3228
|
if (systemSteeringAfterResponse.appliedCount > 0) {
|
|
@@ -3436,99 +3240,17 @@ class AgentEngine {
|
|
|
3436
3240
|
lastContent = '';
|
|
3437
3241
|
continue;
|
|
3438
3242
|
}
|
|
3439
|
-
const messagingSent = this.activeRuns.get(runId)?.messagingSent || false;
|
|
3440
3243
|
if (this.shouldFastCompleteVoiceReply({
|
|
3441
3244
|
options,
|
|
3442
3245
|
toolExecutions,
|
|
3443
3246
|
failedStepCount,
|
|
3444
|
-
messagingSent,
|
|
3247
|
+
messagingSent: this.activeRuns.get(runId)?.messagingSent || false,
|
|
3445
3248
|
lastReply: lastContent,
|
|
3446
3249
|
})) {
|
|
3447
3250
|
break;
|
|
3448
3251
|
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
const messagingCompletion = await this.resolveMessagingCompletionDecision({
|
|
3452
|
-
provider,
|
|
3453
|
-
providerName,
|
|
3454
|
-
model,
|
|
3455
|
-
messages,
|
|
3456
|
-
analysis,
|
|
3457
|
-
plan,
|
|
3458
|
-
tools,
|
|
3459
|
-
toolExecutions,
|
|
3460
|
-
lastReply: lastContent,
|
|
3461
|
-
iteration,
|
|
3462
|
-
maxIterations,
|
|
3463
|
-
runId,
|
|
3464
|
-
conversationId,
|
|
3465
|
-
options: { ...options, runId, userId, agentId },
|
|
3466
|
-
});
|
|
3467
|
-
totalTokens += messagingCompletion.usage || 0;
|
|
3468
|
-
if (messagingCompletion.action === 'continue') {
|
|
3469
|
-
messages.push({
|
|
3470
|
-
role: 'system',
|
|
3471
|
-
content: [
|
|
3472
|
-
messagingCompletion.reason
|
|
3473
|
-
? `Continue working: ${messagingCompletion.reason}.`
|
|
3474
|
-
: 'Continue working autonomously.',
|
|
3475
|
-
'The messaging user has already seen progress. Do not stop until you either have the finished answer now or a concrete blocker reply now.',
|
|
3476
|
-
].join(' ')
|
|
3477
|
-
});
|
|
3478
|
-
lastContent = '';
|
|
3479
|
-
continue;
|
|
3480
|
-
}
|
|
3481
|
-
if (typeof messagingCompletion.content === 'string') {
|
|
3482
|
-
lastContent = messagingCompletion.content;
|
|
3483
|
-
}
|
|
3484
|
-
break;
|
|
3485
|
-
}
|
|
3486
|
-
if (iteration < maxIterations) {
|
|
3487
|
-
const proactiveRunNeedsDecision = (
|
|
3488
|
-
(triggerSource === 'schedule' || triggerSource === 'tasks')
|
|
3489
|
-
&& this.activeRuns.get(runId)?.noResponse !== true
|
|
3490
|
-
&& options.deliveryState?.noResponse !== true
|
|
3491
|
-
);
|
|
3492
|
-
const visibleInterimActivity = hasVisibleInterimActivity(this.activeRuns.get(runId));
|
|
3493
|
-
const fallbackStatus = (
|
|
3494
|
-
proactiveRunNeedsDecision
|
|
3495
|
-
|| toolExecutions.length > 0
|
|
3496
|
-
|| failedStepCount > 0
|
|
3497
|
-
|| messagingSent
|
|
3498
|
-
|| visibleInterimActivity
|
|
3499
|
-
) ? 'continue' : 'complete';
|
|
3500
|
-
const loopState = await runWithModelFallback('loop decision', () => this.decideLoopState({
|
|
3501
|
-
provider,
|
|
3502
|
-
providerName,
|
|
3503
|
-
model,
|
|
3504
|
-
messages,
|
|
3505
|
-
tools,
|
|
3506
|
-
analysis,
|
|
3507
|
-
plan,
|
|
3508
|
-
toolExecutions,
|
|
3509
|
-
lastReply: lastContent,
|
|
3510
|
-
triggerSource,
|
|
3511
|
-
messagingSent,
|
|
3512
|
-
iteration,
|
|
3513
|
-
maxIterations,
|
|
3514
|
-
options: { ...options, runId, userId, agentId },
|
|
3515
|
-
fallbackStatus,
|
|
3516
|
-
}));
|
|
3517
|
-
totalTokens += loopState.usage || 0;
|
|
3518
|
-
if (loopState.decision.status === 'continue') {
|
|
3519
|
-
messages.push({
|
|
3520
|
-
role: 'system',
|
|
3521
|
-
content: [
|
|
3522
|
-
loopState.decision.reason ? `Continue working: ${loopState.decision.reason}.` : 'Continue working autonomously.',
|
|
3523
|
-
messagingSent
|
|
3524
|
-
? 'You already sent a user-facing message in this run. Keep working silently unless you have a materially new finished result or a real external blocker.'
|
|
3525
|
-
: 'Use send_interim_update sparingly if a short real update or question would help. Otherwise keep working until you have the result or a real blocker.',
|
|
3526
|
-
].join(' ')
|
|
3527
|
-
});
|
|
3528
|
-
lastContent = '';
|
|
3529
|
-
continue;
|
|
3530
|
-
}
|
|
3531
|
-
}
|
|
3252
|
+
// AI returned text with no tool calls → trust it as the final answer.
|
|
3253
|
+
directAnswerEligible = true;
|
|
3532
3254
|
break;
|
|
3533
3255
|
}
|
|
3534
3256
|
|
|
@@ -3537,6 +3259,15 @@ class AgentEngine {
|
|
|
3537
3259
|
&& response.toolCalls.every((toolCall) => this.isReadOnlyToolCall(toolCall))
|
|
3538
3260
|
);
|
|
3539
3261
|
if (canRunParallelBatch) {
|
|
3262
|
+
const parallelToolNames = response.toolCalls
|
|
3263
|
+
.map((toolCall) => toolCall.function?.name)
|
|
3264
|
+
.filter(Boolean);
|
|
3265
|
+
this.updateRunProgress(runId, {
|
|
3266
|
+
currentPhase: 'tool',
|
|
3267
|
+
currentStep: `parallel:${iteration}`,
|
|
3268
|
+
currentTool: parallelToolNames.join(', ') || 'parallel tools',
|
|
3269
|
+
currentStepStartedAt: isoNow(),
|
|
3270
|
+
});
|
|
3540
3271
|
const batch = await this.executeReadOnlyBatch(response.toolCalls, {
|
|
3541
3272
|
userId,
|
|
3542
3273
|
runId,
|
|
@@ -3588,6 +3319,14 @@ class AgentEngine {
|
|
|
3588
3319
|
deliverableArtifacts,
|
|
3589
3320
|
compactionMetrics: compactionMetrics.slice(-20),
|
|
3590
3321
|
});
|
|
3322
|
+
this.updateRunProgress(runId, {
|
|
3323
|
+
currentPhase: 'idle',
|
|
3324
|
+
currentStep: null,
|
|
3325
|
+
currentTool: null,
|
|
3326
|
+
currentStepStartedAt: null,
|
|
3327
|
+
}, {
|
|
3328
|
+
verified: true,
|
|
3329
|
+
});
|
|
3591
3330
|
continue;
|
|
3592
3331
|
}
|
|
3593
3332
|
|
|
@@ -3605,53 +3344,20 @@ class AgentEngine {
|
|
|
3605
3344
|
}
|
|
3606
3345
|
|
|
3607
3346
|
// ── task_complete: AI explicitly signals the task is fully done ──
|
|
3608
|
-
//
|
|
3609
|
-
// regular tool execution, it is a loop-exit signal.
|
|
3347
|
+
// Trust the model — no separate judge LLM call needed.
|
|
3610
3348
|
if (toolName === 'task_complete') {
|
|
3611
3349
|
const finalMessage = String(toolArgs.message || '').trim();
|
|
3612
|
-
const confidence = normalizeCompletionConfidence(toolArgs.confidence || 'medium');
|
|
3613
|
-
const completionDecision = shouldAcceptTaskComplete({
|
|
3614
|
-
confidence,
|
|
3615
|
-
requiredConfidence: analysis?.completion_confidence_required || 'medium',
|
|
3616
|
-
iteration,
|
|
3617
|
-
maxIterations,
|
|
3618
|
-
});
|
|
3619
3350
|
this.recordRunEvent(userId, runId, 'task_complete_signaled', {
|
|
3620
|
-
|
|
3621
|
-
requiredConfidence: analysis?.completion_confidence_required || 'medium',
|
|
3622
|
-
accepted: completionDecision.accept,
|
|
3351
|
+
accepted: true,
|
|
3623
3352
|
iteration,
|
|
3624
3353
|
messageLength: finalMessage.length,
|
|
3625
3354
|
}, { agentId });
|
|
3626
3355
|
console.info(
|
|
3627
|
-
`[Run ${shortenRunId(runId)}] task_complete
|
|
3356
|
+
`[Run ${shortenRunId(runId)}] task_complete accepted at iteration=${iteration}`
|
|
3628
3357
|
);
|
|
3629
|
-
|
|
3630
|
-
messages.push({
|
|
3631
|
-
role: 'tool',
|
|
3632
|
-
name: toolName,
|
|
3633
|
-
tool_call_id: toolCall.id,
|
|
3634
|
-
content: JSON.stringify({
|
|
3635
|
-
status: 'continue',
|
|
3636
|
-
reason: completionDecision.reason,
|
|
3637
|
-
required_confidence: analysis?.completion_confidence_required || 'medium',
|
|
3638
|
-
}),
|
|
3639
|
-
});
|
|
3640
|
-
messages.push({
|
|
3641
|
-
role: 'system',
|
|
3642
|
-
content: `${completionDecision.reason} Do not ask the user to decide the next step unless external input is truly required.`
|
|
3643
|
-
});
|
|
3644
|
-
continue;
|
|
3645
|
-
}
|
|
3646
|
-
if (completionDecision.reason) {
|
|
3647
|
-
messages.push({
|
|
3648
|
-
role: 'system',
|
|
3649
|
-
content: completionDecision.reason,
|
|
3650
|
-
});
|
|
3651
|
-
}
|
|
3652
|
-
lastContent = finalMessage; // empty string is valid; downstream handles it
|
|
3358
|
+
lastContent = finalMessage;
|
|
3653
3359
|
directAnswerEligible = true;
|
|
3654
|
-
break;
|
|
3360
|
+
break;
|
|
3655
3361
|
}
|
|
3656
3362
|
|
|
3657
3363
|
const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
|
|
@@ -3712,7 +3418,6 @@ class AgentEngine {
|
|
|
3712
3418
|
currentTool: toolName,
|
|
3713
3419
|
currentStepStartedAt: isoNow(),
|
|
3714
3420
|
}, {
|
|
3715
|
-
verified: true,
|
|
3716
3421
|
stepId,
|
|
3717
3422
|
});
|
|
3718
3423
|
|
|
@@ -3862,6 +3567,11 @@ class AgentEngine {
|
|
|
3862
3567
|
|
|
3863
3568
|
if (toolErrorMessage) {
|
|
3864
3569
|
consecutiveToolFailures += 1;
|
|
3570
|
+
const currentRunMeta = this.getRunMeta(runId);
|
|
3571
|
+
trackErrorPattern(toolErrorMessage, currentRunMeta);
|
|
3572
|
+
const errorKey = normalizeErrorKey(toolErrorMessage);
|
|
3573
|
+
const errorCount = currentRunMeta?.errorPatterns?.get(errorKey) || 0;
|
|
3574
|
+
const patternGuide = buildErrorPatternGuidance(errorKey, errorCount);
|
|
3865
3575
|
const alternativeTools = summarizeAvailableTools(tools, { exclude: toolName });
|
|
3866
3576
|
messages.push({
|
|
3867
3577
|
role: 'system',
|
|
@@ -3870,6 +3580,7 @@ class AgentEngine {
|
|
|
3870
3580
|
'This tool failure is not, by itself, a user-facing blocker.',
|
|
3871
3581
|
'Continue autonomously: retry with corrected arguments, try an alternative tool/path, or verify the outcome using other available tools.',
|
|
3872
3582
|
alternativeTools ? `Other available tools in this run: ${alternativeTools}.` : '',
|
|
3583
|
+
patternGuide || '',
|
|
3873
3584
|
'Only stop and tell the user you are blocked if the remaining issue truly requires an external dependency or user action outside this run.'
|
|
3874
3585
|
].filter(Boolean).join(' ')
|
|
3875
3586
|
});
|
|
@@ -3978,26 +3689,43 @@ class AgentEngine {
|
|
|
3978
3689
|
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
3979
3690
|
|
|
3980
3691
|
if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3692
|
+
// Simplified blank reply recovery: one model call with direct instruction,
|
|
3693
|
+
// then fall back to a deterministic message. No multi-attempt LLM loop.
|
|
3694
|
+
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery model=${model}`);
|
|
3695
|
+
let recoveredTokens = 0;
|
|
3696
|
+
try {
|
|
3697
|
+
const recoveryResponse = await withModelCallTimeout(
|
|
3698
|
+
provider.chat(
|
|
3699
|
+
sanitizeConversationMessages([
|
|
3700
|
+
...messages,
|
|
3701
|
+
{
|
|
3702
|
+
role: 'system',
|
|
3703
|
+
content: buildBlankMessagingReplyPrompt(1, options?.source || null)
|
|
3704
|
+
}
|
|
3705
|
+
]),
|
|
3706
|
+
[],
|
|
3707
|
+
{
|
|
3708
|
+
model,
|
|
3709
|
+
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
3710
|
+
}
|
|
3711
|
+
),
|
|
3712
|
+
options,
|
|
3713
|
+
'Blank messaging reply recovery',
|
|
3714
|
+
);
|
|
3715
|
+
recoveredTokens = recoveryResponse.usage?.totalTokens || 0;
|
|
3716
|
+
lastContent = sanitizeModelOutput(recoveryResponse.content || '', { model });
|
|
3717
|
+
} catch (recoverErr) {
|
|
3718
|
+
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`);
|
|
3719
|
+
}
|
|
3720
|
+
totalTokens += recoveredTokens;
|
|
3721
|
+
if (!normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
3722
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
3723
|
+
}
|
|
3996
3724
|
if (normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
3997
3725
|
messages.push({ role: 'assistant', content: lastContent });
|
|
3998
3726
|
if (conversationId) {
|
|
3999
3727
|
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
4000
|
-
.run(conversationId, 'assistant', lastContent,
|
|
3728
|
+
.run(conversationId, 'assistant', lastContent, recoveredTokens);
|
|
4001
3729
|
}
|
|
4002
3730
|
}
|
|
4003
3731
|
}
|
|
@@ -4139,20 +3867,6 @@ class AgentEngine {
|
|
|
4139
3867
|
refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
|
|
4140
3868
|
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
4141
3869
|
});
|
|
4142
|
-
await this.refreshConversationState({
|
|
4143
|
-
conversationId,
|
|
4144
|
-
runId,
|
|
4145
|
-
provider,
|
|
4146
|
-
providerName,
|
|
4147
|
-
model,
|
|
4148
|
-
finalReply: finalResponseText,
|
|
4149
|
-
analysis,
|
|
4150
|
-
verification,
|
|
4151
|
-
historyWindow,
|
|
4152
|
-
options: { ...options, userId, agentId },
|
|
4153
|
-
}).catch((err) => {
|
|
4154
|
-
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
4155
|
-
});
|
|
4156
3870
|
}
|
|
4157
3871
|
}
|
|
4158
3872
|
|
|
@@ -4186,6 +3900,23 @@ class AgentEngine {
|
|
|
4186
3900
|
}
|
|
4187
3901
|
}
|
|
4188
3902
|
|
|
3903
|
+
if (conversationId && options.skipConversationMaintenance !== true) {
|
|
3904
|
+
await this.refreshConversationState({
|
|
3905
|
+
conversationId,
|
|
3906
|
+
runId,
|
|
3907
|
+
provider,
|
|
3908
|
+
providerName,
|
|
3909
|
+
model,
|
|
3910
|
+
finalReply: finalResponseText,
|
|
3911
|
+
analysis,
|
|
3912
|
+
verification,
|
|
3913
|
+
historyWindow,
|
|
3914
|
+
options: { ...options, userId, agentId },
|
|
3915
|
+
}).catch((err) => {
|
|
3916
|
+
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
|
|
4189
3920
|
console.info(
|
|
4190
3921
|
`[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}`
|
|
4191
3922
|
);
|
|
@@ -4272,6 +4003,8 @@ class AgentEngine {
|
|
|
4272
4003
|
triggerSource === 'messaging'
|
|
4273
4004
|
&& options.source
|
|
4274
4005
|
&& options.chatId
|
|
4006
|
+
&& runMeta?.finalDeliverySent !== true
|
|
4007
|
+
&& runMeta?.messagingSent !== true
|
|
4275
4008
|
&& err?.disableAutonomousRetry !== true
|
|
4276
4009
|
&& !isRateLimitError
|
|
4277
4010
|
&& retryCount < this.getMessagingRetryLimit(maxIterations)
|
|
@@ -4342,7 +4075,7 @@ class AgentEngine {
|
|
|
4342
4075
|
let messagingFailureContent = '';
|
|
4343
4076
|
let sendSucceeded = false;
|
|
4344
4077
|
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
4345
|
-
if (!runMeta?.messagingSent) {
|
|
4078
|
+
if (!runMeta?.finalDeliverySent && !runMeta?.messagingSent) {
|
|
4346
4079
|
const manager = this.messagingManager;
|
|
4347
4080
|
if (manager) {
|
|
4348
4081
|
const failureScenario = buildMessagingFailureScenario({
|
|
@@ -4359,10 +4092,14 @@ class AgentEngine {
|
|
|
4359
4092
|
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)}`
|
|
4360
4093
|
}
|
|
4361
4094
|
]);
|
|
4362
|
-
const modelReply = await
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4095
|
+
const modelReply = await withModelCallTimeout(
|
|
4096
|
+
provider.chat(failedMessage, [], {
|
|
4097
|
+
model,
|
|
4098
|
+
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
4099
|
+
}),
|
|
4100
|
+
options,
|
|
4101
|
+
'Messaging failure reply',
|
|
4102
|
+
);
|
|
4366
4103
|
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
4367
4104
|
if (normalizeOutgoingMessage(drafted, options?.source || null)) {
|
|
4368
4105
|
messagingFailureContent = drafted.trim();
|
|
@@ -4381,7 +4118,14 @@ class AgentEngine {
|
|
|
4381
4118
|
}
|
|
4382
4119
|
|
|
4383
4120
|
try {
|
|
4384
|
-
await manager.sendMessage(
|
|
4121
|
+
const deliveryResult = await manager.sendMessage(
|
|
4122
|
+
userId,
|
|
4123
|
+
options.source,
|
|
4124
|
+
options.chatId,
|
|
4125
|
+
messagingFailureContent,
|
|
4126
|
+
{ runId, agentId },
|
|
4127
|
+
);
|
|
4128
|
+
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
4385
4129
|
sendSucceeded = true;
|
|
4386
4130
|
if (runMeta) {
|
|
4387
4131
|
runMeta.lastSentMessage = messagingFailureContent;
|