neoagent 3.2.1-beta.0 → 3.2.1-beta.10
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/docs/agent-run-lifecycle.md +10 -0
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +11 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/agents.js +35 -2
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +496 -166
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +121 -5
- package/server/services/ai/loop/conversation_loop.js +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +4 -4
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -55,6 +55,16 @@ const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('../
|
|
|
55
55
|
const { enforceRateLimits } = require('../rate_limits');
|
|
56
56
|
const { ToolRepetitionGuard } = require('../repetitionGuard');
|
|
57
57
|
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
58
|
+
const {
|
|
59
|
+
normalizeModelSelections,
|
|
60
|
+
resolveModelSelection,
|
|
61
|
+
} = require('../model_identity');
|
|
62
|
+
const {
|
|
63
|
+
isModelCoolingDown,
|
|
64
|
+
recordModelFailure,
|
|
65
|
+
recordModelSuccess,
|
|
66
|
+
} = require('../model_failure_cache');
|
|
67
|
+
const { getProviderForUser } = require('../provider_selector');
|
|
58
68
|
const { IterationBudget } = require('./iteration_budget');
|
|
59
69
|
const {
|
|
60
70
|
buildBlankAfterToolFailureGuidance,
|
|
@@ -116,7 +126,7 @@ const {
|
|
|
116
126
|
const {
|
|
117
127
|
requestModelResponse: requestModelResponseImpl,
|
|
118
128
|
requestStructuredJson: requestStructuredJsonImpl,
|
|
119
|
-
|
|
129
|
+
runAbortableModelCall,
|
|
120
130
|
} = require('./model_io');
|
|
121
131
|
const {
|
|
122
132
|
publishInterimUpdate: publishInterimUpdateImpl,
|
|
@@ -144,6 +154,7 @@ const {
|
|
|
144
154
|
buildModelFailureLoopPrompt,
|
|
145
155
|
} = require('../messagingFallback');
|
|
146
156
|
const {
|
|
157
|
+
assessResearchAdequacy,
|
|
147
158
|
classifyToolExecution,
|
|
148
159
|
gatheredNewEvidence,
|
|
149
160
|
isSubstantiveProgressToolName,
|
|
@@ -164,6 +175,7 @@ const {
|
|
|
164
175
|
normalizeRetrievalPlan,
|
|
165
176
|
shouldEnhanceRetrieval,
|
|
166
177
|
} = require('../../memory/retrieval_reasoning');
|
|
178
|
+
const { isAbortError, throwIfAborted } = require('../../../utils/abort');
|
|
167
179
|
|
|
168
180
|
function generateTitle(task) {
|
|
169
181
|
if (!task || typeof task !== 'string') return 'Untitled';
|
|
@@ -291,13 +303,34 @@ function summarizeReadTargets(toolExecutions = []) {
|
|
|
291
303
|
return targets.join('; ');
|
|
292
304
|
}
|
|
293
305
|
|
|
294
|
-
function buildNoProgressWrapupPrompt({
|
|
306
|
+
function buildNoProgressWrapupPrompt({
|
|
307
|
+
readOnlyCount = 0,
|
|
308
|
+
alreadyRead = '',
|
|
309
|
+
platform = null,
|
|
310
|
+
researchAdequacy = null,
|
|
311
|
+
} = {}) {
|
|
312
|
+
const adequacy = researchAdequacy && typeof researchAdequacy === 'object'
|
|
313
|
+
? researchAdequacy
|
|
314
|
+
: null;
|
|
315
|
+
const researchIncomplete = adequacy
|
|
316
|
+
&& adequacy.adequate === false
|
|
317
|
+
&& adequacy.intensity
|
|
318
|
+
&& adequacy.intensity !== 'none';
|
|
295
319
|
return [
|
|
296
320
|
`This is the final turn for this run (no further tool calls; ${Math.max(0, Number(readOnlyCount) || 0)} read-only turns without a state change).`,
|
|
297
321
|
alreadyRead ? `Already gathered: ${alreadyRead}.` : '',
|
|
322
|
+
researchIncomplete
|
|
323
|
+
? `Research coverage is incomplete: ${adequacy.reason || 'requested targets still lack primary evidence.'}`
|
|
324
|
+
: '',
|
|
325
|
+
researchIncomplete && adequacy.uncoveredTargets?.length
|
|
326
|
+
? `Uncovered research targets: ${adequacy.uncoveredTargets.join('; ')}.`
|
|
327
|
+
: '',
|
|
298
328
|
'Write the answer now from everything you have already gathered in this conversation. Deliver the useful result you DO have — calendar, weather, emails, search findings, whatever was collected — formatted as the actual answer to the original request.',
|
|
299
329
|
'If one part could not be retrieved, still deliver everything else and note the missing part in at most one short clause. Never withhold a useful answer because a single detail is missing.',
|
|
300
|
-
'
|
|
330
|
+
'Do not invent entities, products, people, files, outcomes, or next-step work that is not already supported by tool evidence in this conversation.',
|
|
331
|
+
researchIncomplete
|
|
332
|
+
? 'Because research is incomplete, clearly label assumptions and missing targets. Prefer a partial verified answer or a concrete blocker over a confident fabricated comparison.'
|
|
333
|
+
: 'Only report a pure blocker if you genuinely gathered nothing usable at all. Do not describe the result as unfinished, unconfirmed, "blocked", or "still working" when you have something useful — this IS the final answer.',
|
|
301
334
|
buildMaxIterationWrapupPrompt(platform),
|
|
302
335
|
].filter(Boolean).join('\n\n');
|
|
303
336
|
}
|
|
@@ -403,145 +436,75 @@ function buildAutonomyPolicyFromAnalysis(analysis = {}) {
|
|
|
403
436
|
};
|
|
404
437
|
}
|
|
405
438
|
|
|
406
|
-
async function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
|
|
416
|
-
|
|
417
|
-
const knownModelIds = new Set(models.map((m) => m.id));
|
|
418
|
-
const selectableModels = models.filter((m) => m.available !== false);
|
|
419
|
-
|
|
420
|
-
enabledIds = Array.isArray(enabledIds)
|
|
421
|
-
? enabledIds
|
|
422
|
-
.map((id) => String(id))
|
|
423
|
-
.filter((id) => knownModelIds.has(id))
|
|
424
|
-
: [];
|
|
425
|
-
|
|
426
|
-
let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
|
|
427
|
-
if (availableModels.length === 0) {
|
|
428
|
-
enabledIds = selectableModels.map((m) => m.id);
|
|
429
|
-
availableModels = [...selectableModels];
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
|
|
433
|
-
|
|
434
|
-
if (!fallbackModel) {
|
|
435
|
-
throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
let selectedModelDef = fallbackModel;
|
|
439
|
-
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
440
|
-
|
|
441
|
-
if (modelOverride && typeof modelOverride === 'string') {
|
|
442
|
-
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
443
|
-
// An explicit override (e.g. a failure fallback picked by getFailureFallbackModelId)
|
|
444
|
-
// may intentionally sit outside the user's enabled_models pool — that's the whole
|
|
445
|
-
// point when no in-pool cross-provider option exists. Only require availability.
|
|
446
|
-
if (requested && requested.available !== false) {
|
|
447
|
-
selectedModelDef = requested;
|
|
448
|
-
return {
|
|
449
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
450
|
-
model: selectedModelDef.id,
|
|
451
|
-
providerName: selectedModelDef.provider
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
457
|
-
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
458
|
-
} else {
|
|
459
|
-
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
460
|
-
? providerConfig.selectionHint
|
|
461
|
-
: {};
|
|
462
|
-
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
463
|
-
const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
|
|
464
|
-
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
465
|
-
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
|
|
466
|
-
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
467
|
-
? preferredPurpose
|
|
468
|
-
: '';
|
|
469
|
-
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
470
|
-
const chooseForPurpose = (purpose) => {
|
|
471
|
-
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
472
|
-
if (candidates.length === 0) return null;
|
|
473
|
-
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
474
|
-
return [...candidates].sort((left, right) => (
|
|
475
|
-
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
476
|
-
))[0];
|
|
477
|
-
}
|
|
478
|
-
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
479
|
-
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
|
|
480
|
-
}
|
|
481
|
-
return candidates[0];
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
if (smarterSelection && requestedPurpose) {
|
|
485
|
-
selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
486
|
-
} else if (smarterSelection && highAutonomy) {
|
|
487
|
-
selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
488
|
-
} else if (isSubagent) {
|
|
489
|
-
selectedModelDef = chooseForPurpose('fast') || fallbackModel;
|
|
490
|
-
} else {
|
|
491
|
-
selectedModelDef = chooseForPurpose('general') || fallbackModel;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return {
|
|
496
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
497
|
-
model: selectedModelDef.id,
|
|
498
|
-
providerName: selectedModelDef.provider
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null, failureError = null) {
|
|
439
|
+
async function getFailureFallbackModelId(
|
|
440
|
+
userId,
|
|
441
|
+
agentId,
|
|
442
|
+
currentModelId,
|
|
443
|
+
preferredFallbackId = null,
|
|
444
|
+
failureError = null,
|
|
445
|
+
signal = null,
|
|
446
|
+
excludedModelIds = [],
|
|
447
|
+
) {
|
|
503
448
|
const { getSupportedModels } = require('../models');
|
|
504
449
|
const aiSettings = getAiSettings(userId, agentId);
|
|
505
|
-
const models = await getSupportedModels(userId, agentId);
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
450
|
+
const models = await getSupportedModels(userId, agentId, { signal });
|
|
451
|
+
const excluded = new Set(
|
|
452
|
+
[...excludedModelIds]
|
|
453
|
+
.map((id) => String(id || '').trim())
|
|
454
|
+
.filter(Boolean),
|
|
455
|
+
);
|
|
456
|
+
const availableModels = models.filter(
|
|
457
|
+
(model) => model.available !== false
|
|
458
|
+
&& !isModelCoolingDown(userId, agentId, model.id)
|
|
459
|
+
&& !excluded.has(model.id),
|
|
460
|
+
);
|
|
461
|
+
const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
|
|
462
|
+
? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
|
|
510
463
|
: [];
|
|
511
|
-
const
|
|
464
|
+
const enabledIds = normalizeModelSelections(availableModels, configuredEnabledIds);
|
|
465
|
+
const pool = configuredEnabledIds.length > 0
|
|
512
466
|
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
513
467
|
: availableModels;
|
|
514
|
-
const fallbackSearchPool = pool
|
|
515
|
-
const currentModel =
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
468
|
+
const fallbackSearchPool = pool;
|
|
469
|
+
const currentModel = resolveModelSelection(models, currentModelId);
|
|
470
|
+
|
|
471
|
+
// Provider-wide failures (credentials, throttling, or service outages) are
|
|
472
|
+
// likely to affect another model on the same provider. Prefer a different
|
|
473
|
+
// provider before consulting the configured fallback.
|
|
474
|
+
const failureStatus = Number(
|
|
475
|
+
failureError?.status
|
|
476
|
+
?? failureError?.statusCode
|
|
477
|
+
?? failureError?.response?.status,
|
|
478
|
+
);
|
|
479
|
+
const isProviderScopedFailure = (
|
|
480
|
+
failureStatus === 401
|
|
481
|
+
|| failureStatus === 403
|
|
482
|
+
|| failureStatus === 429
|
|
483
|
+
|| (failureStatus >= 500 && failureStatus < 600)
|
|
484
|
+
|| /rate.?limit|free-models-per|service unavailable|provider unavailable|authentication|api key/i
|
|
485
|
+
.test(String(failureError?.message || ''))
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (preferredFallbackId && !isProviderScopedFailure) {
|
|
489
|
+
const preferred = resolveModelSelection(fallbackSearchPool, preferredFallbackId)
|
|
490
|
+
|| resolveModelSelection(availableModels, preferredFallbackId);
|
|
491
|
+
if (preferred && preferred.id !== currentModel?.id) return preferred.id;
|
|
528
492
|
}
|
|
529
493
|
|
|
530
494
|
if (currentModel?.provider) {
|
|
531
495
|
const differentProvider = fallbackSearchPool.find((model) =>
|
|
532
|
-
model.id !==
|
|
496
|
+
model.id !== currentModel.id && model.provider !== currentModel.provider);
|
|
533
497
|
if (differentProvider) return differentProvider.id;
|
|
534
498
|
}
|
|
535
499
|
|
|
536
|
-
// If no different-provider model exists, still try the preferred fallback
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (preferred) return preferred.id;
|
|
500
|
+
// If no different-provider model exists, still try the preferred fallback.
|
|
501
|
+
if (preferredFallbackId) {
|
|
502
|
+
const preferred = resolveModelSelection(fallbackSearchPool, preferredFallbackId)
|
|
503
|
+
|| resolveModelSelection(availableModels, preferredFallbackId);
|
|
504
|
+
if (preferred && preferred.id !== currentModel?.id) return preferred.id;
|
|
542
505
|
}
|
|
543
506
|
|
|
544
|
-
const differentModel = fallbackSearchPool.find((model) => model.id !==
|
|
507
|
+
const differentModel = fallbackSearchPool.find((model) => model.id !== currentModel?.id);
|
|
545
508
|
return differentModel?.id || null;
|
|
546
509
|
}
|
|
547
510
|
|
|
@@ -552,6 +515,7 @@ function estimateTokenValue(value) {
|
|
|
552
515
|
}
|
|
553
516
|
|
|
554
517
|
async function runConversation(engine, userId, userMessage, options = {}, _modelOverride = null) {
|
|
518
|
+
throwIfAborted(options.signal, 'Agent run aborted before startup.');
|
|
555
519
|
const triggerType = options.triggerType || 'user';
|
|
556
520
|
const { resolveAgentId } = require('../../agents/manager');
|
|
557
521
|
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
@@ -563,7 +527,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
563
527
|
const triggerSource = options.triggerSource || 'web';
|
|
564
528
|
let provider = null;
|
|
565
529
|
let model = null;
|
|
530
|
+
let modelSelectionId = null;
|
|
566
531
|
let providerName = null;
|
|
532
|
+
const modelTurnFailedModelSelectionIds = new Set();
|
|
567
533
|
let messages = [];
|
|
568
534
|
let iteration = 0;
|
|
569
535
|
let totalTokens = 0;
|
|
@@ -572,7 +538,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
572
538
|
let failedStepCount = 0;
|
|
573
539
|
let toolExecutions = [];
|
|
574
540
|
let deliverableWorkflow = null;
|
|
575
|
-
let
|
|
541
|
+
let detachExternalAbort = null;
|
|
542
|
+
const runTitle = generateTitle(userMessage);
|
|
543
|
+
let runRecordCreated = false;
|
|
576
544
|
const timelineService = app?.locals?.timelineService || null;
|
|
577
545
|
|
|
578
546
|
const { releaseReservation } = enforceRateLimits(userId, {
|
|
@@ -589,6 +557,59 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
589
557
|
// analysis when the mode is known. See the post-analysis policy rebuild below.
|
|
590
558
|
let loopPolicy = buildLoopPolicy(aiSettings, triggerType, 'execute', options);
|
|
591
559
|
let maxIterations = loopPolicy.maxIterations;
|
|
560
|
+
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
561
|
+
const requestedModel = String(
|
|
562
|
+
_modelOverride
|
|
563
|
+
|| (triggerType === 'subagent' ? aiSettings.default_subagent_model : aiSettings.default_chat_model)
|
|
564
|
+
|| 'auto',
|
|
565
|
+
).trim();
|
|
566
|
+
try {
|
|
567
|
+
db.prepare(`INSERT INTO agent_runs(
|
|
568
|
+
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
569
|
+
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
|
|
570
|
+
runId,
|
|
571
|
+
userId,
|
|
572
|
+
agentId,
|
|
573
|
+
runTitle,
|
|
574
|
+
triggerType,
|
|
575
|
+
triggerSource,
|
|
576
|
+
requestedModel,
|
|
577
|
+
Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
|
|
578
|
+
);
|
|
579
|
+
runRecordCreated = true;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (/unique|primary key|constraint/i.test(String(error?.message || ''))) {
|
|
582
|
+
const conflict = new Error(`A run with id "${runId}" already exists.`);
|
|
583
|
+
conflict.code = 'RUN_ID_CONFLICT';
|
|
584
|
+
throw conflict;
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
const startupAbortController = new AbortController();
|
|
589
|
+
engine.activeRuns.set(runId, {
|
|
590
|
+
userId,
|
|
591
|
+
agentId,
|
|
592
|
+
title: runTitle,
|
|
593
|
+
status: 'running',
|
|
594
|
+
aborted: false,
|
|
595
|
+
abortController: startupAbortController,
|
|
596
|
+
pauseAvailable: false,
|
|
597
|
+
toolPids: new Set(),
|
|
598
|
+
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
599
|
+
});
|
|
600
|
+
if (options.signal) {
|
|
601
|
+
const abortFromExternal = () => {
|
|
602
|
+
engine.interruptRun(
|
|
603
|
+
runId,
|
|
604
|
+
String(options.signal.reason || 'Agent run interrupted by its caller.'),
|
|
605
|
+
);
|
|
606
|
+
};
|
|
607
|
+
if (options.signal.aborted) abortFromExternal();
|
|
608
|
+
else options.signal.addEventListener('abort', abortFromExternal, { once: true });
|
|
609
|
+
detachExternalAbort = () => {
|
|
610
|
+
options.signal.removeEventListener('abort', abortFromExternal);
|
|
611
|
+
};
|
|
612
|
+
}
|
|
592
613
|
const providerStatusConfig = {
|
|
593
614
|
agentId,
|
|
594
615
|
onStatus: (status) => {
|
|
@@ -605,18 +626,61 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
605
626
|
userMessage,
|
|
606
627
|
triggerType === 'subagent',
|
|
607
628
|
_modelOverride,
|
|
608
|
-
providerStatusConfig
|
|
629
|
+
{ ...providerStatusConfig, signal: startupAbortController.signal }
|
|
609
630
|
);
|
|
610
631
|
provider = selectedProvider.provider;
|
|
611
632
|
model = selectedProvider.model;
|
|
633
|
+
modelSelectionId = selectedProvider.modelSelectionId;
|
|
612
634
|
providerName = selectedProvider.providerName;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
635
|
+
if (
|
|
636
|
+
startupAbortController.signal.aborted
|
|
637
|
+
|| engine.getRunMeta(runId)?.status !== 'running'
|
|
638
|
+
) {
|
|
639
|
+
const startupError = new Error(
|
|
640
|
+
String(startupAbortController.signal.reason || 'Run stopped during model selection.'),
|
|
641
|
+
);
|
|
642
|
+
startupError.name = 'AbortError';
|
|
643
|
+
startupError.code = 'ABORT_ERR';
|
|
644
|
+
throw startupError;
|
|
645
|
+
}
|
|
646
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
647
|
+
.run(modelSelectionId, runId);
|
|
648
|
+
const recordFailedModel = (failedSelectionId, error, excludedModels) => {
|
|
649
|
+
excludedModels.add(failedSelectionId);
|
|
650
|
+
recordModelFailure(userId, agentId, failedSelectionId, error);
|
|
651
|
+
};
|
|
652
|
+
const switchToFallbackModel = async (
|
|
653
|
+
failedSelectionId,
|
|
654
|
+
error,
|
|
655
|
+
phase,
|
|
656
|
+
excludedModels,
|
|
657
|
+
) => {
|
|
658
|
+
recordFailedModel(failedSelectionId, error, excludedModels);
|
|
659
|
+
const fallbackModelId = await getFailureFallbackModelId(
|
|
660
|
+
userId,
|
|
661
|
+
agentId,
|
|
662
|
+
failedSelectionId,
|
|
663
|
+
aiSettings.fallback_model_id,
|
|
664
|
+
error,
|
|
665
|
+
engine.getRunMeta(runId)?.abortController?.signal,
|
|
666
|
+
excludedModels,
|
|
667
|
+
);
|
|
668
|
+
if (!fallbackModelId || fallbackModelId === failedSelectionId) return false;
|
|
669
|
+
const failureSummary = summarizeForLog(error?.message || error, 180);
|
|
670
|
+
console.log(
|
|
671
|
+
`[Engine] ${phase} failed on ${failedSelectionId}: ${failureSummary}; attempting fallback to: ${fallbackModelId}`
|
|
672
|
+
);
|
|
673
|
+
engine.recordRunEvent(userId, runId, 'model_fallback', {
|
|
674
|
+
phase,
|
|
675
|
+
failedModel: failedSelectionId,
|
|
676
|
+
fallbackModel: fallbackModelId,
|
|
677
|
+
errorCode: error?.code || null,
|
|
678
|
+
errorStatus: error?.status || error?.statusCode || error?.response?.status || null,
|
|
679
|
+
error: failureSummary,
|
|
680
|
+
}, { agentId });
|
|
617
681
|
engine.emit(userId, 'run:interim', {
|
|
618
682
|
runId,
|
|
619
|
-
message: `Model service failed on ${
|
|
683
|
+
message: `Model service failed on ${failedSelectionId}; retrying with ${fallbackModelId}.`,
|
|
620
684
|
phase: 'model_fallback'
|
|
621
685
|
});
|
|
622
686
|
const fallback = await getProviderForUser(
|
|
@@ -624,46 +688,50 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
624
688
|
userMessage,
|
|
625
689
|
triggerType === 'subagent',
|
|
626
690
|
fallbackModelId,
|
|
627
|
-
|
|
691
|
+
{
|
|
692
|
+
...providerStatusConfig,
|
|
693
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
694
|
+
}
|
|
628
695
|
);
|
|
629
696
|
provider = fallback.provider;
|
|
630
697
|
model = fallback.model;
|
|
698
|
+
modelSelectionId = fallback.modelSelectionId;
|
|
631
699
|
providerName = fallback.providerName;
|
|
700
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
701
|
+
.run(modelSelectionId, runId);
|
|
702
|
+
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
703
|
+
model,
|
|
704
|
+
modelSelectionId,
|
|
705
|
+
providerName,
|
|
706
|
+
});
|
|
632
707
|
return true;
|
|
633
708
|
};
|
|
634
709
|
const runWithModelFallback = async (phase, fn) => {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
710
|
+
let recoveries = 0;
|
|
711
|
+
const failedModels = new Set();
|
|
712
|
+
while (true) {
|
|
713
|
+
try {
|
|
714
|
+
const result = await fn();
|
|
715
|
+
recordModelSuccess(userId, agentId, modelSelectionId);
|
|
716
|
+
return result;
|
|
717
|
+
} catch (err) {
|
|
718
|
+
if (recoveries >= loopPolicy.maxModelFailureRecoveries) {
|
|
719
|
+
recordFailedModel(modelSelectionId, err, failedModels);
|
|
720
|
+
throw err;
|
|
721
|
+
}
|
|
722
|
+
const failedSelectionId = modelSelectionId;
|
|
723
|
+
const switched = await switchToFallbackModel(
|
|
724
|
+
failedSelectionId,
|
|
725
|
+
err,
|
|
726
|
+
phase,
|
|
727
|
+
failedModels,
|
|
728
|
+
);
|
|
729
|
+
if (!switched) throw err;
|
|
730
|
+
recoveries += 1;
|
|
731
|
+
}
|
|
642
732
|
}
|
|
643
733
|
};
|
|
644
734
|
|
|
645
|
-
runTitle = generateTitle(userMessage);
|
|
646
|
-
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
647
|
-
db.prepare(`INSERT INTO agent_runs(
|
|
648
|
-
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
649
|
-
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
650
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
651
|
-
status = 'running',
|
|
652
|
-
model = excluded.model,
|
|
653
|
-
updated_at = datetime('now'),
|
|
654
|
-
completed_at = NULL,
|
|
655
|
-
error = NULL,
|
|
656
|
-
metadata_json = COALESCE(agent_runs.metadata_json, excluded.metadata_json)`).run(
|
|
657
|
-
runId,
|
|
658
|
-
userId,
|
|
659
|
-
agentId,
|
|
660
|
-
runTitle,
|
|
661
|
-
triggerType,
|
|
662
|
-
triggerSource,
|
|
663
|
-
model,
|
|
664
|
-
Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
|
|
665
|
-
);
|
|
666
|
-
|
|
667
735
|
const retryMessagingState = options.messagingRetryState || {};
|
|
668
736
|
const carriedFinalMessage = String(retryMessagingState.lastFinalMessage || '').trim();
|
|
669
737
|
const carriedExplicitMessageSent = retryMessagingState.explicitMessageSent === true;
|
|
@@ -685,6 +753,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
685
753
|
userId,
|
|
686
754
|
agentId,
|
|
687
755
|
title: runTitle,
|
|
756
|
+
model,
|
|
757
|
+
modelSelectionId,
|
|
758
|
+
providerName,
|
|
688
759
|
status: 'running',
|
|
689
760
|
aborted: false,
|
|
690
761
|
messagingSent: false,
|
|
@@ -709,6 +780,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
709
780
|
voiceSessionId: options.voiceSessionId || null,
|
|
710
781
|
steeringQueue: [],
|
|
711
782
|
systemSteeringQueue: [],
|
|
783
|
+
abortController: startupAbortController,
|
|
784
|
+
pauseAvailable: false,
|
|
712
785
|
toolPids: new Set(),
|
|
713
786
|
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
714
787
|
repetitionGuard: new ToolRepetitionGuard(),
|
|
@@ -733,10 +806,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
733
806
|
goalContract: carriedGoalContract,
|
|
734
807
|
});
|
|
735
808
|
engine.startMessagingProgressSupervisor(runId);
|
|
736
|
-
engine.emit(userId, 'run:start', {
|
|
809
|
+
engine.emit(userId, 'run:start', {
|
|
810
|
+
runId,
|
|
811
|
+
agentId,
|
|
812
|
+
title: runTitle,
|
|
813
|
+
model,
|
|
814
|
+
modelSelectionId,
|
|
815
|
+
provider: providerName,
|
|
816
|
+
triggerType,
|
|
817
|
+
triggerSource,
|
|
818
|
+
});
|
|
737
819
|
engine.recordRunEvent(userId, runId, 'run_started', {
|
|
738
820
|
title: runTitle,
|
|
739
821
|
model,
|
|
822
|
+
modelSelectionId,
|
|
823
|
+
provider: providerName,
|
|
740
824
|
triggerType,
|
|
741
825
|
triggerSource,
|
|
742
826
|
}, { agentId });
|
|
@@ -750,7 +834,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
750
834
|
triggerSource,
|
|
751
835
|
});
|
|
752
836
|
console.info(
|
|
753
|
-
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${
|
|
837
|
+
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${modelSelectionId} title=${summarizeForLog(runTitle, 120)}`
|
|
754
838
|
);
|
|
755
839
|
|
|
756
840
|
const systemPrompt = await engine.buildSystemPrompt(userId, {
|
|
@@ -881,7 +965,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
881
965
|
if (triggerSource === 'messaging') {
|
|
882
966
|
const runMetaForCompose = engine.getRunMeta(runId);
|
|
883
967
|
if (runMetaForCompose) {
|
|
884
|
-
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
968
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false, signal = null } = {}) => {
|
|
885
969
|
try {
|
|
886
970
|
const rm = engine.getRunMeta(runId);
|
|
887
971
|
const ledger = rm?.progressLedger || {};
|
|
@@ -898,7 +982,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
898
982
|
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
899
983
|
const contextBlock = [
|
|
900
984
|
buildProgressUpdatePrompt(),
|
|
901
|
-
stalled ? '
|
|
985
|
+
stalled ? 'No verified progress has occurred for the stall threshold. State that fact plainly if it matters; do not reassure, promise continued work, or imply activity beyond the evidence.' : '',
|
|
902
986
|
'',
|
|
903
987
|
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
904
988
|
currentTool ? `Doing now: using ${currentTool}` : '',
|
|
@@ -908,17 +992,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
908
992
|
// Reuse the run's real system prompt so the update follows the same voice and
|
|
909
993
|
// formatting guidelines as every other message (single source of truth).
|
|
910
994
|
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
911
|
-
|| 'You are
|
|
912
|
-
const resp = await
|
|
913
|
-
provider.chat(
|
|
995
|
+
|| 'You are the user\'s favorite contact: warm, direct, and competent.';
|
|
996
|
+
const resp = await runAbortableModelCall(
|
|
997
|
+
(signal) => provider.chat(
|
|
914
998
|
[
|
|
915
999
|
{ role: 'system', content: sysContent },
|
|
916
1000
|
{ role: 'user', content: contextBlock },
|
|
917
1001
|
],
|
|
918
1002
|
[],
|
|
919
|
-
{
|
|
1003
|
+
{
|
|
1004
|
+
model,
|
|
1005
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
1006
|
+
signal,
|
|
1007
|
+
},
|
|
920
1008
|
),
|
|
921
|
-
|
|
1009
|
+
{
|
|
1010
|
+
...options,
|
|
1011
|
+
signals: [rm?.abortController?.signal, signal],
|
|
1012
|
+
},
|
|
922
1013
|
'Progress update compose',
|
|
923
1014
|
);
|
|
924
1015
|
return sanitizeModelOutput(resp.content || '', { model });
|
|
@@ -1029,6 +1120,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1029
1120
|
null,
|
|
1030
1121
|
{
|
|
1031
1122
|
...providerStatusConfig,
|
|
1123
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1032
1124
|
selectionHint: {
|
|
1033
1125
|
purpose: requestedPurpose,
|
|
1034
1126
|
complexity: analysis?.complexity,
|
|
@@ -1038,15 +1130,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1038
1130
|
},
|
|
1039
1131
|
}
|
|
1040
1132
|
);
|
|
1041
|
-
if (selectedAfterAnalysis.
|
|
1133
|
+
if (selectedAfterAnalysis.modelSelectionId !== modelSelectionId) {
|
|
1042
1134
|
provider = selectedAfterAnalysis.provider;
|
|
1043
1135
|
model = selectedAfterAnalysis.model;
|
|
1136
|
+
modelSelectionId = selectedAfterAnalysis.modelSelectionId;
|
|
1044
1137
|
providerName = selectedAfterAnalysis.providerName;
|
|
1045
1138
|
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
1046
|
-
.run(
|
|
1139
|
+
.run(modelSelectionId, runId);
|
|
1140
|
+
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
1141
|
+
model,
|
|
1142
|
+
modelSelectionId,
|
|
1143
|
+
providerName,
|
|
1144
|
+
});
|
|
1047
1145
|
engine.emit(userId, 'run:interim', {
|
|
1048
1146
|
runId,
|
|
1049
|
-
message: `Switched to ${
|
|
1147
|
+
message: `Switched to ${modelSelectionId} for this run after task analysis.`,
|
|
1050
1148
|
phase: 'model_selection'
|
|
1051
1149
|
});
|
|
1052
1150
|
}
|
|
@@ -1207,8 +1305,15 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1207
1305
|
// regardless of which iteration they fall in.
|
|
1208
1306
|
let consecutiveToolFailures = 0;
|
|
1209
1307
|
const iterationBudget = new IterationBudget(maxIterations);
|
|
1308
|
+
const activeRunMeta = engine.getRunMeta(runId);
|
|
1309
|
+
if (activeRunMeta) activeRunMeta.pauseAvailable = !directAnswerEligible;
|
|
1210
1310
|
|
|
1211
1311
|
while (!directAnswerEligible && iterationBudget.consume()) {
|
|
1312
|
+
const lifecycleAtStart = await engine.checkpointLifecycle(runId, 'iteration_boundary', {
|
|
1313
|
+
iteration: iterationBudget.used,
|
|
1314
|
+
stepIndex,
|
|
1315
|
+
});
|
|
1316
|
+
if (lifecycleAtStart?.action === 'stop' || lifecycleAtStart?.action === 'interrupt') break;
|
|
1212
1317
|
if (engine.isRunStopped(runId)) break;
|
|
1213
1318
|
iteration = iterationBudget.used;
|
|
1214
1319
|
|
|
@@ -1230,7 +1335,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1230
1335
|
reason,
|
|
1231
1336
|
stoppedBy: hookResult.stopped_by || hookResult.stoppedBy || null,
|
|
1232
1337
|
}, { agentId });
|
|
1233
|
-
|
|
1338
|
+
engine.stopRun(runId, reason);
|
|
1234
1339
|
break;
|
|
1235
1340
|
}
|
|
1236
1341
|
const systemSteering = String(hookResult?.systemSteering || hookResult?.system_steering || '').trim();
|
|
@@ -1289,6 +1394,47 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1289
1394
|
continue;
|
|
1290
1395
|
}
|
|
1291
1396
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1397
|
+
const researchAdequacyAtCap = assessResearchAdequacy({
|
|
1398
|
+
analysis,
|
|
1399
|
+
goalContext: runGoalCtx,
|
|
1400
|
+
toolExecutions,
|
|
1401
|
+
});
|
|
1402
|
+
// Incomplete multi-target research is not "stuck" yet if primary sources
|
|
1403
|
+
// remain uncovered. Give one guided research continuation instead of
|
|
1404
|
+
// force-finishing with a guessed comparison.
|
|
1405
|
+
if (
|
|
1406
|
+
researchAdequacyAtCap.adequate === false
|
|
1407
|
+
&& researchAdequacyAtCap.intensity !== 'none'
|
|
1408
|
+
&& iterMeta
|
|
1409
|
+
&& iterMeta.researchAdequacyDeferralUsed !== true
|
|
1410
|
+
&& (
|
|
1411
|
+
researchAdequacyAtCap.uncoveredTargets.length > 0
|
|
1412
|
+
|| researchAdequacyAtCap.primarySourceCount < researchAdequacyAtCap.requiredPrimarySources
|
|
1413
|
+
)
|
|
1414
|
+
) {
|
|
1415
|
+
iterMeta.researchAdequacyDeferralUsed = true;
|
|
1416
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, churnNudgeThreshold - 1);
|
|
1417
|
+
messages.push({
|
|
1418
|
+
role: 'system',
|
|
1419
|
+
content: [
|
|
1420
|
+
'Research self-check: evidence is still incomplete, so this run must not force-finish yet.',
|
|
1421
|
+
researchAdequacyAtCap.reason ? `Gap: ${researchAdequacyAtCap.reason}` : '',
|
|
1422
|
+
researchAdequacyAtCap.nextActions?.length
|
|
1423
|
+
? `Next safe steps:\n- ${researchAdequacyAtCap.nextActions.join('\n- ')}`
|
|
1424
|
+
: '',
|
|
1425
|
+
'Open or fetch primary sources for uncovered targets now. Do not invent the missing comparison from memory or search snippets alone.',
|
|
1426
|
+
].filter(Boolean).join('\n'),
|
|
1427
|
+
});
|
|
1428
|
+
engine.recordRunEvent(userId, runId, 'read_only_wrapup_deferred_for_research', {
|
|
1429
|
+
iteration,
|
|
1430
|
+
readOnlyCount,
|
|
1431
|
+
intensity: researchAdequacyAtCap.intensity,
|
|
1432
|
+
uncoveredTargets: researchAdequacyAtCap.uncoveredTargets,
|
|
1433
|
+
primarySourceCount: researchAdequacyAtCap.primarySourceCount,
|
|
1434
|
+
requiredPrimarySources: researchAdequacyAtCap.requiredPrimarySources,
|
|
1435
|
+
}, { agentId });
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1292
1438
|
triggerForceWrapup = true;
|
|
1293
1439
|
} else if (readOnlyCount >= churnNudgeThreshold) {
|
|
1294
1440
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
@@ -1353,8 +1499,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1353
1499
|
});
|
|
1354
1500
|
let wrapTokens = 0;
|
|
1355
1501
|
try {
|
|
1356
|
-
const wrapResponse = await
|
|
1357
|
-
provider.chat(
|
|
1502
|
+
const wrapResponse = await runAbortableModelCall(
|
|
1503
|
+
(signal) => provider.chat(
|
|
1358
1504
|
sanitizeConversationMessages([
|
|
1359
1505
|
...messages,
|
|
1360
1506
|
{
|
|
@@ -1363,13 +1509,22 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1363
1509
|
readOnlyCount,
|
|
1364
1510
|
alreadyRead,
|
|
1365
1511
|
platform: options?.source || null,
|
|
1512
|
+
researchAdequacy: assessResearchAdequacy({
|
|
1513
|
+
analysis,
|
|
1514
|
+
goalContext: runGoalCtx,
|
|
1515
|
+
toolExecutions,
|
|
1516
|
+
}),
|
|
1366
1517
|
}),
|
|
1367
1518
|
},
|
|
1368
1519
|
]),
|
|
1369
1520
|
[],
|
|
1370
|
-
{
|
|
1521
|
+
{
|
|
1522
|
+
model,
|
|
1523
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
1524
|
+
signal,
|
|
1525
|
+
},
|
|
1371
1526
|
),
|
|
1372
|
-
options,
|
|
1527
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
1373
1528
|
'No-progress wrap-up',
|
|
1374
1529
|
);
|
|
1375
1530
|
wrapTokens = wrapResponse.usage?.totalTokens || 0;
|
|
@@ -1422,9 +1577,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1422
1577
|
let metrics = engine.estimatePromptMetrics(messages, tools);
|
|
1423
1578
|
const contextWindow = provider.getContextWindow(model);
|
|
1424
1579
|
if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
|
|
1425
|
-
messages = await
|
|
1426
|
-
compact(messages, provider, model, contextWindow),
|
|
1427
|
-
options,
|
|
1580
|
+
messages = await runAbortableModelCall(
|
|
1581
|
+
(signal) => compact(messages, provider, model, contextWindow, { signal }),
|
|
1582
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
1428
1583
|
`Context compaction before iteration ${iteration}`,
|
|
1429
1584
|
);
|
|
1430
1585
|
messages = sanitizeConversationMessages(messages);
|
|
@@ -1444,41 +1599,59 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1444
1599
|
let responseModel = model;
|
|
1445
1600
|
let streamContent = '';
|
|
1446
1601
|
|
|
1447
|
-
const tryModelCall = async (
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1602
|
+
const tryModelCall = async () => {
|
|
1603
|
+
let requestMessages = messages;
|
|
1604
|
+
let requestPhase = 'model_turn';
|
|
1605
|
+
while (true) {
|
|
1606
|
+
try {
|
|
1607
|
+
const modelCall = await engine.requestModelResponse({
|
|
1608
|
+
provider,
|
|
1609
|
+
providerName,
|
|
1610
|
+
model,
|
|
1611
|
+
messages: requestMessages,
|
|
1612
|
+
tools,
|
|
1613
|
+
options: {
|
|
1614
|
+
...options,
|
|
1615
|
+
userId,
|
|
1616
|
+
agentId,
|
|
1617
|
+
runId,
|
|
1618
|
+
phase: requestPhase,
|
|
1619
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1620
|
+
},
|
|
1621
|
+
runId,
|
|
1622
|
+
iteration,
|
|
1623
|
+
});
|
|
1624
|
+
response = modelCall.response;
|
|
1625
|
+
responseModel = modelCall.responseModel;
|
|
1626
|
+
streamContent = modelCall.streamContent;
|
|
1627
|
+
recordModelSuccess(userId, agentId, modelSelectionId);
|
|
1628
|
+
return;
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1631
|
+
const runSignal = engine.getRunMeta(runId)?.abortController?.signal;
|
|
1632
|
+
if (isAbortError(err) || runSignal?.aborted) throw err;
|
|
1633
|
+
if (modelFailureRecoveries >= loopPolicy.maxModelFailureRecoveries) {
|
|
1634
|
+
recordFailedModel(
|
|
1635
|
+
modelSelectionId,
|
|
1636
|
+
err,
|
|
1637
|
+
modelTurnFailedModelSelectionIds,
|
|
1638
|
+
);
|
|
1639
|
+
throw err;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1468
1642
|
const failedModel = model;
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
fallbackModelId,
|
|
1475
|
-
providerStatusConfig
|
|
1643
|
+
const switched = await switchToFallbackModel(
|
|
1644
|
+
modelSelectionId,
|
|
1645
|
+
err,
|
|
1646
|
+
'model turn',
|
|
1647
|
+
modelTurnFailedModelSelectionIds,
|
|
1476
1648
|
);
|
|
1477
|
-
|
|
1478
|
-
model = fallback.model;
|
|
1479
|
-
providerName = fallback.providerName;
|
|
1649
|
+
if (!switched) throw err;
|
|
1480
1650
|
|
|
1481
|
-
|
|
1651
|
+
modelFailureRecoveries += 1;
|
|
1652
|
+
failedStepCount += 1;
|
|
1653
|
+
requestPhase = 'model_turn_fallback';
|
|
1654
|
+
requestMessages = sanitizeConversationMessages([
|
|
1482
1655
|
...messages,
|
|
1483
1656
|
{
|
|
1484
1657
|
role: 'system',
|
|
@@ -1489,22 +1662,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1489
1662
|
})
|
|
1490
1663
|
}
|
|
1491
1664
|
]);
|
|
1492
|
-
|
|
1493
|
-
const fallbackCall = await engine.requestModelResponse({
|
|
1494
|
-
provider,
|
|
1495
|
-
providerName,
|
|
1496
|
-
model,
|
|
1497
|
-
messages: retryMessages,
|
|
1498
|
-
tools,
|
|
1499
|
-
options: { ...options, userId },
|
|
1500
|
-
runId,
|
|
1501
|
-
iteration,
|
|
1502
|
-
});
|
|
1503
|
-
response = fallbackCall.response;
|
|
1504
|
-
responseModel = fallbackCall.responseModel;
|
|
1505
|
-
streamContent = fallbackCall.streamContent;
|
|
1506
|
-
} else {
|
|
1507
|
-
throw err;
|
|
1508
1665
|
}
|
|
1509
1666
|
}
|
|
1510
1667
|
};
|
|
@@ -1512,30 +1669,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1512
1669
|
try {
|
|
1513
1670
|
await tryModelCall();
|
|
1514
1671
|
} catch (err) {
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
nextModel: model,
|
|
1528
|
-
errorMessage: modelError
|
|
1529
|
-
})
|
|
1530
|
-
});
|
|
1531
|
-
engine.emit(userId, 'run:interim', {
|
|
1532
|
-
runId,
|
|
1533
|
-
message: 'Model call failed; adapting and retrying autonomously.',
|
|
1534
|
-
phase: 'recovering'
|
|
1535
|
-
});
|
|
1672
|
+
const lifecycleAbort = err?.name === 'AbortError'
|
|
1673
|
+
|| err?.code === 'ABORT_ERR'
|
|
1674
|
+
|| /abort/i.test(String(err?.name || ''))
|
|
1675
|
+
|| engine.getRunMeta(runId)?.abortController?.signal?.aborted === true;
|
|
1676
|
+
const lifecycleControl = await engine.checkpointLifecycle(runId, 'model_boundary', {
|
|
1677
|
+
iteration,
|
|
1678
|
+
stepIndex,
|
|
1679
|
+
});
|
|
1680
|
+
if (engine.isRunStopped(runId)) break;
|
|
1681
|
+
if (lifecycleAbort && !lifecycleControl && engine.getRunMeta(runId)?.status === 'running') {
|
|
1682
|
+
iterationBudget.refund();
|
|
1683
|
+
iteration = iterationBudget.used;
|
|
1536
1684
|
continue;
|
|
1537
1685
|
}
|
|
1538
|
-
|
|
1686
|
+
if (lifecycleControl?.action === 'stop' || lifecycleControl?.action === 'interrupt') break;
|
|
1539
1687
|
throw err;
|
|
1540
1688
|
}
|
|
1541
1689
|
|
|
@@ -1543,6 +1691,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1543
1691
|
response = { content: streamContent, toolCalls: [], finishReason: 'stop', usage: null };
|
|
1544
1692
|
}
|
|
1545
1693
|
|
|
1694
|
+
const lifecycleAfterModel = await engine.checkpointLifecycle(runId, 'model_boundary', {
|
|
1695
|
+
iteration,
|
|
1696
|
+
stepIndex,
|
|
1697
|
+
});
|
|
1698
|
+
if (lifecycleAfterModel?.action === 'stop' || lifecycleAfterModel?.action === 'interrupt') break;
|
|
1699
|
+
|
|
1546
1700
|
if (response.usage) {
|
|
1547
1701
|
totalTokens += response.usage.totalTokens || 0;
|
|
1548
1702
|
}
|
|
@@ -1564,10 +1718,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1564
1718
|
toolCallCount: response.toolCalls?.length || 0,
|
|
1565
1719
|
contentPreview: String(lastContent || streamContent || '').slice(0, 240),
|
|
1566
1720
|
}, { agentId });
|
|
1567
|
-
engine.updateRunProgress(runId, {}, {
|
|
1568
|
-
verified: true,
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
1721
|
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
1572
1722
|
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
1573
1723
|
if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
|
|
@@ -1677,13 +1827,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1677
1827
|
iteration,
|
|
1678
1828
|
}, { agentId });
|
|
1679
1829
|
if (loopDecision.decision.status === 'continue') {
|
|
1830
|
+
const researchGuidance = loopDecision.researchAdequacy?.adequate === false
|
|
1831
|
+
? [
|
|
1832
|
+
`Research still incomplete: ${loopDecision.researchAdequacy.reason || 'gather primary evidence for remaining targets.'}`,
|
|
1833
|
+
loopDecision.researchAdequacy.nextActions?.length
|
|
1834
|
+
? `Next safe steps: ${loopDecision.researchAdequacy.nextActions.join(' ')}`
|
|
1835
|
+
: '',
|
|
1836
|
+
].filter(Boolean).join(' ')
|
|
1837
|
+
: '';
|
|
1680
1838
|
messages.push({
|
|
1681
1839
|
role: 'system',
|
|
1682
1840
|
content: [
|
|
1683
1841
|
'The run self-check determined the latest assistant text is not terminal.',
|
|
1684
1842
|
'Continue with the next safe tool/model step.',
|
|
1685
1843
|
'If the text was only a progress note, do not repeat it; either make progress or provide a real final/blocker reply.',
|
|
1686
|
-
|
|
1844
|
+
'Do not invent missing targets, outcomes, or tool results. Gather evidence or return a truthful partial/blocker answer.',
|
|
1845
|
+
researchGuidance,
|
|
1846
|
+
].filter(Boolean).join(' '),
|
|
1687
1847
|
});
|
|
1688
1848
|
lastContent = '';
|
|
1689
1849
|
continue;
|
|
@@ -1694,7 +1854,10 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1694
1854
|
|
|
1695
1855
|
const canRunParallelBatch = (
|
|
1696
1856
|
response.toolCalls.length > 1
|
|
1697
|
-
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(
|
|
1857
|
+
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(
|
|
1858
|
+
toolCall,
|
|
1859
|
+
tools.find((tool) => tool?.name === toolCall?.function?.name) || null,
|
|
1860
|
+
))
|
|
1698
1861
|
);
|
|
1699
1862
|
if (canRunParallelBatch) {
|
|
1700
1863
|
const parallelToolNames = response.toolCalls
|
|
@@ -1718,6 +1881,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1718
1881
|
iteration,
|
|
1719
1882
|
options,
|
|
1720
1883
|
});
|
|
1884
|
+
const lifecycleAfterBatch = await engine.checkpointLifecycle(runId, 'tool_boundary', {
|
|
1885
|
+
iteration,
|
|
1886
|
+
stepIndex: batch.endingStepIndex,
|
|
1887
|
+
});
|
|
1888
|
+
if (lifecycleAfterBatch?.action === 'stop' || lifecycleAfterBatch?.action === 'interrupt') break;
|
|
1721
1889
|
stepIndex = batch.endingStepIndex;
|
|
1722
1890
|
let batchGatheredNewEvidence = false;
|
|
1723
1891
|
for (const item of batch.results) {
|
|
@@ -1726,6 +1894,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1726
1894
|
item.toolArgs,
|
|
1727
1895
|
item.result,
|
|
1728
1896
|
item.error || '',
|
|
1897
|
+
tools.find((tool) => tool?.name === item.toolName) || null,
|
|
1729
1898
|
);
|
|
1730
1899
|
execution.input = item.toolArgs;
|
|
1731
1900
|
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
@@ -1765,7 +1934,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1765
1934
|
currentTool: null,
|
|
1766
1935
|
currentStepStartedAt: null,
|
|
1767
1936
|
}, {
|
|
1768
|
-
verified:
|
|
1937
|
+
verified: batchGatheredNewEvidence,
|
|
1769
1938
|
});
|
|
1770
1939
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1771
1940
|
const iterMeta = engine.getRunMeta(runId);
|
|
@@ -1922,6 +2091,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1922
2091
|
|
|
1923
2092
|
let toolResult;
|
|
1924
2093
|
let toolErrorMessage = '';
|
|
2094
|
+
let toolInterruptedForPause = false;
|
|
1925
2095
|
try {
|
|
1926
2096
|
toolResult = await engine.executeTool(toolName, toolArgs, {
|
|
1927
2097
|
userId,
|
|
@@ -1938,6 +2108,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1938
2108
|
deliveryState: options.deliveryState || null,
|
|
1939
2109
|
allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
|
|
1940
2110
|
allowExternalSideEffects: options.allowExternalSideEffects === true,
|
|
2111
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1941
2112
|
});
|
|
1942
2113
|
engine.detachProcessFromRun(runId, toolResult?.pid);
|
|
1943
2114
|
toolErrorMessage = inferToolFailureMessage(toolName, toolResult);
|
|
@@ -1973,17 +2144,30 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1973
2144
|
);
|
|
1974
2145
|
}
|
|
1975
2146
|
} catch (err) {
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2147
|
+
const currentRunMeta = engine.getRunMeta(runId);
|
|
2148
|
+
toolInterruptedForPause = currentRunMeta?.status === 'pausing'
|
|
2149
|
+
&& currentRunMeta.abortController?.signal?.aborted === true;
|
|
2150
|
+
toolErrorMessage = toolInterruptedForPause
|
|
2151
|
+
? 'The tool was interrupted for pause after dispatch; its external outcome is unknown.'
|
|
2152
|
+
: String(err.message || 'Tool execution failed');
|
|
2153
|
+
toolResult = toolInterruptedForPause
|
|
2154
|
+
? { status: 'outcome_unknown', error: toolErrorMessage }
|
|
2155
|
+
: { error: err.message };
|
|
2156
|
+
if (!toolInterruptedForPause) failedStepCount++;
|
|
1979
2157
|
engine.detachProcessFromRun(runId, toolResult?.pid);
|
|
1980
2158
|
db.prepare('UPDATE agent_steps SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
1981
|
-
.run('failed',
|
|
1982
|
-
engine.emit(userId, 'run:tool_end', {
|
|
1983
|
-
|
|
2159
|
+
.run(toolInterruptedForPause ? 'paused' : 'failed', toolErrorMessage, stepId);
|
|
2160
|
+
engine.emit(userId, 'run:tool_end', {
|
|
2161
|
+
runId,
|
|
2162
|
+
stepId,
|
|
2163
|
+
toolName,
|
|
2164
|
+
error: toolErrorMessage,
|
|
2165
|
+
status: toolInterruptedForPause ? 'paused' : 'failed',
|
|
2166
|
+
});
|
|
2167
|
+
engine.recordRunEvent(userId, runId, toolInterruptedForPause ? 'tool_paused' : 'tool_failed', {
|
|
1984
2168
|
toolName,
|
|
1985
|
-
status: 'failed',
|
|
1986
|
-
error:
|
|
2169
|
+
status: toolInterruptedForPause ? 'paused' : 'failed',
|
|
2170
|
+
error: toolErrorMessage,
|
|
1987
2171
|
durationMs: Date.now() - stepStartedAt,
|
|
1988
2172
|
}, { agentId, stepId });
|
|
1989
2173
|
console.warn(
|
|
@@ -1991,11 +2175,26 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1991
2175
|
);
|
|
1992
2176
|
}
|
|
1993
2177
|
|
|
1994
|
-
const
|
|
2178
|
+
const lifecycleAfterTool = await engine.checkpointLifecycle(runId, 'tool_boundary', {
|
|
2179
|
+
iteration,
|
|
2180
|
+
stepIndex,
|
|
2181
|
+
});
|
|
2182
|
+
if (lifecycleAfterTool?.action === 'stop' || lifecycleAfterTool?.action === 'interrupt') break;
|
|
2183
|
+
|
|
2184
|
+
const execution = classifyToolExecution(
|
|
2185
|
+
toolName,
|
|
2186
|
+
toolArgs,
|
|
2187
|
+
toolResult,
|
|
2188
|
+
toolErrorMessage,
|
|
2189
|
+
tools.find((tool) => tool?.name === toolName) || null,
|
|
2190
|
+
);
|
|
1995
2191
|
execution.input = toolArgs;
|
|
1996
2192
|
const repetitionObservation = repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1997
|
-
|
|
1998
|
-
|
|
2193
|
+
const toolMadeConcreteProgress = (
|
|
2194
|
+
(execution.stateChanged && isProgressToolCall(toolName, toolArgs))
|
|
2195
|
+
|| gatheredNewEvidence(execution, repetitionObservation)
|
|
2196
|
+
);
|
|
2197
|
+
if (toolMadeConcreteProgress) {
|
|
1999
2198
|
iterationConcreteProgress = true;
|
|
2000
2199
|
}
|
|
2001
2200
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
@@ -2054,7 +2253,13 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2054
2253
|
tools = engine.getActiveTools(runId);
|
|
2055
2254
|
}
|
|
2056
2255
|
|
|
2057
|
-
if (
|
|
2256
|
+
if (toolInterruptedForPause) {
|
|
2257
|
+
consecutiveToolFailures = 0;
|
|
2258
|
+
messages.push({
|
|
2259
|
+
role: 'system',
|
|
2260
|
+
content: `The outcome of "${toolName}" is unknown because pause interrupted it after dispatch. Do not repeat the call. First inspect or query the affected state with a safe read-only tool, then continue based on verified evidence.`,
|
|
2261
|
+
});
|
|
2262
|
+
} else if (toolErrorMessage) {
|
|
2058
2263
|
consecutiveToolFailures += 1;
|
|
2059
2264
|
const currentRunMeta = engine.getRunMeta(runId);
|
|
2060
2265
|
trackErrorPattern(toolErrorMessage, currentRunMeta);
|
|
@@ -2137,7 +2342,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2137
2342
|
currentTool: null,
|
|
2138
2343
|
currentStepStartedAt: null,
|
|
2139
2344
|
}, {
|
|
2140
|
-
verified:
|
|
2345
|
+
verified: toolMadeConcreteProgress,
|
|
2141
2346
|
stepId,
|
|
2142
2347
|
});
|
|
2143
2348
|
|
|
@@ -2182,22 +2387,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2182
2387
|
if (!engine.activeRuns.has(runId)) break;
|
|
2183
2388
|
}
|
|
2184
2389
|
|
|
2390
|
+
const finalizingRunMeta = engine.getRunMeta(runId);
|
|
2391
|
+
if (finalizingRunMeta) finalizingRunMeta.pauseAvailable = false;
|
|
2392
|
+
|
|
2185
2393
|
if (engine.isRunStopped(runId)) {
|
|
2186
2394
|
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
2187
|
-
const
|
|
2395
|
+
const persistedTerminal = db.prepare(
|
|
2396
|
+
'SELECT status, error FROM agent_runs WHERE id = ?',
|
|
2397
|
+
).get(runId);
|
|
2398
|
+
const terminalStatus = persistedTerminal?.status === 'interrupted'
|
|
2399
|
+
|| stoppedRunMeta?.status === 'interrupted'
|
|
2400
|
+
? 'interrupted'
|
|
2401
|
+
: 'stopped';
|
|
2188
2402
|
const stopReason = stoppedRunMeta?.stopReason || null;
|
|
2189
|
-
db.prepare(
|
|
2190
|
-
`UPDATE agent_runs
|
|
2191
|
-
SET status = ?,
|
|
2192
|
-
error = COALESCE(?, error),
|
|
2193
|
-
updated_at = datetime('now'),
|
|
2194
|
-
completed_at = datetime('now')
|
|
2195
|
-
WHERE id = ?`
|
|
2196
|
-
).run(terminalStatus, stopReason, runId);
|
|
2197
2403
|
console.warn(
|
|
2198
2404
|
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2199
2405
|
);
|
|
2200
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2406
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2201
2407
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2202
2408
|
engine.activeRuns.delete(runId);
|
|
2203
2409
|
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
@@ -2248,16 +2454,20 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2248
2454
|
if (budgetExhaustedWithoutCompletion) {
|
|
2249
2455
|
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
2250
2456
|
try {
|
|
2251
|
-
const wrapResponse = await
|
|
2252
|
-
provider.chat(
|
|
2457
|
+
const wrapResponse = await runAbortableModelCall(
|
|
2458
|
+
(signal) => provider.chat(
|
|
2253
2459
|
sanitizeConversationMessages([
|
|
2254
2460
|
...messages,
|
|
2255
2461
|
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
2256
2462
|
]),
|
|
2257
2463
|
[],
|
|
2258
|
-
{
|
|
2464
|
+
{
|
|
2465
|
+
model,
|
|
2466
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2467
|
+
signal,
|
|
2468
|
+
},
|
|
2259
2469
|
),
|
|
2260
|
-
options,
|
|
2470
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
2261
2471
|
'Max-iteration wrap-up',
|
|
2262
2472
|
);
|
|
2263
2473
|
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
@@ -2290,8 +2500,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2290
2500
|
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery model=${model}`);
|
|
2291
2501
|
let recoveredTokens = 0;
|
|
2292
2502
|
try {
|
|
2293
|
-
const recoveryResponse = await
|
|
2294
|
-
provider.chat(
|
|
2503
|
+
const recoveryResponse = await runAbortableModelCall(
|
|
2504
|
+
(signal) => provider.chat(
|
|
2295
2505
|
sanitizeConversationMessages([
|
|
2296
2506
|
...messages,
|
|
2297
2507
|
{
|
|
@@ -2302,10 +2512,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2302
2512
|
[],
|
|
2303
2513
|
{
|
|
2304
2514
|
model,
|
|
2305
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2515
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2516
|
+
signal,
|
|
2306
2517
|
}
|
|
2307
2518
|
),
|
|
2308
|
-
options,
|
|
2519
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
2309
2520
|
'Blank messaging reply recovery',
|
|
2310
2521
|
);
|
|
2311
2522
|
recoveredTokens = recoveryResponse.usage?.totalTokens || 0;
|
|
@@ -2318,7 +2529,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2318
2529
|
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2319
2530
|
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2320
2531
|
// a deterministic message when the model returned nothing usable.
|
|
2321
|
-
const recoveredVisible = Boolean(
|
|
2532
|
+
const recoveredVisible = Boolean(
|
|
2533
|
+
normalizeOutgoingMessage(lastContent, options?.source || null),
|
|
2534
|
+
);
|
|
2322
2535
|
if (!recoveredVisible) {
|
|
2323
2536
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2324
2537
|
}
|
|
@@ -2336,6 +2549,22 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2336
2549
|
}
|
|
2337
2550
|
}
|
|
2338
2551
|
|
|
2552
|
+
const lifecycleBeforeFinalize = await engine.checkpointLifecycle(runId, 'finalization_boundary', {
|
|
2553
|
+
iteration,
|
|
2554
|
+
stepIndex,
|
|
2555
|
+
});
|
|
2556
|
+
if (
|
|
2557
|
+
engine.isRunStopped(runId)
|
|
2558
|
+
|| lifecycleBeforeFinalize?.action === 'stop'
|
|
2559
|
+
|| lifecycleBeforeFinalize?.action === 'interrupt'
|
|
2560
|
+
) {
|
|
2561
|
+
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
2562
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2563
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
2564
|
+
engine.activeRuns.delete(runId);
|
|
2565
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2339
2568
|
if (
|
|
2340
2569
|
!normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
2341
2570
|
&& !messagingSent
|
|
@@ -2448,6 +2677,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2448
2677
|
});
|
|
2449
2678
|
}
|
|
2450
2679
|
|
|
2680
|
+
// Final reply terminality is decided by the AI completion judge / task_complete path.
|
|
2681
|
+
// Do not phrase-match natural-language drafts here.
|
|
2682
|
+
|
|
2451
2683
|
if (deliverableWorkflow && deliverablePlan) {
|
|
2452
2684
|
engine.recordRunEvent(userId, runId, 'deliverable_validation_started', {
|
|
2453
2685
|
type: deliverableWorkflow.selection.type,
|
|
@@ -2492,8 +2724,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2492
2724
|
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2493
2725
|
.run(totalTokens, conversationId);
|
|
2494
2726
|
if (options.skipConversationMaintenance !== true) {
|
|
2495
|
-
|
|
2496
|
-
|
|
2727
|
+
engine.trackBackgroundTask(
|
|
2728
|
+
(signal) => refreshConversationSummary(
|
|
2729
|
+
conversationId,
|
|
2730
|
+
provider,
|
|
2731
|
+
model,
|
|
2732
|
+
historyWindow,
|
|
2733
|
+
false,
|
|
2734
|
+
{ signal },
|
|
2735
|
+
),
|
|
2736
|
+
{
|
|
2737
|
+
key: `conversation-summary:${conversationId}`,
|
|
2738
|
+
signal: runMeta?.abortController?.signal || null,
|
|
2739
|
+
},
|
|
2740
|
+
).catch((err) => {
|
|
2741
|
+
if (!isAbortError(err, runMeta?.abortController?.signal) && !engine.shuttingDown) {
|
|
2742
|
+
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
2743
|
+
}
|
|
2497
2744
|
});
|
|
2498
2745
|
}
|
|
2499
2746
|
}
|
|
@@ -2515,6 +2762,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2515
2762
|
// Fallback: if this was a messaging-triggered run and no final delivery
|
|
2516
2763
|
// was already sent in this run, auto-send the final assistant text.
|
|
2517
2764
|
// Interim progress updates do not suppress this final delivery.
|
|
2765
|
+
await engine.stopMessagingProgressSupervisor(runId);
|
|
2518
2766
|
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
2519
2767
|
if (engine.shouldSendMessagingFinalFallback(runMeta, lastContent || '', options.source) && !lastFinalDeliveryMessage) {
|
|
2520
2768
|
await engine.deliverMessagingFinalFallback({
|
|
@@ -2528,8 +2776,17 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2528
2776
|
}
|
|
2529
2777
|
}
|
|
2530
2778
|
|
|
2531
|
-
|
|
2532
|
-
|
|
2779
|
+
const completionWon = engine.completeRun(runId, {
|
|
2780
|
+
totalTokens,
|
|
2781
|
+
finalResponse: finalResponseText || null,
|
|
2782
|
+
});
|
|
2783
|
+
if (!completionWon) {
|
|
2784
|
+
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
2785
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2786
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
2787
|
+
engine.activeRuns.delete(runId);
|
|
2788
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
2789
|
+
}
|
|
2533
2790
|
|
|
2534
2791
|
if (conversationId && options.skipConversationMaintenance !== true) {
|
|
2535
2792
|
await engine.refreshConversationState({
|
|
@@ -2542,7 +2799,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2542
2799
|
analysis,
|
|
2543
2800
|
verification,
|
|
2544
2801
|
historyWindow,
|
|
2545
|
-
options: {
|
|
2802
|
+
options: {
|
|
2803
|
+
...options,
|
|
2804
|
+
userId,
|
|
2805
|
+
agentId,
|
|
2806
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal || null,
|
|
2807
|
+
},
|
|
2546
2808
|
}).catch((err) => {
|
|
2547
2809
|
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
2548
2810
|
});
|
|
@@ -2551,7 +2813,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2551
2813
|
console.info(
|
|
2552
2814
|
`[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}`
|
|
2553
2815
|
);
|
|
2554
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2816
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2555
2817
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2556
2818
|
engine.activeRuns.delete(runId);
|
|
2557
2819
|
engine.emit(userId, 'run:complete', {
|
|
@@ -2584,12 +2846,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2584
2846
|
// Fire-and-forget: plugins can use this for self-improvement, memory
|
|
2585
2847
|
// consolidation, analytics, or other post-run housekeeping.
|
|
2586
2848
|
if (globalHooks.has('on_loop_end')) {
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2849
|
+
engine.trackBackgroundTask(
|
|
2850
|
+
(signal) => globalHooks.run('on_loop_end', {
|
|
2851
|
+
userId, runId, agentId, status: 'completed',
|
|
2852
|
+
iterations: iteration, totalTokens,
|
|
2853
|
+
taskAnalysis: analysis,
|
|
2854
|
+
finalContent: finalResponseText,
|
|
2855
|
+
signal,
|
|
2856
|
+
}),
|
|
2857
|
+
{ signal: runMeta?.abortController?.signal || null },
|
|
2858
|
+
).catch(() => {});
|
|
2593
2859
|
}
|
|
2594
2860
|
if (engine.learningManager) {
|
|
2595
2861
|
try {
|
|
@@ -2615,25 +2881,37 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2615
2881
|
|
|
2616
2882
|
return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
|
|
2617
2883
|
} catch (err) {
|
|
2884
|
+
if (!runRecordCreated) throw err;
|
|
2618
2885
|
if (engine.isRunStopped(runId)) {
|
|
2619
|
-
|
|
2620
|
-
|
|
2886
|
+
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
2887
|
+
const persistedTerminal = db.prepare(
|
|
2888
|
+
'SELECT status, error FROM agent_runs WHERE id = ?',
|
|
2889
|
+
).get(runId);
|
|
2890
|
+
const terminalStatus = persistedTerminal?.status === 'interrupted'
|
|
2891
|
+
|| stoppedRunMeta?.status === 'interrupted'
|
|
2892
|
+
? 'interrupted'
|
|
2893
|
+
: 'stopped';
|
|
2621
2894
|
console.warn(
|
|
2622
|
-
`[Run ${shortenRunId(runId)}]
|
|
2895
|
+
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2623
2896
|
);
|
|
2624
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2897
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2625
2898
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2626
2899
|
engine.activeRuns.delete(runId);
|
|
2627
|
-
engine.emit(userId, 'run:stopped', {
|
|
2628
|
-
|
|
2900
|
+
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
2901
|
+
runId,
|
|
2902
|
+
triggerSource,
|
|
2903
|
+
reason: stoppedRunMeta?.stopReason || persistedTerminal?.error || null,
|
|
2904
|
+
});
|
|
2905
|
+
engine.recordRunEvent(userId, terminalStatus === 'interrupted' ? 'run_interrupted' : 'run_stopped', {
|
|
2629
2906
|
triggerSource,
|
|
2630
2907
|
totalTokens,
|
|
2631
2908
|
iterations: iteration,
|
|
2632
2909
|
}, { agentId });
|
|
2633
|
-
return { runId, content: '', totalTokens, iterations: iteration, status:
|
|
2910
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminalStatus };
|
|
2634
2911
|
}
|
|
2635
2912
|
|
|
2636
2913
|
const runMeta = engine.activeRuns.get(runId);
|
|
2914
|
+
await engine.stopMessagingProgressSupervisor(runId);
|
|
2637
2915
|
|
|
2638
2916
|
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2639
2917
|
|| err?.deliverableValidation?.summary
|
|
@@ -2657,16 +2935,19 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2657
2935
|
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)}`
|
|
2658
2936
|
}
|
|
2659
2937
|
]);
|
|
2660
|
-
const modelReply = await
|
|
2661
|
-
provider.chat(failedMessage, [], {
|
|
2938
|
+
const modelReply = await runAbortableModelCall(
|
|
2939
|
+
(signal) => provider.chat(failedMessage, [], {
|
|
2662
2940
|
model,
|
|
2663
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2941
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2942
|
+
signal,
|
|
2664
2943
|
}),
|
|
2665
|
-
options,
|
|
2944
|
+
{ ...options, signal: runMeta?.abortController?.signal },
|
|
2666
2945
|
'Messaging failure reply',
|
|
2667
2946
|
);
|
|
2668
2947
|
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2669
|
-
if (
|
|
2948
|
+
if (
|
|
2949
|
+
normalizeOutgoingMessage(drafted, options?.source || null)
|
|
2950
|
+
) {
|
|
2670
2951
|
messagingFailureContent = drafted.trim();
|
|
2671
2952
|
}
|
|
2672
2953
|
} catch {
|
|
@@ -2688,7 +2969,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2688
2969
|
options.source,
|
|
2689
2970
|
options.chatId,
|
|
2690
2971
|
messagingFailureContent,
|
|
2691
|
-
{
|
|
2972
|
+
{
|
|
2973
|
+
runId,
|
|
2974
|
+
agentId,
|
|
2975
|
+
signal: runMeta?.abortController?.signal || null,
|
|
2976
|
+
},
|
|
2692
2977
|
);
|
|
2693
2978
|
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
2694
2979
|
sendSucceeded = true;
|
|
@@ -2705,20 +2990,25 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2705
2990
|
}
|
|
2706
2991
|
}
|
|
2707
2992
|
|
|
2708
|
-
|
|
2709
|
-
.
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
);
|
|
2993
|
+
const failureWon = engine.failRun(runId, {
|
|
2994
|
+
error: err.message,
|
|
2995
|
+
finalResponse: sendSucceeded
|
|
2996
|
+
? (messagingFailureContent || null)
|
|
2997
|
+
: (deliverableFailureResponse || null),
|
|
2998
|
+
totalTokens,
|
|
2999
|
+
});
|
|
3000
|
+
if (!failureWon) {
|
|
3001
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
3002
|
+
engine.stopMessagingProgressSupervisor(runId);
|
|
3003
|
+
engine.activeRuns.delete(runId);
|
|
3004
|
+
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
3005
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
3006
|
+
}
|
|
2717
3007
|
console.error(
|
|
2718
3008
|
`[Run ${shortenRunId(runId)}] failed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} error=${summarizeForLog(err.message, 180)}`
|
|
2719
3009
|
);
|
|
2720
3010
|
|
|
2721
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
3011
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2722
3012
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2723
3013
|
engine.activeRuns.delete(runId);
|
|
2724
3014
|
engine.emit(userId, 'run:error', { runId, error: err.message });
|
|
@@ -2728,17 +3018,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2728
3018
|
iterations: iteration,
|
|
2729
3019
|
deliverableType: deliverableWorkflow?.selection?.type || null,
|
|
2730
3020
|
}, { agentId });
|
|
2731
|
-
timelineService?.recordRunLifecycle?.({
|
|
2732
|
-
userId,
|
|
2733
|
-
agentId,
|
|
2734
|
-
runId,
|
|
2735
|
-
title: runTitle,
|
|
2736
|
-
eventKind: 'run_failed',
|
|
2737
|
-
status: 'failed',
|
|
2738
|
-
triggerSource,
|
|
2739
|
-
error: err.message,
|
|
2740
|
-
});
|
|
2741
|
-
|
|
2742
3021
|
if (messagingFailureContent) {
|
|
2743
3022
|
return {
|
|
2744
3023
|
runId,
|
|
@@ -2751,6 +3030,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2751
3030
|
|
|
2752
3031
|
throw err;
|
|
2753
3032
|
} finally {
|
|
3033
|
+
detachExternalAbort?.();
|
|
2754
3034
|
releaseReservation();
|
|
2755
3035
|
}
|
|
2756
3036
|
}
|