neoagent 3.2.1-beta.0 → 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/docs/agent-run-lifecycle.md +10 -0
- 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 +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- 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/agents.js +35 -2
- 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 +496 -166
- 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 +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- 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 +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -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 +4 -4
- 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 +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- 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,10 +19,21 @@ 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');
|
|
24
|
+
const {
|
|
25
|
+
TERMINAL_STATUSES,
|
|
26
|
+
checkpointRun,
|
|
27
|
+
closeRun,
|
|
28
|
+
getRunControl,
|
|
29
|
+
requestRunControl,
|
|
30
|
+
transitionRun,
|
|
31
|
+
} = require('./lifecycle');
|
|
23
32
|
const {
|
|
24
33
|
buildChurnAssessmentPrompt,
|
|
25
34
|
buildCompletionDecisionPrompt,
|
|
35
|
+
enforceTerminalReplyDecision,
|
|
36
|
+
enforceChurnAssessment,
|
|
26
37
|
normalizeChurnAssessment,
|
|
27
38
|
normalizeCompletionDecision,
|
|
28
39
|
resolveRunGoalContext,
|
|
@@ -60,7 +71,7 @@ const {
|
|
|
60
71
|
const {
|
|
61
72
|
requestModelResponse: requestModelResponseImpl,
|
|
62
73
|
requestStructuredJson: requestStructuredJsonImpl,
|
|
63
|
-
|
|
74
|
+
runAbortableModelCall,
|
|
64
75
|
} = require('./model_io');
|
|
65
76
|
const {
|
|
66
77
|
publishInterimUpdate: publishInterimUpdateImpl,
|
|
@@ -76,6 +87,7 @@ const {
|
|
|
76
87
|
clampRunContext,
|
|
77
88
|
} = require('../messagingFallback');
|
|
78
89
|
const {
|
|
90
|
+
assessResearchAdequacy,
|
|
79
91
|
summarizeToolExecutions,
|
|
80
92
|
} = require('../toolEvidence');
|
|
81
93
|
const {
|
|
@@ -90,6 +102,13 @@ const {
|
|
|
90
102
|
normalizeRetrievalPlan,
|
|
91
103
|
shouldEnhanceRetrieval,
|
|
92
104
|
} = require('../../memory/retrieval_reasoning');
|
|
105
|
+
const {
|
|
106
|
+
createAbortError,
|
|
107
|
+
createLinkedAbortController,
|
|
108
|
+
isAbortError,
|
|
109
|
+
runWithAbortTimeout,
|
|
110
|
+
throwIfAborted,
|
|
111
|
+
} = require('../../../utils/abort');
|
|
93
112
|
|
|
94
113
|
function buildInitialRunMetadata(options = {}) {
|
|
95
114
|
const metadata = {};
|
|
@@ -99,6 +118,9 @@ function buildInitialRunMetadata(options = {}) {
|
|
|
99
118
|
if (options.widgetId != null && String(options.widgetId).trim()) {
|
|
100
119
|
metadata.widgetId = options.widgetId;
|
|
101
120
|
}
|
|
121
|
+
if (options.messagingInboundJobId != null && String(options.messagingInboundJobId).trim()) {
|
|
122
|
+
metadata.messagingInboundJobId = String(options.messagingInboundJobId).trim();
|
|
123
|
+
}
|
|
102
124
|
return metadata;
|
|
103
125
|
}
|
|
104
126
|
|
|
@@ -124,99 +146,6 @@ function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
|
|
|
124
146
|
};
|
|
125
147
|
}
|
|
126
148
|
|
|
127
|
-
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
|
|
128
|
-
const { getSupportedModels, createProviderInstance } = require('../models');
|
|
129
|
-
const agentId = providerConfig.agentId || null;
|
|
130
|
-
const aiSettings = getAiSettings(userId, agentId);
|
|
131
|
-
const models = await getSupportedModels(userId, agentId);
|
|
132
|
-
|
|
133
|
-
let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
|
|
134
|
-
const defaultChatModel = aiSettings.default_chat_model || 'auto';
|
|
135
|
-
const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
|
|
136
|
-
const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
|
|
137
|
-
|
|
138
|
-
const knownModelIds = new Set(models.map((m) => m.id));
|
|
139
|
-
const selectableModels = models.filter((m) => m.available !== false);
|
|
140
|
-
|
|
141
|
-
enabledIds = Array.isArray(enabledIds)
|
|
142
|
-
? enabledIds
|
|
143
|
-
.map((id) => String(id))
|
|
144
|
-
.filter((id) => knownModelIds.has(id))
|
|
145
|
-
: [];
|
|
146
|
-
|
|
147
|
-
let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
|
|
148
|
-
if (availableModels.length === 0) {
|
|
149
|
-
enabledIds = selectableModels.map((m) => m.id);
|
|
150
|
-
availableModels = [...selectableModels];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
|
|
154
|
-
|
|
155
|
-
if (!fallbackModel) {
|
|
156
|
-
throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let selectedModelDef = fallbackModel;
|
|
160
|
-
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
161
|
-
|
|
162
|
-
if (modelOverride && typeof modelOverride === 'string') {
|
|
163
|
-
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
164
|
-
if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
|
|
165
|
-
selectedModelDef = requested;
|
|
166
|
-
return {
|
|
167
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
168
|
-
model: selectedModelDef.id,
|
|
169
|
-
providerName: selectedModelDef.provider
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
175
|
-
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
176
|
-
} else {
|
|
177
|
-
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
178
|
-
? providerConfig.selectionHint
|
|
179
|
-
: {};
|
|
180
|
-
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
181
|
-
const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
|
|
182
|
-
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
183
|
-
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
|
|
184
|
-
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
185
|
-
? preferredPurpose
|
|
186
|
-
: '';
|
|
187
|
-
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
188
|
-
const chooseForPurpose = (purpose) => {
|
|
189
|
-
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
190
|
-
if (candidates.length === 0) return null;
|
|
191
|
-
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
192
|
-
return [...candidates].sort((left, right) => (
|
|
193
|
-
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
194
|
-
))[0];
|
|
195
|
-
}
|
|
196
|
-
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
197
|
-
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
|
|
198
|
-
}
|
|
199
|
-
return candidates[0];
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
if (smarterSelection && requestedPurpose) {
|
|
203
|
-
selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
204
|
-
} else if (smarterSelection && highAutonomy) {
|
|
205
|
-
selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
206
|
-
} else if (isSubagent) {
|
|
207
|
-
selectedModelDef = chooseForPurpose('fast') || fallbackModel;
|
|
208
|
-
} else {
|
|
209
|
-
selectedModelDef = chooseForPurpose('general') || fallbackModel;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
215
|
-
model: selectedModelDef.id,
|
|
216
|
-
providerName: selectedModelDef.provider
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
149
|
function estimateTokenValue(value) {
|
|
221
150
|
if (!value) return 0;
|
|
222
151
|
if (typeof value === 'string') return Math.ceil(value.length / 4);
|
|
@@ -227,6 +156,13 @@ class AgentEngine {
|
|
|
227
156
|
constructor(io, services = {}) {
|
|
228
157
|
this.io = io;
|
|
229
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;
|
|
230
166
|
this.subagents = new Map();
|
|
231
167
|
this.app = services.app || null;
|
|
232
168
|
this.browserController = services.browserController || null;
|
|
@@ -242,6 +178,107 @@ class AgentEngine {
|
|
|
242
178
|
this.messagingDeliveryRetry = services.messagingDeliveryRetry || {};
|
|
243
179
|
}
|
|
244
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
|
+
|
|
245
282
|
async buildSystemPrompt(userId, context = {}) {
|
|
246
283
|
const { buildSystemPromptSections } = require('../systemPrompt');
|
|
247
284
|
const { MemoryManager } = require('../../memory/manager');
|
|
@@ -280,18 +317,34 @@ class AgentEngine {
|
|
|
280
317
|
options = {},
|
|
281
318
|
returnDetails = false,
|
|
282
319
|
}) {
|
|
283
|
-
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
|
+
});
|
|
284
325
|
|
|
285
326
|
const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
|
|
286
327
|
if (pendingChunks.length) {
|
|
287
|
-
this.extractPendingChunks(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
});
|
|
295
348
|
}
|
|
296
349
|
|
|
297
350
|
const decision = shouldEnhanceRetrieval(initial);
|
|
@@ -328,7 +381,7 @@ class AgentEngine {
|
|
|
328
381
|
normalize: (raw) => normalizeRetrievalPlan(raw, query),
|
|
329
382
|
fallback: normalizeRetrievalPlan({}, query),
|
|
330
383
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
331
|
-
telemetry: { runId, stepId, userId, agentId },
|
|
384
|
+
telemetry: { runId, stepId, userId, agentId, signal },
|
|
332
385
|
phase: 'memory_retrieval_plan',
|
|
333
386
|
});
|
|
334
387
|
plan = planned.value;
|
|
@@ -339,6 +392,7 @@ class AgentEngine {
|
|
|
339
392
|
agentId,
|
|
340
393
|
validAt: plan.validAt,
|
|
341
394
|
includeHistory: plan.temporalMode === 'historical',
|
|
395
|
+
signal,
|
|
342
396
|
}));
|
|
343
397
|
}
|
|
344
398
|
merged = mergeRetrievalResults(resultSets, 30);
|
|
@@ -353,7 +407,7 @@ class AgentEngine {
|
|
|
353
407
|
normalize: (raw) => normalizeRerankResult(raw, merged),
|
|
354
408
|
fallback: merged,
|
|
355
409
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
356
|
-
telemetry: { runId, stepId, userId, agentId },
|
|
410
|
+
telemetry: { runId, stepId, userId, agentId, signal },
|
|
357
411
|
phase: 'memory_retrieval_rerank',
|
|
358
412
|
});
|
|
359
413
|
reranked = rerankResponse.value;
|
|
@@ -361,6 +415,7 @@ class AgentEngine {
|
|
|
361
415
|
reranked = merged;
|
|
362
416
|
}
|
|
363
417
|
} catch (error) {
|
|
418
|
+
if (isAbortError(error, signal)) throw error;
|
|
364
419
|
console.warn('[Memory] Retrieval enhancement failed:', error.message);
|
|
365
420
|
plan = null;
|
|
366
421
|
merged = initial;
|
|
@@ -399,10 +454,8 @@ class AgentEngine {
|
|
|
399
454
|
providerName,
|
|
400
455
|
model,
|
|
401
456
|
memoryManager,
|
|
457
|
+
signal = null,
|
|
402
458
|
}) {
|
|
403
|
-
const ids = chunks.map((c) => c.id);
|
|
404
|
-
memoryManager.markChunksExtracted?.(ids, { success: true });
|
|
405
|
-
|
|
406
459
|
const consolidationSchema = JSON.stringify({
|
|
407
460
|
memory_candidates: [{
|
|
408
461
|
memory: 'Concise standalone fact.',
|
|
@@ -423,6 +476,7 @@ class AgentEngine {
|
|
|
423
476
|
|
|
424
477
|
for (const chunk of chunks) {
|
|
425
478
|
try {
|
|
479
|
+
throwIfAborted(signal, 'Memory extraction aborted.');
|
|
426
480
|
const result = await this.requestStructuredJson({
|
|
427
481
|
provider,
|
|
428
482
|
providerName,
|
|
@@ -439,6 +493,7 @@ class AgentEngine {
|
|
|
439
493
|
maxTokens: 800,
|
|
440
494
|
normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
|
|
441
495
|
fallback: [],
|
|
496
|
+
telemetry: { userId, agentId, signal },
|
|
442
497
|
phase: 'document_extraction',
|
|
443
498
|
});
|
|
444
499
|
|
|
@@ -450,10 +505,13 @@ class AgentEngine {
|
|
|
450
505
|
trustLevel: 'external_source',
|
|
451
506
|
sourceChunkMemoryId: chunk.id,
|
|
452
507
|
},
|
|
508
|
+
signal,
|
|
453
509
|
});
|
|
454
510
|
}
|
|
511
|
+
memoryManager.markChunksExtracted?.([chunk.id], { success: true });
|
|
455
512
|
} catch (err) {
|
|
456
513
|
memoryManager.markChunksExtracted?.([chunk.id], { success: false });
|
|
514
|
+
if (isAbortError(err, signal)) throw err;
|
|
457
515
|
console.warn('[Memory] Document chunk extraction failed:', err.message);
|
|
458
516
|
}
|
|
459
517
|
}
|
|
@@ -516,6 +574,7 @@ class AgentEngine {
|
|
|
516
574
|
? deliverableResult.artifacts.slice(0, 6)
|
|
517
575
|
: [],
|
|
518
576
|
},
|
|
577
|
+
signal: this.getRunMeta(runId)?.abortController?.signal || null,
|
|
519
578
|
},
|
|
520
579
|
);
|
|
521
580
|
} catch (error) {
|
|
@@ -735,6 +794,11 @@ class AgentEngine {
|
|
|
735
794
|
}) {
|
|
736
795
|
const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
|
|
737
796
|
const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
|
|
797
|
+
const researchAdequacy = assessResearchAdequacy({
|
|
798
|
+
analysis,
|
|
799
|
+
goalContext,
|
|
800
|
+
toolExecutions,
|
|
801
|
+
});
|
|
738
802
|
const response = await this.requestStructuredJson({
|
|
739
803
|
provider,
|
|
740
804
|
providerName,
|
|
@@ -750,6 +814,8 @@ class AgentEngine {
|
|
|
750
814
|
lastReply,
|
|
751
815
|
iteration,
|
|
752
816
|
maxIterations,
|
|
817
|
+
analysis,
|
|
818
|
+
researchAdequacy,
|
|
753
819
|
}),
|
|
754
820
|
maxTokens: 500,
|
|
755
821
|
normalize: (raw) => normalizeCompletionDecision(raw, 'continue'),
|
|
@@ -759,9 +825,15 @@ class AgentEngine {
|
|
|
759
825
|
phase: 'completion_decision',
|
|
760
826
|
});
|
|
761
827
|
return {
|
|
762
|
-
decision: response.value,
|
|
828
|
+
decision: enforceTerminalReplyDecision(response.value, lastReply, {
|
|
829
|
+
analysis,
|
|
830
|
+
goalContext,
|
|
831
|
+
toolExecutions,
|
|
832
|
+
researchAdequacy,
|
|
833
|
+
}),
|
|
763
834
|
usage: response.usage,
|
|
764
835
|
raw: response.raw,
|
|
836
|
+
researchAdequacy,
|
|
765
837
|
};
|
|
766
838
|
}
|
|
767
839
|
|
|
@@ -847,6 +919,12 @@ class AgentEngine {
|
|
|
847
919
|
goalContext,
|
|
848
920
|
toolExecutions,
|
|
849
921
|
iteration,
|
|
922
|
+
analysis,
|
|
923
|
+
researchAdequacy: assessResearchAdequacy({
|
|
924
|
+
analysis,
|
|
925
|
+
goalContext,
|
|
926
|
+
toolExecutions,
|
|
927
|
+
}),
|
|
850
928
|
}),
|
|
851
929
|
maxTokens: 200,
|
|
852
930
|
normalize: normalizeChurnAssessment,
|
|
@@ -855,9 +933,20 @@ class AgentEngine {
|
|
|
855
933
|
telemetry: options,
|
|
856
934
|
phase: 'churn_assessment',
|
|
857
935
|
});
|
|
936
|
+
const researchAdequacy = assessResearchAdequacy({
|
|
937
|
+
analysis,
|
|
938
|
+
goalContext,
|
|
939
|
+
toolExecutions,
|
|
940
|
+
});
|
|
858
941
|
return {
|
|
859
|
-
assessment: response.value,
|
|
942
|
+
assessment: enforceChurnAssessment(response.value, {
|
|
943
|
+
analysis,
|
|
944
|
+
goalContext,
|
|
945
|
+
toolExecutions,
|
|
946
|
+
researchAdequacy,
|
|
947
|
+
}),
|
|
860
948
|
usage: response.usage,
|
|
949
|
+
researchAdequacy,
|
|
861
950
|
};
|
|
862
951
|
}
|
|
863
952
|
|
|
@@ -924,11 +1013,12 @@ class AgentEngine {
|
|
|
924
1013
|
}
|
|
925
1014
|
];
|
|
926
1015
|
|
|
927
|
-
const response = await
|
|
928
|
-
provider.chat(promptMessages, [], {
|
|
1016
|
+
const response = await runAbortableModelCall(
|
|
1017
|
+
(signal) => provider.chat(promptMessages, [], {
|
|
929
1018
|
model,
|
|
930
1019
|
maxTokens: 800,
|
|
931
1020
|
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1021
|
+
signal,
|
|
932
1022
|
}),
|
|
933
1023
|
options,
|
|
934
1024
|
'Conversation state refresh',
|
|
@@ -959,6 +1049,7 @@ class AgentEngine {
|
|
|
959
1049
|
agentId: options.agentId || null,
|
|
960
1050
|
conversationId,
|
|
961
1051
|
runId,
|
|
1052
|
+
signal: options.signal,
|
|
962
1053
|
},
|
|
963
1054
|
);
|
|
964
1055
|
const { invalidateSystemPromptCache } = require('../systemPrompt');
|
|
@@ -975,8 +1066,8 @@ class AgentEngine {
|
|
|
975
1066
|
return executeToolImpl(this, toolName, args, context);
|
|
976
1067
|
}
|
|
977
1068
|
|
|
978
|
-
isReadOnlyToolCall(toolCall) {
|
|
979
|
-
return isReadOnlyToolCallImpl(
|
|
1069
|
+
isReadOnlyToolCall(toolCall, toolDefinition = null) {
|
|
1070
|
+
return isReadOnlyToolCallImpl(toolCall, toolDefinition);
|
|
980
1071
|
}
|
|
981
1072
|
|
|
982
1073
|
async executeReadOnlyBatch(toolCalls, context = {}) {
|
|
@@ -1073,6 +1164,68 @@ class AgentEngine {
|
|
|
1073
1164
|
return isRunStoppedImpl(this, runId);
|
|
1074
1165
|
}
|
|
1075
1166
|
|
|
1167
|
+
async checkpointLifecycle(runId, phase, state = {}) {
|
|
1168
|
+
const runMeta = this.activeRuns.get(runId);
|
|
1169
|
+
if (!runMeta) return { action: 'stop' };
|
|
1170
|
+
const control = getRunControl(runId);
|
|
1171
|
+
if (!control) return null;
|
|
1172
|
+
if (control.action !== 'pause') return control;
|
|
1173
|
+
|
|
1174
|
+
checkpointRun(runId, phase, {
|
|
1175
|
+
iteration: Number(state.iteration) || 0,
|
|
1176
|
+
stepIndex: Number(state.stepIndex) || 0,
|
|
1177
|
+
currentPhase: runMeta.progressLedger?.currentPhase || phase,
|
|
1178
|
+
activeTools: (runMeta.activeTools || []).map((tool) => tool.name),
|
|
1179
|
+
goalContract: runMeta.goalContract || null,
|
|
1180
|
+
progressLedger: this.buildProgressLedgerSnapshot(runMeta),
|
|
1181
|
+
});
|
|
1182
|
+
transitionRun(runId, 'paused', {}, ['running', 'pausing']);
|
|
1183
|
+
db.transaction(() => {
|
|
1184
|
+
db.prepare(
|
|
1185
|
+
`UPDATE agent_steps
|
|
1186
|
+
SET status = 'paused', error = COALESCE(NULLIF(error, ''), 'Paused by request.'), completed_at = COALESCE(completed_at, datetime('now'))
|
|
1187
|
+
WHERE run_id = ? AND status = 'running'`,
|
|
1188
|
+
).run(runId);
|
|
1189
|
+
db.prepare(
|
|
1190
|
+
`UPDATE pending_approvals
|
|
1191
|
+
SET status = 'expired', decided_at = COALESCE(decided_at, datetime('now')), updated_at = datetime('now')
|
|
1192
|
+
WHERE run_id = ? AND status = 'pending'`,
|
|
1193
|
+
).run(runId);
|
|
1194
|
+
})();
|
|
1195
|
+
runMeta.status = 'paused';
|
|
1196
|
+
runMeta.abortController = null;
|
|
1197
|
+
this.stopMessagingProgressSupervisor(runId);
|
|
1198
|
+
this.emit(runMeta.userId, 'run:paused', { runId, reason: control.reason || null });
|
|
1199
|
+
this.recordRunEvent(runMeta.userId, runId, 'run_paused', {
|
|
1200
|
+
phase,
|
|
1201
|
+
reason: control.reason || null,
|
|
1202
|
+
}, { agentId: runMeta.agentId });
|
|
1203
|
+
|
|
1204
|
+
await new Promise((resolve) => { runMeta.resumeRun = resolve; });
|
|
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
|
+
}
|
|
1215
|
+
runMeta.abortController = new AbortController();
|
|
1216
|
+
runMeta.status = 'running';
|
|
1217
|
+
this.startMessagingProgressSupervisor(runId);
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
completeRun(runId, fields = {}) {
|
|
1222
|
+
return closeRun(runId, 'completed', fields, ['running']);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
failRun(runId, fields = {}) {
|
|
1226
|
+
return closeRun(runId, 'failed', fields, ['running']);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1076
1229
|
attachProcessToRun(runId, pid) {
|
|
1077
1230
|
return attachProcessToRunImpl(this, runId, pid);
|
|
1078
1231
|
}
|
|
@@ -1099,6 +1252,8 @@ class AgentEngine {
|
|
|
1099
1252
|
messagingSent = false,
|
|
1100
1253
|
lastReply = '',
|
|
1101
1254
|
}) {
|
|
1255
|
+
// Voice fast-path is structural only: short reply, no tools/failures.
|
|
1256
|
+
// Content terminality is left to the model completion judge.
|
|
1102
1257
|
return options.latencyProfile === 'voice'
|
|
1103
1258
|
&& toolExecutions.length === 0
|
|
1104
1259
|
&& failedStepCount === 0
|
|
@@ -1222,15 +1377,37 @@ class AgentEngine {
|
|
|
1222
1377
|
}
|
|
1223
1378
|
|
|
1224
1379
|
async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
+
}
|
|
1230
1395
|
}
|
|
1231
1396
|
|
|
1232
1397
|
async spawnSubagent(userId, parentRunId, task, options = {}) {
|
|
1233
|
-
|
|
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
|
+
}
|
|
1234
1411
|
const parentDepth = Math.max(0, Number(parentRunMeta?.subagentDepth) || 0);
|
|
1235
1412
|
if (parentDepth >= 1) {
|
|
1236
1413
|
return {
|
|
@@ -1240,24 +1417,77 @@ class AgentEngine {
|
|
|
1240
1417
|
|
|
1241
1418
|
const aiSettings = getAiSettings(userId, options.agentId || null);
|
|
1242
1419
|
const maxSubagentsPerRun = Math.max(1, Number(aiSettings.subagent_max_children_per_run) || 10);
|
|
1243
|
-
const
|
|
1244
|
-
.filter((record) => record.parentRunId === parentRunId);
|
|
1245
|
-
if (
|
|
1420
|
+
const countExistingSubagents = () => Array.from(this.subagents.values())
|
|
1421
|
+
.filter((record) => record.parentRunId === parentRunId).length;
|
|
1422
|
+
if (countExistingSubagents() >= maxSubagentsPerRun) {
|
|
1246
1423
|
return {
|
|
1247
|
-
error: `This run has already spawned ${
|
|
1424
|
+
error: `This run has already spawned ${countExistingSubagents()} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1248
1425
|
};
|
|
1249
1426
|
}
|
|
1250
1427
|
|
|
1251
1428
|
const handle = uuidv4();
|
|
1252
1429
|
const childRunId = uuidv4();
|
|
1253
1430
|
let relevantMemories = [];
|
|
1431
|
+
const startupLink = createLinkedAbortController([
|
|
1432
|
+
this.lifecycleAbortController.signal,
|
|
1433
|
+
parentRunMeta.abortController?.signal,
|
|
1434
|
+
options.signal,
|
|
1435
|
+
]);
|
|
1436
|
+
let memoryRecall = null;
|
|
1254
1437
|
try {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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.');
|
|
1261
1491
|
const subEngine = new AgentEngine(this.io, {
|
|
1262
1492
|
app: options.app || this.app,
|
|
1263
1493
|
browserController: this.browserController,
|
|
@@ -1303,6 +1533,8 @@ class AgentEngine {
|
|
|
1303
1533
|
error: null,
|
|
1304
1534
|
engine: subEngine,
|
|
1305
1535
|
promise: null,
|
|
1536
|
+
settled: false,
|
|
1537
|
+
deleteWhenSettled: false,
|
|
1306
1538
|
};
|
|
1307
1539
|
this.subagents.set(handle, record);
|
|
1308
1540
|
this.emit(userId, 'run:subagent', {
|
|
@@ -1326,9 +1558,11 @@ class AgentEngine {
|
|
|
1326
1558
|
agentId: options.agentId || null,
|
|
1327
1559
|
subagentDepth: parentDepth + 1,
|
|
1328
1560
|
disallowedToolNames: ['spawn_subagent'],
|
|
1561
|
+
signal: childLink.signal,
|
|
1329
1562
|
},
|
|
1330
1563
|
options.model || null
|
|
1331
1564
|
);
|
|
1565
|
+
if (record.status === 'cancelled' || childLink.signal.aborted) return record;
|
|
1332
1566
|
record.status = result.status || 'completed';
|
|
1333
1567
|
let structured = null;
|
|
1334
1568
|
try {
|
|
@@ -1356,16 +1590,23 @@ class AgentEngine {
|
|
|
1356
1590
|
});
|
|
1357
1591
|
return record;
|
|
1358
1592
|
} catch (err) {
|
|
1359
|
-
record.status
|
|
1360
|
-
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));
|
|
1361
1596
|
this.emit(userId, 'run:subagent', {
|
|
1362
1597
|
runId: parentRunId,
|
|
1363
1598
|
handle,
|
|
1364
1599
|
childRunId,
|
|
1365
|
-
status:
|
|
1366
|
-
error:
|
|
1600
|
+
status: record.status,
|
|
1601
|
+
error: record.error,
|
|
1367
1602
|
});
|
|
1368
|
-
|
|
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
|
+
}
|
|
1369
1610
|
}
|
|
1370
1611
|
})();
|
|
1371
1612
|
|
|
@@ -1502,21 +1743,43 @@ class AgentEngine {
|
|
|
1502
1743
|
}));
|
|
1503
1744
|
}
|
|
1504
1745
|
|
|
1505
|
-
cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
1506
|
-
if (!parentRunId) return;
|
|
1746
|
+
async cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
1747
|
+
if (!parentRunId) return { cancelled: 0, timedOut: 0 };
|
|
1507
1748
|
const cancelRunning = options.cancelRunning !== false;
|
|
1508
|
-
|
|
1749
|
+
const shutdowns = [];
|
|
1750
|
+
const records = [];
|
|
1751
|
+
for (const [handle, record] of Array.from(this.subagents.entries())) {
|
|
1509
1752
|
if (record.parentRunId !== parentRunId) continue;
|
|
1753
|
+
records.push([handle, record]);
|
|
1754
|
+
record.deleteWhenSettled = true;
|
|
1510
1755
|
if (cancelRunning && record.status === 'running') {
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
record.
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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);
|
|
1517
1775
|
}
|
|
1518
|
-
this.subagents.delete(handle);
|
|
1519
1776
|
}
|
|
1777
|
+
return {
|
|
1778
|
+
cancelled: shutdowns.length,
|
|
1779
|
+
timedOut: results.filter((result) => (
|
|
1780
|
+
result.status === 'fulfilled' && result.value?.timedOut === true
|
|
1781
|
+
)).length,
|
|
1782
|
+
};
|
|
1520
1783
|
}
|
|
1521
1784
|
|
|
1522
1785
|
async waitForSubagent(handle, options = {}) {
|
|
@@ -1579,7 +1842,6 @@ class AgentEngine {
|
|
|
1579
1842
|
};
|
|
1580
1843
|
}
|
|
1581
1844
|
|
|
1582
|
-
record.engine?.abort(record.childRunId);
|
|
1583
1845
|
record.status = 'cancelled';
|
|
1584
1846
|
this.emit(record.userId, 'run:subagent', {
|
|
1585
1847
|
runId: record.parentRunId,
|
|
@@ -1588,11 +1850,23 @@ class AgentEngine {
|
|
|
1588
1850
|
status: 'cancelled',
|
|
1589
1851
|
});
|
|
1590
1852
|
|
|
1591
|
-
|
|
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
|
+
};
|
|
1592
1862
|
}
|
|
1593
1863
|
|
|
1594
1864
|
interruptRun(runId, reason = 'Server shutting down while run was in progress.') {
|
|
1595
1865
|
const runMeta = this.activeRuns.get(runId);
|
|
1866
|
+
const persistedRun = runMeta || db.prepare('SELECT user_id AS userId FROM agent_runs WHERE id = ?').get(runId);
|
|
1867
|
+
if (persistedRun?.userId != null) {
|
|
1868
|
+
requestRunControl(runId, persistedRun.userId, 'interrupt', reason);
|
|
1869
|
+
}
|
|
1596
1870
|
const delegatedChildren = db.prepare(
|
|
1597
1871
|
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
1598
1872
|
).all(runId);
|
|
@@ -1600,6 +1874,8 @@ class AgentEngine {
|
|
|
1600
1874
|
runMeta.status = 'interrupted';
|
|
1601
1875
|
runMeta.stopReason = reason;
|
|
1602
1876
|
runMeta.aborted = true;
|
|
1877
|
+
runMeta.abortController?.abort(reason);
|
|
1878
|
+
runMeta.resumeRun?.();
|
|
1603
1879
|
this.emit(runMeta.userId, 'run:stopping', { runId });
|
|
1604
1880
|
for (const pid of runMeta.toolPids) {
|
|
1605
1881
|
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
@@ -1621,14 +1897,7 @@ class AgentEngine {
|
|
|
1621
1897
|
completed_at = datetime('now')
|
|
1622
1898
|
WHERE parent_run_id = ? AND status = 'running'`
|
|
1623
1899
|
).run(reason, runId);
|
|
1624
|
-
|
|
1625
|
-
`UPDATE agent_runs
|
|
1626
|
-
SET status = 'interrupted',
|
|
1627
|
-
error = COALESCE(NULLIF(error, ''), ?),
|
|
1628
|
-
updated_at = datetime('now'),
|
|
1629
|
-
completed_at = datetime('now')
|
|
1630
|
-
WHERE id = ?`
|
|
1631
|
-
).run(reason, runId);
|
|
1900
|
+
closeRun(runId, 'interrupted', { error: reason }, ['running', 'pausing', 'paused', 'resuming']);
|
|
1632
1901
|
}
|
|
1633
1902
|
|
|
1634
1903
|
interruptAllActiveRuns(reason = 'Server shutting down while run was in progress.') {
|
|
@@ -1637,15 +1906,26 @@ class AgentEngine {
|
|
|
1637
1906
|
}
|
|
1638
1907
|
}
|
|
1639
1908
|
|
|
1640
|
-
stopRun(runId) {
|
|
1909
|
+
stopRun(runId, reason = 'Stopped by request.') {
|
|
1910
|
+
const stopReason = String(reason || 'Stopped by request.').slice(0, 1000);
|
|
1641
1911
|
const runMeta = this.activeRuns.get(runId);
|
|
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;
|
|
1916
|
+
if (persistedRun?.userId != null) {
|
|
1917
|
+
requestRunControl(runId, persistedRun.userId, 'stop', stopReason);
|
|
1918
|
+
}
|
|
1642
1919
|
const delegatedChildren = db.prepare(
|
|
1643
1920
|
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
1644
1921
|
).all(runId);
|
|
1645
1922
|
if (runMeta) {
|
|
1646
1923
|
runMeta.status = 'stopped';
|
|
1924
|
+
runMeta.stopReason = stopReason;
|
|
1647
1925
|
runMeta.aborted = true;
|
|
1648
|
-
|
|
1926
|
+
runMeta.abortController?.abort(stopReason);
|
|
1927
|
+
runMeta.resumeRun?.();
|
|
1928
|
+
this.emit(runMeta.userId, 'run:stopping', { runId, reason: stopReason });
|
|
1649
1929
|
for (const pid of runMeta.toolPids) {
|
|
1650
1930
|
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
1651
1931
|
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
@@ -1655,24 +1935,74 @@ class AgentEngine {
|
|
|
1655
1935
|
}
|
|
1656
1936
|
for (const child of delegatedChildren) {
|
|
1657
1937
|
if (child.child_run_id && child.child_run_id !== runId) {
|
|
1658
|
-
this.stopRun(child.child_run_id);
|
|
1938
|
+
this.stopRun(child.child_run_id, stopReason);
|
|
1659
1939
|
}
|
|
1660
1940
|
}
|
|
1661
1941
|
db.prepare(
|
|
1662
|
-
|
|
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;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
pauseRun(runId, { userId, reason = '' } = {}) {
|
|
1957
|
+
if (!runId || userId == null) return false;
|
|
1958
|
+
const runMeta = this.activeRuns.get(runId);
|
|
1959
|
+
if (
|
|
1960
|
+
!runMeta
|
|
1961
|
+
|| Number(runMeta.userId) !== Number(userId)
|
|
1962
|
+
|| runMeta.status !== 'running'
|
|
1963
|
+
|| runMeta.pauseAvailable !== true
|
|
1964
|
+
) return false;
|
|
1965
|
+
const result = requestRunControl(runId, userId, 'pause', reason);
|
|
1966
|
+
if (!result.accepted) return false;
|
|
1967
|
+
transitionRun(runId, 'pausing', {}, ['running']);
|
|
1968
|
+
runMeta.status = 'pausing';
|
|
1969
|
+
runMeta.abortController?.abort('Run paused.');
|
|
1970
|
+
for (const pid of runMeta.toolPids) {
|
|
1971
|
+
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
1972
|
+
void this.runtimeManager.killCommand(runMeta.userId, pid, 'paused');
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
this.emit(runMeta.userId, 'run:pausing', { runId, reason: reason || null });
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
resumeRun(runId, { userId } = {}) {
|
|
1980
|
+
if (!runId || userId == null) return false;
|
|
1981
|
+
const runMeta = this.activeRuns.get(runId);
|
|
1982
|
+
if (!runMeta || Number(runMeta.userId) !== Number(userId) || runMeta.status !== 'paused') return false;
|
|
1983
|
+
if (!transitionRun(runId, 'resuming', {}, ['paused'])) return false;
|
|
1984
|
+
db.prepare(
|
|
1985
|
+
`UPDATE agent_run_controls SET consumed_at = datetime('now')
|
|
1986
|
+
WHERE run_id = ? AND action = 'pause' AND consumed_at IS NULL`,
|
|
1663
1987
|
).run(runId);
|
|
1664
|
-
|
|
1988
|
+
transitionRun(runId, 'running', {}, ['resuming']);
|
|
1989
|
+
this.emit(runMeta.userId, 'run:resumed', { runId });
|
|
1990
|
+
this.recordRunEvent(runMeta.userId, runId, 'run_resumed', {}, { agentId: runMeta.agentId });
|
|
1991
|
+
runMeta.resumeRun?.();
|
|
1992
|
+
return true;
|
|
1665
1993
|
}
|
|
1666
1994
|
|
|
1667
|
-
abort(runId, { userId } = {}) {
|
|
1995
|
+
abort(runId, { userId, reason = 'Stopped by request.' } = {}) {
|
|
1668
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;
|
|
1669
2001
|
if (userId != null) {
|
|
1670
|
-
// Ownership gate
|
|
1671
|
-
|
|
1672
|
-
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;
|
|
1673
2004
|
}
|
|
1674
|
-
this.stopRun(runId);
|
|
1675
|
-
return true;
|
|
2005
|
+
return this.stopRun(runId, reason);
|
|
1676
2006
|
}
|
|
1677
2007
|
|
|
1678
2008
|
abortAll(userId) {
|