neoagent 3.2.1-beta.1 → 3.2.1-beta.11
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 +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -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 +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- 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 +12 -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 +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- 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 +20 -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 +452 -176
- 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 +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -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/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -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 +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 +135 -97
- 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/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- 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/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -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 +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -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 +13 -8
- 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/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- 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/logger.js +19 -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,7 +780,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
709
780
|
voiceSessionId: options.voiceSessionId || null,
|
|
710
781
|
steeringQueue: [],
|
|
711
782
|
systemSteeringQueue: [],
|
|
712
|
-
abortController:
|
|
783
|
+
abortController: startupAbortController,
|
|
713
784
|
pauseAvailable: false,
|
|
714
785
|
toolPids: new Set(),
|
|
715
786
|
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
@@ -720,6 +791,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
720
791
|
? {
|
|
721
792
|
platform: options.source || null,
|
|
722
793
|
chatId: options.chatId || null,
|
|
794
|
+
behavior: options.context?.socialIntelligence || null,
|
|
723
795
|
}
|
|
724
796
|
: null,
|
|
725
797
|
goalContract: carriedGoalContract,
|
|
@@ -734,11 +806,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
734
806
|
progressLedger,
|
|
735
807
|
goalContract: carriedGoalContract,
|
|
736
808
|
});
|
|
737
|
-
|
|
738
|
-
|
|
809
|
+
if (options.context?.socialIntelligence?.isGroup !== true) {
|
|
810
|
+
engine.startMessagingProgressSupervisor(runId);
|
|
811
|
+
}
|
|
812
|
+
engine.emit(userId, 'run:start', {
|
|
813
|
+
runId,
|
|
814
|
+
agentId,
|
|
815
|
+
title: runTitle,
|
|
816
|
+
model,
|
|
817
|
+
modelSelectionId,
|
|
818
|
+
provider: providerName,
|
|
819
|
+
triggerType,
|
|
820
|
+
triggerSource,
|
|
821
|
+
});
|
|
739
822
|
engine.recordRunEvent(userId, runId, 'run_started', {
|
|
740
823
|
title: runTitle,
|
|
741
824
|
model,
|
|
825
|
+
modelSelectionId,
|
|
826
|
+
provider: providerName,
|
|
742
827
|
triggerType,
|
|
743
828
|
triggerSource,
|
|
744
829
|
}, { agentId });
|
|
@@ -752,7 +837,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
752
837
|
triggerSource,
|
|
753
838
|
});
|
|
754
839
|
console.info(
|
|
755
|
-
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${
|
|
840
|
+
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${modelSelectionId} title=${summarizeForLog(runTitle, 120)}`
|
|
756
841
|
);
|
|
757
842
|
|
|
758
843
|
const systemPrompt = await engine.buildSystemPrompt(userId, {
|
|
@@ -760,6 +845,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
760
845
|
userMessage,
|
|
761
846
|
agentId,
|
|
762
847
|
triggerSource,
|
|
848
|
+
memoryAudience: options.memoryAudience || 'owner',
|
|
763
849
|
});
|
|
764
850
|
// Pass short descriptions so the model always knows every available tool.
|
|
765
851
|
// compactToolDefinition caps tool desc at 120 chars, param desc at 70 chars.
|
|
@@ -883,7 +969,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
883
969
|
if (triggerSource === 'messaging') {
|
|
884
970
|
const runMetaForCompose = engine.getRunMeta(runId);
|
|
885
971
|
if (runMetaForCompose) {
|
|
886
|
-
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
972
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false, signal = null } = {}) => {
|
|
887
973
|
try {
|
|
888
974
|
const rm = engine.getRunMeta(runId);
|
|
889
975
|
const ledger = rm?.progressLedger || {};
|
|
@@ -900,7 +986,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
900
986
|
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
901
987
|
const contextBlock = [
|
|
902
988
|
buildProgressUpdatePrompt(),
|
|
903
|
-
stalled ? '
|
|
989
|
+
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
990
|
'',
|
|
905
991
|
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
906
992
|
currentTool ? `Doing now: using ${currentTool}` : '',
|
|
@@ -910,17 +996,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
910
996
|
// Reuse the run's real system prompt so the update follows the same voice and
|
|
911
997
|
// formatting guidelines as every other message (single source of truth).
|
|
912
998
|
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
913
|
-
|| '
|
|
914
|
-
const resp = await
|
|
915
|
-
provider.chat(
|
|
999
|
+
|| 'Respond directly and accurately using only the verified progress evidence.';
|
|
1000
|
+
const resp = await runAbortableModelCall(
|
|
1001
|
+
(signal) => provider.chat(
|
|
916
1002
|
[
|
|
917
1003
|
{ role: 'system', content: sysContent },
|
|
918
1004
|
{ role: 'user', content: contextBlock },
|
|
919
1005
|
],
|
|
920
1006
|
[],
|
|
921
|
-
{
|
|
1007
|
+
{
|
|
1008
|
+
model,
|
|
1009
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
1010
|
+
signal,
|
|
1011
|
+
},
|
|
922
1012
|
),
|
|
923
|
-
|
|
1013
|
+
{
|
|
1014
|
+
...options,
|
|
1015
|
+
signals: [rm?.abortController?.signal, signal],
|
|
1016
|
+
},
|
|
924
1017
|
'Progress update compose',
|
|
925
1018
|
);
|
|
926
1019
|
return sanitizeModelOutput(resp.content || '', { model });
|
|
@@ -1031,6 +1124,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1031
1124
|
null,
|
|
1032
1125
|
{
|
|
1033
1126
|
...providerStatusConfig,
|
|
1127
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1034
1128
|
selectionHint: {
|
|
1035
1129
|
purpose: requestedPurpose,
|
|
1036
1130
|
complexity: analysis?.complexity,
|
|
@@ -1040,15 +1134,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1040
1134
|
},
|
|
1041
1135
|
}
|
|
1042
1136
|
);
|
|
1043
|
-
if (selectedAfterAnalysis.
|
|
1137
|
+
if (selectedAfterAnalysis.modelSelectionId !== modelSelectionId) {
|
|
1044
1138
|
provider = selectedAfterAnalysis.provider;
|
|
1045
1139
|
model = selectedAfterAnalysis.model;
|
|
1140
|
+
modelSelectionId = selectedAfterAnalysis.modelSelectionId;
|
|
1046
1141
|
providerName = selectedAfterAnalysis.providerName;
|
|
1047
1142
|
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
1048
|
-
.run(
|
|
1143
|
+
.run(modelSelectionId, runId);
|
|
1144
|
+
Object.assign(engine.getRunMeta(runId) || {}, {
|
|
1145
|
+
model,
|
|
1146
|
+
modelSelectionId,
|
|
1147
|
+
providerName,
|
|
1148
|
+
});
|
|
1049
1149
|
engine.emit(userId, 'run:interim', {
|
|
1050
1150
|
runId,
|
|
1051
|
-
message: `Switched to ${
|
|
1151
|
+
message: `Switched to ${modelSelectionId} for this run after task analysis.`,
|
|
1052
1152
|
phase: 'model_selection'
|
|
1053
1153
|
});
|
|
1054
1154
|
}
|
|
@@ -1239,7 +1339,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1239
1339
|
reason,
|
|
1240
1340
|
stoppedBy: hookResult.stopped_by || hookResult.stoppedBy || null,
|
|
1241
1341
|
}, { agentId });
|
|
1242
|
-
|
|
1342
|
+
engine.stopRun(runId, reason);
|
|
1243
1343
|
break;
|
|
1244
1344
|
}
|
|
1245
1345
|
const systemSteering = String(hookResult?.systemSteering || hookResult?.system_steering || '').trim();
|
|
@@ -1298,6 +1398,47 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1298
1398
|
continue;
|
|
1299
1399
|
}
|
|
1300
1400
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1401
|
+
const researchAdequacyAtCap = assessResearchAdequacy({
|
|
1402
|
+
analysis,
|
|
1403
|
+
goalContext: runGoalCtx,
|
|
1404
|
+
toolExecutions,
|
|
1405
|
+
});
|
|
1406
|
+
// Incomplete multi-target research is not "stuck" yet if primary sources
|
|
1407
|
+
// remain uncovered. Give one guided research continuation instead of
|
|
1408
|
+
// force-finishing with a guessed comparison.
|
|
1409
|
+
if (
|
|
1410
|
+
researchAdequacyAtCap.adequate === false
|
|
1411
|
+
&& researchAdequacyAtCap.intensity !== 'none'
|
|
1412
|
+
&& iterMeta
|
|
1413
|
+
&& iterMeta.researchAdequacyDeferralUsed !== true
|
|
1414
|
+
&& (
|
|
1415
|
+
researchAdequacyAtCap.uncoveredTargets.length > 0
|
|
1416
|
+
|| researchAdequacyAtCap.primarySourceCount < researchAdequacyAtCap.requiredPrimarySources
|
|
1417
|
+
)
|
|
1418
|
+
) {
|
|
1419
|
+
iterMeta.researchAdequacyDeferralUsed = true;
|
|
1420
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, churnNudgeThreshold - 1);
|
|
1421
|
+
messages.push({
|
|
1422
|
+
role: 'system',
|
|
1423
|
+
content: [
|
|
1424
|
+
'Research self-check: evidence is still incomplete, so this run must not force-finish yet.',
|
|
1425
|
+
researchAdequacyAtCap.reason ? `Gap: ${researchAdequacyAtCap.reason}` : '',
|
|
1426
|
+
researchAdequacyAtCap.nextActions?.length
|
|
1427
|
+
? `Next safe steps:\n- ${researchAdequacyAtCap.nextActions.join('\n- ')}`
|
|
1428
|
+
: '',
|
|
1429
|
+
'Open or fetch primary sources for uncovered targets now. Do not invent the missing comparison from memory or search snippets alone.',
|
|
1430
|
+
].filter(Boolean).join('\n'),
|
|
1431
|
+
});
|
|
1432
|
+
engine.recordRunEvent(userId, runId, 'read_only_wrapup_deferred_for_research', {
|
|
1433
|
+
iteration,
|
|
1434
|
+
readOnlyCount,
|
|
1435
|
+
intensity: researchAdequacyAtCap.intensity,
|
|
1436
|
+
uncoveredTargets: researchAdequacyAtCap.uncoveredTargets,
|
|
1437
|
+
primarySourceCount: researchAdequacyAtCap.primarySourceCount,
|
|
1438
|
+
requiredPrimarySources: researchAdequacyAtCap.requiredPrimarySources,
|
|
1439
|
+
}, { agentId });
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1301
1442
|
triggerForceWrapup = true;
|
|
1302
1443
|
} else if (readOnlyCount >= churnNudgeThreshold) {
|
|
1303
1444
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
@@ -1362,8 +1503,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1362
1503
|
});
|
|
1363
1504
|
let wrapTokens = 0;
|
|
1364
1505
|
try {
|
|
1365
|
-
const wrapResponse = await
|
|
1366
|
-
provider.chat(
|
|
1506
|
+
const wrapResponse = await runAbortableModelCall(
|
|
1507
|
+
(signal) => provider.chat(
|
|
1367
1508
|
sanitizeConversationMessages([
|
|
1368
1509
|
...messages,
|
|
1369
1510
|
{
|
|
@@ -1372,13 +1513,22 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1372
1513
|
readOnlyCount,
|
|
1373
1514
|
alreadyRead,
|
|
1374
1515
|
platform: options?.source || null,
|
|
1516
|
+
researchAdequacy: assessResearchAdequacy({
|
|
1517
|
+
analysis,
|
|
1518
|
+
goalContext: runGoalCtx,
|
|
1519
|
+
toolExecutions,
|
|
1520
|
+
}),
|
|
1375
1521
|
}),
|
|
1376
1522
|
},
|
|
1377
1523
|
]),
|
|
1378
1524
|
[],
|
|
1379
|
-
{
|
|
1525
|
+
{
|
|
1526
|
+
model,
|
|
1527
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
1528
|
+
signal,
|
|
1529
|
+
},
|
|
1380
1530
|
),
|
|
1381
|
-
options,
|
|
1531
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
1382
1532
|
'No-progress wrap-up',
|
|
1383
1533
|
);
|
|
1384
1534
|
wrapTokens = wrapResponse.usage?.totalTokens || 0;
|
|
@@ -1431,9 +1581,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1431
1581
|
let metrics = engine.estimatePromptMetrics(messages, tools);
|
|
1432
1582
|
const contextWindow = provider.getContextWindow(model);
|
|
1433
1583
|
if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
|
|
1434
|
-
messages = await
|
|
1435
|
-
compact(messages, provider, model, contextWindow),
|
|
1436
|
-
options,
|
|
1584
|
+
messages = await runAbortableModelCall(
|
|
1585
|
+
(signal) => compact(messages, provider, model, contextWindow, { signal }),
|
|
1586
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
1437
1587
|
`Context compaction before iteration ${iteration}`,
|
|
1438
1588
|
);
|
|
1439
1589
|
messages = sanitizeConversationMessages(messages);
|
|
@@ -1453,48 +1603,59 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1453
1603
|
let responseModel = model;
|
|
1454
1604
|
let streamContent = '';
|
|
1455
1605
|
|
|
1456
|
-
const tryModelCall = async (
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1606
|
+
const tryModelCall = async () => {
|
|
1607
|
+
let requestMessages = messages;
|
|
1608
|
+
let requestPhase = 'model_turn';
|
|
1609
|
+
while (true) {
|
|
1610
|
+
try {
|
|
1611
|
+
const modelCall = await engine.requestModelResponse({
|
|
1612
|
+
provider,
|
|
1613
|
+
providerName,
|
|
1614
|
+
model,
|
|
1615
|
+
messages: requestMessages,
|
|
1616
|
+
tools,
|
|
1617
|
+
options: {
|
|
1618
|
+
...options,
|
|
1619
|
+
userId,
|
|
1620
|
+
agentId,
|
|
1621
|
+
runId,
|
|
1622
|
+
phase: requestPhase,
|
|
1623
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal,
|
|
1624
|
+
},
|
|
1468
1625
|
runId,
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1626
|
+
iteration,
|
|
1627
|
+
});
|
|
1628
|
+
response = modelCall.response;
|
|
1629
|
+
responseModel = modelCall.responseModel;
|
|
1630
|
+
streamContent = modelCall.streamContent;
|
|
1631
|
+
recordModelSuccess(userId, agentId, modelSelectionId);
|
|
1632
|
+
return;
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1635
|
+
const runSignal = engine.getRunMeta(runId)?.abortController?.signal;
|
|
1636
|
+
if (isAbortError(err) || runSignal?.aborted) throw err;
|
|
1637
|
+
if (modelFailureRecoveries >= loopPolicy.maxModelFailureRecoveries) {
|
|
1638
|
+
recordFailedModel(
|
|
1639
|
+
modelSelectionId,
|
|
1640
|
+
err,
|
|
1641
|
+
modelTurnFailedModelSelectionIds,
|
|
1642
|
+
);
|
|
1643
|
+
throw err;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1484
1646
|
const failedModel = model;
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
fallbackModelId,
|
|
1491
|
-
providerStatusConfig
|
|
1647
|
+
const switched = await switchToFallbackModel(
|
|
1648
|
+
modelSelectionId,
|
|
1649
|
+
err,
|
|
1650
|
+
'model turn',
|
|
1651
|
+
modelTurnFailedModelSelectionIds,
|
|
1492
1652
|
);
|
|
1493
|
-
|
|
1494
|
-
model = fallback.model;
|
|
1495
|
-
providerName = fallback.providerName;
|
|
1653
|
+
if (!switched) throw err;
|
|
1496
1654
|
|
|
1497
|
-
|
|
1655
|
+
modelFailureRecoveries += 1;
|
|
1656
|
+
failedStepCount += 1;
|
|
1657
|
+
requestPhase = 'model_turn_fallback';
|
|
1658
|
+
requestMessages = sanitizeConversationMessages([
|
|
1498
1659
|
...messages,
|
|
1499
1660
|
{
|
|
1500
1661
|
role: 'system',
|
|
@@ -1505,22 +1666,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1505
1666
|
})
|
|
1506
1667
|
}
|
|
1507
1668
|
]);
|
|
1508
|
-
|
|
1509
|
-
const fallbackCall = await engine.requestModelResponse({
|
|
1510
|
-
provider,
|
|
1511
|
-
providerName,
|
|
1512
|
-
model,
|
|
1513
|
-
messages: retryMessages,
|
|
1514
|
-
tools,
|
|
1515
|
-
options: { ...options, userId },
|
|
1516
|
-
runId,
|
|
1517
|
-
iteration,
|
|
1518
|
-
});
|
|
1519
|
-
response = fallbackCall.response;
|
|
1520
|
-
responseModel = fallbackCall.responseModel;
|
|
1521
|
-
streamContent = fallbackCall.streamContent;
|
|
1522
|
-
} else {
|
|
1523
|
-
throw err;
|
|
1524
1669
|
}
|
|
1525
1670
|
}
|
|
1526
1671
|
};
|
|
@@ -1543,30 +1688,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1543
1688
|
continue;
|
|
1544
1689
|
}
|
|
1545
1690
|
if (lifecycleControl?.action === 'stop' || lifecycleControl?.action === 'interrupt') break;
|
|
1546
|
-
const modelError = String(err?.message || 'Model call failed');
|
|
1547
|
-
|
|
1548
|
-
if (modelFailureRecoveries < loopPolicy.maxModelFailureRecoveries) {
|
|
1549
|
-
const failedModel = model;
|
|
1550
|
-
const switched = await switchToFallbackModel(failedModel, err, 'model turn');
|
|
1551
|
-
if (!switched) throw err;
|
|
1552
|
-
modelFailureRecoveries += 1;
|
|
1553
|
-
failedStepCount += 1;
|
|
1554
|
-
messages.push({
|
|
1555
|
-
role: 'system',
|
|
1556
|
-
content: buildModelFailureLoopPrompt({
|
|
1557
|
-
failedModel,
|
|
1558
|
-
nextModel: model,
|
|
1559
|
-
errorMessage: modelError
|
|
1560
|
-
})
|
|
1561
|
-
});
|
|
1562
|
-
engine.emit(userId, 'run:interim', {
|
|
1563
|
-
runId,
|
|
1564
|
-
message: 'Model call failed; adapting and retrying autonomously.',
|
|
1565
|
-
phase: 'recovering'
|
|
1566
|
-
});
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
1691
|
throw err;
|
|
1571
1692
|
}
|
|
1572
1693
|
|
|
@@ -1601,10 +1722,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1601
1722
|
toolCallCount: response.toolCalls?.length || 0,
|
|
1602
1723
|
contentPreview: String(lastContent || streamContent || '').slice(0, 240),
|
|
1603
1724
|
}, { agentId });
|
|
1604
|
-
engine.updateRunProgress(runId, {}, {
|
|
1605
|
-
verified: true,
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
1725
|
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
1609
1726
|
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
1610
1727
|
if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
|
|
@@ -1650,15 +1767,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1650
1767
|
lastContent = '';
|
|
1651
1768
|
continue;
|
|
1652
1769
|
}
|
|
1653
|
-
if (engine.shouldFastCompleteVoiceReply({
|
|
1654
|
-
options,
|
|
1655
|
-
toolExecutions,
|
|
1656
|
-
failedStepCount,
|
|
1657
|
-
messagingSent: engine.activeRuns.get(runId)?.messagingSent || false,
|
|
1658
|
-
lastReply: lastContent,
|
|
1659
|
-
})) {
|
|
1660
|
-
break;
|
|
1661
|
-
}
|
|
1662
1770
|
if (shouldContinueAfterBlankToolFailure({
|
|
1663
1771
|
lastContent,
|
|
1664
1772
|
failedStepCount,
|
|
@@ -1714,13 +1822,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1714
1822
|
iteration,
|
|
1715
1823
|
}, { agentId });
|
|
1716
1824
|
if (loopDecision.decision.status === 'continue') {
|
|
1825
|
+
const researchGuidance = loopDecision.researchAdequacy?.adequate === false
|
|
1826
|
+
? [
|
|
1827
|
+
`Research still incomplete: ${loopDecision.researchAdequacy.reason || 'gather primary evidence for remaining targets.'}`,
|
|
1828
|
+
loopDecision.researchAdequacy.nextActions?.length
|
|
1829
|
+
? `Next safe steps: ${loopDecision.researchAdequacy.nextActions.join(' ')}`
|
|
1830
|
+
: '',
|
|
1831
|
+
].filter(Boolean).join(' ')
|
|
1832
|
+
: '';
|
|
1717
1833
|
messages.push({
|
|
1718
1834
|
role: 'system',
|
|
1719
1835
|
content: [
|
|
1720
1836
|
'The run self-check determined the latest assistant text is not terminal.',
|
|
1721
1837
|
'Continue with the next safe tool/model step.',
|
|
1722
1838
|
'If the text was only a progress note, do not repeat it; either make progress or provide a real final/blocker reply.',
|
|
1723
|
-
|
|
1839
|
+
'Do not invent missing targets, outcomes, or tool results. Gather evidence or return a truthful partial/blocker answer.',
|
|
1840
|
+
researchGuidance,
|
|
1841
|
+
].filter(Boolean).join(' '),
|
|
1724
1842
|
});
|
|
1725
1843
|
lastContent = '';
|
|
1726
1844
|
continue;
|
|
@@ -1731,7 +1849,10 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1731
1849
|
|
|
1732
1850
|
const canRunParallelBatch = (
|
|
1733
1851
|
response.toolCalls.length > 1
|
|
1734
|
-
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(
|
|
1852
|
+
&& response.toolCalls.every((toolCall) => engine.isReadOnlyToolCall(
|
|
1853
|
+
toolCall,
|
|
1854
|
+
tools.find((tool) => tool?.name === toolCall?.function?.name) || null,
|
|
1855
|
+
))
|
|
1735
1856
|
);
|
|
1736
1857
|
if (canRunParallelBatch) {
|
|
1737
1858
|
const parallelToolNames = response.toolCalls
|
|
@@ -1768,6 +1889,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1768
1889
|
item.toolArgs,
|
|
1769
1890
|
item.result,
|
|
1770
1891
|
item.error || '',
|
|
1892
|
+
tools.find((tool) => tool?.name === item.toolName) || null,
|
|
1771
1893
|
);
|
|
1772
1894
|
execution.input = item.toolArgs;
|
|
1773
1895
|
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
@@ -1807,7 +1929,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1807
1929
|
currentTool: null,
|
|
1808
1930
|
currentStepStartedAt: null,
|
|
1809
1931
|
}, {
|
|
1810
|
-
verified:
|
|
1932
|
+
verified: batchGatheredNewEvidence,
|
|
1811
1933
|
});
|
|
1812
1934
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1813
1935
|
const iterMeta = engine.getRunMeta(runId);
|
|
@@ -2054,11 +2176,20 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2054
2176
|
});
|
|
2055
2177
|
if (lifecycleAfterTool?.action === 'stop' || lifecycleAfterTool?.action === 'interrupt') break;
|
|
2056
2178
|
|
|
2057
|
-
const execution = classifyToolExecution(
|
|
2179
|
+
const execution = classifyToolExecution(
|
|
2180
|
+
toolName,
|
|
2181
|
+
toolArgs,
|
|
2182
|
+
toolResult,
|
|
2183
|
+
toolErrorMessage,
|
|
2184
|
+
tools.find((tool) => tool?.name === toolName) || null,
|
|
2185
|
+
);
|
|
2058
2186
|
execution.input = toolArgs;
|
|
2059
2187
|
const repetitionObservation = repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
2060
|
-
|
|
2061
|
-
|
|
2188
|
+
const toolMadeConcreteProgress = (
|
|
2189
|
+
(execution.stateChanged && isProgressToolCall(toolName, toolArgs))
|
|
2190
|
+
|| gatheredNewEvidence(execution, repetitionObservation)
|
|
2191
|
+
);
|
|
2192
|
+
if (toolMadeConcreteProgress) {
|
|
2062
2193
|
iterationConcreteProgress = true;
|
|
2063
2194
|
}
|
|
2064
2195
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
@@ -2206,7 +2337,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2206
2337
|
currentTool: null,
|
|
2207
2338
|
currentStepStartedAt: null,
|
|
2208
2339
|
}, {
|
|
2209
|
-
verified:
|
|
2340
|
+
verified: toolMadeConcreteProgress,
|
|
2210
2341
|
stepId,
|
|
2211
2342
|
});
|
|
2212
2343
|
|
|
@@ -2256,20 +2387,18 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2256
2387
|
|
|
2257
2388
|
if (engine.isRunStopped(runId)) {
|
|
2258
2389
|
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
2259
|
-
const
|
|
2390
|
+
const persistedTerminal = db.prepare(
|
|
2391
|
+
'SELECT status, error FROM agent_runs WHERE id = ?',
|
|
2392
|
+
).get(runId);
|
|
2393
|
+
const terminalStatus = persistedTerminal?.status === 'interrupted'
|
|
2394
|
+
|| stoppedRunMeta?.status === 'interrupted'
|
|
2395
|
+
? 'interrupted'
|
|
2396
|
+
: 'stopped';
|
|
2260
2397
|
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
2398
|
console.warn(
|
|
2270
2399
|
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2271
2400
|
);
|
|
2272
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2401
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2273
2402
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2274
2403
|
engine.activeRuns.delete(runId);
|
|
2275
2404
|
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
@@ -2320,16 +2449,20 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2320
2449
|
if (budgetExhaustedWithoutCompletion) {
|
|
2321
2450
|
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
2322
2451
|
try {
|
|
2323
|
-
const wrapResponse = await
|
|
2324
|
-
provider.chat(
|
|
2452
|
+
const wrapResponse = await runAbortableModelCall(
|
|
2453
|
+
(signal) => provider.chat(
|
|
2325
2454
|
sanitizeConversationMessages([
|
|
2326
2455
|
...messages,
|
|
2327
2456
|
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
2328
2457
|
]),
|
|
2329
2458
|
[],
|
|
2330
|
-
{
|
|
2459
|
+
{
|
|
2460
|
+
model,
|
|
2461
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2462
|
+
signal,
|
|
2463
|
+
},
|
|
2331
2464
|
),
|
|
2332
|
-
options,
|
|
2465
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
2333
2466
|
'Max-iteration wrap-up',
|
|
2334
2467
|
);
|
|
2335
2468
|
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
@@ -2362,8 +2495,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2362
2495
|
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery model=${model}`);
|
|
2363
2496
|
let recoveredTokens = 0;
|
|
2364
2497
|
try {
|
|
2365
|
-
const recoveryResponse = await
|
|
2366
|
-
provider.chat(
|
|
2498
|
+
const recoveryResponse = await runAbortableModelCall(
|
|
2499
|
+
(signal) => provider.chat(
|
|
2367
2500
|
sanitizeConversationMessages([
|
|
2368
2501
|
...messages,
|
|
2369
2502
|
{
|
|
@@ -2374,10 +2507,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2374
2507
|
[],
|
|
2375
2508
|
{
|
|
2376
2509
|
model,
|
|
2377
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2510
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2511
|
+
signal,
|
|
2378
2512
|
}
|
|
2379
2513
|
),
|
|
2380
|
-
options,
|
|
2514
|
+
{ ...options, signal: engine.getRunMeta(runId)?.abortController?.signal },
|
|
2381
2515
|
'Blank messaging reply recovery',
|
|
2382
2516
|
);
|
|
2383
2517
|
recoveredTokens = recoveryResponse.usage?.totalTokens || 0;
|
|
@@ -2390,7 +2524,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2390
2524
|
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2391
2525
|
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2392
2526
|
// a deterministic message when the model returned nothing usable.
|
|
2393
|
-
const recoveredVisible = Boolean(
|
|
2527
|
+
const recoveredVisible = Boolean(
|
|
2528
|
+
normalizeOutgoingMessage(lastContent, options?.source || null),
|
|
2529
|
+
);
|
|
2394
2530
|
if (!recoveredVisible) {
|
|
2395
2531
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2396
2532
|
}
|
|
@@ -2418,7 +2554,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2418
2554
|
|| lifecycleBeforeFinalize?.action === 'interrupt'
|
|
2419
2555
|
) {
|
|
2420
2556
|
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
2421
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2557
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2422
2558
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2423
2559
|
engine.activeRuns.delete(runId);
|
|
2424
2560
|
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
@@ -2536,6 +2672,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2536
2672
|
});
|
|
2537
2673
|
}
|
|
2538
2674
|
|
|
2675
|
+
// Final reply terminality is decided by the AI completion judge / task_complete path.
|
|
2676
|
+
// Do not phrase-match natural-language drafts here.
|
|
2677
|
+
|
|
2539
2678
|
if (deliverableWorkflow && deliverablePlan) {
|
|
2540
2679
|
engine.recordRunEvent(userId, runId, 'deliverable_validation_started', {
|
|
2541
2680
|
type: deliverableWorkflow.selection.type,
|
|
@@ -2580,8 +2719,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2580
2719
|
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2581
2720
|
.run(totalTokens, conversationId);
|
|
2582
2721
|
if (options.skipConversationMaintenance !== true) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2722
|
+
engine.trackBackgroundTask(
|
|
2723
|
+
(signal) => refreshConversationSummary(
|
|
2724
|
+
conversationId,
|
|
2725
|
+
provider,
|
|
2726
|
+
model,
|
|
2727
|
+
historyWindow,
|
|
2728
|
+
false,
|
|
2729
|
+
{ signal },
|
|
2730
|
+
),
|
|
2731
|
+
{
|
|
2732
|
+
key: `conversation-summary:${conversationId}`,
|
|
2733
|
+
signal: runMeta?.abortController?.signal || null,
|
|
2734
|
+
},
|
|
2735
|
+
).catch((err) => {
|
|
2736
|
+
if (!isAbortError(err, runMeta?.abortController?.signal) && !engine.shuttingDown) {
|
|
2737
|
+
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
2738
|
+
}
|
|
2585
2739
|
});
|
|
2586
2740
|
}
|
|
2587
2741
|
}
|
|
@@ -2603,6 +2757,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2603
2757
|
// Fallback: if this was a messaging-triggered run and no final delivery
|
|
2604
2758
|
// was already sent in this run, auto-send the final assistant text.
|
|
2605
2759
|
// Interim progress updates do not suppress this final delivery.
|
|
2760
|
+
await engine.stopMessagingProgressSupervisor(runId);
|
|
2606
2761
|
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
2607
2762
|
if (engine.shouldSendMessagingFinalFallback(runMeta, lastContent || '', options.source) && !lastFinalDeliveryMessage) {
|
|
2608
2763
|
await engine.deliverMessagingFinalFallback({
|
|
@@ -2622,7 +2777,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2622
2777
|
});
|
|
2623
2778
|
if (!completionWon) {
|
|
2624
2779
|
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
2625
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2780
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2626
2781
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2627
2782
|
engine.activeRuns.delete(runId);
|
|
2628
2783
|
return { runId, content: '', totalTokens, iterations: iteration, status: terminal };
|
|
@@ -2639,7 +2794,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2639
2794
|
analysis,
|
|
2640
2795
|
verification,
|
|
2641
2796
|
historyWindow,
|
|
2642
|
-
options: {
|
|
2797
|
+
options: {
|
|
2798
|
+
...options,
|
|
2799
|
+
userId,
|
|
2800
|
+
agentId,
|
|
2801
|
+
signal: engine.getRunMeta(runId)?.abortController?.signal || null,
|
|
2802
|
+
},
|
|
2643
2803
|
}).catch((err) => {
|
|
2644
2804
|
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
2645
2805
|
});
|
|
@@ -2648,7 +2808,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2648
2808
|
console.info(
|
|
2649
2809
|
`[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
2810
|
);
|
|
2651
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2811
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2652
2812
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2653
2813
|
engine.activeRuns.delete(runId);
|
|
2654
2814
|
engine.emit(userId, 'run:complete', {
|
|
@@ -2681,12 +2841,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2681
2841
|
// Fire-and-forget: plugins can use this for self-improvement, memory
|
|
2682
2842
|
// consolidation, analytics, or other post-run housekeeping.
|
|
2683
2843
|
if (globalHooks.has('on_loop_end')) {
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2844
|
+
engine.trackBackgroundTask(
|
|
2845
|
+
(signal) => globalHooks.run('on_loop_end', {
|
|
2846
|
+
userId, runId, agentId, status: 'completed',
|
|
2847
|
+
iterations: iteration, totalTokens,
|
|
2848
|
+
taskAnalysis: analysis,
|
|
2849
|
+
finalContent: finalResponseText,
|
|
2850
|
+
signal,
|
|
2851
|
+
}),
|
|
2852
|
+
{ signal: runMeta?.abortController?.signal || null },
|
|
2853
|
+
).catch(() => {});
|
|
2690
2854
|
}
|
|
2691
2855
|
if (engine.learningManager) {
|
|
2692
2856
|
try {
|
|
@@ -2712,25 +2876,37 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2712
2876
|
|
|
2713
2877
|
return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
|
|
2714
2878
|
} catch (err) {
|
|
2879
|
+
if (!runRecordCreated) throw err;
|
|
2715
2880
|
if (engine.isRunStopped(runId)) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2881
|
+
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
2882
|
+
const persistedTerminal = db.prepare(
|
|
2883
|
+
'SELECT status, error FROM agent_runs WHERE id = ?',
|
|
2884
|
+
).get(runId);
|
|
2885
|
+
const terminalStatus = persistedTerminal?.status === 'interrupted'
|
|
2886
|
+
|| stoppedRunMeta?.status === 'interrupted'
|
|
2887
|
+
? 'interrupted'
|
|
2888
|
+
: 'stopped';
|
|
2718
2889
|
console.warn(
|
|
2719
|
-
`[Run ${shortenRunId(runId)}]
|
|
2890
|
+
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
2720
2891
|
);
|
|
2721
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2892
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2722
2893
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2723
2894
|
engine.activeRuns.delete(runId);
|
|
2724
|
-
engine.emit(userId, 'run:stopped', {
|
|
2725
|
-
|
|
2895
|
+
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
2896
|
+
runId,
|
|
2897
|
+
triggerSource,
|
|
2898
|
+
reason: stoppedRunMeta?.stopReason || persistedTerminal?.error || null,
|
|
2899
|
+
});
|
|
2900
|
+
engine.recordRunEvent(userId, terminalStatus === 'interrupted' ? 'run_interrupted' : 'run_stopped', {
|
|
2726
2901
|
triggerSource,
|
|
2727
2902
|
totalTokens,
|
|
2728
2903
|
iterations: iteration,
|
|
2729
2904
|
}, { agentId });
|
|
2730
|
-
return { runId, content: '', totalTokens, iterations: iteration, status:
|
|
2905
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminalStatus };
|
|
2731
2906
|
}
|
|
2732
2907
|
|
|
2733
2908
|
const runMeta = engine.activeRuns.get(runId);
|
|
2909
|
+
await engine.stopMessagingProgressSupervisor(runId);
|
|
2734
2910
|
|
|
2735
2911
|
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2736
2912
|
|| err?.deliverableValidation?.summary
|
|
@@ -2754,16 +2930,19 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2754
2930
|
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
2931
|
}
|
|
2756
2932
|
]);
|
|
2757
|
-
const modelReply = await
|
|
2758
|
-
provider.chat(failedMessage, [], {
|
|
2933
|
+
const modelReply = await runAbortableModelCall(
|
|
2934
|
+
(signal) => provider.chat(failedMessage, [], {
|
|
2759
2935
|
model,
|
|
2760
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2936
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options),
|
|
2937
|
+
signal,
|
|
2761
2938
|
}),
|
|
2762
|
-
options,
|
|
2939
|
+
{ ...options, signal: runMeta?.abortController?.signal },
|
|
2763
2940
|
'Messaging failure reply',
|
|
2764
2941
|
);
|
|
2765
2942
|
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2766
|
-
if (
|
|
2943
|
+
if (
|
|
2944
|
+
normalizeOutgoingMessage(drafted, options?.source || null)
|
|
2945
|
+
) {
|
|
2767
2946
|
messagingFailureContent = drafted.trim();
|
|
2768
2947
|
}
|
|
2769
2948
|
} catch {
|
|
@@ -2785,7 +2964,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2785
2964
|
options.source,
|
|
2786
2965
|
options.chatId,
|
|
2787
2966
|
messagingFailureContent,
|
|
2788
|
-
{
|
|
2967
|
+
{
|
|
2968
|
+
runId,
|
|
2969
|
+
agentId,
|
|
2970
|
+
signal: runMeta?.abortController?.signal || null,
|
|
2971
|
+
},
|
|
2789
2972
|
);
|
|
2790
2973
|
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
2791
2974
|
sendSucceeded = true;
|
|
@@ -2810,7 +2993,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2810
2993
|
totalTokens,
|
|
2811
2994
|
});
|
|
2812
2995
|
if (!failureWon) {
|
|
2813
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2996
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2814
2997
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2815
2998
|
engine.activeRuns.delete(runId);
|
|
2816
2999
|
const terminal = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status || 'stopped';
|
|
@@ -2820,7 +3003,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2820
3003
|
`[Run ${shortenRunId(runId)}] failed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} error=${summarizeForLog(err.message, 180)}`
|
|
2821
3004
|
);
|
|
2822
3005
|
|
|
2823
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
3006
|
+
await engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2824
3007
|
engine.stopMessagingProgressSupervisor(runId);
|
|
2825
3008
|
engine.activeRuns.delete(runId);
|
|
2826
3009
|
engine.emit(userId, 'run:error', { runId, error: err.message });
|
|
@@ -2830,17 +3013,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2830
3013
|
iterations: iteration,
|
|
2831
3014
|
deliverableType: deliverableWorkflow?.selection?.type || null,
|
|
2832
3015
|
}, { 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
3016
|
if (messagingFailureContent) {
|
|
2845
3017
|
return {
|
|
2846
3018
|
runId,
|
|
@@ -2853,6 +3025,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2853
3025
|
|
|
2854
3026
|
throw err;
|
|
2855
3027
|
} finally {
|
|
3028
|
+
detachExternalAbort?.();
|
|
2856
3029
|
releaseReservation();
|
|
2857
3030
|
}
|
|
2858
3031
|
}
|