neoagent 3.2.1-beta.1 → 3.2.1-beta.3
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- 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/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -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 +1 -1
- package/server/public/main.dart.js +30803 -30805
- package/server/routes/admin.js +1 -1
- 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 +5 -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/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +358 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- 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 +20 -45
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- 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 +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -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 +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- 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/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 +158 -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/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 +87 -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 +84 -9
- 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/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- 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/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- 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,11 @@ 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 { getProviderForUser } = require('../provider_selector');
|
|
58
63
|
const { IterationBudget } = require('./iteration_budget');
|
|
59
64
|
const {
|
|
60
65
|
buildBlankAfterToolFailureGuidance,
|
|
@@ -116,7 +121,7 @@ const {
|
|
|
116
121
|
const {
|
|
117
122
|
requestModelResponse: requestModelResponseImpl,
|
|
118
123
|
requestStructuredJson: requestStructuredJsonImpl,
|
|
119
|
-
|
|
124
|
+
runAbortableModelCall,
|
|
120
125
|
} = require('./model_io');
|
|
121
126
|
const {
|
|
122
127
|
publishInterimUpdate: publishInterimUpdateImpl,
|
|
@@ -143,6 +148,7 @@ const {
|
|
|
143
148
|
buildDeterministicMessagingErrorReply,
|
|
144
149
|
buildModelFailureLoopPrompt,
|
|
145
150
|
} = require('../messagingFallback');
|
|
151
|
+
const { isDeferredWorkReply } = require('../terminal_reply');
|
|
146
152
|
const {
|
|
147
153
|
classifyToolExecution,
|
|
148
154
|
gatheredNewEvidence,
|
|
@@ -164,6 +170,7 @@ const {
|
|
|
164
170
|
normalizeRetrievalPlan,
|
|
165
171
|
shouldEnhanceRetrieval,
|
|
166
172
|
} = require('../../memory/retrieval_reasoning');
|
|
173
|
+
const { isAbortError, throwIfAborted } = require('../../../utils/abort');
|
|
167
174
|
|
|
168
175
|
function generateTitle(task) {
|
|
169
176
|
if (!task || typeof task !== 'string') return 'Untitled';
|
|
@@ -403,145 +410,55 @@ function buildAutonomyPolicyFromAnalysis(analysis = {}) {
|
|
|
403
410
|
};
|
|
404
411
|
}
|
|
405
412
|
|
|
406
|
-
async function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
|
|
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) {
|
|
413
|
+
async function getFailureFallbackModelId(
|
|
414
|
+
userId,
|
|
415
|
+
agentId,
|
|
416
|
+
currentModelId,
|
|
417
|
+
preferredFallbackId = null,
|
|
418
|
+
failureError = null,
|
|
419
|
+
signal = null,
|
|
420
|
+
) {
|
|
503
421
|
const { getSupportedModels } = require('../models');
|
|
504
422
|
const aiSettings = getAiSettings(userId, agentId);
|
|
505
|
-
const models = await getSupportedModels(userId, agentId);
|
|
423
|
+
const models = await getSupportedModels(userId, agentId, { signal });
|
|
506
424
|
const availableModels = models.filter((model) => model.available !== false);
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
? aiSettings.enabled_models.map((id) => String(id)).filter((id) => knownIds.has(id))
|
|
425
|
+
const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
|
|
426
|
+
? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
|
|
510
427
|
: [];
|
|
511
|
-
const
|
|
428
|
+
const enabledIds = normalizeModelSelections(availableModels, configuredEnabledIds);
|
|
429
|
+
const pool = configuredEnabledIds.length > 0
|
|
512
430
|
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
513
431
|
: availableModels;
|
|
514
|
-
const fallbackSearchPool = pool
|
|
515
|
-
const currentModel = pool
|
|
516
|
-
|| availableModels
|
|
517
|
-
|| null;
|
|
432
|
+
const fallbackSearchPool = pool;
|
|
433
|
+
const currentModel = resolveModelSelection(pool, currentModelId)
|
|
434
|
+
|| resolveModelSelection(availableModels, currentModelId);
|
|
518
435
|
|
|
519
436
|
// When the failure is a provider-level rate limit, the preferred fallback is
|
|
520
437
|
// likely on the same provider and will hit the same limit. Skip it and prefer
|
|
521
438
|
// a fallback from a different provider instead.
|
|
522
439
|
const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
|
|
523
440
|
|
|
524
|
-
if (preferredFallbackId &&
|
|
525
|
-
const preferred = fallbackSearchPool
|
|
526
|
-
|| availableModels
|
|
527
|
-
if (preferred) return preferred.id;
|
|
441
|
+
if (preferredFallbackId && !isProviderRateLimit) {
|
|
442
|
+
const preferred = resolveModelSelection(fallbackSearchPool, preferredFallbackId)
|
|
443
|
+
|| resolveModelSelection(availableModels, preferredFallbackId);
|
|
444
|
+
if (preferred && preferred.id !== currentModel?.id) return preferred.id;
|
|
528
445
|
}
|
|
529
446
|
|
|
530
447
|
if (currentModel?.provider) {
|
|
531
448
|
const differentProvider = fallbackSearchPool.find((model) =>
|
|
532
|
-
model.id !==
|
|
449
|
+
model.id !== currentModel.id && model.provider !== currentModel.provider);
|
|
533
450
|
if (differentProvider) return differentProvider.id;
|
|
534
451
|
}
|
|
535
452
|
|
|
536
453
|
// If no different-provider model exists, still try the preferred fallback
|
|
537
454
|
// even on rate limits (it's better than nothing).
|
|
538
|
-
if (preferredFallbackId
|
|
539
|
-
const preferred = fallbackSearchPool
|
|
540
|
-
|| availableModels
|
|
541
|
-
if (preferred) return preferred.id;
|
|
455
|
+
if (preferredFallbackId) {
|
|
456
|
+
const preferred = resolveModelSelection(fallbackSearchPool, preferredFallbackId)
|
|
457
|
+
|| resolveModelSelection(availableModels, preferredFallbackId);
|
|
458
|
+
if (preferred && preferred.id !== currentModel?.id) return preferred.id;
|
|
542
459
|
}
|
|
543
460
|
|
|
544
|
-
const differentModel = fallbackSearchPool.find((model) => model.id !==
|
|
461
|
+
const differentModel = fallbackSearchPool.find((model) => model.id !== currentModel?.id);
|
|
545
462
|
return differentModel?.id || null;
|
|
546
463
|
}
|
|
547
464
|
|
|
@@ -552,6 +469,7 @@ function estimateTokenValue(value) {
|
|
|
552
469
|
}
|
|
553
470
|
|
|
554
471
|
async function runConversation(engine, userId, userMessage, options = {}, _modelOverride = null) {
|
|
472
|
+
throwIfAborted(options.signal, 'Agent run aborted before startup.');
|
|
555
473
|
const triggerType = options.triggerType || 'user';
|
|
556
474
|
const { resolveAgentId } = require('../../agents/manager');
|
|
557
475
|
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
@@ -563,6 +481,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
563
481
|
const triggerSource = options.triggerSource || 'web';
|
|
564
482
|
let provider = null;
|
|
565
483
|
let model = null;
|
|
484
|
+
let modelSelectionId = null;
|
|
566
485
|
let providerName = null;
|
|
567
486
|
let messages = [];
|
|
568
487
|
let iteration = 0;
|
|
@@ -572,7 +491,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
572
491
|
let failedStepCount = 0;
|
|
573
492
|
let toolExecutions = [];
|
|
574
493
|
let deliverableWorkflow = null;
|
|
575
|
-
let
|
|
494
|
+
let detachExternalAbort = null;
|
|
495
|
+
const runTitle = generateTitle(userMessage);
|
|
496
|
+
let runRecordCreated = false;
|
|
576
497
|
const timelineService = app?.locals?.timelineService || null;
|
|
577
498
|
|
|
578
499
|
const { releaseReservation } = enforceRateLimits(userId, {
|
|
@@ -589,6 +510,59 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
589
510
|
// analysis when the mode is known. See the post-analysis policy rebuild below.
|
|
590
511
|
let loopPolicy = buildLoopPolicy(aiSettings, triggerType, 'execute', options);
|
|
591
512
|
let maxIterations = loopPolicy.maxIterations;
|
|
513
|
+
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
514
|
+
const requestedModel = String(
|
|
515
|
+
_modelOverride
|
|
516
|
+
|| (triggerType === 'subagent' ? aiSettings.default_subagent_model : aiSettings.default_chat_model)
|
|
517
|
+
|| 'auto',
|
|
518
|
+
).trim();
|
|
519
|
+
try {
|
|
520
|
+
db.prepare(`INSERT INTO agent_runs(
|
|
521
|
+
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
522
|
+
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
|
|
523
|
+
runId,
|
|
524
|
+
userId,
|
|
525
|
+
agentId,
|
|
526
|
+
runTitle,
|
|
527
|
+
triggerType,
|
|
528
|
+
triggerSource,
|
|
529
|
+
requestedModel,
|
|
530
|
+
Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
|
|
531
|
+
);
|
|
532
|
+
runRecordCreated = true;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (/unique|primary key|constraint/i.test(String(error?.message || ''))) {
|
|
535
|
+
const conflict = new Error(`A run with id "${runId}" already exists.`);
|
|
536
|
+
conflict.code = 'RUN_ID_CONFLICT';
|
|
537
|
+
throw conflict;
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
const startupAbortController = new AbortController();
|
|
542
|
+
engine.activeRuns.set(runId, {
|
|
543
|
+
userId,
|
|
544
|
+
agentId,
|
|
545
|
+
title: runTitle,
|
|
546
|
+
status: 'running',
|
|
547
|
+
aborted: false,
|
|
548
|
+
abortController: startupAbortController,
|
|
549
|
+
pauseAvailable: false,
|
|
550
|
+
toolPids: new Set(),
|
|
551
|
+
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
552
|
+
});
|
|
553
|
+
if (options.signal) {
|
|
554
|
+
const abortFromExternal = () => {
|
|
555
|
+
engine.interruptRun(
|
|
556
|
+
runId,
|
|
557
|
+
String(options.signal.reason || 'Agent run interrupted by its caller.'),
|
|
558
|
+
);
|
|
559
|
+
};
|
|
560
|
+
if (options.signal.aborted) abortFromExternal();
|
|
561
|
+
else options.signal.addEventListener('abort', abortFromExternal, { once: true });
|
|
562
|
+
detachExternalAbort = () => {
|
|
563
|
+
options.signal.removeEventListener('abort', abortFromExternal);
|
|
564
|
+
};
|
|
565
|
+
}
|
|
592
566
|
const providerStatusConfig = {
|
|
593
567
|
agentId,
|
|
594
568
|
onStatus: (status) => {
|
|
@@ -605,18 +579,39 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
605
579
|
userMessage,
|
|
606
580
|
triggerType === 'subagent',
|
|
607
581
|
_modelOverride,
|
|
608
|
-
providerStatusConfig
|
|
582
|
+
{ ...providerStatusConfig, signal: startupAbortController.signal }
|
|
609
583
|
);
|
|
610
584
|
provider = selectedProvider.provider;
|
|
611
585
|
model = selectedProvider.model;
|
|
586
|
+
modelSelectionId = selectedProvider.modelSelectionId;
|
|
612
587
|
providerName = selectedProvider.providerName;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
588
|
+
if (
|
|
589
|
+
startupAbortController.signal.aborted
|
|
590
|
+
|| engine.getRunMeta(runId)?.status !== 'running'
|
|
591
|
+
) {
|
|
592
|
+
const startupError = new Error(
|
|
593
|
+
String(startupAbortController.signal.reason || 'Run stopped during model selection.'),
|
|
594
|
+
);
|
|
595
|
+
startupError.name = 'AbortError';
|
|
596
|
+
startupError.code = 'ABORT_ERR';
|
|
597
|
+
throw startupError;
|
|
598
|
+
}
|
|
599
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
600
|
+
.run(modelSelectionId, runId);
|
|
601
|
+
const switchToFallbackModel = async (failedSelectionId, error, phase) => {
|
|
602
|
+
const fallbackModelId = await getFailureFallbackModelId(
|
|
603
|
+
userId,
|
|
604
|
+
agentId,
|
|
605
|
+
failedSelectionId,
|
|
606
|
+
aiSettings.fallback_model_id,
|
|
607
|
+
error,
|
|
608
|
+
engine.getRunMeta(runId)?.abortController?.signal,
|
|
609
|
+
);
|
|
610
|
+
if (!fallbackModelId || fallbackModelId === failedSelectionId) return false;
|
|
611
|
+
console.log(`[Engine] ${phase} failed on ${failedSelectionId}; attempting fallback to: ${fallbackModelId}`);
|
|
617
612
|
engine.emit(userId, 'run:interim', {
|
|
618
613
|
runId,
|
|
619
|
-
message: `Model service failed on ${
|
|
614
|
+
message: `Model service failed on ${failedSelectionId}; retrying with ${fallbackModelId}.`,
|
|
620
615
|
phase: 'model_fallback'
|
|
621
616
|
});
|
|
622
617
|
const fallback = await getProviderForUser(
|
|
@@ -624,46 +619,35 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
624
619
|
userMessage,
|
|
625
620
|
triggerType === 'subagent',
|
|
626
621
|
fallbackModelId,
|
|
627
|
-
|
|
622
|
+
{
|
|
623
|
+
...providerStatusConfig,
|
|
624
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
625
|
+
}
|
|
628
626
|
);
|
|
629
627
|
provider = fallback.provider;
|
|
630
628
|
model = fallback.model;
|
|
629
|
+
modelSelectionId = fallback.modelSelectionId;
|
|
631
630
|
providerName = fallback.providerName;
|
|
631
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
632
|
+
.run(modelSelectionId, runId);
|
|
633
|
+
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
634
|
+
model,
|
|
635
|
+
modelSelectionId,
|
|
636
|
+
providerName,
|
|
637
|
+
});
|
|
632
638
|
return true;
|
|
633
639
|
};
|
|
634
640
|
const runWithModelFallback = async (phase, fn) => {
|
|
635
641
|
try {
|
|
636
642
|
return await fn();
|
|
637
643
|
} catch (err) {
|
|
638
|
-
const
|
|
639
|
-
const switched = await switchToFallbackModel(
|
|
644
|
+
const failedSelectionId = modelSelectionId;
|
|
645
|
+
const switched = await switchToFallbackModel(failedSelectionId, err, phase);
|
|
640
646
|
if (!switched) throw err;
|
|
641
647
|
return await fn();
|
|
642
648
|
}
|
|
643
649
|
};
|
|
644
650
|
|
|
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
651
|
const retryMessagingState = options.messagingRetryState || {};
|
|
668
652
|
const carriedFinalMessage = String(retryMessagingState.lastFinalMessage || '').trim();
|
|
669
653
|
const carriedExplicitMessageSent = retryMessagingState.explicitMessageSent === true;
|
|
@@ -685,6 +669,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
685
669
|
userId,
|
|
686
670
|
agentId,
|
|
687
671
|
title: runTitle,
|
|
672
|
+
model,
|
|
673
|
+
modelSelectionId,
|
|
674
|
+
providerName,
|
|
688
675
|
status: 'running',
|
|
689
676
|
aborted: false,
|
|
690
677
|
messagingSent: false,
|
|
@@ -709,7 +696,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
709
696
|
voiceSessionId: options.voiceSessionId || null,
|
|
710
697
|
steeringQueue: [],
|
|
711
698
|
systemSteeringQueue: [],
|
|
712
|
-
abortController:
|
|
699
|
+
abortController: startupAbortController,
|
|
713
700
|
pauseAvailable: false,
|
|
714
701
|
toolPids: new Set(),
|
|
715
702
|
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
@@ -735,10 +722,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
735
722
|
goalContract: carriedGoalContract,
|
|
736
723
|
});
|
|
737
724
|
engine.startMessagingProgressSupervisor(runId);
|
|
738
|
-
engine.emit(userId, 'run:start', {
|
|
725
|
+
engine.emit(userId, 'run:start', {
|
|
726
|
+
runId,
|
|
727
|
+
agentId,
|
|
728
|
+
title: runTitle,
|
|
729
|
+
model,
|
|
730
|
+
modelSelectionId,
|
|
731
|
+
provider: providerName,
|
|
732
|
+
triggerType,
|
|
733
|
+
triggerSource,
|
|
734
|
+
});
|
|
739
735
|
engine.recordRunEvent(userId, runId, 'run_started', {
|
|
740
736
|
title: runTitle,
|
|
741
737
|
model,
|
|
738
|
+
modelSelectionId,
|
|
739
|
+
provider: providerName,
|
|
742
740
|
triggerType,
|
|
743
741
|
triggerSource,
|
|
744
742
|
}, { agentId });
|
|
@@ -752,7 +750,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
752
750
|
triggerSource,
|
|
753
751
|
});
|
|
754
752
|
console.info(
|
|
755
|
-
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${
|
|
753
|
+
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${modelSelectionId} title=${summarizeForLog(runTitle, 120)}`
|
|
756
754
|
);
|
|
757
755
|
|
|
758
756
|
const systemPrompt = await engine.buildSystemPrompt(userId, {
|
|
@@ -883,7 +881,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
883
881
|
if (triggerSource === 'messaging') {
|
|
884
882
|
const runMetaForCompose = engine.getRunMeta(runId);
|
|
885
883
|
if (runMetaForCompose) {
|
|
886
|
-
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
884
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false, signal = null } = {}) => {
|
|
887
885
|
try {
|
|
888
886
|
const rm = engine.getRunMeta(runId);
|
|
889
887
|
const ledger = rm?.progressLedger || {};
|
|
@@ -900,7 +898,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
900
898
|
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
901
899
|
const contextBlock = [
|
|
902
900
|
buildProgressUpdatePrompt(),
|
|
903
|
-
stalled ? '
|
|
901
|
+
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.' : '',
|
|
904
902
|
'',
|
|
905
903
|
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
906
904
|
currentTool ? `Doing now: using ${currentTool}` : '',
|
|
@@ -911,16 +909,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
911
909
|
// formatting guidelines as every other message (single source of truth).
|
|
912
910
|
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
913
911
|
|| 'You are a helpful assistant.';
|
|
914
|
-
const resp = await
|
|
915
|
-
provider.chat(
|
|
912
|
+
const resp = await runAbortableModelCall(
|
|
913
|
+
(signal) => provider.chat(
|
|
916
914
|
[
|
|
917
915
|
{ role: 'system', content: sysContent },
|
|
918
916
|
{ role: 'user', content: contextBlock },
|
|
919
917
|
],
|
|
920
918
|
[],
|
|
921
|
-
{
|
|
919
|
+
{
|
|
920
|
+
model,
|
|
921
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
922
|
+
signal,
|
|
923
|
+
},
|
|
922
924
|
),
|
|
923
|
-
|
|
925
|
+
{
|
|
926
|
+
...options,
|
|
927
|
+
signals: [rm?.abortController?.signal, signal],
|
|
928
|
+
},
|
|
924
929
|
'Progress update compose',
|
|
925
930
|
);
|
|
926
931
|
return sanitizeModelOutput(resp.content || '', { model });
|
|
@@ -1031,6 +1036,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1031
1036
|
null,
|
|
1032
1037
|
{
|
|
1033
1038
|
...providerStatusConfig,
|
|
1039
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1034
1040
|
selectionHint: {
|
|
1035
1041
|
purpose: requestedPurpose,
|
|
1036
1042
|
complexity: analysis?.complexity,
|
|
@@ -1040,15 +1046,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1040
1046
|
},
|
|
1041
1047
|
}
|
|
1042
1048
|
);
|
|
1043
|
-
if (selectedAfterAnalysis.
|
|
1049
|
+
if (selectedAfterAnalysis.modelSelectionId !== modelSelectionId) {
|
|
1044
1050
|
provider = selectedAfterAnalysis.provider;
|
|
1045
1051
|
model = selectedAfterAnalysis.model;
|
|
1052
|
+
modelSelectionId = selectedAfterAnalysis.modelSelectionId;
|
|
1046
1053
|
providerName = selectedAfterAnalysis.providerName;
|
|
1047
1054
|
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
1048
|
-
.run(
|
|
1055
|
+
.run(modelSelectionId, runId);
|
|
1056
|
+
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
1057
|
+
model,
|
|
1058
|
+
modelSelectionId,
|
|
1059
|
+
providerName,
|
|
1060
|
+
});
|
|
1049
1061
|
engine.emit(userId, 'run:interim', {
|
|
1050
1062
|
runId,
|
|
1051
|
-
message: `Switched to ${
|
|
1063
|
+
message: `Switched to ${modelSelectionId} for this run after task analysis.`,
|
|
1052
1064
|
phase: 'model_selection'
|
|
1053
1065
|
});
|
|
1054
1066
|
}
|
|
@@ -1239,7 +1251,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1239
1251
|
reason,
|
|
1240
1252
|
stoppedBy: hookResult.stopped_by || hookResult.stoppedBy || null,
|
|
1241
1253
|
}, { agentId });
|
|
1242
|
-
|
|
1254
|
+
engine.stopRun(runId, reason);
|
|
1243
1255
|
break;
|
|
1244
1256
|
}
|
|
1245
1257
|
const systemSteering = String(hookResult?.systemSteering || hookResult?.system_steering || '').trim();
|
|
@@ -1362,8 +1374,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1362
1374
|
});
|
|
1363
1375
|
let wrapTokens = 0;
|
|
1364
1376
|
try {
|
|
1365
|
-
const wrapResponse = await
|
|
1366
|
-
provider.chat(
|
|
1377
|
+
const wrapResponse = await runAbortableModelCall(
|
|
1378
|
+
(signal) => provider.chat(
|
|
1367
1379
|
sanitizeConversationMessages([
|
|
1368
1380
|
...messages,
|
|
1369
1381
|
{
|
|
@@ -1376,9 +1388,13 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1376
1388
|
},
|
|
1377
1389
|
]),
|
|
1378
1390
|
[],
|
|
1379
|
-
{
|
|
1391
|
+
{
|
|
1392
|
+
model,
|
|
1393
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
1394
|
+
signal,
|
|
1395
|
+
},
|
|
1380
1396
|
),
|
|
1381
|
-
options,
|
|
1397
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
1382
1398
|
'No-progress wrap-up',
|
|
1383
1399
|
);
|
|
1384
1400
|
wrapTokens = wrapResponse.usage?.totalTokens || 0;
|
|
@@ -1387,7 +1403,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1387
1403
|
console.warn(`[Run ${shortenRunId(runId)}] no_progress_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1388
1404
|
}
|
|
1389
1405
|
totalTokens += wrapTokens;
|
|
1390
|
-
const usableWrap = normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
1406
|
+
const usableWrap = normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
1407
|
+
&& !isDeferredWorkReply(lastContent);
|
|
1391
1408
|
if (!usableWrap) {
|
|
1392
1409
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1393
1410
|
}
|
|
@@ -1431,9 +1448,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1431
1448
|
let metrics = engine.estimatePromptMetrics(messages, tools);
|
|
1432
1449
|
const contextWindow = provider.getContextWindow(model);
|
|
1433
1450
|
if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
|
|
1434
|
-
messages = await
|
|
1435
|
-
compact(messages, provider, model, contextWindow),
|
|
1436
|
-
options,
|
|
1451
|
+
messages = await runAbortableModelCall(
|
|
1452
|
+
(signal) => compact(messages, provider, model, contextWindow, { signal }),
|
|
1453
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
1437
1454
|
`Context compaction before iteration ${iteration}`,
|
|
1438
1455
|
);
|
|
1439
1456
|
messages = sanitizeConversationMessages(messages);
|
|
@@ -1478,7 +1495,14 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1478
1495
|
} catch (err) {
|
|
1479
1496
|
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1480
1497
|
const fallbackModelId = retryForFallback
|
|
1481
|
-
? await getFailureFallbackModelId(
|
|
1498
|
+
? await getFailureFallbackModelId(
|
|
1499
|
+
userId,
|
|
1500
|
+
agentId,
|
|
1501
|
+
modelSelectionId,
|
|
1502
|
+
aiSettings.fallback_model_id,
|
|
1503
|
+
err,
|
|
1504
|
+
engine.getRunMeta(runId)?.abortController?.signal,
|
|
1505
|
+
)
|
|
1482
1506
|
: null;
|
|
1483
1507
|
if (fallbackModelId) {
|
|
1484
1508
|
const failedModel = model;
|
|
@@ -1488,11 +1512,22 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1488
1512
|
userMessage,
|
|
1489
1513
|
triggerType === 'subagent',
|
|
1490
1514
|
fallbackModelId,
|
|
1491
|
-
|
|
1515
|
+
{
|
|
1516
|
+
...providerStatusConfig,
|
|
1517
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1518
|
+
}
|
|
1492
1519
|
);
|
|
1493
1520
|
provider = fallback.provider;
|
|
1494
1521
|
model = fallback.model;
|
|
1522
|
+
modelSelectionId = fallback.modelSelectionId;
|
|
1495
1523
|
providerName = fallback.providerName;
|
|
1524
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
1525
|
+
.run(modelSelectionId, runId);
|
|
1526
|
+
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
1527
|
+
model,
|
|
1528
|
+
modelSelectionId,
|
|
1529
|
+
providerName,
|
|
1530
|
+
});
|
|
1496
1531
|
|
|
1497
1532
|
const retryMessages = sanitizeConversationMessages([
|
|
1498
1533
|
...messages,
|
|
@@ -1512,7 +1547,14 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1512
1547
|
model,
|
|
1513
1548
|
messages: retryMessages,
|
|
1514
1549
|
tools,
|
|
1515
|
-
options: {
|
|
1550
|
+
options: {
|
|
1551
|
+
...options,
|
|
1552
|
+
userId,
|
|
1553
|
+
agentId,
|
|
1554
|
+
runId,
|
|
1555
|
+
phase: 'model_turn_fallback',
|
|
1556
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1557
|
+
},
|
|
1516
1558
|
runId,
|
|
1517
1559
|
iteration,
|
|
1518
1560
|
});
|
|
@@ -1547,7 +1589,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1547
1589
|
|
|
1548
1590
|
if (modelFailureRecoveries < loopPolicy.maxModelFailureRecoveries) {
|
|
1549
1591
|
const failedModel = model;
|
|
1550
|
-
const switched = await switchToFallbackModel(
|
|
1592
|
+
const switched = await switchToFallbackModel(modelSelectionId, err, 'model turn');
|
|
1551
1593
|
if (!switched) throw err;
|
|
1552
1594
|
modelFailureRecoveries += 1;
|
|
1553
1595
|
failedStepCount += 1;
|
|
@@ -1601,10 +1643,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1601
1643
|
toolCallCount: response.toolCalls?.length || 0,
|
|
1602
1644
|
contentPreview: String(lastContent || streamContent || '').slice(0, 240),
|
|
1603
1645
|
}, { agentId });
|
|
1604
|
-
engine.updateRunProgress(runId, {}, {
|
|
1605
|
-
verified: true,
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
1646
|
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
1609
1647
|
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
1610
1648
|
if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
|
|
@@ -1731,7 +1769,10 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1731
1769
|
|
|
1732
1770
|
const canRunParallelBatch = (
|
|
1733
1771
|
response.toolCalls.length > 1
|
|
1734
|
-
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(
|
|
1772
|
+
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(
|
|
1773
|
+
toolCall,
|
|
1774
|
+
tools.find((tool) => tool?.name === toolCall?.function?.name) || null,
|
|
1775
|
+
))
|
|
1735
1776
|
);
|
|
1736
1777
|
if (canRunParallelBatch) {
|
|
1737
1778
|
const parallelToolNames = response.toolCalls
|
|
@@ -1768,6 +1809,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1768
1809
|
item.toolArgs,
|
|
1769
1810
|
item.result,
|
|
1770
1811
|
item.error || '',
|
|
1812
|
+
tools.find((tool) => tool?.name === item.toolName) || null,
|
|
1771
1813
|
);
|
|
1772
1814
|
execution.input = item.toolArgs;
|
|
1773
1815
|
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
@@ -1807,7 +1849,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1807
1849
|
currentTool: null,
|
|
1808
1850
|
currentStepStartedAt: null,
|
|
1809
1851
|
}, {
|
|
1810
|
-
verified:
|
|
1852
|
+
verified: batchGatheredNewEvidence,
|
|
1811
1853
|
});
|
|
1812
1854
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1813
1855
|
const iterMeta = engine.getRunMeta(runId);
|
|
@@ -2054,11 +2096,20 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2054
2096
|
});
|
|
2055
2097
|
if (lifecycleAfterTool?.action === 'stop' || lifecycleAfterTool?.action === 'interrupt') break;
|
|
2056
2098
|
|
|
2057
|
-
const execution = classifyToolExecution(
|
|
2099
|
+
const execution = classifyToolExecution(
|
|
2100
|
+
toolName,
|
|
2101
|
+
toolArgs,
|
|
2102
|
+
toolResult,
|
|
2103
|
+
toolErrorMessage,
|
|
2104
|
+
tools.find((tool) => tool?.name === toolName) || null,
|
|
2105
|
+
);
|
|
2058
2106
|
execution.input = toolArgs;
|
|
2059
2107
|
const repetitionObservation = repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
2060
|
-
|
|
2061
|
-
|
|
2108
|
+
const toolMadeConcreteProgress = (
|
|
2109
|
+
(execution.stateChanged && isProgressToolCall(toolName, toolArgs))
|
|
2110
|
+
|| gatheredNewEvidence(execution, repetitionObservation)
|
|
2111
|
+
);
|
|
2112
|
+
if (toolMadeConcreteProgress) {
|
|
2062
2113
|
iterationConcreteProgress = true;
|
|
2063
2114
|
}
|
|
2064
2115
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
@@ -2206,7 +2257,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2206
2257
|
currentTool: null,
|
|
2207
2258
|
currentStepStartedAt: null,
|
|
2208
2259
|
}, {
|
|
2209
|
-
verified:
|
|
2260
|
+
verified: toolMadeConcreteProgress,
|
|
2210
2261
|
stepId,
|
|
2211
2262
|
});
|
|
2212
2263
|
|
|
@@ -2256,20 +2307,18 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2256
2307
|
|
|
2257
2308
|
if (engine.isRunStopped(runId)) {
|
|
2258
2309
|
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
2259
|
-
const
|
|
2310
|
+
const persistedTerminal = db.prepare(
|
|
2311
|
+
'SELECT status, error FROM agent_runs WHERE id = ?',
|
|
2312
|
+
).get(runId);
|
|
2313
|
+
const terminalStatus = persistedTerminal?.status === 'interrupted'
|
|
2314
|
+
|| stoppedRunMeta?.status === 'interrupted'
|
|
2315
|
+
? 'interrupted'
|
|
2316
|
+
: 'stopped';
|
|
2260
2317
|
const stopReason = stoppedRunMeta?.stopReason || null;
|
|
2261
|
-
db.prepare(
|
|
2262
|
-
`UPDATE agent_runs
|
|
2263
|
-
SET status = ?,
|
|
2264
|
-
error = COALESCE(?, error),
|
|
2265
|
-
updated_at = datetime('now'),
|
|
2266
|
-
completed_at = datetime('now')
|
|
2267
|
-
WHERE id = ?`
|
|
2268
|
-
).run(terminalStatus, stopReason, runId);
|
|
2269
2318
|
console.warn(
|
|
2270
2319
|
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2271
2320
|
);
|
|
2272
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2321
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2273
2322
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2274
2323
|
engine.activeRuns.delete(runId);
|
|
2275
2324
|
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
@@ -2320,16 +2369,20 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2320
2369
|
if (budgetExhaustedWithoutCompletion) {
|
|
2321
2370
|
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
2322
2371
|
try {
|
|
2323
|
-
const wrapResponse = await
|
|
2324
|
-
provider.chat(
|
|
2372
|
+
const wrapResponse = await runAbortableModelCall(
|
|
2373
|
+
(signal) => provider.chat(
|
|
2325
2374
|
sanitizeConversationMessages([
|
|
2326
2375
|
...messages,
|
|
2327
2376
|
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
2328
2377
|
]),
|
|
2329
2378
|
[],
|
|
2330
|
-
{
|
|
2379
|
+
{
|
|
2380
|
+
model,
|
|
2381
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2382
|
+
signal,
|
|
2383
|
+
},
|
|
2331
2384
|
),
|
|
2332
|
-
options,
|
|
2385
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
2333
2386
|
'Max-iteration wrap-up',
|
|
2334
2387
|
);
|
|
2335
2388
|
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
@@ -2337,7 +2390,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2337
2390
|
// On budget exhaustion the model's last text is an untrustworthy mid-thought
|
|
2338
2391
|
// fragment. Replace it with the wrap-up answer, or a clean deterministic
|
|
2339
2392
|
// fallback if the wrap-up came back empty — never deliver the fragment.
|
|
2340
|
-
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null)
|
|
2393
|
+
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null)
|
|
2394
|
+
&& !isDeferredWorkReply(wrapText);
|
|
2341
2395
|
lastContent = usableWrap
|
|
2342
2396
|
? wrapText
|
|
2343
2397
|
: buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
@@ -2362,8 +2416,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2362
2416
|
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery model=${model}`);
|
|
2363
2417
|
let recoveredTokens = 0;
|
|
2364
2418
|
try {
|
|
2365
|
-
const recoveryResponse = await
|
|
2366
|
-
provider.chat(
|
|
2419
|
+
const recoveryResponse = await runAbortableModelCall(
|
|
2420
|
+
(signal) => provider.chat(
|
|
2367
2421
|
sanitizeConversationMessages([
|
|
2368
2422
|
...messages,
|
|
2369
2423
|
{
|
|
@@ -2374,10 +2428,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2374
2428
|
[],
|
|
2375
2429
|
{
|
|
2376
2430
|
model,
|
|
2377
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2431
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2432
|
+
signal,
|
|
2378
2433
|
}
|
|
2379
2434
|
),
|
|
2380
|
-
options,
|
|
2435
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
2381
2436
|
'Blank messaging reply recovery',
|
|
2382
2437
|
);
|
|
2383
2438
|
recoveredTokens = recoveryResponse.usage?.totalTokens || 0;
|
|
@@ -2390,7 +2445,10 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2390
2445
|
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2391
2446
|
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2392
2447
|
// a deterministic message when the model returned nothing usable.
|
|
2393
|
-
const recoveredVisible = Boolean(
|
|
2448
|
+
const recoveredVisible = Boolean(
|
|
2449
|
+
normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
2450
|
+
&& !isDeferredWorkReply(lastContent),
|
|
2451
|
+
);
|
|
2394
2452
|
if (!recoveredVisible) {
|
|
2395
2453
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2396
2454
|
}
|
|
@@ -2418,7 +2476,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2418
2476
|
|| lifecycleBeforeFinalize?.action === 'interrupt'
|
|
2419
2477
|
) {
|
|
2420
2478
|
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
2421
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2479
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2422
2480
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2423
2481
|
engine.activeRuns.delete(runId);
|
|
2424
2482
|
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
@@ -2536,6 +2594,19 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2536
2594
|
});
|
|
2537
2595
|
}
|
|
2538
2596
|
|
|
2597
|
+
if (!messagingSent && isDeferredWorkReply(finalResponseText)) {
|
|
2598
|
+
engine.recordRunEvent(userId, runId, 'non_terminal_final_reply_rejected', {
|
|
2599
|
+
iteration,
|
|
2600
|
+
contentPreview: String(finalResponseText || '').slice(0, 240),
|
|
2601
|
+
}, { agentId });
|
|
2602
|
+
finalResponseText = buildDeterministicMessagingFallback({
|
|
2603
|
+
failedStepCount,
|
|
2604
|
+
stepIndex,
|
|
2605
|
+
toolExecutions,
|
|
2606
|
+
});
|
|
2607
|
+
lastContent = finalResponseText;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2539
2610
|
if (deliverableWorkflow && deliverablePlan) {
|
|
2540
2611
|
engine.recordRunEvent(userId, runId, 'deliverable_validation_started', {
|
|
2541
2612
|
type: deliverableWorkflow.selection.type,
|
|
@@ -2580,8 +2651,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2580
2651
|
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2581
2652
|
.run(totalTokens, conversationId);
|
|
2582
2653
|
if (options.skipConversationMaintenance !== true) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2654
|
+
engine.trackBackgroundTask(
|
|
2655
|
+
(signal) => refreshConversationSummary(
|
|
2656
|
+
conversationId,
|
|
2657
|
+
provider,
|
|
2658
|
+
model,
|
|
2659
|
+
historyWindow,
|
|
2660
|
+
false,
|
|
2661
|
+
{ signal },
|
|
2662
|
+
),
|
|
2663
|
+
{
|
|
2664
|
+
key: `conversation-summary:${conversationId}`,
|
|
2665
|
+
signal: runMeta?.abortController?.signal || null,
|
|
2666
|
+
},
|
|
2667
|
+
).catch((err) => {
|
|
2668
|
+
if (!isAbortError(err, runMeta?.abortController?.signal) && !engine.shuttingDown) {
|
|
2669
|
+
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
2670
|
+
}
|
|
2585
2671
|
});
|
|
2586
2672
|
}
|
|
2587
2673
|
}
|
|
@@ -2603,6 +2689,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2603
2689
|
// Fallback: if this was a messaging-triggered run and no final delivery
|
|
2604
2690
|
// was already sent in this run, auto-send the final assistant text.
|
|
2605
2691
|
// Interim progress updates do not suppress this final delivery.
|
|
2692
|
+
await engine.stopMessagingProgressSupervisor(runId);
|
|
2606
2693
|
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
2607
2694
|
if (engine.shouldSendMessagingFinalFallback(runMeta, lastContent || '', options.source) && !lastFinalDeliveryMessage) {
|
|
2608
2695
|
await engine.deliverMessagingFinalFallback({
|
|
@@ -2622,7 +2709,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2622
2709
|
});
|
|
2623
2710
|
if (!completionWon) {
|
|
2624
2711
|
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
2625
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2712
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2626
2713
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2627
2714
|
engine.activeRuns.delete(runId);
|
|
2628
2715
|
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
@@ -2639,7 +2726,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2639
2726
|
analysis,
|
|
2640
2727
|
verification,
|
|
2641
2728
|
historyWindow,
|
|
2642
|
-
options: {
|
|
2729
|
+
options: {
|
|
2730
|
+
...options,
|
|
2731
|
+
userId,
|
|
2732
|
+
agentId,
|
|
2733
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal || null,
|
|
2734
|
+
},
|
|
2643
2735
|
}).catch((err) => {
|
|
2644
2736
|
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
2645
2737
|
});
|
|
@@ -2648,7 +2740,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2648
2740
|
console.info(
|
|
2649
2741
|
`[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}`
|
|
2650
2742
|
);
|
|
2651
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2743
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2652
2744
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2653
2745
|
engine.activeRuns.delete(runId);
|
|
2654
2746
|
engine.emit(userId, 'run:complete', {
|
|
@@ -2681,12 +2773,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2681
2773
|
// Fire-and-forget: plugins can use this for self-improvement, memory
|
|
2682
2774
|
// consolidation, analytics, or other post-run housekeeping.
|
|
2683
2775
|
if (globalHooks.has('on_loop_end')) {
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2776
|
+
engine.trackBackgroundTask(
|
|
2777
|
+
(signal) => globalHooks.run('on_loop_end', {
|
|
2778
|
+
userId, runId, agentId, status: 'completed',
|
|
2779
|
+
iterations: iteration, totalTokens,
|
|
2780
|
+
taskAnalysis: analysis,
|
|
2781
|
+
finalContent: finalResponseText,
|
|
2782
|
+
signal,
|
|
2783
|
+
}),
|
|
2784
|
+
{ signal: runMeta?.abortController?.signal || null },
|
|
2785
|
+
).catch(() => {});
|
|
2690
2786
|
}
|
|
2691
2787
|
if (engine.learningManager) {
|
|
2692
2788
|
try {
|
|
@@ -2712,25 +2808,37 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2712
2808
|
|
|
2713
2809
|
return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
|
|
2714
2810
|
} catch (err) {
|
|
2811
|
+
if (!runRecordCreated) throw err;
|
|
2715
2812
|
if (engine.isRunStopped(runId)) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2813
|
+
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
2814
|
+
const persistedTerminal = db.prepare(
|
|
2815
|
+
'SELECT status, error FROM agent_runs WHERE id = ?',
|
|
2816
|
+
).get(runId);
|
|
2817
|
+
const terminalStatus = persistedTerminal?.status === 'interrupted'
|
|
2818
|
+
|| stoppedRunMeta?.status === 'interrupted'
|
|
2819
|
+
? 'interrupted'
|
|
2820
|
+
: 'stopped';
|
|
2718
2821
|
console.warn(
|
|
2719
|
-
`[Run ${shortenRunId(runId)}]
|
|
2822
|
+
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2720
2823
|
);
|
|
2721
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2824
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2722
2825
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2723
2826
|
engine.activeRuns.delete(runId);
|
|
2724
|
-
engine.emit(userId, 'run:stopped', {
|
|
2725
|
-
|
|
2827
|
+
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
2828
|
+
runId,
|
|
2829
|
+
triggerSource,
|
|
2830
|
+
reason: stoppedRunMeta?.stopReason || persistedTerminal?.error || null,
|
|
2831
|
+
});
|
|
2832
|
+
engine.recordRunEvent(userId, terminalStatus === 'interrupted' ? 'run_interrupted' : 'run_stopped', {
|
|
2726
2833
|
triggerSource,
|
|
2727
2834
|
totalTokens,
|
|
2728
2835
|
iterations: iteration,
|
|
2729
2836
|
}, { agentId });
|
|
2730
|
-
return { runId, content: '', totalTokens, iterations: iteration, status:
|
|
2837
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminalStatus };
|
|
2731
2838
|
}
|
|
2732
2839
|
|
|
2733
2840
|
const runMeta = engine.activeRuns.get(runId);
|
|
2841
|
+
await engine.stopMessagingProgressSupervisor(runId);
|
|
2734
2842
|
|
|
2735
2843
|
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2736
2844
|
|| err?.deliverableValidation?.summary
|
|
@@ -2754,16 +2862,20 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2754
2862
|
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)}`
|
|
2755
2863
|
}
|
|
2756
2864
|
]);
|
|
2757
|
-
const modelReply = await
|
|
2758
|
-
provider.chat(failedMessage, [], {
|
|
2865
|
+
const modelReply = await runAbortableModelCall(
|
|
2866
|
+
(signal) => provider.chat(failedMessage, [], {
|
|
2759
2867
|
model,
|
|
2760
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2868
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2869
|
+
signal,
|
|
2761
2870
|
}),
|
|
2762
|
-
options,
|
|
2871
|
+
{ ...options, signal: runMeta?.abortController?.signal },
|
|
2763
2872
|
'Messaging failure reply',
|
|
2764
2873
|
);
|
|
2765
2874
|
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2766
|
-
if (
|
|
2875
|
+
if (
|
|
2876
|
+
normalizeOutgoingMessage(drafted, options?.source || null)
|
|
2877
|
+
&& !isDeferredWorkReply(drafted)
|
|
2878
|
+
) {
|
|
2767
2879
|
messagingFailureContent = drafted.trim();
|
|
2768
2880
|
}
|
|
2769
2881
|
} catch {
|
|
@@ -2785,7 +2897,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2785
2897
|
options.source,
|
|
2786
2898
|
options.chatId,
|
|
2787
2899
|
messagingFailureContent,
|
|
2788
|
-
{
|
|
2900
|
+
{
|
|
2901
|
+
runId,
|
|
2902
|
+
agentId,
|
|
2903
|
+
signal: runMeta?.abortController?.signal || null,
|
|
2904
|
+
},
|
|
2789
2905
|
);
|
|
2790
2906
|
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
2791
2907
|
sendSucceeded = true;
|
|
@@ -2810,7 +2926,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2810
2926
|
totalTokens,
|
|
2811
2927
|
});
|
|
2812
2928
|
if (!failureWon) {
|
|
2813
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2929
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2814
2930
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2815
2931
|
engine.activeRuns.delete(runId);
|
|
2816
2932
|
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
@@ -2820,7 +2936,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2820
2936
|
`[Run ${shortenRunId(runId)}] failed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} error=${summarizeForLog(err.message, 180)}`
|
|
2821
2937
|
);
|
|
2822
2938
|
|
|
2823
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2939
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2824
2940
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2825
2941
|
engine.activeRuns.delete(runId);
|
|
2826
2942
|
engine.emit(userId, 'run:error', { runId, error: err.message });
|
|
@@ -2830,17 +2946,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2830
2946
|
iterations: iteration,
|
|
2831
2947
|
deliverableType: deliverableWorkflow?.selection?.type || null,
|
|
2832
2948
|
}, { agentId });
|
|
2833
|
-
timelineService?.recordRunLifecycle?.({
|
|
2834
|
-
userId,
|
|
2835
|
-
agentId,
|
|
2836
|
-
runId,
|
|
2837
|
-
title: runTitle,
|
|
2838
|
-
eventKind: 'run_failed',
|
|
2839
|
-
status: 'failed',
|
|
2840
|
-
triggerSource,
|
|
2841
|
-
error: err.message,
|
|
2842
|
-
});
|
|
2843
|
-
|
|
2844
2949
|
if (messagingFailureContent) {
|
|
2845
2950
|
return {
|
|
2846
2951
|
runId,
|
|
@@ -2853,6 +2958,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2853
2958
|
|
|
2854
2959
|
throw err;
|
|
2855
2960
|
} finally {
|
|
2961
|
+
detachExternalAbort?.();
|
|
2856
2962
|
releaseReservation();
|
|
2857
2963
|
}
|
|
2858
2964
|
}
|