neoagent 2.4.1-beta.8 → 2.4.1
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/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -30,7 +30,6 @@ const {
|
|
|
30
30
|
const { getCapabilityHealth, summarizeCapabilityHealth } = require('./capabilityHealth');
|
|
31
31
|
const {
|
|
32
32
|
buildPlatformFormattingGuide,
|
|
33
|
-
normalizeOutgoingMessageForPlatform,
|
|
34
33
|
splitOutgoingMessageForPlatform,
|
|
35
34
|
} = require('../messaging/formatting_guides');
|
|
36
35
|
const {
|
|
@@ -49,6 +48,27 @@ const {
|
|
|
49
48
|
} = require('./deliverables');
|
|
50
49
|
const { buildLoopPolicy, resolveToolResultLimits } = require('./loopPolicy');
|
|
51
50
|
const { globalHooks } = require('./hooks');
|
|
51
|
+
const { withProviderRetry, isTransientError } = require('./providerRetry');
|
|
52
|
+
const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('./completion');
|
|
53
|
+
const { shortenRunId, summarizeForLog, parseMaybeJson } = require('./logFormat');
|
|
54
|
+
const {
|
|
55
|
+
normalizeOutgoingMessage,
|
|
56
|
+
clampRunContext,
|
|
57
|
+
joinSentMessages,
|
|
58
|
+
normalizeInterimText,
|
|
59
|
+
buildBlankMessagingReplyPrompt,
|
|
60
|
+
buildDeterministicMessagingFallback,
|
|
61
|
+
buildMessagingFailureScenario,
|
|
62
|
+
buildDeterministicMessagingErrorReply,
|
|
63
|
+
buildModelFailureLoopPrompt,
|
|
64
|
+
} = require('./messagingFallback');
|
|
65
|
+
const {
|
|
66
|
+
classifyToolExecution,
|
|
67
|
+
summarizeToolExecutions,
|
|
68
|
+
summarizeAvailableTools,
|
|
69
|
+
inferToolFailureMessage,
|
|
70
|
+
buildAutonomousRecoveryContext,
|
|
71
|
+
} = require('./toolEvidence');
|
|
52
72
|
|
|
53
73
|
function generateTitle(task) {
|
|
54
74
|
if (!task || typeof task !== 'string') return 'Untitled';
|
|
@@ -61,6 +81,17 @@ function generateTitle(task) {
|
|
|
61
81
|
return cleaned.slice(0, 90);
|
|
62
82
|
}
|
|
63
83
|
|
|
84
|
+
function buildInitialRunMetadata(options = {}) {
|
|
85
|
+
const metadata = {};
|
|
86
|
+
if (options.taskId != null && String(options.taskId).trim()) {
|
|
87
|
+
metadata.taskId = options.taskId;
|
|
88
|
+
}
|
|
89
|
+
if (options.widgetId != null && String(options.widgetId).trim()) {
|
|
90
|
+
metadata.widgetId = options.widgetId;
|
|
91
|
+
}
|
|
92
|
+
return metadata;
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
function planningDepthForForceMode(forceMode) {
|
|
65
96
|
return forceMode === 'plan_execute' ? 'deep' : 'light';
|
|
66
97
|
}
|
|
@@ -78,6 +109,11 @@ function buildSkipTaskAnalysisResult(forceMode) {
|
|
|
78
109
|
draft_reply: '',
|
|
79
110
|
goal: 'Complete the user request accurately.',
|
|
80
111
|
success_criteria: [],
|
|
112
|
+
complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
|
|
113
|
+
autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
|
|
114
|
+
progress_update_policy: 'optional',
|
|
115
|
+
parallel_work: false,
|
|
116
|
+
completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
|
|
81
117
|
};
|
|
82
118
|
}
|
|
83
119
|
|
|
@@ -87,6 +123,11 @@ function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
|
|
|
87
123
|
verification_need: 'light',
|
|
88
124
|
planning_depth: planningDepthForForceMode(forceMode),
|
|
89
125
|
goal: userMessage ? String(userMessage).trim().slice(0, 300) : '',
|
|
126
|
+
complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
|
|
127
|
+
autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
|
|
128
|
+
progress_update_policy: 'optional',
|
|
129
|
+
parallel_work: false,
|
|
130
|
+
completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
|
|
90
131
|
};
|
|
91
132
|
}
|
|
92
133
|
|
|
@@ -97,6 +138,19 @@ function applyForcedAnalysisMode(analysis, forceMode) {
|
|
|
97
138
|
...analysis,
|
|
98
139
|
mode: 'plan_execute',
|
|
99
140
|
planning_depth: 'deep',
|
|
141
|
+
complexity: 'complex',
|
|
142
|
+
autonomy_level: 'high',
|
|
143
|
+
completion_confidence_required: analysis.completion_confidence_required || 'high',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildAutonomyPolicyFromAnalysis(analysis = {}) {
|
|
148
|
+
return {
|
|
149
|
+
complexity: analysis.complexity || 'standard',
|
|
150
|
+
autonomy_level: analysis.autonomy_level || 'normal',
|
|
151
|
+
progress_update_policy: analysis.progress_update_policy || 'optional',
|
|
152
|
+
parallel_work: analysis.parallel_work === true,
|
|
153
|
+
completion_confidence_required: analysis.completion_confidence_required || 'medium',
|
|
100
154
|
};
|
|
101
155
|
}
|
|
102
156
|
|
|
@@ -150,22 +204,19 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
|
|
|
150
204
|
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
151
205
|
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
152
206
|
} else {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (isPlanning) {
|
|
207
|
+
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
208
|
+
? providerConfig.selectionHint
|
|
209
|
+
: {};
|
|
210
|
+
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
211
|
+
const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
|
|
212
|
+
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
213
|
+
? preferredPurpose
|
|
214
|
+
: '';
|
|
215
|
+
|
|
216
|
+
if (smarterSelection && requestedPurpose) {
|
|
217
|
+
selectedModelDef = availableModels.find((m) => m.purpose === requestedPurpose) || fallbackModel;
|
|
218
|
+
} else if (smarterSelection && highAutonomy) {
|
|
166
219
|
selectedModelDef = availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
|
|
167
|
-
} else if (isCoding) {
|
|
168
|
-
selectedModelDef = availableModels.find((m) => m.purpose === 'coding') || availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
|
|
169
220
|
} else if (isSubagent) {
|
|
170
221
|
selectedModelDef = availableModels.find((m) => m.purpose === 'fast') || fallbackModel;
|
|
171
222
|
} else {
|
|
@@ -180,7 +231,7 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
|
|
|
180
231
|
};
|
|
181
232
|
}
|
|
182
233
|
|
|
183
|
-
async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null) {
|
|
234
|
+
async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null, failureError = null) {
|
|
184
235
|
const { getSupportedModels } = require('./models');
|
|
185
236
|
const aiSettings = getAiSettings(userId, agentId);
|
|
186
237
|
const models = await getSupportedModels(userId, agentId);
|
|
@@ -196,7 +247,12 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
|
|
|
196
247
|
|| availableModels.find((model) => model.id === currentModelId)
|
|
197
248
|
|| null;
|
|
198
249
|
|
|
199
|
-
|
|
250
|
+
// When the failure is a provider-level rate limit, the preferred fallback is
|
|
251
|
+
// likely on the same provider and will hit the same limit. Skip it and prefer
|
|
252
|
+
// a fallback from a different provider instead.
|
|
253
|
+
const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
|
|
254
|
+
|
|
255
|
+
if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
|
|
200
256
|
const preferred = pool.find((model) => model.id === preferredFallbackId)
|
|
201
257
|
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
202
258
|
if (preferred) return preferred.id;
|
|
@@ -208,6 +264,14 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
|
|
|
208
264
|
if (differentProvider) return differentProvider.id;
|
|
209
265
|
}
|
|
210
266
|
|
|
267
|
+
// If no different-provider model exists, still try the preferred fallback
|
|
268
|
+
// even on rate limits (it's better than nothing).
|
|
269
|
+
if (preferredFallbackId && preferredFallbackId !== currentModelId) {
|
|
270
|
+
const preferred = pool.find((model) => model.id === preferredFallbackId)
|
|
271
|
+
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
272
|
+
if (preferred) return preferred.id;
|
|
273
|
+
}
|
|
274
|
+
|
|
211
275
|
const differentModel = pool.find((model) => model.id !== currentModelId)
|
|
212
276
|
|| availableModels.find((model) => model.id !== currentModelId);
|
|
213
277
|
return differentModel?.id || null;
|
|
@@ -219,434 +283,9 @@ function estimateTokenValue(value) {
|
|
|
219
283
|
return Math.ceil(JSON.stringify(value).length / 4);
|
|
220
284
|
}
|
|
221
285
|
|
|
222
|
-
function normalizeOutgoingMessage(content, platform = null, options = {}) {
|
|
223
|
-
const normalized = normalizeOutgoingMessageForPlatform(platform, content);
|
|
224
|
-
if (options.collapseWhitespace === false) {
|
|
225
|
-
return normalized;
|
|
226
|
-
}
|
|
227
|
-
return normalized.replace(/\s+/g, ' ').trim();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function joinSentMessages(messages = []) {
|
|
231
|
-
if (!Array.isArray(messages)) return '';
|
|
232
|
-
return messages
|
|
233
|
-
.map((message) => String(message || '').trim())
|
|
234
|
-
.filter(Boolean)
|
|
235
|
-
.join('\n\n');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function normalizeInterimText(content, platform = null) {
|
|
239
|
-
return normalizeOutgoingMessageForPlatform(platform, content, {
|
|
240
|
-
stripNoResponseMarker: false,
|
|
241
|
-
}).trim();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function buildBlankMessagingReplyPrompt(attempt, platform = null) {
|
|
245
|
-
const formattingGuide = buildPlatformFormattingGuide(platform);
|
|
246
|
-
if (attempt <= 1) {
|
|
247
|
-
return `You must send one non-empty reply for the external messaging user right now. Do not call tools. Give either: (a) the concrete outcome, or (b) a clear blocker. If tool work already happened, summarize what you actually tried and where it got blocked. Do not ask the user to repeat the original request. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return `Your previous reply was empty. Return one non-empty message now. Do not call tools. If needed, apologize briefly and explain the blocker in one sentence. Use the run evidence already in the conversation instead of asking the user to restate the task. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function parseToolExecutionSummary(item) {
|
|
254
|
-
if (!item?.summary) return null;
|
|
255
|
-
try {
|
|
256
|
-
const parsed = JSON.parse(item.summary);
|
|
257
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
258
|
-
} catch {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function toolWorkDescription(toolName) {
|
|
264
|
-
const name = String(toolName || '');
|
|
265
|
-
if (name === 'execute_command') return 'ran shell commands';
|
|
266
|
-
if (name === 'read_file' || name === 'search_files' || name === 'list_directory') return 'checked files';
|
|
267
|
-
if (name === 'web_search' || name === 'http_request') return 'looked up supporting information';
|
|
268
|
-
if (name.startsWith('browser_')) return 'checked the browser state';
|
|
269
|
-
if (name.startsWith('android_')) return 'checked the Android state';
|
|
270
|
-
if (name === 'read_health_data' || name.startsWith('recordings_')) return 'checked stored data';
|
|
271
|
-
return '';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function summarizeRecentWork(toolExecutions = []) {
|
|
275
|
-
const descriptions = [];
|
|
276
|
-
for (const item of toolExecutions.slice(-6)) {
|
|
277
|
-
const description = toolWorkDescription(item?.toolName);
|
|
278
|
-
if (!description || descriptions.includes(description)) continue;
|
|
279
|
-
descriptions.push(description);
|
|
280
|
-
if (descriptions.length >= 2) break;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (descriptions.length === 0) return '';
|
|
284
|
-
if (descriptions.length === 1) return `I ${descriptions[0]}`;
|
|
285
|
-
return `I ${descriptions[0]} and ${descriptions[1]}`;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function hasFailureSignal(text) {
|
|
289
|
-
const normalized = normalizeOutgoingMessage(text);
|
|
290
|
-
if (!normalized) return false;
|
|
291
|
-
return /\b(error|failed|failure|traceback|exception|timed out|timeout|not found|no such file|permission denied|unable to|cannot|could not|module not found)\b/i.test(normalized);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function extractToolFailureMessage(item) {
|
|
295
|
-
const directError = normalizeOutgoingMessage(item?.error || '');
|
|
296
|
-
if (directError) return directError;
|
|
297
|
-
|
|
298
|
-
const summary = parseToolExecutionSummary(item);
|
|
299
|
-
if (!summary) return '';
|
|
300
|
-
|
|
301
|
-
const candidates = [
|
|
302
|
-
summary.message,
|
|
303
|
-
summary.note,
|
|
304
|
-
summary.stderr,
|
|
305
|
-
summary.stdout,
|
|
306
|
-
summary.content,
|
|
307
|
-
summary.excerpt,
|
|
308
|
-
summary.result,
|
|
309
|
-
summary.summary,
|
|
310
|
-
];
|
|
311
|
-
|
|
312
|
-
if (summary.status === 'error') {
|
|
313
|
-
for (const candidate of candidates) {
|
|
314
|
-
const normalized = normalizeOutgoingMessage(candidate || '');
|
|
315
|
-
if (normalized) return normalized;
|
|
316
|
-
}
|
|
317
|
-
if (summary.exitCode != null) {
|
|
318
|
-
return `The last shell command exited with code ${summary.exitCode}`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
for (const candidate of candidates) {
|
|
323
|
-
const normalized = normalizeOutgoingMessage(candidate || '');
|
|
324
|
-
if (hasFailureSignal(normalized)) return normalized;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return '';
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions = [] }) {
|
|
331
|
-
const workSummary = summarizeRecentWork(toolExecutions);
|
|
332
|
-
const blocker = [...toolExecutions].reverse()
|
|
333
|
-
.map((item) => extractToolFailureMessage(item))
|
|
334
|
-
.find(Boolean);
|
|
335
|
-
|
|
336
|
-
if (workSummary && blocker) {
|
|
337
|
-
return `${workSummary}, but I got blocked: ${blocker}. I do not have a confirmed finished result yet.`;
|
|
338
|
-
}
|
|
339
|
-
if (blocker) {
|
|
340
|
-
return `I got blocked while working on this: ${blocker}. I do not have a confirmed finished result yet.`;
|
|
341
|
-
}
|
|
342
|
-
if (workSummary && stepIndex > 0) {
|
|
343
|
-
return `${workSummary}, but I do not have a confirmed finished result yet.`;
|
|
344
|
-
}
|
|
345
|
-
if (failedStepCount > 0) {
|
|
346
|
-
return 'I ran into a tool problem while working on your request, so I do not have a confirmed finished result yet.';
|
|
347
|
-
}
|
|
348
|
-
if (stepIndex > 0) {
|
|
349
|
-
return 'I completed part of the work, but I do not have a confirmed finished result yet.';
|
|
350
|
-
}
|
|
351
|
-
return 'I could not produce a reliable final reply just now.';
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function buildMessagingFailureScenario({ err, failedStepCount, stepIndex, toolExecutions = [] }) {
|
|
355
|
-
const parts = [];
|
|
356
|
-
const runtimeError = normalizeOutgoingMessage(err?.message || '');
|
|
357
|
-
const workSummary = summarizeRecentWork(toolExecutions);
|
|
358
|
-
const blocker = [...toolExecutions].reverse()
|
|
359
|
-
.map((item) => extractToolFailureMessage(item))
|
|
360
|
-
.find(Boolean);
|
|
361
|
-
|
|
362
|
-
if (runtimeError) {
|
|
363
|
-
parts.push(`Runtime error: ${summarizeForLog(runtimeError, 260)}.`);
|
|
364
|
-
}
|
|
365
|
-
if (workSummary) {
|
|
366
|
-
parts.push(`Observed work before failure: ${workSummary}.`);
|
|
367
|
-
}
|
|
368
|
-
if (blocker) {
|
|
369
|
-
parts.push(`Most specific blocker from run evidence: ${summarizeForLog(blocker, 260)}.`);
|
|
370
|
-
}
|
|
371
|
-
if (stepIndex > 0) {
|
|
372
|
-
parts.push(`Completed steps before failure: ${stepIndex}.`);
|
|
373
|
-
}
|
|
374
|
-
if (failedStepCount > 0) {
|
|
375
|
-
parts.push(`Failed tool steps: ${failedStepCount}.`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return parts.join(' ');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function buildDeterministicMessagingErrorReply({ err, failedStepCount, stepIndex, toolExecutions = [] }) {
|
|
382
|
-
const message = normalizeOutgoingMessage(err?.message || '');
|
|
383
|
-
if (/no ai providers? are currently available/i.test(message)) {
|
|
384
|
-
return 'I cannot continue right now because no AI provider is available for this account. Please check the provider settings.';
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (/(timeout|timed out)/i.test(message)) {
|
|
388
|
-
return 'I hit a timeout while processing your request and could not finish it reliably.';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const blocker = [...toolExecutions].reverse()
|
|
392
|
-
.map((item) => extractToolFailureMessage(item))
|
|
393
|
-
.find(Boolean);
|
|
394
|
-
if (blocker) {
|
|
395
|
-
return `I got blocked while checking this: ${blocker}.`;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (message) {
|
|
399
|
-
return `I got blocked while working on this: ${message}.`;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function buildModelFailureLoopPrompt({ failedModel, nextModel, errorMessage }) {
|
|
406
|
-
return [
|
|
407
|
-
`The previous model call on "${failedModel}" failed with: ${summarizeForLog(errorMessage, 220)}.`,
|
|
408
|
-
`Continue on "${nextModel}" and recover autonomously.`,
|
|
409
|
-
'If a previous plan depended on that failed call, adjust your approach and proceed end-to-end.',
|
|
410
|
-
'Only ask the user for help if no safe path remains.'
|
|
411
|
-
].join(' ');
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function clampRunContext(text, maxChars) {
|
|
415
|
-
const value = normalizeOutgoingMessage(text);
|
|
416
|
-
if (!value) return '';
|
|
417
|
-
if (value.length <= maxChars) return value;
|
|
418
|
-
return `${value.slice(0, maxChars)}...`;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function shortenRunId(runId) {
|
|
422
|
-
const value = String(runId || '').trim();
|
|
423
|
-
if (!value) return 'unknown';
|
|
424
|
-
return value.length <= 8 ? value : value.slice(0, 8);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function summarizeForLog(value, maxChars = 220) {
|
|
428
|
-
if (value == null) return '';
|
|
429
|
-
|
|
430
|
-
let text = '';
|
|
431
|
-
if (typeof value === 'string') {
|
|
432
|
-
text = value;
|
|
433
|
-
} else {
|
|
434
|
-
try {
|
|
435
|
-
text = JSON.stringify(value);
|
|
436
|
-
} catch {
|
|
437
|
-
text = String(value);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
442
|
-
if (normalized.length <= maxChars) return normalized;
|
|
443
|
-
return `${normalized.slice(0, maxChars)}...`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function parseMaybeJson(value, fallback = null) {
|
|
447
|
-
if (!value) return fallback;
|
|
448
|
-
if (typeof value === 'object') return value;
|
|
449
|
-
try {
|
|
450
|
-
return JSON.parse(value);
|
|
451
|
-
} catch {
|
|
452
|
-
return fallback;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '') {
|
|
457
|
-
const name = String(toolName || '');
|
|
458
|
-
const evidenceRelevantPrefixes = ['browser_', 'android_'];
|
|
459
|
-
const evidenceRelevantExact = new Set([
|
|
460
|
-
'web_search',
|
|
461
|
-
'http_request',
|
|
462
|
-
'read_file',
|
|
463
|
-
'search_files',
|
|
464
|
-
'list_directory',
|
|
465
|
-
'session_search',
|
|
466
|
-
'memory_recall',
|
|
467
|
-
'analyze_image',
|
|
468
|
-
'read_health_data',
|
|
469
|
-
'recordings_list',
|
|
470
|
-
'recordings_get',
|
|
471
|
-
'recordings_search',
|
|
472
|
-
'list_tasks',
|
|
473
|
-
'wait_subagent',
|
|
474
|
-
]);
|
|
475
|
-
const stateChangingExact = new Set([
|
|
476
|
-
'execute_command',
|
|
477
|
-
'write_file',
|
|
478
|
-
'edit_file',
|
|
479
|
-
'send_interim_update',
|
|
480
|
-
'send_message',
|
|
481
|
-
'make_call',
|
|
482
|
-
'create_skill',
|
|
483
|
-
'update_skill',
|
|
484
|
-
'delete_skill',
|
|
485
|
-
'create_task',
|
|
486
|
-
'update_task',
|
|
487
|
-
'delete_task',
|
|
488
|
-
'create_ai_widget',
|
|
489
|
-
'update_ai_widget',
|
|
490
|
-
'delete_ai_widget',
|
|
491
|
-
'save_widget_snapshot',
|
|
492
|
-
'mcp_add_server',
|
|
493
|
-
'mcp_remove_server',
|
|
494
|
-
'spawn_subagent',
|
|
495
|
-
'cancel_subagent',
|
|
496
|
-
]);
|
|
497
|
-
|
|
498
|
-
const evidenceSource = name.startsWith('browser_')
|
|
499
|
-
? 'browser'
|
|
500
|
-
: name.startsWith('android_')
|
|
501
|
-
? 'android'
|
|
502
|
-
: name.startsWith('mcp_')
|
|
503
|
-
? 'mcp'
|
|
504
|
-
: name.startsWith('memory_') || name === 'session_search'
|
|
505
|
-
? 'memory'
|
|
506
|
-
: name === 'web_search'
|
|
507
|
-
? 'search'
|
|
508
|
-
: name === 'http_request'
|
|
509
|
-
? 'http'
|
|
510
|
-
: ['read_file', 'search_files', 'list_directory', 'write_file', 'edit_file'].includes(name)
|
|
511
|
-
? 'files'
|
|
512
|
-
: name === 'execute_command'
|
|
513
|
-
? 'command'
|
|
514
|
-
: name.includes('skill')
|
|
515
|
-
? 'skills'
|
|
516
|
-
: (name === 'create_task' || name === 'update_task' || name === 'delete_task' || name === 'list_tasks' || name.includes('widget'))
|
|
517
|
-
? 'tasks'
|
|
518
|
-
: name === 'send_message' || name === 'make_call'
|
|
519
|
-
? 'messaging'
|
|
520
|
-
: name.startsWith('recordings_') || name === 'read_health_data'
|
|
521
|
-
? 'data'
|
|
522
|
-
: name === 'analyze_image'
|
|
523
|
-
? 'vision'
|
|
524
|
-
: name.includes('subagent')
|
|
525
|
-
? 'subagent'
|
|
526
|
-
: 'tool';
|
|
527
|
-
|
|
528
|
-
const evidenceRelevant = evidenceRelevantExact.has(name)
|
|
529
|
-
|| evidenceRelevantPrefixes.some((prefix) => name.startsWith(prefix));
|
|
530
|
-
const stateChanged = stateChangingExact.has(name)
|
|
531
|
-
|| name.startsWith('android_')
|
|
532
|
-
|| ['browser_click', 'browser_type', 'browser_evaluate'].includes(name);
|
|
533
|
-
|
|
534
|
-
let normalizedError = String(errorMessage || result?.error || '').trim();
|
|
535
|
-
if (!normalizedError && name === 'execute_command' && result && typeof result === 'object') {
|
|
536
|
-
if (result.timedOut) {
|
|
537
|
-
normalizedError = `Command timed out after ${result.durationMs || 'unknown'} ms`;
|
|
538
|
-
} else if (result.killed || result.signal) {
|
|
539
|
-
normalizedError = 'Command was killed before it finished';
|
|
540
|
-
} else if (typeof result.exitCode === 'number' && result.exitCode !== 0) {
|
|
541
|
-
normalizedError = summarizeForLog(result.stderr || result.stdout || `Command exited with code ${result.exitCode}`, 220);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (!normalizedError && result && typeof result === 'object') {
|
|
546
|
-
const nestedResult = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
|
|
547
|
-
? result.result
|
|
548
|
-
: null;
|
|
549
|
-
const detail = normalizeOutgoingMessage(
|
|
550
|
-
result.reason
|
|
551
|
-
|| result.message
|
|
552
|
-
|| nestedResult?.reason
|
|
553
|
-
|| nestedResult?.message
|
|
554
|
-
|| ''
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
if (result.skipped === true || nestedResult?.skipped === true) {
|
|
558
|
-
normalizedError = detail || 'Tool reported skipped outcome.';
|
|
559
|
-
} else if (result.success === false || nestedResult?.success === false) {
|
|
560
|
-
normalizedError = detail || 'Tool reported success=false.';
|
|
561
|
-
} else if (result.sent === false || nestedResult?.sent === false) {
|
|
562
|
-
normalizedError = detail || 'Tool reported sent=false.';
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
toolName: name,
|
|
568
|
-
ok: !normalizedError,
|
|
569
|
-
error: normalizedError,
|
|
570
|
-
evidenceSource,
|
|
571
|
-
evidenceRelevant,
|
|
572
|
-
stateChanged,
|
|
573
|
-
dependsOnOutput: true,
|
|
574
|
-
summary: compactToolResult(name, toolArgs, result || { error: errorMessage || 'Tool failed' }, {
|
|
575
|
-
softLimit: 500,
|
|
576
|
-
hardLimit: 900,
|
|
577
|
-
}),
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function summarizeToolExecutions(toolExecutions = [], maxItems = 10) {
|
|
582
|
-
return toolExecutions.slice(-maxItems).map((item, index) => {
|
|
583
|
-
const status = item.ok ? 'ok' : `error=${item.error}`;
|
|
584
|
-
return `${index + 1}. ${item.toolName} [${item.evidenceSource}] ${status} :: ${clampRunContext(item.summary || '', 220)}`;
|
|
585
|
-
}).join('\n');
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function summarizeAvailableTools(tools = [], { exclude = [] } = {}) {
|
|
589
|
-
const excluded = new Set((Array.isArray(exclude) ? exclude : [exclude]).filter(Boolean));
|
|
590
|
-
return tools
|
|
591
|
-
.map((tool) => String(tool?.name || '').trim())
|
|
592
|
-
.filter((name) => name && !excluded.has(name))
|
|
593
|
-
.slice(0, 24)
|
|
594
|
-
.join(', ');
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function inferToolFailureMessage(toolName, result) {
|
|
598
|
-
const explicitError = normalizeOutgoingMessage(result?.error || '');
|
|
599
|
-
if (explicitError) return explicitError;
|
|
600
|
-
|
|
601
|
-
if (!result || typeof result !== 'object') return '';
|
|
602
|
-
|
|
603
|
-
if (toolName === 'execute_command') {
|
|
604
|
-
if (result.timedOut) {
|
|
605
|
-
return `Command timed out after ${result.durationMs || 'unknown'} ms`;
|
|
606
|
-
}
|
|
607
|
-
if (result.killed || result.signal) {
|
|
608
|
-
return 'Command was killed before it finished';
|
|
609
|
-
}
|
|
610
|
-
if (typeof result.exitCode === 'number' && result.exitCode !== 0) {
|
|
611
|
-
return summarizeForLog(result.stderr || result.stdout || `Command exited with code ${result.exitCode}`, 220);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (toolName === 'http_request' && typeof result.status === 'number' && result.status >= 400) {
|
|
616
|
-
const bodySnippet = normalizeOutgoingMessage(result.body || '');
|
|
617
|
-
return summarizeForLog(
|
|
618
|
-
bodySnippet
|
|
619
|
-
? `HTTP request returned status ${result.status}: ${bodySnippet}`
|
|
620
|
-
: `HTTP request returned status ${result.status}`,
|
|
621
|
-
240
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return '';
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [], userMessage, visibleMessageSent = false }) {
|
|
629
|
-
const lastFailure = [...toolExecutions].reverse().find((item) => !item.ok);
|
|
630
|
-
const alternativeTools = summarizeAvailableTools(tools, { exclude: lastFailure?.toolName || null });
|
|
631
|
-
const parts = [
|
|
632
|
-
'This is an internal recovery retry for the same user task. Continue the task instead of stopping.',
|
|
633
|
-
userMessage ? `Original task: ${clampRunContext(userMessage, 260)}` : '',
|
|
634
|
-
lastFailure?.toolName ? `Previous attempt failed on tool: ${lastFailure.toolName}.` : '',
|
|
635
|
-
lastFailure?.error ? `Concrete failure: ${summarizeForLog(lastFailure.error, 260)}.` : '',
|
|
636
|
-
err?.message ? `Run-level error after that failure: ${summarizeForLog(err.message, 220)}.` : '',
|
|
637
|
-
'Do not send a blocker message just because one tool path failed.',
|
|
638
|
-
'Use a different safe approach if available: alternate tool, different query, browser path, HTTP fetch, file/code inspection, or command verification.',
|
|
639
|
-
visibleMessageSent ? 'A user-facing message was already sent in a previous internal attempt. Continue silently unless you have a materially new finished result or a real external blocker.' : '',
|
|
640
|
-
alternativeTools ? `Other available tools in this run: ${alternativeTools}.` : '',
|
|
641
|
-
'Only stop if the remaining problem truly requires an external dependency or user action outside this run.'
|
|
642
|
-
];
|
|
643
|
-
return parts.filter(Boolean).join(' ');
|
|
644
|
-
}
|
|
645
|
-
|
|
646
286
|
class AgentEngine {
|
|
647
287
|
constructor(io, services = {}) {
|
|
648
288
|
this.io = io;
|
|
649
|
-
this.maxIterations = 12;
|
|
650
289
|
this.activeRuns = new Map();
|
|
651
290
|
this.subagents = new Map();
|
|
652
291
|
this.app = services.app || null;
|
|
@@ -878,17 +517,20 @@ class AgentEngine {
|
|
|
878
517
|
fallback = {},
|
|
879
518
|
reasoningEffort,
|
|
880
519
|
}) {
|
|
881
|
-
const response = await
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
520
|
+
const response = await withProviderRetry(
|
|
521
|
+
() => provider.chat(
|
|
522
|
+
sanitizeConversationMessages([
|
|
523
|
+
...messages,
|
|
524
|
+
{ role: 'system', content: prompt },
|
|
525
|
+
]),
|
|
526
|
+
[],
|
|
527
|
+
{
|
|
528
|
+
model,
|
|
529
|
+
maxTokens,
|
|
530
|
+
reasoningEffort: reasoningEffort || this.getReasoningEffort(providerName, {}),
|
|
531
|
+
}
|
|
532
|
+
),
|
|
533
|
+
{ label: `Engine ${model} (structured)` }
|
|
892
534
|
);
|
|
893
535
|
|
|
894
536
|
const parsed = parseJsonObject(response.content || '');
|
|
@@ -914,44 +556,80 @@ class AgentEngine {
|
|
|
914
556
|
model,
|
|
915
557
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
916
558
|
};
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
559
|
+
|
|
560
|
+
const attemptModelCall = async () => {
|
|
561
|
+
let response = null;
|
|
562
|
+
let streamContent = '';
|
|
563
|
+
|
|
564
|
+
if (options.stream !== false) {
|
|
565
|
+
let emittedContent = false;
|
|
566
|
+
const stream = provider.stream(requestMessages, tools, callOptions);
|
|
567
|
+
try {
|
|
568
|
+
for await (const chunk of stream) {
|
|
569
|
+
if (chunk.type === 'content') {
|
|
570
|
+
emittedContent = true;
|
|
571
|
+
streamContent += chunk.content;
|
|
572
|
+
this.emit(options.userId, 'run:stream', {
|
|
573
|
+
runId,
|
|
574
|
+
content: sanitizeModelOutput(streamContent, { model }),
|
|
575
|
+
iteration,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (chunk.type === 'done') {
|
|
579
|
+
response = chunk;
|
|
580
|
+
}
|
|
581
|
+
if (chunk.type === 'tool_calls') {
|
|
582
|
+
response = {
|
|
583
|
+
content: chunk.content || streamContent,
|
|
584
|
+
toolCalls: chunk.toolCalls,
|
|
585
|
+
providerContentBlocks: chunk.providerContentBlocks || null,
|
|
586
|
+
finishReason: 'tool_calls',
|
|
587
|
+
usage: chunk.usage || null,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
// Once tokens have streamed to the client a retry would duplicate
|
|
593
|
+
// output, so only the pre-stream window is safe to replay.
|
|
594
|
+
if (emittedContent) err.__providerRetryUnsafe = true;
|
|
595
|
+
throw err;
|
|
942
596
|
}
|
|
597
|
+
} else {
|
|
598
|
+
response = await provider.chat(requestMessages, tools, callOptions);
|
|
943
599
|
}
|
|
944
|
-
|
|
945
|
-
|
|
600
|
+
|
|
601
|
+
return { response, streamContent };
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const { response, streamContent } = await withProviderRetry(attemptModelCall, {
|
|
605
|
+
...(options.retry || {}),
|
|
606
|
+
label: `Engine ${model}`,
|
|
607
|
+
isRetryable: (err) => !err?.__providerRetryUnsafe && isTransientError(err),
|
|
608
|
+
onRetry: ({ attempt, delayMs }) => {
|
|
609
|
+
this.emit(options.userId, 'run:interim', {
|
|
610
|
+
runId,
|
|
611
|
+
message: `Model service busy; retrying (attempt ${attempt}) in ${Math.max(1, Math.round(delayMs / 1000))}s.`,
|
|
612
|
+
phase: 'recovering',
|
|
613
|
+
});
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const resolvedResponse = response || {
|
|
618
|
+
content: streamContent,
|
|
619
|
+
toolCalls: [],
|
|
620
|
+
finishReason: 'stop',
|
|
621
|
+
usage: null,
|
|
622
|
+
};
|
|
623
|
+
const hasContent = Boolean(String(resolvedResponse.content || streamContent || '').trim());
|
|
624
|
+
const hasToolCalls = Array.isArray(resolvedResponse.toolCalls) && resolvedResponse.toolCalls.length > 0;
|
|
625
|
+
if (!hasContent && !hasToolCalls) {
|
|
626
|
+
const error = new Error(`Model ${model} returned an empty response.`);
|
|
627
|
+
error.code = 'MODEL_EMPTY_RESPONSE';
|
|
628
|
+
throw error;
|
|
946
629
|
}
|
|
947
630
|
|
|
948
631
|
return {
|
|
949
|
-
response:
|
|
950
|
-
content: streamContent,
|
|
951
|
-
toolCalls: [],
|
|
952
|
-
finishReason: 'stop',
|
|
953
|
-
usage: null,
|
|
954
|
-
},
|
|
632
|
+
response: resolvedResponse,
|
|
955
633
|
responseModel: model,
|
|
956
634
|
streamContent,
|
|
957
635
|
};
|
|
@@ -1064,12 +742,14 @@ class AgentEngine {
|
|
|
1064
742
|
'- A progress update is not complete.',
|
|
1065
743
|
'- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
|
|
1066
744
|
'- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
|
|
745
|
+
'- If completion_confidence_required is high and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.',
|
|
1067
746
|
triggerSource === 'messaging' && messagingSent
|
|
1068
747
|
? '- A reply was already delivered to the user via send_message. Use "complete" unless there is concrete remaining work (e.g., a tool call you still need to make) before the task is truly done. Do not send follow-up elaborations or re-introductions.'
|
|
1069
748
|
: triggerSource === 'messaging'
|
|
1070
749
|
? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked. If you already asked for missing user input, choose "blocked" and wait.'
|
|
1071
750
|
: '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
|
|
1072
751
|
analysis?.goal ? `Goal: ${analysis.goal}` : '',
|
|
752
|
+
`Autonomy contract: complexity=${analysis?.complexity || 'standard'}; autonomy_level=${analysis?.autonomy_level || 'normal'}; progress_update_policy=${analysis?.progress_update_policy || 'optional'}; parallel_work=${analysis?.parallel_work === true}; completion_confidence_required=${analysis?.completion_confidence_required || 'medium'}.`,
|
|
1073
753
|
successCriteria.length > 0 ? `Success criteria:\n${successCriteria.map((item, index) => `${index + 1}. ${item}`).join('\n')}` : '',
|
|
1074
754
|
`Current iteration: ${iteration} of ${maxIterations}.`,
|
|
1075
755
|
`Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
|
|
@@ -1101,6 +781,7 @@ class AgentEngine {
|
|
|
1101
781
|
model,
|
|
1102
782
|
messages,
|
|
1103
783
|
analysis,
|
|
784
|
+
tools,
|
|
1104
785
|
toolExecutions,
|
|
1105
786
|
finalReply,
|
|
1106
787
|
options,
|
|
@@ -1117,6 +798,7 @@ class AgentEngine {
|
|
|
1117
798
|
messages,
|
|
1118
799
|
prompt: buildVerifierPrompt({
|
|
1119
800
|
analysis,
|
|
801
|
+
tools,
|
|
1120
802
|
toolExecutionSummary: summarizeToolExecutions(toolExecutions),
|
|
1121
803
|
evidenceSources,
|
|
1122
804
|
finalReply,
|
|
@@ -1424,7 +1106,9 @@ class AgentEngine {
|
|
|
1424
1106
|
}
|
|
1425
1107
|
|
|
1426
1108
|
getMessagingRetryLimit(maxIterations) {
|
|
1427
|
-
|
|
1109
|
+
// Cap at 3: more than 3 autonomous messaging retries indicates a structural
|
|
1110
|
+
// problem (model unavailable, bad config) that more retries won't solve.
|
|
1111
|
+
return Math.min(3, Math.max(1, maxIterations));
|
|
1428
1112
|
}
|
|
1429
1113
|
|
|
1430
1114
|
buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
|
|
@@ -1565,7 +1249,7 @@ class AgentEngine {
|
|
|
1565
1249
|
let model = selectedProvider.model;
|
|
1566
1250
|
let providerName = selectedProvider.providerName;
|
|
1567
1251
|
const switchToFallbackModel = async (failedModel, error, phase) => {
|
|
1568
|
-
const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id);
|
|
1252
|
+
const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
|
|
1569
1253
|
if (!fallbackModelId || fallbackModelId === failedModel) return false;
|
|
1570
1254
|
console.log(`[Engine] ${phase} failed on ${failedModel}; attempting fallback to: ${fallbackModelId}`);
|
|
1571
1255
|
this.emit(userId, 'run:interim', {
|
|
@@ -1597,8 +1281,19 @@ class AgentEngine {
|
|
|
1597
1281
|
};
|
|
1598
1282
|
|
|
1599
1283
|
const runTitle = generateTitle(userMessage);
|
|
1600
|
-
|
|
1601
|
-
|
|
1284
|
+
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
1285
|
+
db.prepare(`INSERT OR REPLACE INTO agent_runs(
|
|
1286
|
+
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
1287
|
+
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
|
|
1288
|
+
runId,
|
|
1289
|
+
userId,
|
|
1290
|
+
agentId,
|
|
1291
|
+
runTitle,
|
|
1292
|
+
triggerType,
|
|
1293
|
+
triggerSource,
|
|
1294
|
+
model,
|
|
1295
|
+
Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
|
|
1296
|
+
);
|
|
1602
1297
|
|
|
1603
1298
|
const retryMessagingState = options.messagingRetryState || {};
|
|
1604
1299
|
const carriedVisibleMessage = String(retryMessagingState.lastVisibleMessage || '').trim();
|
|
@@ -1610,6 +1305,7 @@ class AgentEngine {
|
|
|
1610
1305
|
status: 'running',
|
|
1611
1306
|
aborted: false,
|
|
1612
1307
|
messagingSent: false,
|
|
1308
|
+
noResponse: false,
|
|
1613
1309
|
explicitMessageSent: carriedExplicitMessageSent,
|
|
1614
1310
|
lastSentMessage: carriedExplicitMessageSent ? carriedVisibleMessage : '',
|
|
1615
1311
|
sentMessages: [],
|
|
@@ -1658,6 +1354,23 @@ class AgentEngine {
|
|
|
1658
1354
|
const integrationManager = app?.locals?.integrationManager || null;
|
|
1659
1355
|
const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
|
|
1660
1356
|
const tools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
|
|
1357
|
+
const toolNames = tools.map((tool) => tool.name).filter(Boolean);
|
|
1358
|
+
const coreToolStatus = {
|
|
1359
|
+
send_message: toolNames.includes('send_message'),
|
|
1360
|
+
create_task: toolNames.includes('create_task'),
|
|
1361
|
+
list_tasks: toolNames.includes('list_tasks'),
|
|
1362
|
+
update_task: toolNames.includes('update_task'),
|
|
1363
|
+
delete_task: toolNames.includes('delete_task'),
|
|
1364
|
+
};
|
|
1365
|
+
this.recordRunEvent(userId, runId, 'tool_inventory', {
|
|
1366
|
+
total: toolNames.length,
|
|
1367
|
+
builtInTotal: builtInTools.length,
|
|
1368
|
+
mcpTotal: mcpTools.length,
|
|
1369
|
+
core: coreToolStatus,
|
|
1370
|
+
}, { agentId });
|
|
1371
|
+
console.info(
|
|
1372
|
+
`[Run ${shortenRunId(runId)}] tools total=${toolNames.length} builtIns=${builtInTools.length} mcp=${mcpTools.length} core=${JSON.stringify(coreToolStatus)}`
|
|
1373
|
+
);
|
|
1661
1374
|
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine: this });
|
|
1662
1375
|
const capabilitySummary = summarizeCapabilityHealth(capabilityHealth);
|
|
1663
1376
|
const integrationSummary = integrationManager?.summarizeConnectedProviders?.(userId, agentId) || '';
|
|
@@ -1780,11 +1493,53 @@ class AgentEngine {
|
|
|
1780
1493
|
|
|
1781
1494
|
}
|
|
1782
1495
|
|
|
1496
|
+
const activeDefaultModelSetting = triggerType === 'subagent'
|
|
1497
|
+
? aiSettings.default_subagent_model
|
|
1498
|
+
: aiSettings.default_chat_model;
|
|
1499
|
+
if (!_modelOverride && activeDefaultModelSetting === 'auto' && aiSettings.smarter_model_selector !== false) {
|
|
1500
|
+
const requestedPurpose = analysis?.mode === 'plan_execute' || analysis?.complexity === 'complex' || analysis?.autonomy_level === 'high'
|
|
1501
|
+
? 'planning'
|
|
1502
|
+
: triggerType === 'subagent'
|
|
1503
|
+
? 'fast'
|
|
1504
|
+
: '';
|
|
1505
|
+
if (requestedPurpose) {
|
|
1506
|
+
const selectedAfterAnalysis = await getProviderForUser(
|
|
1507
|
+
userId,
|
|
1508
|
+
userMessage,
|
|
1509
|
+
triggerType === 'subagent',
|
|
1510
|
+
null,
|
|
1511
|
+
{
|
|
1512
|
+
...providerStatusConfig,
|
|
1513
|
+
selectionHint: {
|
|
1514
|
+
purpose: requestedPurpose,
|
|
1515
|
+
complexity: analysis?.complexity,
|
|
1516
|
+
autonomyLevel: analysis?.autonomy_level,
|
|
1517
|
+
},
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
if (selectedAfterAnalysis.model !== model) {
|
|
1521
|
+
provider = selectedAfterAnalysis.provider;
|
|
1522
|
+
model = selectedAfterAnalysis.model;
|
|
1523
|
+
providerName = selectedAfterAnalysis.providerName;
|
|
1524
|
+
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
1525
|
+
.run(model, runId);
|
|
1526
|
+
this.emit(userId, 'run:interim', {
|
|
1527
|
+
runId,
|
|
1528
|
+
message: `Switched to ${model} for this run after task analysis.`,
|
|
1529
|
+
phase: 'model_selection'
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1783
1535
|
// Rebuild loop policy with the resolved analysis mode. Runs in both the
|
|
1784
1536
|
// normal path and the skipTaskAnalysis path so that forceMode='plan_execute'
|
|
1785
1537
|
// (or any mode set by buildSkipTaskAnalysisResult) raises the iteration
|
|
1786
1538
|
// ceiling correctly.
|
|
1787
|
-
loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute',
|
|
1539
|
+
loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute', {
|
|
1540
|
+
...options,
|
|
1541
|
+
autonomyPolicy: buildAutonomyPolicyFromAnalysis(analysis),
|
|
1542
|
+
});
|
|
1788
1543
|
maxIterations = loopPolicy.maxIterations;
|
|
1789
1544
|
|
|
1790
1545
|
if (options.skipDeliverableWorkflow !== true) {
|
|
@@ -1960,7 +1715,7 @@ class AgentEngine {
|
|
|
1960
1715
|
} catch (err) {
|
|
1961
1716
|
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
1962
1717
|
const fallbackModelId = retryForFallback
|
|
1963
|
-
? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id)
|
|
1718
|
+
? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id, err)
|
|
1964
1719
|
: null;
|
|
1965
1720
|
if (fallbackModelId) {
|
|
1966
1721
|
const failedModel = model;
|
|
@@ -2101,7 +1856,17 @@ class AgentEngine {
|
|
|
2101
1856
|
break;
|
|
2102
1857
|
}
|
|
2103
1858
|
if (iteration < maxIterations) {
|
|
2104
|
-
const
|
|
1859
|
+
const proactiveRunNeedsDecision = (
|
|
1860
|
+
(triggerSource === 'schedule' || triggerSource === 'tasks')
|
|
1861
|
+
&& this.activeRuns.get(runId)?.noResponse !== true
|
|
1862
|
+
&& options.deliveryState?.noResponse !== true
|
|
1863
|
+
);
|
|
1864
|
+
const fallbackStatus = (
|
|
1865
|
+
proactiveRunNeedsDecision
|
|
1866
|
+
|| toolExecutions.length > 0
|
|
1867
|
+
|| failedStepCount > 0
|
|
1868
|
+
|| messagingSent
|
|
1869
|
+
) ? 'continue' : 'complete';
|
|
2105
1870
|
const loopState = await runWithModelFallback('loop decision', () => this.decideLoopState({
|
|
2106
1871
|
provider,
|
|
2107
1872
|
providerName,
|
|
@@ -2155,15 +1920,46 @@ class AgentEngine {
|
|
|
2155
1920
|
// regular tool execution, it is a loop-exit signal.
|
|
2156
1921
|
if (toolName === 'task_complete') {
|
|
2157
1922
|
const finalMessage = String(toolArgs.message || '').trim();
|
|
1923
|
+
const confidence = normalizeCompletionConfidence(toolArgs.confidence || 'medium');
|
|
1924
|
+
const completionDecision = shouldAcceptTaskComplete({
|
|
1925
|
+
confidence,
|
|
1926
|
+
requiredConfidence: analysis?.completion_confidence_required || 'medium',
|
|
1927
|
+
iteration,
|
|
1928
|
+
maxIterations,
|
|
1929
|
+
});
|
|
2158
1930
|
this.recordRunEvent(userId, runId, 'task_complete_signaled', {
|
|
2159
|
-
confidence
|
|
1931
|
+
confidence,
|
|
1932
|
+
requiredConfidence: analysis?.completion_confidence_required || 'medium',
|
|
1933
|
+
accepted: completionDecision.accept,
|
|
2160
1934
|
iteration,
|
|
2161
1935
|
messageLength: finalMessage.length,
|
|
2162
1936
|
}, { agentId });
|
|
2163
1937
|
console.info(
|
|
2164
|
-
`[Run ${shortenRunId(runId)}] task_complete signaled at iteration=${iteration} confidence=${
|
|
1938
|
+
`[Run ${shortenRunId(runId)}] task_complete signaled at iteration=${iteration} confidence=${confidence} accepted=${completionDecision.accept}`
|
|
2165
1939
|
);
|
|
2166
|
-
|
|
1940
|
+
if (!completionDecision.accept) {
|
|
1941
|
+
messages.push({
|
|
1942
|
+
role: 'tool',
|
|
1943
|
+
name: toolName,
|
|
1944
|
+
tool_call_id: toolCall.id,
|
|
1945
|
+
content: JSON.stringify({
|
|
1946
|
+
status: 'continue',
|
|
1947
|
+
reason: completionDecision.reason,
|
|
1948
|
+
required_confidence: analysis?.completion_confidence_required || 'medium',
|
|
1949
|
+
}),
|
|
1950
|
+
});
|
|
1951
|
+
messages.push({
|
|
1952
|
+
role: 'system',
|
|
1953
|
+
content: `${completionDecision.reason} Do not ask the user to decide the next step unless external input is truly required.`
|
|
1954
|
+
});
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
if (completionDecision.reason) {
|
|
1958
|
+
messages.push({
|
|
1959
|
+
role: 'system',
|
|
1960
|
+
content: completionDecision.reason,
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
2167
1963
|
lastContent = finalMessage; // empty string is valid; downstream handles it
|
|
2168
1964
|
directAnswerEligible = true;
|
|
2169
1965
|
break; // exit the for-loop; the while condition will also exit
|
|
@@ -2467,6 +2263,18 @@ class AgentEngine {
|
|
|
2467
2263
|
&& !messagingSent
|
|
2468
2264
|
&& runMeta?.widgetSnapshotSaved !== true
|
|
2469
2265
|
) {
|
|
2266
|
+
const explicitNoResponse = (
|
|
2267
|
+
runMeta?.noResponse === true
|
|
2268
|
+
|| options.deliveryState?.noResponse === true
|
|
2269
|
+
);
|
|
2270
|
+
if (
|
|
2271
|
+
(triggerSource === 'schedule' || triggerSource === 'tasks')
|
|
2272
|
+
&& !explicitNoResponse
|
|
2273
|
+
) {
|
|
2274
|
+
throw new Error(
|
|
2275
|
+
'Background run ended without producing a result or an explicit no-response decision.',
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2470
2278
|
if (iteration >= maxIterations) {
|
|
2471
2279
|
throw new Error(`Iteration limit reached before explicit completion after ${maxIterations} iterations.`);
|
|
2472
2280
|
}
|
|
@@ -2501,6 +2309,7 @@ class AgentEngine {
|
|
|
2501
2309
|
model,
|
|
2502
2310
|
messages,
|
|
2503
2311
|
analysis,
|
|
2312
|
+
tools,
|
|
2504
2313
|
toolExecutions,
|
|
2505
2314
|
finalReply: finalResponseText,
|
|
2506
2315
|
options,
|
|
@@ -2709,11 +2518,16 @@ class AgentEngine {
|
|
|
2709
2518
|
|
|
2710
2519
|
const runMeta = this.activeRuns.get(runId);
|
|
2711
2520
|
const retryCount = Number(options.messagingAutonomousRetryCount || 0);
|
|
2521
|
+
// Rate-limit errors (429) must not trigger messaging retries: the model
|
|
2522
|
+
// won't be available in the milliseconds between retries, so spawning new
|
|
2523
|
+
// runs just compounds the rate-limit pressure with no benefit.
|
|
2524
|
+
const isRateLimitError = /429|rate.?limit|free-models-per/i.test(String(err?.message || ''));
|
|
2712
2525
|
const canRetryMessagingRun = (
|
|
2713
2526
|
triggerSource === 'messaging'
|
|
2714
2527
|
&& options.source
|
|
2715
2528
|
&& options.chatId
|
|
2716
2529
|
&& err?.disableAutonomousRetry !== true
|
|
2530
|
+
&& !isRateLimitError
|
|
2717
2531
|
&& retryCount < this.getMessagingRetryLimit(maxIterations)
|
|
2718
2532
|
);
|
|
2719
2533
|
|
|
@@ -3197,8 +3011,15 @@ class AgentEngine {
|
|
|
3197
3011
|
db.prepare("UPDATE agent_runs SET status = 'stopped', updated_at = datetime('now') WHERE id = ?").run(runId);
|
|
3198
3012
|
}
|
|
3199
3013
|
|
|
3200
|
-
abort(runId) {
|
|
3201
|
-
if (runId)
|
|
3014
|
+
abort(runId, { userId } = {}) {
|
|
3015
|
+
if (!runId) return false;
|
|
3016
|
+
if (userId != null) {
|
|
3017
|
+
// Ownership gate: never let one user abort another user's active run.
|
|
3018
|
+
const runMeta = this.activeRuns.get(runId);
|
|
3019
|
+
if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
|
|
3020
|
+
}
|
|
3021
|
+
this.stopRun(runId);
|
|
3022
|
+
return true;
|
|
3202
3023
|
}
|
|
3203
3024
|
|
|
3204
3025
|
abortAll(userId) {
|
|
@@ -3229,4 +3050,4 @@ class AgentEngine {
|
|
|
3229
3050
|
}
|
|
3230
3051
|
}
|
|
3231
3052
|
|
|
3232
|
-
module.exports = { AgentEngine, getProviderForUser };
|
|
3053
|
+
module.exports = { AgentEngine, buildInitialRunMetadata, getProviderForUser };
|