neoagent 3.2.1-beta.1 → 3.2.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +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 +11 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +390 -162
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +121 -5
- package/server/services/ai/loop/conversation_loop.js +510 -332
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/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 +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -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) {
|
|
@@ -742,6 +794,11 @@ class AgentEngine {
|
|
|
742
794
|
}) {
|
|
743
795
|
const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
|
|
744
796
|
const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
|
|
797
|
+
const researchAdequacy = assessResearchAdequacy({
|
|
798
|
+
analysis,
|
|
799
|
+
goalContext,
|
|
800
|
+
toolExecutions,
|
|
801
|
+
});
|
|
745
802
|
const response = await this.requestStructuredJson({
|
|
746
803
|
provider,
|
|
747
804
|
providerName,
|
|
@@ -757,6 +814,8 @@ class AgentEngine {
|
|
|
757
814
|
lastReply,
|
|
758
815
|
iteration,
|
|
759
816
|
maxIterations,
|
|
817
|
+
analysis,
|
|
818
|
+
researchAdequacy,
|
|
760
819
|
}),
|
|
761
820
|
maxTokens: 500,
|
|
762
821
|
normalize: (raw) => normalizeCompletionDecision(raw, 'continue'),
|
|
@@ -766,9 +825,15 @@ class AgentEngine {
|
|
|
766
825
|
phase: 'completion_decision',
|
|
767
826
|
});
|
|
768
827
|
return {
|
|
769
|
-
decision: response.value,
|
|
828
|
+
decision: enforceTerminalReplyDecision(response.value, lastReply, {
|
|
829
|
+
analysis,
|
|
830
|
+
goalContext,
|
|
831
|
+
toolExecutions,
|
|
832
|
+
researchAdequacy,
|
|
833
|
+
}),
|
|
770
834
|
usage: response.usage,
|
|
771
835
|
raw: response.raw,
|
|
836
|
+
researchAdequacy,
|
|
772
837
|
};
|
|
773
838
|
}
|
|
774
839
|
|
|
@@ -854,6 +919,12 @@ class AgentEngine {
|
|
|
854
919
|
goalContext,
|
|
855
920
|
toolExecutions,
|
|
856
921
|
iteration,
|
|
922
|
+
analysis,
|
|
923
|
+
researchAdequacy: assessResearchAdequacy({
|
|
924
|
+
analysis,
|
|
925
|
+
goalContext,
|
|
926
|
+
toolExecutions,
|
|
927
|
+
}),
|
|
857
928
|
}),
|
|
858
929
|
maxTokens: 200,
|
|
859
930
|
normalize: normalizeChurnAssessment,
|
|
@@ -862,9 +933,20 @@ class AgentEngine {
|
|
|
862
933
|
telemetry: options,
|
|
863
934
|
phase: 'churn_assessment',
|
|
864
935
|
});
|
|
936
|
+
const researchAdequacy = assessResearchAdequacy({
|
|
937
|
+
analysis,
|
|
938
|
+
goalContext,
|
|
939
|
+
toolExecutions,
|
|
940
|
+
});
|
|
865
941
|
return {
|
|
866
|
-
assessment: response.value,
|
|
942
|
+
assessment: enforceChurnAssessment(response.value, {
|
|
943
|
+
analysis,
|
|
944
|
+
goalContext,
|
|
945
|
+
toolExecutions,
|
|
946
|
+
researchAdequacy,
|
|
947
|
+
}),
|
|
867
948
|
usage: response.usage,
|
|
949
|
+
researchAdequacy,
|
|
868
950
|
};
|
|
869
951
|
}
|
|
870
952
|
|
|
@@ -931,11 +1013,12 @@ class AgentEngine {
|
|
|
931
1013
|
}
|
|
932
1014
|
];
|
|
933
1015
|
|
|
934
|
-
const response = await
|
|
935
|
-
provider.chat(promptMessages, [], {
|
|
1016
|
+
const response = await runAbortableModelCall(
|
|
1017
|
+
(signal) => provider.chat(promptMessages, [], {
|
|
936
1018
|
model,
|
|
937
1019
|
maxTokens: 800,
|
|
938
1020
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1021
|
+
signal,
|
|
939
1022
|
}),
|
|
940
1023
|
options,
|
|
941
1024
|
'Conversation state refresh',
|
|
@@ -966,6 +1049,7 @@ class AgentEngine {
|
|
|
966
1049
|
agentId: options.agentId || null,
|
|
967
1050
|
conversationId,
|
|
968
1051
|
runId,
|
|
1052
|
+
signal: options.signal,
|
|
969
1053
|
},
|
|
970
1054
|
);
|
|
971
1055
|
const { invalidateSystemPromptCache } = require('../systemPrompt');
|
|
@@ -982,8 +1066,8 @@ class AgentEngine {
|
|
|
982
1066
|
return executeToolImpl(this, toolName, args, context);
|
|
983
1067
|
}
|
|
984
1068
|
|
|
985
|
-
isReadOnlyToolCall(toolCall) {
|
|
986
|
-
return isReadOnlyToolCallImpl(
|
|
1069
|
+
isReadOnlyToolCall(toolCall, toolDefinition = null) {
|
|
1070
|
+
return isReadOnlyToolCallImpl(toolCall, toolDefinition);
|
|
987
1071
|
}
|
|
988
1072
|
|
|
989
1073
|
async executeReadOnlyBatch(toolCalls, context = {}) {
|
|
@@ -1119,6 +1203,15 @@ class AgentEngine {
|
|
|
1119
1203
|
|
|
1120
1204
|
await new Promise((resolve) => { runMeta.resumeRun = resolve; });
|
|
1121
1205
|
runMeta.resumeRun = null;
|
|
1206
|
+
if (runMeta.status === 'stopped' || runMeta.status === 'interrupted') {
|
|
1207
|
+
return { action: runMeta.status === 'interrupted' ? 'interrupt' : 'stop' };
|
|
1208
|
+
}
|
|
1209
|
+
const persistedStatus = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status;
|
|
1210
|
+
if (persistedStatus !== 'running') {
|
|
1211
|
+
const action = persistedStatus === 'interrupted' ? 'interrupt' : 'stop';
|
|
1212
|
+
runMeta.status = persistedStatus || 'stopped';
|
|
1213
|
+
return { action };
|
|
1214
|
+
}
|
|
1122
1215
|
runMeta.abortController = new AbortController();
|
|
1123
1216
|
runMeta.status = 'running';
|
|
1124
1217
|
this.startMessagingProgressSupervisor(runId);
|
|
@@ -1159,6 +1252,8 @@ class AgentEngine {
|
|
|
1159
1252
|
messagingSent = false,
|
|
1160
1253
|
lastReply = '',
|
|
1161
1254
|
}) {
|
|
1255
|
+
// Voice fast-path is structural only: short reply, no tools/failures.
|
|
1256
|
+
// Content terminality is left to the model completion judge.
|
|
1162
1257
|
return options.latencyProfile === 'voice'
|
|
1163
1258
|
&& toolExecutions.length === 0
|
|
1164
1259
|
&& failedStepCount === 0
|
|
@@ -1282,15 +1377,37 @@ class AgentEngine {
|
|
|
1282
1377
|
}
|
|
1283
1378
|
|
|
1284
1379
|
async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1380
|
+
if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
|
|
1381
|
+
const error = createAbortError(
|
|
1382
|
+
this.lifecycleAbortController.signal,
|
|
1383
|
+
'Agent engine is shutting down.',
|
|
1384
|
+
);
|
|
1385
|
+
error.code = error.code || 'AGENT_ENGINE_SHUTTING_DOWN';
|
|
1386
|
+
throw error;
|
|
1387
|
+
}
|
|
1388
|
+
const runPromise = runConversation(this, userId, userMessage, options, _modelOverride);
|
|
1389
|
+
this.activeRunPromises.add(runPromise);
|
|
1390
|
+
try {
|
|
1391
|
+
return await runPromise;
|
|
1392
|
+
} finally {
|
|
1393
|
+
this.activeRunPromises.delete(runPromise);
|
|
1394
|
+
}
|
|
1290
1395
|
}
|
|
1291
1396
|
|
|
1292
1397
|
async spawnSubagent(userId, parentRunId, task, options = {}) {
|
|
1293
|
-
|
|
1398
|
+
if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
|
|
1399
|
+
throw createAbortError(this.lifecycleAbortController.signal, 'Agent engine is shutting down.');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
let parentRunMeta = this.getRunMeta(parentRunId);
|
|
1403
|
+
if (
|
|
1404
|
+
!parentRunMeta
|
|
1405
|
+
|| parentRunMeta.userId !== userId
|
|
1406
|
+
|| parentRunMeta.status !== 'running'
|
|
1407
|
+
|| parentRunMeta.aborted === true
|
|
1408
|
+
) {
|
|
1409
|
+
return { error: 'The parent run is no longer active, so a sub-agent cannot be started.' };
|
|
1410
|
+
}
|
|
1294
1411
|
const parentDepth = Math.max(0, Number(parentRunMeta?.subagentDepth) || 0);
|
|
1295
1412
|
if (parentDepth >= 1) {
|
|
1296
1413
|
return {
|
|
@@ -1300,24 +1417,77 @@ class AgentEngine {
|
|
|
1300
1417
|
|
|
1301
1418
|
const aiSettings = getAiSettings(userId, options.agentId || null);
|
|
1302
1419
|
const maxSubagentsPerRun = Math.max(1, Number(aiSettings.subagent_max_children_per_run) || 10);
|
|
1303
|
-
const
|
|
1304
|
-
.filter((record) => record.parentRunId === parentRunId);
|
|
1305
|
-
if (
|
|
1420
|
+
const countExistingSubagents = () => Array.from(this.subagents.values())
|
|
1421
|
+
.filter((record) => record.parentRunId === parentRunId).length;
|
|
1422
|
+
if (countExistingSubagents() >= maxSubagentsPerRun) {
|
|
1306
1423
|
return {
|
|
1307
|
-
error: `This run has already spawned ${
|
|
1424
|
+
error: `This run has already spawned ${countExistingSubagents()} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1308
1425
|
};
|
|
1309
1426
|
}
|
|
1310
1427
|
|
|
1311
1428
|
const handle = uuidv4();
|
|
1312
1429
|
const childRunId = uuidv4();
|
|
1313
1430
|
let relevantMemories = [];
|
|
1431
|
+
const startupLink = createLinkedAbortController([
|
|
1432
|
+
this.lifecycleAbortController.signal,
|
|
1433
|
+
parentRunMeta.abortController?.signal,
|
|
1434
|
+
options.signal,
|
|
1435
|
+
]);
|
|
1436
|
+
let memoryRecall = null;
|
|
1314
1437
|
try {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1438
|
+
throwIfAborted(startupLink.signal, 'Sub-agent startup was aborted.');
|
|
1439
|
+
memoryRecall = this.memoryManager
|
|
1440
|
+
? runWithAbortTimeout(
|
|
1441
|
+
(signal) => this.memoryManager.recallMemory(userId, task, 4, {
|
|
1442
|
+
agentId: options.agentId || null,
|
|
1443
|
+
signal,
|
|
1444
|
+
}),
|
|
1445
|
+
{
|
|
1446
|
+
label: 'Sub-agent memory recall',
|
|
1447
|
+
signal: startupLink.signal,
|
|
1448
|
+
},
|
|
1449
|
+
)
|
|
1450
|
+
: Promise.resolve([]);
|
|
1451
|
+
this.subagentStartupTasks.add(memoryRecall);
|
|
1452
|
+
relevantMemories = await memoryRecall;
|
|
1453
|
+
throwIfAborted(startupLink.signal, 'Sub-agent startup was aborted.');
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
if (isAbortError(error, startupLink.signal)) throw error;
|
|
1456
|
+
console.warn('[AgentEngine] Sub-agent memory recall failed:', error?.message || error);
|
|
1457
|
+
} finally {
|
|
1458
|
+
if (memoryRecall) this.subagentStartupTasks.delete(memoryRecall);
|
|
1459
|
+
startupLink.cleanup();
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
parentRunMeta = this.getRunMeta(parentRunId);
|
|
1463
|
+
if (
|
|
1464
|
+
this.shuttingDown
|
|
1465
|
+
|| this.lifecycleAbortController.signal.aborted
|
|
1466
|
+
|| !parentRunMeta
|
|
1467
|
+
|| parentRunMeta.userId !== userId
|
|
1468
|
+
|| parentRunMeta.status !== 'running'
|
|
1469
|
+
|| parentRunMeta.aborted === true
|
|
1470
|
+
) {
|
|
1471
|
+
throw createAbortError(
|
|
1472
|
+
this.lifecycleAbortController.signal.aborted
|
|
1473
|
+
? this.lifecycleAbortController.signal
|
|
1474
|
+
: parentRunMeta?.abortController?.signal,
|
|
1475
|
+
'The parent run stopped before the sub-agent could start.',
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
const currentSubagentCount = countExistingSubagents();
|
|
1479
|
+
if (currentSubagentCount >= maxSubagentsPerRun) {
|
|
1480
|
+
return {
|
|
1481
|
+
error: `This run has already spawned ${currentSubagentCount} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const childLink = createLinkedAbortController([
|
|
1486
|
+
this.lifecycleAbortController.signal,
|
|
1487
|
+
parentRunMeta.abortController?.signal,
|
|
1488
|
+
options.signal,
|
|
1489
|
+
]);
|
|
1490
|
+
throwIfAborted(childLink.signal, 'Sub-agent startup was aborted.');
|
|
1321
1491
|
const subEngine = new AgentEngine(this.io, {
|
|
1322
1492
|
app: options.app || this.app,
|
|
1323
1493
|
browserController: this.browserController,
|
|
@@ -1363,6 +1533,8 @@ class AgentEngine {
|
|
|
1363
1533
|
error: null,
|
|
1364
1534
|
engine: subEngine,
|
|
1365
1535
|
promise: null,
|
|
1536
|
+
settled: false,
|
|
1537
|
+
deleteWhenSettled: false,
|
|
1366
1538
|
};
|
|
1367
1539
|
this.subagents.set(handle, record);
|
|
1368
1540
|
this.emit(userId, 'run:subagent', {
|
|
@@ -1386,9 +1558,11 @@ class AgentEngine {
|
|
|
1386
1558
|
agentId: options.agentId || null,
|
|
1387
1559
|
subagentDepth: parentDepth + 1,
|
|
1388
1560
|
disallowedToolNames: ['spawn_subagent'],
|
|
1561
|
+
signal: childLink.signal,
|
|
1389
1562
|
},
|
|
1390
1563
|
options.model || null
|
|
1391
1564
|
);
|
|
1565
|
+
if (record.status === 'cancelled' || childLink.signal.aborted) return record;
|
|
1392
1566
|
record.status = result.status || 'completed';
|
|
1393
1567
|
let structured = null;
|
|
1394
1568
|
try {
|
|
@@ -1416,16 +1590,23 @@ class AgentEngine {
|
|
|
1416
1590
|
});
|
|
1417
1591
|
return record;
|
|
1418
1592
|
} catch (err) {
|
|
1419
|
-
record.status
|
|
1420
|
-
record.
|
|
1593
|
+
const cancelled = record.status === 'cancelled' || isAbortError(err, childLink.signal);
|
|
1594
|
+
record.status = cancelled ? 'cancelled' : 'failed';
|
|
1595
|
+
record.error = cancelled ? null : (err?.message || String(err));
|
|
1421
1596
|
this.emit(userId, 'run:subagent', {
|
|
1422
1597
|
runId: parentRunId,
|
|
1423
1598
|
handle,
|
|
1424
1599
|
childRunId,
|
|
1425
|
-
status:
|
|
1426
|
-
error:
|
|
1600
|
+
status: record.status,
|
|
1601
|
+
error: record.error,
|
|
1427
1602
|
});
|
|
1428
|
-
|
|
1603
|
+
return record;
|
|
1604
|
+
} finally {
|
|
1605
|
+
record.settled = true;
|
|
1606
|
+
childLink.cleanup();
|
|
1607
|
+
if (record.deleteWhenSettled && this.subagents.get(handle) === record) {
|
|
1608
|
+
this.subagents.delete(handle);
|
|
1609
|
+
}
|
|
1429
1610
|
}
|
|
1430
1611
|
})();
|
|
1431
1612
|
|
|
@@ -1562,21 +1743,43 @@ class AgentEngine {
|
|
|
1562
1743
|
}));
|
|
1563
1744
|
}
|
|
1564
1745
|
|
|
1565
|
-
cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
1566
|
-
if (!parentRunId) return;
|
|
1746
|
+
async cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
1747
|
+
if (!parentRunId) return { cancelled: 0, timedOut: 0 };
|
|
1567
1748
|
const cancelRunning = options.cancelRunning !== false;
|
|
1568
|
-
|
|
1749
|
+
const shutdowns = [];
|
|
1750
|
+
const records = [];
|
|
1751
|
+
for (const [handle, record] of Array.from(this.subagents.entries())) {
|
|
1569
1752
|
if (record.parentRunId !== parentRunId) continue;
|
|
1753
|
+
records.push([handle, record]);
|
|
1754
|
+
record.deleteWhenSettled = true;
|
|
1570
1755
|
if (cancelRunning && record.status === 'running') {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
record.
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1756
|
+
record.status = 'cancelled';
|
|
1757
|
+
this.emit(record.userId, 'run:subagent', {
|
|
1758
|
+
runId: record.parentRunId,
|
|
1759
|
+
handle,
|
|
1760
|
+
childRunId: record.childRunId,
|
|
1761
|
+
status: 'cancelled',
|
|
1762
|
+
});
|
|
1763
|
+
shutdowns.push(Promise.resolve(record.engine?.shutdown?.({
|
|
1764
|
+
reason: 'Parent run finished before the sub-agent completed.',
|
|
1765
|
+
timeoutMs: Math.max(100, Number(options.timeoutMs) || 10000),
|
|
1766
|
+
})));
|
|
1767
|
+
} else if (record.status === 'running') {
|
|
1768
|
+
record.deleteWhenSettled = false;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const results = await Promise.allSettled(shutdowns);
|
|
1772
|
+
for (const [handle, record] of records) {
|
|
1773
|
+
if ((record.settled || !record.promise) && this.subagents.get(handle) === record) {
|
|
1774
|
+
this.subagents.delete(handle);
|
|
1577
1775
|
}
|
|
1578
|
-
this.subagents.delete(handle);
|
|
1579
1776
|
}
|
|
1777
|
+
return {
|
|
1778
|
+
cancelled: shutdowns.length,
|
|
1779
|
+
timedOut: results.filter((result) => (
|
|
1780
|
+
result.status === 'fulfilled' && result.value?.timedOut === true
|
|
1781
|
+
)).length,
|
|
1782
|
+
};
|
|
1580
1783
|
}
|
|
1581
1784
|
|
|
1582
1785
|
async waitForSubagent(handle, options = {}) {
|
|
@@ -1639,7 +1842,6 @@ class AgentEngine {
|
|
|
1639
1842
|
};
|
|
1640
1843
|
}
|
|
1641
1844
|
|
|
1642
|
-
record.engine?.abort(record.childRunId);
|
|
1643
1845
|
record.status = 'cancelled';
|
|
1644
1846
|
this.emit(record.userId, 'run:subagent', {
|
|
1645
1847
|
runId: record.parentRunId,
|
|
@@ -1648,7 +1850,15 @@ class AgentEngine {
|
|
|
1648
1850
|
status: 'cancelled',
|
|
1649
1851
|
});
|
|
1650
1852
|
|
|
1651
|
-
|
|
1853
|
+
const shutdown = await record.engine?.shutdown?.({
|
|
1854
|
+
reason: 'Sub-agent cancelled by its parent run.',
|
|
1855
|
+
timeoutMs: Math.max(100, Number(options.timeoutMs) || 10000),
|
|
1856
|
+
});
|
|
1857
|
+
return {
|
|
1858
|
+
handle,
|
|
1859
|
+
status: 'cancelled',
|
|
1860
|
+
timedOut: shutdown?.timedOut === true,
|
|
1861
|
+
};
|
|
1652
1862
|
}
|
|
1653
1863
|
|
|
1654
1864
|
interruptRun(runId, reason = 'Server shutting down while run was in progress.') {
|
|
@@ -1665,6 +1875,7 @@ class AgentEngine {
|
|
|
1665
1875
|
runMeta.stopReason = reason;
|
|
1666
1876
|
runMeta.aborted = true;
|
|
1667
1877
|
runMeta.abortController?.abort(reason);
|
|
1878
|
+
runMeta.resumeRun?.();
|
|
1668
1879
|
this.emit(runMeta.userId, 'run:stopping', { runId });
|
|
1669
1880
|
for (const pid of runMeta.toolPids) {
|
|
1670
1881
|
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
@@ -1695,20 +1906,26 @@ class AgentEngine {
|
|
|
1695
1906
|
}
|
|
1696
1907
|
}
|
|
1697
1908
|
|
|
1698
|
-
stopRun(runId) {
|
|
1909
|
+
stopRun(runId, reason = 'Stopped by request.') {
|
|
1910
|
+
const stopReason = String(reason || 'Stopped by request.').slice(0, 1000);
|
|
1699
1911
|
const runMeta = this.activeRuns.get(runId);
|
|
1700
|
-
const persistedRun = runMeta || db.prepare(
|
|
1912
|
+
const persistedRun = runMeta || db.prepare(
|
|
1913
|
+
'SELECT user_id AS userId, status FROM agent_runs WHERE id = ?',
|
|
1914
|
+
).get(runId);
|
|
1915
|
+
if (!persistedRun || TERMINAL_STATUSES.has(persistedRun.status)) return false;
|
|
1701
1916
|
if (persistedRun?.userId != null) {
|
|
1702
|
-
requestRunControl(runId, persistedRun.userId, 'stop',
|
|
1917
|
+
requestRunControl(runId, persistedRun.userId, 'stop', stopReason);
|
|
1703
1918
|
}
|
|
1704
1919
|
const delegatedChildren = db.prepare(
|
|
1705
1920
|
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
1706
1921
|
).all(runId);
|
|
1707
1922
|
if (runMeta) {
|
|
1708
1923
|
runMeta.status = 'stopped';
|
|
1924
|
+
runMeta.stopReason = stopReason;
|
|
1709
1925
|
runMeta.aborted = true;
|
|
1710
|
-
runMeta.abortController?.abort(
|
|
1711
|
-
|
|
1926
|
+
runMeta.abortController?.abort(stopReason);
|
|
1927
|
+
runMeta.resumeRun?.();
|
|
1928
|
+
this.emit(runMeta.userId, 'run:stopping', { runId, reason: stopReason });
|
|
1712
1929
|
for (const pid of runMeta.toolPids) {
|
|
1713
1930
|
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
1714
1931
|
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
@@ -1718,13 +1935,22 @@ class AgentEngine {
|
|
|
1718
1935
|
}
|
|
1719
1936
|
for (const child of delegatedChildren) {
|
|
1720
1937
|
if (child.child_run_id && child.child_run_id !== runId) {
|
|
1721
|
-
this.stopRun(child.child_run_id);
|
|
1938
|
+
this.stopRun(child.child_run_id, stopReason);
|
|
1722
1939
|
}
|
|
1723
1940
|
}
|
|
1724
1941
|
db.prepare(
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1942
|
+
`UPDATE agent_delegations
|
|
1943
|
+
SET status = 'stopped', error = COALESCE(NULLIF(error, ''), ?),
|
|
1944
|
+
updated_at = datetime('now'), completed_at = datetime('now')
|
|
1945
|
+
WHERE parent_run_id = ? AND status = 'running'`
|
|
1946
|
+
).run(stopReason, runId);
|
|
1947
|
+
closeRun(
|
|
1948
|
+
runId,
|
|
1949
|
+
'stopped',
|
|
1950
|
+
{ error: stopReason },
|
|
1951
|
+
['running', 'pausing', 'paused', 'resuming'],
|
|
1952
|
+
);
|
|
1953
|
+
return true;
|
|
1728
1954
|
}
|
|
1729
1955
|
|
|
1730
1956
|
pauseRun(runId, { userId, reason = '' } = {}) {
|
|
@@ -1766,15 +1992,17 @@ class AgentEngine {
|
|
|
1766
1992
|
return true;
|
|
1767
1993
|
}
|
|
1768
1994
|
|
|
1769
|
-
abort(runId, { userId } = {}) {
|
|
1995
|
+
abort(runId, { userId, reason = 'Stopped by request.' } = {}) {
|
|
1770
1996
|
if (!runId) return false;
|
|
1997
|
+
const runMeta = this.activeRuns.get(runId);
|
|
1998
|
+
const persistedRun = runMeta
|
|
1999
|
+
|| db.prepare('SELECT user_id AS userId, status FROM agent_runs WHERE id = ?').get(runId);
|
|
2000
|
+
if (!persistedRun || TERMINAL_STATUSES.has(persistedRun.status)) return false;
|
|
1771
2001
|
if (userId != null) {
|
|
1772
|
-
// Ownership gate
|
|
1773
|
-
|
|
1774
|
-
if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
|
|
2002
|
+
// Ownership gate applies to both active and persisted runs.
|
|
2003
|
+
if (Number(persistedRun.userId) !== Number(userId)) return false;
|
|
1775
2004
|
}
|
|
1776
|
-
this.stopRun(runId);
|
|
1777
|
-
return true;
|
|
2005
|
+
return this.stopRun(runId, reason);
|
|
1778
2006
|
}
|
|
1779
2007
|
|
|
1780
2008
|
abortAll(userId) {
|