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
|
@@ -19,8 +19,10 @@ const {
|
|
|
19
19
|
const { summarizeCapabilityHealth } = require('../capabilityHealth');
|
|
20
20
|
const { shouldAcceptTaskComplete } = require('../completion');
|
|
21
21
|
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
22
|
+
const { getProviderForUser } = require('../provider_selector');
|
|
22
23
|
const { runConversation } = require('./conversation_loop');
|
|
23
24
|
const {
|
|
25
|
+
TERMINAL_STATUSES,
|
|
24
26
|
checkpointRun,
|
|
25
27
|
closeRun,
|
|
26
28
|
getRunControl,
|
|
@@ -30,6 +32,8 @@ const {
|
|
|
30
32
|
const {
|
|
31
33
|
buildChurnAssessmentPrompt,
|
|
32
34
|
buildCompletionDecisionPrompt,
|
|
35
|
+
enforceTerminalReplyDecision,
|
|
36
|
+
enforceChurnAssessment,
|
|
33
37
|
normalizeChurnAssessment,
|
|
34
38
|
normalizeCompletionDecision,
|
|
35
39
|
resolveRunGoalContext,
|
|
@@ -67,7 +71,7 @@ const {
|
|
|
67
71
|
const {
|
|
68
72
|
requestModelResponse: requestModelResponseImpl,
|
|
69
73
|
requestStructuredJson: requestStructuredJsonImpl,
|
|
70
|
-
|
|
74
|
+
runAbortableModelCall,
|
|
71
75
|
} = require('./model_io');
|
|
72
76
|
const {
|
|
73
77
|
publishInterimUpdate: publishInterimUpdateImpl,
|
|
@@ -83,6 +87,7 @@ const {
|
|
|
83
87
|
clampRunContext,
|
|
84
88
|
} = require('../messagingFallback');
|
|
85
89
|
const {
|
|
90
|
+
assessResearchAdequacy,
|
|
86
91
|
summarizeToolExecutions,
|
|
87
92
|
} = require('../toolEvidence');
|
|
88
93
|
const {
|
|
@@ -97,6 +102,13 @@ const {
|
|
|
97
102
|
normalizeRetrievalPlan,
|
|
98
103
|
shouldEnhanceRetrieval,
|
|
99
104
|
} = require('../../memory/retrieval_reasoning');
|
|
105
|
+
const {
|
|
106
|
+
createAbortError,
|
|
107
|
+
createLinkedAbortController,
|
|
108
|
+
isAbortError,
|
|
109
|
+
runWithAbortTimeout,
|
|
110
|
+
throwIfAborted,
|
|
111
|
+
} = require('../../../utils/abort');
|
|
100
112
|
|
|
101
113
|
function buildInitialRunMetadata(options = {}) {
|
|
102
114
|
const metadata = {};
|
|
@@ -106,6 +118,9 @@ function buildInitialRunMetadata(options = {}) {
|
|
|
106
118
|
if (options.widgetId != null && String(options.widgetId).trim()) {
|
|
107
119
|
metadata.widgetId = options.widgetId;
|
|
108
120
|
}
|
|
121
|
+
if (options.messagingInboundJobId != null && String(options.messagingInboundJobId).trim()) {
|
|
122
|
+
metadata.messagingInboundJobId = String(options.messagingInboundJobId).trim();
|
|
123
|
+
}
|
|
109
124
|
return metadata;
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -131,99 +146,6 @@ function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
|
|
|
131
146
|
};
|
|
132
147
|
}
|
|
133
148
|
|
|
134
|
-
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
|
|
135
|
-
const { getSupportedModels, createProviderInstance } = require('../models');
|
|
136
|
-
const agentId = providerConfig.agentId || null;
|
|
137
|
-
const aiSettings = getAiSettings(userId, agentId);
|
|
138
|
-
const models = await getSupportedModels(userId, agentId);
|
|
139
|
-
|
|
140
|
-
let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
|
|
141
|
-
const defaultChatModel = aiSettings.default_chat_model || 'auto';
|
|
142
|
-
const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
|
|
143
|
-
const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
|
|
144
|
-
|
|
145
|
-
const knownModelIds = new Set(models.map((m) => m.id));
|
|
146
|
-
const selectableModels = models.filter((m) => m.available !== false);
|
|
147
|
-
|
|
148
|
-
enabledIds = Array.isArray(enabledIds)
|
|
149
|
-
? enabledIds
|
|
150
|
-
.map((id) => String(id))
|
|
151
|
-
.filter((id) => knownModelIds.has(id))
|
|
152
|
-
: [];
|
|
153
|
-
|
|
154
|
-
let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
|
|
155
|
-
if (availableModels.length === 0) {
|
|
156
|
-
enabledIds = selectableModels.map((m) => m.id);
|
|
157
|
-
availableModels = [...selectableModels];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
|
|
161
|
-
|
|
162
|
-
if (!fallbackModel) {
|
|
163
|
-
throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let selectedModelDef = fallbackModel;
|
|
167
|
-
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
168
|
-
|
|
169
|
-
if (modelOverride && typeof modelOverride === 'string') {
|
|
170
|
-
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
171
|
-
if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
|
|
172
|
-
selectedModelDef = requested;
|
|
173
|
-
return {
|
|
174
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
175
|
-
model: selectedModelDef.id,
|
|
176
|
-
providerName: selectedModelDef.provider
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
182
|
-
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
183
|
-
} else {
|
|
184
|
-
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
185
|
-
? providerConfig.selectionHint
|
|
186
|
-
: {};
|
|
187
|
-
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
188
|
-
const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
|
|
189
|
-
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
190
|
-
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
|
|
191
|
-
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
192
|
-
? preferredPurpose
|
|
193
|
-
: '';
|
|
194
|
-
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
195
|
-
const chooseForPurpose = (purpose) => {
|
|
196
|
-
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
197
|
-
if (candidates.length === 0) return null;
|
|
198
|
-
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
199
|
-
return [...candidates].sort((left, right) => (
|
|
200
|
-
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
201
|
-
))[0];
|
|
202
|
-
}
|
|
203
|
-
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
204
|
-
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
|
|
205
|
-
}
|
|
206
|
-
return candidates[0];
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
if (smarterSelection && requestedPurpose) {
|
|
210
|
-
selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
211
|
-
} else if (smarterSelection && highAutonomy) {
|
|
212
|
-
selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
213
|
-
} else if (isSubagent) {
|
|
214
|
-
selectedModelDef = chooseForPurpose('fast') || fallbackModel;
|
|
215
|
-
} else {
|
|
216
|
-
selectedModelDef = chooseForPurpose('general') || fallbackModel;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
222
|
-
model: selectedModelDef.id,
|
|
223
|
-
providerName: selectedModelDef.provider
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
149
|
function estimateTokenValue(value) {
|
|
228
150
|
if (!value) return 0;
|
|
229
151
|
if (typeof value === 'string') return Math.ceil(value.length / 4);
|
|
@@ -234,6 +156,13 @@ class AgentEngine {
|
|
|
234
156
|
constructor(io, services = {}) {
|
|
235
157
|
this.io = io;
|
|
236
158
|
this.activeRuns = new Map();
|
|
159
|
+
this.activeRunPromises = new Set();
|
|
160
|
+
this.backgroundTasks = new Set();
|
|
161
|
+
this.backgroundTaskQueues = new Map();
|
|
162
|
+
this.subagentStartupTasks = new Set();
|
|
163
|
+
this.lifecycleAbortController = new AbortController();
|
|
164
|
+
this.shuttingDown = false;
|
|
165
|
+
this.shutdownPromise = null;
|
|
237
166
|
this.subagents = new Map();
|
|
238
167
|
this.app = services.app || null;
|
|
239
168
|
this.browserController = services.browserController || null;
|
|
@@ -249,6 +178,107 @@ class AgentEngine {
|
|
|
249
178
|
this.messagingDeliveryRetry = services.messagingDeliveryRetry || {};
|
|
250
179
|
}
|
|
251
180
|
|
|
181
|
+
trackBackgroundTask(task, options = {}) {
|
|
182
|
+
if (typeof task !== 'function') {
|
|
183
|
+
throw new TypeError('Background task must be a function.');
|
|
184
|
+
}
|
|
185
|
+
if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
|
|
186
|
+
return Promise.reject(createAbortError(
|
|
187
|
+
this.lifecycleAbortController.signal,
|
|
188
|
+
'Agent engine is shutting down.',
|
|
189
|
+
));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const key = String(options.key || '').trim();
|
|
193
|
+
const previous = key ? this.backgroundTaskQueues.get(key) : null;
|
|
194
|
+
if (previous && options.coalesce === true) return previous;
|
|
195
|
+
|
|
196
|
+
const linked = createLinkedAbortController([
|
|
197
|
+
this.lifecycleAbortController.signal,
|
|
198
|
+
options.signal,
|
|
199
|
+
]);
|
|
200
|
+
const tracked = Promise.resolve().then(async () => {
|
|
201
|
+
if (previous) {
|
|
202
|
+
try {
|
|
203
|
+
await previous;
|
|
204
|
+
} catch {
|
|
205
|
+
// A failed predecessor must not permanently poison this queue.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throwIfAborted(linked.signal, 'Background task aborted.');
|
|
209
|
+
return task(linked.signal);
|
|
210
|
+
}).finally(() => {
|
|
211
|
+
linked.cleanup();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.backgroundTasks.add(tracked);
|
|
215
|
+
if (key) this.backgroundTaskQueues.set(key, tracked);
|
|
216
|
+
const cleanup = () => {
|
|
217
|
+
this.backgroundTasks.delete(tracked);
|
|
218
|
+
if (key && this.backgroundTaskQueues.get(key) === tracked) {
|
|
219
|
+
this.backgroundTaskQueues.delete(key);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
tracked.then(cleanup, cleanup);
|
|
223
|
+
return tracked;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
shutdown(options = {}) {
|
|
227
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
228
|
+
|
|
229
|
+
const reason = String(
|
|
230
|
+
options.reason || 'Server shutting down while agent work was in progress.',
|
|
231
|
+
);
|
|
232
|
+
const timeoutMs = Math.max(100, Number(options.timeoutMs) || 10000);
|
|
233
|
+
this.shuttingDown = true;
|
|
234
|
+
this.lifecycleAbortController.abort(reason);
|
|
235
|
+
this.interruptAllActiveRuns(reason);
|
|
236
|
+
|
|
237
|
+
const childShutdowns = [];
|
|
238
|
+
const childPromises = [];
|
|
239
|
+
for (const record of this.subagents.values()) {
|
|
240
|
+
if (typeof record.engine?.shutdown === 'function') {
|
|
241
|
+
childShutdowns.push(record.engine.shutdown({ reason, timeoutMs }));
|
|
242
|
+
}
|
|
243
|
+
if (record.promise) childPromises.push(record.promise);
|
|
244
|
+
}
|
|
245
|
+
const pending = [
|
|
246
|
+
...this.activeRunPromises,
|
|
247
|
+
...this.backgroundTasks,
|
|
248
|
+
...this.subagentStartupTasks,
|
|
249
|
+
...childShutdowns,
|
|
250
|
+
...childPromises,
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
this.shutdownPromise = (async () => {
|
|
254
|
+
if (pending.length === 0) {
|
|
255
|
+
return { state: 'stopped', timedOut: false, pendingCount: 0 };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let timeout = null;
|
|
259
|
+
const timeoutResult = Symbol('agent-engine-shutdown-timeout');
|
|
260
|
+
const result = await Promise.race([
|
|
261
|
+
Promise.allSettled(pending),
|
|
262
|
+
new Promise((resolve) => {
|
|
263
|
+
timeout = setTimeout(() => resolve(timeoutResult), timeoutMs);
|
|
264
|
+
}),
|
|
265
|
+
]);
|
|
266
|
+
if (timeout) clearTimeout(timeout);
|
|
267
|
+
const timedOut = result === timeoutResult;
|
|
268
|
+
return {
|
|
269
|
+
state: timedOut ? 'timeout' : 'stopped',
|
|
270
|
+
timedOut,
|
|
271
|
+
pendingCount: timedOut
|
|
272
|
+
? this.activeRunPromises.size
|
|
273
|
+
+ this.backgroundTasks.size
|
|
274
|
+
+ this.subagentStartupTasks.size
|
|
275
|
+
+ Array.from(this.subagents.values()).filter((record) => !record.settled).length
|
|
276
|
+
: 0,
|
|
277
|
+
};
|
|
278
|
+
})();
|
|
279
|
+
return this.shutdownPromise;
|
|
280
|
+
}
|
|
281
|
+
|
|
252
282
|
async buildSystemPrompt(userId, context = {}) {
|
|
253
283
|
const { buildSystemPromptSections } = require('../systemPrompt');
|
|
254
284
|
const { MemoryManager } = require('../../memory/manager');
|
|
@@ -287,18 +317,34 @@ class AgentEngine {
|
|
|
287
317
|
options = {},
|
|
288
318
|
returnDetails = false,
|
|
289
319
|
}) {
|
|
290
|
-
const
|
|
320
|
+
const signal = options.signal || this.getRunMeta(runId)?.abortController?.signal || null;
|
|
321
|
+
const initial = await memoryManager.recallMemory(userId, query, 12, {
|
|
322
|
+
agentId,
|
|
323
|
+
signal,
|
|
324
|
+
});
|
|
291
325
|
|
|
292
326
|
const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
|
|
293
327
|
if (pendingChunks.length) {
|
|
294
|
-
this.extractPendingChunks(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
328
|
+
this.trackBackgroundTask((backgroundSignal) => this.extractPendingChunks(
|
|
329
|
+
pendingChunks,
|
|
330
|
+
{
|
|
331
|
+
userId,
|
|
332
|
+
agentId,
|
|
333
|
+
provider,
|
|
334
|
+
providerName,
|
|
335
|
+
model,
|
|
336
|
+
memoryManager,
|
|
337
|
+
signal: backgroundSignal,
|
|
338
|
+
},
|
|
339
|
+
), {
|
|
340
|
+
key: `memory-extraction:${userId}:${agentId || 'main'}`,
|
|
341
|
+
coalesce: true,
|
|
342
|
+
signal,
|
|
343
|
+
}).catch((err) => {
|
|
344
|
+
if (!isAbortError(err, signal) && !this.shuttingDown) {
|
|
345
|
+
console.warn('[Memory] Background chunk extraction failed:', err.message);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
302
348
|
}
|
|
303
349
|
|
|
304
350
|
const decision = shouldEnhanceRetrieval(initial);
|
|
@@ -335,7 +381,7 @@ class AgentEngine {
|
|
|
335
381
|
normalize: (raw) => normalizeRetrievalPlan(raw, query),
|
|
336
382
|
fallback: normalizeRetrievalPlan({}, query),
|
|
337
383
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
338
|
-
telemetry: { runId, stepId, userId, agentId },
|
|
384
|
+
telemetry: { runId, stepId, userId, agentId, signal },
|
|
339
385
|
phase: 'memory_retrieval_plan',
|
|
340
386
|
});
|
|
341
387
|
plan = planned.value;
|
|
@@ -346,6 +392,7 @@ class AgentEngine {
|
|
|
346
392
|
agentId,
|
|
347
393
|
validAt: plan.validAt,
|
|
348
394
|
includeHistory: plan.temporalMode === 'historical',
|
|
395
|
+
signal,
|
|
349
396
|
}));
|
|
350
397
|
}
|
|
351
398
|
merged = mergeRetrievalResults(resultSets, 30);
|
|
@@ -360,7 +407,7 @@ class AgentEngine {
|
|
|
360
407
|
normalize: (raw) => normalizeRerankResult(raw, merged),
|
|
361
408
|
fallback: merged,
|
|
362
409
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
363
|
-
telemetry: { runId, stepId, userId, agentId },
|
|
410
|
+
telemetry: { runId, stepId, userId, agentId, signal },
|
|
364
411
|
phase: 'memory_retrieval_rerank',
|
|
365
412
|
});
|
|
366
413
|
reranked = rerankResponse.value;
|
|
@@ -368,6 +415,7 @@ class AgentEngine {
|
|
|
368
415
|
reranked = merged;
|
|
369
416
|
}
|
|
370
417
|
} catch (error) {
|
|
418
|
+
if (isAbortError(error, signal)) throw error;
|
|
371
419
|
console.warn('[Memory] Retrieval enhancement failed:', error.message);
|
|
372
420
|
plan = null;
|
|
373
421
|
merged = initial;
|
|
@@ -406,10 +454,8 @@ class AgentEngine {
|
|
|
406
454
|
providerName,
|
|
407
455
|
model,
|
|
408
456
|
memoryManager,
|
|
457
|
+
signal = null,
|
|
409
458
|
}) {
|
|
410
|
-
const ids = chunks.map((c) => c.id);
|
|
411
|
-
memoryManager.markChunksExtracted?.(ids, { success: true });
|
|
412
|
-
|
|
413
459
|
const consolidationSchema = JSON.stringify({
|
|
414
460
|
memory_candidates: [{
|
|
415
461
|
memory: 'Concise standalone fact.',
|
|
@@ -430,6 +476,7 @@ class AgentEngine {
|
|
|
430
476
|
|
|
431
477
|
for (const chunk of chunks) {
|
|
432
478
|
try {
|
|
479
|
+
throwIfAborted(signal, 'Memory extraction aborted.');
|
|
433
480
|
const result = await this.requestStructuredJson({
|
|
434
481
|
provider,
|
|
435
482
|
providerName,
|
|
@@ -446,6 +493,7 @@ class AgentEngine {
|
|
|
446
493
|
maxTokens: 800,
|
|
447
494
|
normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
|
|
448
495
|
fallback: [],
|
|
496
|
+
telemetry: { userId, agentId, signal },
|
|
449
497
|
phase: 'document_extraction',
|
|
450
498
|
});
|
|
451
499
|
|
|
@@ -457,10 +505,13 @@ class AgentEngine {
|
|
|
457
505
|
trustLevel: 'external_source',
|
|
458
506
|
sourceChunkMemoryId: chunk.id,
|
|
459
507
|
},
|
|
508
|
+
signal,
|
|
460
509
|
});
|
|
461
510
|
}
|
|
511
|
+
memoryManager.markChunksExtracted?.([chunk.id], { success: true });
|
|
462
512
|
} catch (err) {
|
|
463
513
|
memoryManager.markChunksExtracted?.([chunk.id], { success: false });
|
|
514
|
+
if (isAbortError(err, signal)) throw err;
|
|
464
515
|
console.warn('[Memory] Document chunk extraction failed:', err.message);
|
|
465
516
|
}
|
|
466
517
|
}
|
|
@@ -523,6 +574,7 @@ class AgentEngine {
|
|
|
523
574
|
? deliverableResult.artifacts.slice(0, 6)
|
|
524
575
|
: [],
|
|
525
576
|
},
|
|
577
|
+
signal: this.getRunMeta(runId)?.abortController?.signal || null,
|
|
526
578
|
},
|
|
527
579
|
);
|
|
528
580
|
} catch (error) {
|
|
@@ -586,6 +638,53 @@ class AgentEngine {
|
|
|
586
638
|
});
|
|
587
639
|
}
|
|
588
640
|
|
|
641
|
+
async inferStructured({
|
|
642
|
+
userId,
|
|
643
|
+
agentId = null,
|
|
644
|
+
modelId = null,
|
|
645
|
+
purpose = 'fast',
|
|
646
|
+
system,
|
|
647
|
+
prompt,
|
|
648
|
+
maxTokens = 400,
|
|
649
|
+
fallback = {},
|
|
650
|
+
signal = null,
|
|
651
|
+
}) {
|
|
652
|
+
const selected = await getProviderForUser(
|
|
653
|
+
userId,
|
|
654
|
+
prompt,
|
|
655
|
+
false,
|
|
656
|
+
modelId,
|
|
657
|
+
{
|
|
658
|
+
agentId,
|
|
659
|
+
signal,
|
|
660
|
+
selectionHint: {
|
|
661
|
+
purpose: purpose === 'general' ? 'general' : 'fast',
|
|
662
|
+
costMode: purpose === 'fast' ? 'economy' : 'balanced_auto',
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
const result = await this.requestStructuredJson({
|
|
667
|
+
provider: selected.provider,
|
|
668
|
+
providerName: selected.providerName,
|
|
669
|
+
model: selected.model,
|
|
670
|
+
messages: system ? [{ role: 'system', content: String(system) }] : [],
|
|
671
|
+
prompt: String(prompt || ''),
|
|
672
|
+
maxTokens,
|
|
673
|
+
normalize: (value) => value,
|
|
674
|
+
fallback,
|
|
675
|
+
telemetry: { userId, agentId, signal },
|
|
676
|
+
phase: 'behavior_inference',
|
|
677
|
+
});
|
|
678
|
+
return {
|
|
679
|
+
parsed: result.value,
|
|
680
|
+
raw: result.raw,
|
|
681
|
+
usage: result.usage,
|
|
682
|
+
model: selected.model,
|
|
683
|
+
modelSelectionId: selected.modelSelectionId,
|
|
684
|
+
providerName: selected.providerName,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
589
688
|
async requestModelResponse({
|
|
590
689
|
provider,
|
|
591
690
|
providerName,
|
|
@@ -742,6 +841,11 @@ class AgentEngine {
|
|
|
742
841
|
}) {
|
|
743
842
|
const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
|
|
744
843
|
const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
|
|
844
|
+
const researchAdequacy = assessResearchAdequacy({
|
|
845
|
+
analysis,
|
|
846
|
+
goalContext,
|
|
847
|
+
toolExecutions,
|
|
848
|
+
});
|
|
745
849
|
const response = await this.requestStructuredJson({
|
|
746
850
|
provider,
|
|
747
851
|
providerName,
|
|
@@ -757,6 +861,8 @@ class AgentEngine {
|
|
|
757
861
|
lastReply,
|
|
758
862
|
iteration,
|
|
759
863
|
maxIterations,
|
|
864
|
+
analysis,
|
|
865
|
+
researchAdequacy,
|
|
760
866
|
}),
|
|
761
867
|
maxTokens: 500,
|
|
762
868
|
normalize: (raw) => normalizeCompletionDecision(raw, 'continue'),
|
|
@@ -766,9 +872,15 @@ class AgentEngine {
|
|
|
766
872
|
phase: 'completion_decision',
|
|
767
873
|
});
|
|
768
874
|
return {
|
|
769
|
-
decision: response.value,
|
|
875
|
+
decision: enforceTerminalReplyDecision(response.value, lastReply, {
|
|
876
|
+
analysis,
|
|
877
|
+
goalContext,
|
|
878
|
+
toolExecutions,
|
|
879
|
+
researchAdequacy,
|
|
880
|
+
}),
|
|
770
881
|
usage: response.usage,
|
|
771
882
|
raw: response.raw,
|
|
883
|
+
researchAdequacy,
|
|
772
884
|
};
|
|
773
885
|
}
|
|
774
886
|
|
|
@@ -854,6 +966,12 @@ class AgentEngine {
|
|
|
854
966
|
goalContext,
|
|
855
967
|
toolExecutions,
|
|
856
968
|
iteration,
|
|
969
|
+
analysis,
|
|
970
|
+
researchAdequacy: assessResearchAdequacy({
|
|
971
|
+
analysis,
|
|
972
|
+
goalContext,
|
|
973
|
+
toolExecutions,
|
|
974
|
+
}),
|
|
857
975
|
}),
|
|
858
976
|
maxTokens: 200,
|
|
859
977
|
normalize: normalizeChurnAssessment,
|
|
@@ -862,9 +980,20 @@ class AgentEngine {
|
|
|
862
980
|
telemetry: options,
|
|
863
981
|
phase: 'churn_assessment',
|
|
864
982
|
});
|
|
983
|
+
const researchAdequacy = assessResearchAdequacy({
|
|
984
|
+
analysis,
|
|
985
|
+
goalContext,
|
|
986
|
+
toolExecutions,
|
|
987
|
+
});
|
|
865
988
|
return {
|
|
866
|
-
assessment: response.value,
|
|
989
|
+
assessment: enforceChurnAssessment(response.value, {
|
|
990
|
+
analysis,
|
|
991
|
+
goalContext,
|
|
992
|
+
toolExecutions,
|
|
993
|
+
researchAdequacy,
|
|
994
|
+
}),
|
|
867
995
|
usage: response.usage,
|
|
996
|
+
researchAdequacy,
|
|
868
997
|
};
|
|
869
998
|
}
|
|
870
999
|
|
|
@@ -891,6 +1020,15 @@ class AgentEngine {
|
|
|
891
1020
|
content: [
|
|
892
1021
|
'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
|
|
893
1022
|
'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
|
|
1023
|
+
options.memoryScope?.scopeType === 'channel'
|
|
1024
|
+
? [
|
|
1025
|
+
'This is a shared room. Extract only durable room or participant facts that are safe and useful in this room.',
|
|
1026
|
+
'Do not infer or store facts about the authenticated owner from another participant’s message.',
|
|
1027
|
+
options.context?.socialIntelligence?.message?.sender
|
|
1028
|
+
? `For facts about the current speaker, use the canonical subject "${options.source}:${options.context.socialIntelligence.message.sender}".`
|
|
1029
|
+
: '',
|
|
1030
|
+
].filter(Boolean).join(' ')
|
|
1031
|
+
: '',
|
|
894
1032
|
buildMemoryConsolidationInstructions(new Date().toISOString()),
|
|
895
1033
|
'Schema:',
|
|
896
1034
|
JSON.stringify({
|
|
@@ -931,11 +1069,12 @@ class AgentEngine {
|
|
|
931
1069
|
}
|
|
932
1070
|
];
|
|
933
1071
|
|
|
934
|
-
const response = await
|
|
935
|
-
provider.chat(promptMessages, [], {
|
|
1072
|
+
const response = await runAbortableModelCall(
|
|
1073
|
+
(signal) => provider.chat(promptMessages, [], {
|
|
936
1074
|
model,
|
|
937
1075
|
maxTokens: 800,
|
|
938
1076
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1077
|
+
signal,
|
|
939
1078
|
}),
|
|
940
1079
|
options,
|
|
941
1080
|
'Conversation state refresh',
|
|
@@ -966,6 +1105,15 @@ class AgentEngine {
|
|
|
966
1105
|
agentId: options.agentId || null,
|
|
967
1106
|
conversationId,
|
|
968
1107
|
runId,
|
|
1108
|
+
scope: options.memoryScope || undefined,
|
|
1109
|
+
metadata: options.memoryScope
|
|
1110
|
+
? {
|
|
1111
|
+
trustLevel: 'shared_room_conversation',
|
|
1112
|
+
platform: options.source || null,
|
|
1113
|
+
chatId: options.chatId || null,
|
|
1114
|
+
}
|
|
1115
|
+
: undefined,
|
|
1116
|
+
signal: options.signal,
|
|
969
1117
|
},
|
|
970
1118
|
);
|
|
971
1119
|
const { invalidateSystemPromptCache } = require('../systemPrompt');
|
|
@@ -982,8 +1130,8 @@ class AgentEngine {
|
|
|
982
1130
|
return executeToolImpl(this, toolName, args, context);
|
|
983
1131
|
}
|
|
984
1132
|
|
|
985
|
-
isReadOnlyToolCall(toolCall) {
|
|
986
|
-
return isReadOnlyToolCallImpl(
|
|
1133
|
+
isReadOnlyToolCall(toolCall, toolDefinition = null) {
|
|
1134
|
+
return isReadOnlyToolCallImpl(toolCall, toolDefinition);
|
|
987
1135
|
}
|
|
988
1136
|
|
|
989
1137
|
async executeReadOnlyBatch(toolCalls, context = {}) {
|
|
@@ -1119,6 +1267,15 @@ class AgentEngine {
|
|
|
1119
1267
|
|
|
1120
1268
|
await new Promise((resolve) => { runMeta.resumeRun = resolve; });
|
|
1121
1269
|
runMeta.resumeRun = null;
|
|
1270
|
+
if (runMeta.status === 'stopped' || runMeta.status === 'interrupted') {
|
|
1271
|
+
return { action: runMeta.status === 'interrupted' ? 'interrupt' : 'stop' };
|
|
1272
|
+
}
|
|
1273
|
+
const persistedStatus = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status;
|
|
1274
|
+
if (persistedStatus !== 'running') {
|
|
1275
|
+
const action = persistedStatus === 'interrupted' ? 'interrupt' : 'stop';
|
|
1276
|
+
runMeta.status = persistedStatus || 'stopped';
|
|
1277
|
+
return { action };
|
|
1278
|
+
}
|
|
1122
1279
|
runMeta.abortController = new AbortController();
|
|
1123
1280
|
runMeta.status = 'running';
|
|
1124
1281
|
this.startMessagingProgressSupervisor(runId);
|
|
@@ -1152,20 +1309,6 @@ class AgentEngine {
|
|
|
1152
1309
|
return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
|
|
1153
1310
|
}
|
|
1154
1311
|
|
|
1155
|
-
shouldFastCompleteVoiceReply({
|
|
1156
|
-
options = {},
|
|
1157
|
-
toolExecutions = [],
|
|
1158
|
-
failedStepCount = 0,
|
|
1159
|
-
messagingSent = false,
|
|
1160
|
-
lastReply = '',
|
|
1161
|
-
}) {
|
|
1162
|
-
return options.latencyProfile === 'voice'
|
|
1163
|
-
&& toolExecutions.length === 0
|
|
1164
|
-
&& failedStepCount === 0
|
|
1165
|
-
&& !messagingSent
|
|
1166
|
-
&& Boolean(String(lastReply || '').trim());
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
1312
|
getMessagingRetryLimit(maxIterations) {
|
|
1170
1313
|
// Cap at 3: more than 3 autonomous messaging retries indicates a structural
|
|
1171
1314
|
// problem (model unavailable, bad config) that more retries won't solve.
|
|
@@ -1282,15 +1425,37 @@ class AgentEngine {
|
|
|
1282
1425
|
}
|
|
1283
1426
|
|
|
1284
1427
|
async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1428
|
+
if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
|
|
1429
|
+
const error = createAbortError(
|
|
1430
|
+
this.lifecycleAbortController.signal,
|
|
1431
|
+
'Agent engine is shutting down.',
|
|
1432
|
+
);
|
|
1433
|
+
error.code = error.code || 'AGENT_ENGINE_SHUTTING_DOWN';
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
const runPromise = runConversation(this, userId, userMessage, options, _modelOverride);
|
|
1437
|
+
this.activeRunPromises.add(runPromise);
|
|
1438
|
+
try {
|
|
1439
|
+
return await runPromise;
|
|
1440
|
+
} finally {
|
|
1441
|
+
this.activeRunPromises.delete(runPromise);
|
|
1442
|
+
}
|
|
1290
1443
|
}
|
|
1291
1444
|
|
|
1292
1445
|
async spawnSubagent(userId, parentRunId, task, options = {}) {
|
|
1293
|
-
|
|
1446
|
+
if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
|
|
1447
|
+
throw createAbortError(this.lifecycleAbortController.signal, 'Agent engine is shutting down.');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
let parentRunMeta = this.getRunMeta(parentRunId);
|
|
1451
|
+
if (
|
|
1452
|
+
!parentRunMeta
|
|
1453
|
+
|| parentRunMeta.userId !== userId
|
|
1454
|
+
|| parentRunMeta.status !== 'running'
|
|
1455
|
+
|| parentRunMeta.aborted === true
|
|
1456
|
+
) {
|
|
1457
|
+
return { error: 'The parent run is no longer active, so a sub-agent cannot be started.' };
|
|
1458
|
+
}
|
|
1294
1459
|
const parentDepth = Math.max(0, Number(parentRunMeta?.subagentDepth) || 0);
|
|
1295
1460
|
if (parentDepth >= 1) {
|
|
1296
1461
|
return {
|
|
@@ -1300,24 +1465,77 @@ class AgentEngine {
|
|
|
1300
1465
|
|
|
1301
1466
|
const aiSettings = getAiSettings(userId, options.agentId || null);
|
|
1302
1467
|
const maxSubagentsPerRun = Math.max(1, Number(aiSettings.subagent_max_children_per_run) || 10);
|
|
1303
|
-
const
|
|
1304
|
-
.filter((record) => record.parentRunId === parentRunId);
|
|
1305
|
-
if (
|
|
1468
|
+
const countExistingSubagents = () => Array.from(this.subagents.values())
|
|
1469
|
+
.filter((record) => record.parentRunId === parentRunId).length;
|
|
1470
|
+
if (countExistingSubagents() >= maxSubagentsPerRun) {
|
|
1306
1471
|
return {
|
|
1307
|
-
error: `This run has already spawned ${
|
|
1472
|
+
error: `This run has already spawned ${countExistingSubagents()} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1308
1473
|
};
|
|
1309
1474
|
}
|
|
1310
1475
|
|
|
1311
1476
|
const handle = uuidv4();
|
|
1312
1477
|
const childRunId = uuidv4();
|
|
1313
1478
|
let relevantMemories = [];
|
|
1479
|
+
const startupLink = createLinkedAbortController([
|
|
1480
|
+
this.lifecycleAbortController.signal,
|
|
1481
|
+
parentRunMeta.abortController?.signal,
|
|
1482
|
+
options.signal,
|
|
1483
|
+
]);
|
|
1484
|
+
let memoryRecall = null;
|
|
1314
1485
|
try {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1486
|
+
throwIfAborted(startupLink.signal, 'Sub-agent startup was aborted.');
|
|
1487
|
+
memoryRecall = this.memoryManager
|
|
1488
|
+
? runWithAbortTimeout(
|
|
1489
|
+
(signal) => this.memoryManager.recallMemory(userId, task, 4, {
|
|
1490
|
+
agentId: options.agentId || null,
|
|
1491
|
+
signal,
|
|
1492
|
+
}),
|
|
1493
|
+
{
|
|
1494
|
+
label: 'Sub-agent memory recall',
|
|
1495
|
+
signal: startupLink.signal,
|
|
1496
|
+
},
|
|
1497
|
+
)
|
|
1498
|
+
: Promise.resolve([]);
|
|
1499
|
+
this.subagentStartupTasks.add(memoryRecall);
|
|
1500
|
+
relevantMemories = await memoryRecall;
|
|
1501
|
+
throwIfAborted(startupLink.signal, 'Sub-agent startup was aborted.');
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
if (isAbortError(error, startupLink.signal)) throw error;
|
|
1504
|
+
console.warn('[AgentEngine] Sub-agent memory recall failed:', error?.message || error);
|
|
1505
|
+
} finally {
|
|
1506
|
+
if (memoryRecall) this.subagentStartupTasks.delete(memoryRecall);
|
|
1507
|
+
startupLink.cleanup();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
parentRunMeta = this.getRunMeta(parentRunId);
|
|
1511
|
+
if (
|
|
1512
|
+
this.shuttingDown
|
|
1513
|
+
|| this.lifecycleAbortController.signal.aborted
|
|
1514
|
+
|| !parentRunMeta
|
|
1515
|
+
|| parentRunMeta.userId !== userId
|
|
1516
|
+
|| parentRunMeta.status !== 'running'
|
|
1517
|
+
|| parentRunMeta.aborted === true
|
|
1518
|
+
) {
|
|
1519
|
+
throw createAbortError(
|
|
1520
|
+
this.lifecycleAbortController.signal.aborted
|
|
1521
|
+
? this.lifecycleAbortController.signal
|
|
1522
|
+
: parentRunMeta?.abortController?.signal,
|
|
1523
|
+
'The parent run stopped before the sub-agent could start.',
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
const currentSubagentCount = countExistingSubagents();
|
|
1527
|
+
if (currentSubagentCount >= maxSubagentsPerRun) {
|
|
1528
|
+
return {
|
|
1529
|
+
error: `This run has already spawned ${currentSubagentCount} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const childLink = createLinkedAbortController([
|
|
1534
|
+
this.lifecycleAbortController.signal,
|
|
1535
|
+
parentRunMeta.abortController?.signal,
|
|
1536
|
+
options.signal,
|
|
1537
|
+
]);
|
|
1538
|
+
throwIfAborted(childLink.signal, 'Sub-agent startup was aborted.');
|
|
1321
1539
|
const subEngine = new AgentEngine(this.io, {
|
|
1322
1540
|
app: options.app || this.app,
|
|
1323
1541
|
browserController: this.browserController,
|
|
@@ -1363,6 +1581,8 @@ class AgentEngine {
|
|
|
1363
1581
|
error: null,
|
|
1364
1582
|
engine: subEngine,
|
|
1365
1583
|
promise: null,
|
|
1584
|
+
settled: false,
|
|
1585
|
+
deleteWhenSettled: false,
|
|
1366
1586
|
};
|
|
1367
1587
|
this.subagents.set(handle, record);
|
|
1368
1588
|
this.emit(userId, 'run:subagent', {
|
|
@@ -1386,9 +1606,11 @@ class AgentEngine {
|
|
|
1386
1606
|
agentId: options.agentId || null,
|
|
1387
1607
|
subagentDepth: parentDepth + 1,
|
|
1388
1608
|
disallowedToolNames: ['spawn_subagent'],
|
|
1609
|
+
signal: childLink.signal,
|
|
1389
1610
|
},
|
|
1390
1611
|
options.model || null
|
|
1391
1612
|
);
|
|
1613
|
+
if (record.status === 'cancelled' || childLink.signal.aborted) return record;
|
|
1392
1614
|
record.status = result.status || 'completed';
|
|
1393
1615
|
let structured = null;
|
|
1394
1616
|
try {
|
|
@@ -1416,16 +1638,23 @@ class AgentEngine {
|
|
|
1416
1638
|
});
|
|
1417
1639
|
return record;
|
|
1418
1640
|
} catch (err) {
|
|
1419
|
-
record.status
|
|
1420
|
-
record.
|
|
1641
|
+
const cancelled = record.status === 'cancelled' || isAbortError(err, childLink.signal);
|
|
1642
|
+
record.status = cancelled ? 'cancelled' : 'failed';
|
|
1643
|
+
record.error = cancelled ? null : (err?.message || String(err));
|
|
1421
1644
|
this.emit(userId, 'run:subagent', {
|
|
1422
1645
|
runId: parentRunId,
|
|
1423
1646
|
handle,
|
|
1424
1647
|
childRunId,
|
|
1425
|
-
status:
|
|
1426
|
-
error:
|
|
1648
|
+
status: record.status,
|
|
1649
|
+
error: record.error,
|
|
1427
1650
|
});
|
|
1428
|
-
|
|
1651
|
+
return record;
|
|
1652
|
+
} finally {
|
|
1653
|
+
record.settled = true;
|
|
1654
|
+
childLink.cleanup();
|
|
1655
|
+
if (record.deleteWhenSettled && this.subagents.get(handle) === record) {
|
|
1656
|
+
this.subagents.delete(handle);
|
|
1657
|
+
}
|
|
1429
1658
|
}
|
|
1430
1659
|
})();
|
|
1431
1660
|
|
|
@@ -1562,21 +1791,43 @@ class AgentEngine {
|
|
|
1562
1791
|
}));
|
|
1563
1792
|
}
|
|
1564
1793
|
|
|
1565
|
-
cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
1566
|
-
if (!parentRunId) return;
|
|
1794
|
+
async cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
1795
|
+
if (!parentRunId) return { cancelled: 0, timedOut: 0 };
|
|
1567
1796
|
const cancelRunning = options.cancelRunning !== false;
|
|
1568
|
-
|
|
1797
|
+
const shutdowns = [];
|
|
1798
|
+
const records = [];
|
|
1799
|
+
for (const [handle, record] of Array.from(this.subagents.entries())) {
|
|
1569
1800
|
if (record.parentRunId !== parentRunId) continue;
|
|
1801
|
+
records.push([handle, record]);
|
|
1802
|
+
record.deleteWhenSettled = true;
|
|
1570
1803
|
if (cancelRunning && record.status === 'running') {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
record.
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1804
|
+
record.status = 'cancelled';
|
|
1805
|
+
this.emit(record.userId, 'run:subagent', {
|
|
1806
|
+
runId: record.parentRunId,
|
|
1807
|
+
handle,
|
|
1808
|
+
childRunId: record.childRunId,
|
|
1809
|
+
status: 'cancelled',
|
|
1810
|
+
});
|
|
1811
|
+
shutdowns.push(Promise.resolve(record.engine?.shutdown?.({
|
|
1812
|
+
reason: 'Parent run finished before the sub-agent completed.',
|
|
1813
|
+
timeoutMs: Math.max(100, Number(options.timeoutMs) || 10000),
|
|
1814
|
+
})));
|
|
1815
|
+
} else if (record.status === 'running') {
|
|
1816
|
+
record.deleteWhenSettled = false;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const results = await Promise.allSettled(shutdowns);
|
|
1820
|
+
for (const [handle, record] of records) {
|
|
1821
|
+
if ((record.settled || !record.promise) && this.subagents.get(handle) === record) {
|
|
1822
|
+
this.subagents.delete(handle);
|
|
1577
1823
|
}
|
|
1578
|
-
this.subagents.delete(handle);
|
|
1579
1824
|
}
|
|
1825
|
+
return {
|
|
1826
|
+
cancelled: shutdowns.length,
|
|
1827
|
+
timedOut: results.filter((result) => (
|
|
1828
|
+
result.status === 'fulfilled' && result.value?.timedOut === true
|
|
1829
|
+
)).length,
|
|
1830
|
+
};
|
|
1580
1831
|
}
|
|
1581
1832
|
|
|
1582
1833
|
async waitForSubagent(handle, options = {}) {
|
|
@@ -1639,7 +1890,6 @@ class AgentEngine {
|
|
|
1639
1890
|
};
|
|
1640
1891
|
}
|
|
1641
1892
|
|
|
1642
|
-
record.engine?.abort(record.childRunId);
|
|
1643
1893
|
record.status = 'cancelled';
|
|
1644
1894
|
this.emit(record.userId, 'run:subagent', {
|
|
1645
1895
|
runId: record.parentRunId,
|
|
@@ -1648,7 +1898,15 @@ class AgentEngine {
|
|
|
1648
1898
|
status: 'cancelled',
|
|
1649
1899
|
});
|
|
1650
1900
|
|
|
1651
|
-
|
|
1901
|
+
const shutdown = await record.engine?.shutdown?.({
|
|
1902
|
+
reason: 'Sub-agent cancelled by its parent run.',
|
|
1903
|
+
timeoutMs: Math.max(100, Number(options.timeoutMs) || 10000),
|
|
1904
|
+
});
|
|
1905
|
+
return {
|
|
1906
|
+
handle,
|
|
1907
|
+
status: 'cancelled',
|
|
1908
|
+
timedOut: shutdown?.timedOut === true,
|
|
1909
|
+
};
|
|
1652
1910
|
}
|
|
1653
1911
|
|
|
1654
1912
|
interruptRun(runId, reason = 'Server shutting down while run was in progress.') {
|
|
@@ -1665,6 +1923,7 @@ class AgentEngine {
|
|
|
1665
1923
|
runMeta.stopReason = reason;
|
|
1666
1924
|
runMeta.aborted = true;
|
|
1667
1925
|
runMeta.abortController?.abort(reason);
|
|
1926
|
+
runMeta.resumeRun?.();
|
|
1668
1927
|
this.emit(runMeta.userId, 'run:stopping', { runId });
|
|
1669
1928
|
for (const pid of runMeta.toolPids) {
|
|
1670
1929
|
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
@@ -1695,20 +1954,26 @@ class AgentEngine {
|
|
|
1695
1954
|
}
|
|
1696
1955
|
}
|
|
1697
1956
|
|
|
1698
|
-
stopRun(runId) {
|
|
1957
|
+
stopRun(runId, reason = 'Stopped by request.') {
|
|
1958
|
+
const stopReason = String(reason || 'Stopped by request.').slice(0, 1000);
|
|
1699
1959
|
const runMeta = this.activeRuns.get(runId);
|
|
1700
|
-
const persistedRun = runMeta || db.prepare(
|
|
1960
|
+
const persistedRun = runMeta || db.prepare(
|
|
1961
|
+
'SELECT user_id AS userId, status FROM agent_runs WHERE id = ?',
|
|
1962
|
+
).get(runId);
|
|
1963
|
+
if (!persistedRun || TERMINAL_STATUSES.has(persistedRun.status)) return false;
|
|
1701
1964
|
if (persistedRun?.userId != null) {
|
|
1702
|
-
requestRunControl(runId, persistedRun.userId, 'stop',
|
|
1965
|
+
requestRunControl(runId, persistedRun.userId, 'stop', stopReason);
|
|
1703
1966
|
}
|
|
1704
1967
|
const delegatedChildren = db.prepare(
|
|
1705
1968
|
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
1706
1969
|
).all(runId);
|
|
1707
1970
|
if (runMeta) {
|
|
1708
1971
|
runMeta.status = 'stopped';
|
|
1972
|
+
runMeta.stopReason = stopReason;
|
|
1709
1973
|
runMeta.aborted = true;
|
|
1710
|
-
runMeta.abortController?.abort(
|
|
1711
|
-
|
|
1974
|
+
runMeta.abortController?.abort(stopReason);
|
|
1975
|
+
runMeta.resumeRun?.();
|
|
1976
|
+
this.emit(runMeta.userId, 'run:stopping', { runId, reason: stopReason });
|
|
1712
1977
|
for (const pid of runMeta.toolPids) {
|
|
1713
1978
|
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
1714
1979
|
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
@@ -1718,13 +1983,22 @@ class AgentEngine {
|
|
|
1718
1983
|
}
|
|
1719
1984
|
for (const child of delegatedChildren) {
|
|
1720
1985
|
if (child.child_run_id && child.child_run_id !== runId) {
|
|
1721
|
-
this.stopRun(child.child_run_id);
|
|
1986
|
+
this.stopRun(child.child_run_id, stopReason);
|
|
1722
1987
|
}
|
|
1723
1988
|
}
|
|
1724
1989
|
db.prepare(
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1990
|
+
`UPDATE agent_delegations
|
|
1991
|
+
SET status = 'stopped', error = COALESCE(NULLIF(error, ''), ?),
|
|
1992
|
+
updated_at = datetime('now'), completed_at = datetime('now')
|
|
1993
|
+
WHERE parent_run_id = ? AND status = 'running'`
|
|
1994
|
+
).run(stopReason, runId);
|
|
1995
|
+
closeRun(
|
|
1996
|
+
runId,
|
|
1997
|
+
'stopped',
|
|
1998
|
+
{ error: stopReason },
|
|
1999
|
+
['running', 'pausing', 'paused', 'resuming'],
|
|
2000
|
+
);
|
|
2001
|
+
return true;
|
|
1728
2002
|
}
|
|
1729
2003
|
|
|
1730
2004
|
pauseRun(runId, { userId, reason = '' } = {}) {
|
|
@@ -1766,15 +2040,17 @@ class AgentEngine {
|
|
|
1766
2040
|
return true;
|
|
1767
2041
|
}
|
|
1768
2042
|
|
|
1769
|
-
abort(runId, { userId } = {}) {
|
|
2043
|
+
abort(runId, { userId, reason = 'Stopped by request.' } = {}) {
|
|
1770
2044
|
if (!runId) return false;
|
|
2045
|
+
const runMeta = this.activeRuns.get(runId);
|
|
2046
|
+
const persistedRun = runMeta
|
|
2047
|
+
|| db.prepare('SELECT user_id AS userId, status FROM agent_runs WHERE id = ?').get(runId);
|
|
2048
|
+
if (!persistedRun || TERMINAL_STATUSES.has(persistedRun.status)) return false;
|
|
1771
2049
|
if (userId != null) {
|
|
1772
|
-
// Ownership gate
|
|
1773
|
-
|
|
1774
|
-
if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
|
|
2050
|
+
// Ownership gate applies to both active and persisted runs.
|
|
2051
|
+
if (Number(persistedRun.userId) !== Number(userId)) return false;
|
|
1775
2052
|
}
|
|
1776
|
-
this.stopRun(runId);
|
|
1777
|
-
return true;
|
|
2053
|
+
return this.stopRun(runId, reason);
|
|
1778
2054
|
}
|
|
1779
2055
|
|
|
1780
2056
|
abortAll(userId) {
|