neoagent 2.5.2-beta.8 → 3.0.0

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.
Files changed (37) hide show
  1. package/README.md +5 -1
  2. package/docs/integrations.md +11 -7
  3. package/package.json +2 -2
  4. package/server/public/.last_build_id +1 -1
  5. package/server/public/flutter_bootstrap.js +1 -1
  6. package/server/public/main.dart.js +4 -4
  7. package/server/services/ai/deliverables/artifact_helpers.js +108 -16
  8. package/server/services/ai/deliverables/selector.js +17 -0
  9. package/server/services/ai/engine.js +2 -4724
  10. package/server/services/ai/loop/agent_engine_core.js +1590 -0
  11. package/server/services/ai/loop/blank_recovery.js +37 -0
  12. package/server/services/ai/loop/callbacks.js +151 -0
  13. package/server/services/ai/loop/completion_judge.js +252 -0
  14. package/server/services/ai/loop/conversation_loop.js +2447 -0
  15. package/server/services/ai/loop/delivery_state.js +27 -0
  16. package/server/services/ai/loop/error_recovery.js +38 -0
  17. package/server/services/ai/loop/iteration_budget.js +24 -0
  18. package/server/services/ai/loop/messaging_delivery.js +296 -0
  19. package/server/services/ai/loop/model_io.js +258 -0
  20. package/server/services/ai/loop/progress_classification.js +178 -0
  21. package/server/services/ai/loop/progress_monitor.js +81 -0
  22. package/server/services/ai/loop/run_state.js +356 -0
  23. package/server/services/ai/loop/tool_dispatch.js +231 -0
  24. package/server/services/ai/loopPolicy.js +15 -6
  25. package/server/services/ai/messagingFallback.js +23 -1
  26. package/server/services/ai/preModelCompaction.js +31 -0
  27. package/server/services/ai/repetitionGuard.js +47 -2
  28. package/server/services/ai/systemPrompt.js +6 -0
  29. package/server/services/ai/taskAnalysis.js +10 -0
  30. package/server/services/ai/toolEvidence.js +15 -3
  31. package/server/services/ai/toolResult.js +29 -0
  32. package/server/services/ai/toolSelector.js +20 -3
  33. package/server/services/ai/tools.js +196 -26
  34. package/server/services/integrations/github/common.js +2 -2
  35. package/server/services/integrations/github/repos.js +123 -55
  36. package/server/services/runtime/docker-vm-manager.js +21 -1
  37. package/server/services/workspace/manager.js +56 -0
@@ -0,0 +1,1590 @@
1
+ 'use strict';
2
+
3
+ const { v4: uuidv4 } = require('uuid');
4
+ const fs = require('fs');
5
+ const db = require('../../../db/database');
6
+ const {
7
+ getConversationContext,
8
+ } = require('../history');
9
+ const { ensureDefaultAiSettings, getAiSettings } = require('../settings');
10
+ const {
11
+ buildAnalysisPrompt,
12
+ buildPlanPrompt,
13
+ buildVerifierPrompt,
14
+ normalizeExecutionPlan,
15
+ normalizeTaskAnalysis,
16
+ normalizeVerificationResult,
17
+ parseJsonObject,
18
+ } = require('../taskAnalysis');
19
+ const { summarizeCapabilityHealth } = require('../capabilityHealth');
20
+ const { shouldAcceptTaskComplete } = require('../completion');
21
+ const { shortenRunId, summarizeForLog } = require('../logFormat');
22
+ const { runConversation } = require('./conversation_loop');
23
+ const {
24
+ buildCompletionDecisionPrompt,
25
+ normalizeCompletionDecision,
26
+ resolveRunGoalContext,
27
+ } = require('./completion_judge');
28
+ const {
29
+ activateToolsForRun: activateToolsForRunImpl,
30
+ applyQueuedSteering: applyQueuedSteeringImpl,
31
+ applyQueuedSystemSteering: applyQueuedSystemSteeringImpl,
32
+ attachProcessToRun: attachProcessToRunImpl,
33
+ buildProgressLedgerSnapshot: buildProgressLedgerSnapshotImpl,
34
+ detachProcessFromRun: detachProcessFromRunImpl,
35
+ enqueueSteering: enqueueSteeringImpl,
36
+ enqueueSystemSteering: enqueueSystemSteeringImpl,
37
+ findActiveRunForUser: findActiveRunForUserImpl,
38
+ findSteerableRunForUser: findSteerableRunForUserImpl,
39
+ getActiveTools: getActiveToolsImpl,
40
+ initializeToolRuntime: initializeToolRuntimeImpl,
41
+ isRunStopped: isRunStoppedImpl,
42
+ markRunFinalDelivery: markRunFinalDeliveryImpl,
43
+ markRunVisibleProgress: markRunVisibleProgressImpl,
44
+ persistProgressLedger: persistProgressLedgerImpl,
45
+ persistRunMetadata: persistRunMetadataImpl,
46
+ recordRunEventSafe,
47
+ updateRunGoalContract: updateRunGoalContractImpl,
48
+ updateRunProgress: updateRunProgressImpl,
49
+ } = require('./run_state');
50
+ const {
51
+ deliverMessagingFinalFallback: deliverMessagingFinalFallbackImpl,
52
+ sendRuntimeMessagingHeartbeat: sendRuntimeMessagingHeartbeatImpl,
53
+ shouldSendMessagingFinalFallback: shouldSendMessagingFinalFallbackImpl,
54
+ startMessagingProgressSupervisor: startMessagingProgressSupervisorImpl,
55
+ stopMessagingProgressSupervisor: stopMessagingProgressSupervisorImpl,
56
+ tickMessagingProgressSupervisor: tickMessagingProgressSupervisorImpl,
57
+ } = require('./messaging_delivery');
58
+ const {
59
+ requestModelResponse: requestModelResponseImpl,
60
+ requestStructuredJson: requestStructuredJsonImpl,
61
+ withModelCallTimeout,
62
+ } = require('./model_io');
63
+ const {
64
+ publishInterimUpdate: publishInterimUpdateImpl,
65
+ } = require('./callbacks');
66
+ const {
67
+ executeReadOnlyBatch: executeReadOnlyBatchImpl,
68
+ executeTool: executeToolImpl,
69
+ getAvailableTools: getAvailableToolsImpl,
70
+ isReadOnlyToolCall: isReadOnlyToolCallImpl,
71
+ } = require('./tool_dispatch');
72
+ const {
73
+ normalizeOutgoingMessage,
74
+ clampRunContext,
75
+ } = require('../messagingFallback');
76
+ const {
77
+ summarizeToolExecutions,
78
+ } = require('../toolEvidence');
79
+ const {
80
+ buildMemoryConsolidationInstructions,
81
+ normalizeMemoryCandidates,
82
+ } = require('../../memory/consolidation');
83
+ const {
84
+ buildPlannerPrompt,
85
+ buildRerankerPrompt,
86
+ mergeRetrievalResults,
87
+ normalizeRerankResult,
88
+ normalizeRetrievalPlan,
89
+ shouldEnhanceRetrieval,
90
+ } = require('../../memory/retrieval_reasoning');
91
+
92
+ function buildInitialRunMetadata(options = {}) {
93
+ const metadata = {};
94
+ if (options.taskId != null && String(options.taskId).trim()) {
95
+ metadata.taskId = options.taskId;
96
+ }
97
+ if (options.widgetId != null && String(options.widgetId).trim()) {
98
+ metadata.widgetId = options.widgetId;
99
+ }
100
+ return metadata;
101
+ }
102
+
103
+ function isoNow() {
104
+ return new Date().toISOString();
105
+ }
106
+
107
+ function planningDepthForForceMode(forceMode) {
108
+ return forceMode === 'plan_execute' ? 'deep' : 'light';
109
+ }
110
+
111
+ function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
112
+ return {
113
+ mode: forceMode || 'execute',
114
+ verification_need: 'light',
115
+ planning_depth: planningDepthForForceMode(forceMode),
116
+ goal: userMessage ? String(userMessage).trim().slice(0, 300) : '',
117
+ complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
118
+ autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
119
+ progress_update_policy: 'optional',
120
+ parallel_work: false,
121
+ completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
122
+ };
123
+ }
124
+
125
+ async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
126
+ const { getSupportedModels, createProviderInstance } = require('../models');
127
+ const agentId = providerConfig.agentId || null;
128
+ const aiSettings = getAiSettings(userId, agentId);
129
+ const models = await getSupportedModels(userId, agentId);
130
+
131
+ let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
132
+ const defaultChatModel = aiSettings.default_chat_model || 'auto';
133
+ const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
134
+ const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
135
+
136
+ const knownModelIds = new Set(models.map((m) => m.id));
137
+ const selectableModels = models.filter((m) => m.available !== false);
138
+
139
+ enabledIds = Array.isArray(enabledIds)
140
+ ? enabledIds
141
+ .map((id) => String(id))
142
+ .filter((id) => knownModelIds.has(id))
143
+ : [];
144
+
145
+ let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
146
+ if (availableModels.length === 0) {
147
+ enabledIds = selectableModels.map((m) => m.id);
148
+ availableModels = [...selectableModels];
149
+ }
150
+
151
+ const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
152
+
153
+ if (!fallbackModel) {
154
+ throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
155
+ }
156
+
157
+ let selectedModelDef = fallbackModel;
158
+ const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
159
+
160
+ if (modelOverride && typeof modelOverride === 'string') {
161
+ const requested = models.find((m) => m.id === modelOverride.trim());
162
+ if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
163
+ selectedModelDef = requested;
164
+ return {
165
+ provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
166
+ model: selectedModelDef.id,
167
+ providerName: selectedModelDef.provider
168
+ };
169
+ }
170
+ }
171
+
172
+ if (userSelectedDefault && userSelectedDefault !== 'auto') {
173
+ selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
174
+ } else {
175
+ const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
176
+ ? providerConfig.selectionHint
177
+ : {};
178
+ const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
179
+ const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
180
+ const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
181
+ const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
182
+ const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
183
+ ? preferredPurpose
184
+ : '';
185
+ const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
186
+ const chooseForPurpose = (purpose) => {
187
+ const candidates = availableModels.filter((model) => model.purpose === purpose);
188
+ if (candidates.length === 0) return null;
189
+ if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
190
+ return [...candidates].sort((left, right) => (
191
+ (priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
192
+ ))[0];
193
+ }
194
+ if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
195
+ return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
196
+ }
197
+ return candidates[0];
198
+ };
199
+
200
+ if (smarterSelection && requestedPurpose) {
201
+ selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
202
+ } else if (smarterSelection && highAutonomy) {
203
+ selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
204
+ } else if (isSubagent) {
205
+ selectedModelDef = chooseForPurpose('fast') || fallbackModel;
206
+ } else {
207
+ selectedModelDef = chooseForPurpose('general') || fallbackModel;
208
+ }
209
+ }
210
+
211
+ return {
212
+ provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
213
+ model: selectedModelDef.id,
214
+ providerName: selectedModelDef.provider
215
+ };
216
+ }
217
+
218
+ function estimateTokenValue(value) {
219
+ if (!value) return 0;
220
+ if (typeof value === 'string') return Math.ceil(value.length / 4);
221
+ return Math.ceil(JSON.stringify(value).length / 4);
222
+ }
223
+
224
+ class AgentEngine {
225
+ constructor(io, services = {}) {
226
+ this.io = io;
227
+ this.activeRuns = new Map();
228
+ this.subagents = new Map();
229
+ this.app = services.app || null;
230
+ this.browserController = services.browserController || null;
231
+ this.androidController = services.androidController || null;
232
+ this.runtimeManager = services.runtimeManager || null;
233
+ this.workspaceManager = services.workspaceManager || null;
234
+ this.messagingManager = services.messagingManager || null;
235
+ this.mcpManager = services.mcpManager || services.mcpClient || null;
236
+ this.skillRunner = services.skillRunner || null;
237
+ this.taskRuntime = services.taskRuntime || null;
238
+ this.memoryManager = services.memoryManager || null;
239
+ this.voiceRuntimeManager = services.voiceRuntimeManager || null;
240
+ this.messagingDeliveryRetry = services.messagingDeliveryRetry || {};
241
+ }
242
+
243
+ async buildSystemPrompt(userId, context = {}) {
244
+ const { buildSystemPromptSections } = require('../systemPrompt');
245
+ const { MemoryManager } = require('../../memory/manager');
246
+ const memoryManager = this.memoryManager || new MemoryManager();
247
+ const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
248
+ const skillRunner = context.skillRunner || this.skillRunner || null;
249
+ const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
250
+ maxTotalChars: 9000,
251
+ maxDescriptionChars: 180,
252
+ maxTriggerChars: 100,
253
+ }) || '';
254
+ return {
255
+ stable: [promptSections.stable, skillsPrompt].filter(Boolean).join('\n\n'),
256
+ dynamic: promptSections.dynamic,
257
+ };
258
+ }
259
+
260
+ async buildMemoryRecall({
261
+ memoryManager,
262
+ userId,
263
+ agentId,
264
+ query,
265
+ provider,
266
+ providerName,
267
+ model,
268
+ runId,
269
+ stepId = null,
270
+ options = {},
271
+ returnDetails = false,
272
+ }) {
273
+ const initial = await memoryManager.recallMemory(userId, query, 12, { agentId });
274
+
275
+ const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
276
+ if (pendingChunks.length) {
277
+ this.extractPendingChunks(pendingChunks, {
278
+ userId,
279
+ agentId,
280
+ provider,
281
+ providerName,
282
+ model,
283
+ memoryManager,
284
+ }).catch((err) => console.warn('[Memory] Background chunk extraction failed:', err.message));
285
+ }
286
+
287
+ const decision = shouldEnhanceRetrieval(initial);
288
+ if (!decision.enhance) {
289
+ const message = await memoryManager.buildRecallMessage(userId, query, {
290
+ agentId,
291
+ recalled: initial.slice(0, 5),
292
+ });
293
+ return returnDetails
294
+ ? { message, results: initial.slice(0, 12), enhanced: false, reason: decision.reason }
295
+ : message;
296
+ }
297
+
298
+ const stats = memoryManager.getMemoryStats?.(userId, { agentId })
299
+ || { total: initial.length };
300
+ if (!Number(stats.total || 0)) {
301
+ return returnDetails
302
+ ? { message: null, results: [], enhanced: false, reason: 'empty_memory' }
303
+ : null;
304
+ }
305
+
306
+ const startedAt = Date.now();
307
+ let plan = null;
308
+ let merged = initial;
309
+ let reranked = initial;
310
+ try {
311
+ const planned = await this.requestStructuredJson({
312
+ provider,
313
+ providerName,
314
+ model,
315
+ messages: [],
316
+ prompt: buildPlannerPrompt(query, initial, new Date().toISOString()),
317
+ maxTokens: 650,
318
+ normalize: (raw) => normalizeRetrievalPlan(raw, query),
319
+ fallback: normalizeRetrievalPlan({}, query),
320
+ reasoningEffort: this.getReasoningEffort(providerName, options),
321
+ telemetry: { runId, stepId, userId, agentId },
322
+ phase: 'memory_retrieval_plan',
323
+ });
324
+ plan = planned.value;
325
+ const resultSets = [initial];
326
+ for (const variant of plan.queryVariants) {
327
+ if (variant === query && initial.length) continue;
328
+ resultSets.push(await memoryManager.recallMemory(userId, variant, 20, {
329
+ agentId,
330
+ validAt: plan.validAt,
331
+ includeHistory: plan.temporalMode === 'historical',
332
+ }));
333
+ }
334
+ merged = mergeRetrievalResults(resultSets, 30);
335
+ if (merged.length > 1) {
336
+ const rerankResponse = await this.requestStructuredJson({
337
+ provider,
338
+ providerName,
339
+ model,
340
+ messages: [],
341
+ prompt: buildRerankerPrompt(query, plan, merged.slice(0, 24)),
342
+ maxTokens: 1200,
343
+ normalize: (raw) => normalizeRerankResult(raw, merged),
344
+ fallback: merged,
345
+ reasoningEffort: this.getReasoningEffort(providerName, options),
346
+ telemetry: { runId, stepId, userId, agentId },
347
+ phase: 'memory_retrieval_rerank',
348
+ });
349
+ reranked = rerankResponse.value;
350
+ } else {
351
+ reranked = merged;
352
+ }
353
+ } catch (error) {
354
+ console.warn('[Memory] Retrieval enhancement failed:', error.message);
355
+ plan = null;
356
+ merged = initial;
357
+ reranked = initial;
358
+ }
359
+
360
+ memoryManager.recordRetrievalEnhancement?.(userId, {
361
+ query,
362
+ reason: decision.reason,
363
+ plan,
364
+ initialCount: initial.length,
365
+ mergedCount: merged.length,
366
+ resultIds: reranked.slice(0, 5).map((result) => result.id),
367
+ latencyMs: Date.now() - startedAt,
368
+ }, { agentId, runId });
369
+
370
+ const message = await memoryManager.buildRecallMessage(userId, query, {
371
+ agentId,
372
+ recalled: reranked.slice(0, 5),
373
+ });
374
+ return returnDetails
375
+ ? {
376
+ message,
377
+ results: reranked.slice(0, 12),
378
+ enhanced: plan !== null,
379
+ reason: decision.reason,
380
+ plan,
381
+ }
382
+ : message;
383
+ }
384
+
385
+ async extractPendingChunks(chunks, {
386
+ userId,
387
+ agentId,
388
+ provider,
389
+ providerName,
390
+ model,
391
+ memoryManager,
392
+ }) {
393
+ const ids = chunks.map((c) => c.id);
394
+ memoryManager.markChunksExtracted?.(ids, { success: true });
395
+
396
+ const consolidationSchema = JSON.stringify({
397
+ memory_candidates: [{
398
+ memory: 'Concise standalone fact.',
399
+ subject: 'Canonical entity or person.',
400
+ predicate: 'Normalized relationship or attribute.',
401
+ object: 'Current atomic value.',
402
+ relation: 'new | updates | extends | derives',
403
+ category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
404
+ confidence: 0.8,
405
+ importance: 5,
406
+ is_static: false,
407
+ valid_from: null,
408
+ valid_to: null,
409
+ forget_after: null,
410
+ evidence: 'Short source-grounded quote.',
411
+ }],
412
+ }, null, 2);
413
+
414
+ for (const chunk of chunks) {
415
+ try {
416
+ const result = await this.requestStructuredJson({
417
+ provider,
418
+ providerName,
419
+ model,
420
+ messages: [],
421
+ prompt: [
422
+ 'Return JSON only. Extract durable memory facts from the document chunk below.',
423
+ buildMemoryConsolidationInstructions(new Date().toISOString()),
424
+ `Source type: ${chunk.sourceType || 'document'}`,
425
+ chunk.title ? `Document title: ${chunk.title}` : '',
426
+ `Content:\n${String(chunk.content || '').slice(0, 2400)}`,
427
+ `Schema:\n${consolidationSchema}`,
428
+ ].filter(Boolean).join('\n\n'),
429
+ maxTokens: 800,
430
+ normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
431
+ fallback: [],
432
+ phase: 'document_extraction',
433
+ });
434
+
435
+ const candidates = Array.isArray(result.value) ? result.value : [];
436
+ if (candidates.length) {
437
+ await memoryManager.consolidateMemoryCandidates(userId, candidates, {
438
+ agentId,
439
+ metadata: {
440
+ trustLevel: 'external_source',
441
+ sourceChunkMemoryId: chunk.id,
442
+ },
443
+ });
444
+ }
445
+ } catch (err) {
446
+ memoryManager.markChunksExtracted?.([chunk.id], { success: false });
447
+ console.warn('[Memory] Document chunk extraction failed:', err.message);
448
+ }
449
+ }
450
+ }
451
+
452
+ persistRunMetadata(runId, patch = {}) {
453
+ return persistRunMetadataImpl(this, runId, patch);
454
+ }
455
+
456
+ updateRunGoalContract(runId, patch = {}, options = {}) {
457
+ return updateRunGoalContractImpl(this, runId, patch, options);
458
+ }
459
+
460
+ buildProgressLedgerSnapshot(runMeta) {
461
+ return buildProgressLedgerSnapshotImpl(this, runMeta);
462
+ }
463
+
464
+ persistProgressLedger(runId) {
465
+ return persistProgressLedgerImpl(this, runId);
466
+ }
467
+
468
+ updateRunProgress(runId, patch = {}, options = {}) {
469
+ return updateRunProgressImpl(this, runId, patch, options);
470
+ }
471
+
472
+ markRunVisibleProgress(runId, timestamp = isoNow()) {
473
+ return markRunVisibleProgressImpl(this, runId, timestamp);
474
+ }
475
+
476
+ markRunFinalDelivery(runId, content = '', timestamp = isoNow()) {
477
+ return markRunFinalDeliveryImpl(this, runId, content, timestamp);
478
+ }
479
+
480
+ recordRunEvent(userId, runId, eventType, payload = {}, options = {}) {
481
+ return recordRunEventSafe(this, userId, runId, eventType, payload, options);
482
+ }
483
+
484
+ async persistDeliverableMemory(userId, runId, agentId, deliverableResult) {
485
+ if (!this.memoryManager?.saveMemory || !deliverableResult?.summary) return;
486
+ try {
487
+ await this.memoryManager.saveMemory(
488
+ userId,
489
+ deliverableResult.summary,
490
+ 'tasks',
491
+ deliverableResult.validation?.status === 'passed' ? 7 : 5,
492
+ {
493
+ agentId,
494
+ sourceRef: {
495
+ sourceType: 'deliverable_run',
496
+ sourceId: runId,
497
+ sourceLabel: deliverableResult.type || 'deliverable',
498
+ },
499
+ metadata: {
500
+ deliverableType: deliverableResult.type,
501
+ status: deliverableResult.status,
502
+ artifactCount: Array.isArray(deliverableResult.artifacts)
503
+ ? deliverableResult.artifacts.length
504
+ : 0,
505
+ artifacts: Array.isArray(deliverableResult.artifacts)
506
+ ? deliverableResult.artifacts.slice(0, 6)
507
+ : [],
508
+ },
509
+ },
510
+ );
511
+ } catch (error) {
512
+ console.error('[Engine] Failed to persist deliverable memory:', error?.message || error);
513
+ }
514
+ }
515
+
516
+ async publishInterimUpdate({
517
+ userId,
518
+ runId,
519
+ agentId = null,
520
+ triggerSource = 'web',
521
+ conversationId = null,
522
+ platform = null,
523
+ chatId = null,
524
+ content,
525
+ kind,
526
+ expectsReply = false,
527
+ deferFollowUp = false,
528
+ } = {}) {
529
+ return publishInterimUpdateImpl(this, {
530
+ userId,
531
+ runId,
532
+ agentId,
533
+ triggerSource,
534
+ conversationId,
535
+ platform,
536
+ chatId,
537
+ content,
538
+ kind,
539
+ expectsReply,
540
+ deferFollowUp,
541
+ });
542
+ }
543
+
544
+ async requestStructuredJson({
545
+ provider,
546
+ providerName,
547
+ model,
548
+ messages,
549
+ prompt,
550
+ maxTokens = 1400,
551
+ normalize,
552
+ fallback = {},
553
+ reasoningEffort,
554
+ telemetry = null,
555
+ phase = 'structured',
556
+ }) {
557
+ return requestStructuredJsonImpl(this, {
558
+ provider,
559
+ providerName,
560
+ model,
561
+ messages,
562
+ prompt,
563
+ maxTokens,
564
+ normalize,
565
+ fallback,
566
+ reasoningEffort,
567
+ telemetry,
568
+ phase,
569
+ });
570
+ }
571
+
572
+ async requestModelResponse({
573
+ provider,
574
+ providerName,
575
+ model,
576
+ messages,
577
+ tools,
578
+ options,
579
+ runId,
580
+ iteration,
581
+ }) {
582
+ return requestModelResponseImpl(this, {
583
+ provider,
584
+ providerName,
585
+ model,
586
+ messages,
587
+ tools,
588
+ options,
589
+ runId,
590
+ iteration,
591
+ });
592
+ }
593
+
594
+ async analyzeTask({
595
+ provider,
596
+ providerName,
597
+ model,
598
+ messages,
599
+ tools,
600
+ capabilityHealth,
601
+ forceMode,
602
+ userMessage,
603
+ options,
604
+ }) {
605
+ const summary = summarizeCapabilityHealth(capabilityHealth);
606
+ const response = await this.requestStructuredJson({
607
+ provider,
608
+ providerName,
609
+ model,
610
+ messages,
611
+ prompt: buildAnalysisPrompt({
612
+ capabilityHealth: summary,
613
+ tools,
614
+ forceMode,
615
+ }),
616
+ maxTokens: 1100,
617
+ normalize: normalizeTaskAnalysis,
618
+ fallback: buildAnalyzeTaskFallback(forceMode, userMessage),
619
+ reasoningEffort: this.getReasoningEffort(providerName, options),
620
+ telemetry: options,
621
+ phase: 'task_analysis',
622
+ });
623
+
624
+ return {
625
+ analysis: response.value,
626
+ raw: response.raw,
627
+ usage: response.usage,
628
+ capabilitySummary: summary,
629
+ };
630
+ }
631
+
632
+ async createExecutionPlan({
633
+ provider,
634
+ providerName,
635
+ model,
636
+ messages,
637
+ analysis,
638
+ capabilitySummary,
639
+ options,
640
+ }) {
641
+ const response = await this.requestStructuredJson({
642
+ provider,
643
+ providerName,
644
+ model,
645
+ messages,
646
+ prompt: buildPlanPrompt(analysis, capabilitySummary),
647
+ maxTokens: 1400,
648
+ normalize: normalizeExecutionPlan,
649
+ fallback: {
650
+ success_criteria: analysis.success_criteria,
651
+ },
652
+ reasoningEffort: this.getReasoningEffort(providerName, options),
653
+ telemetry: options,
654
+ phase: 'execution_plan',
655
+ });
656
+
657
+ return {
658
+ plan: response.value,
659
+ raw: response.raw,
660
+ usage: response.usage,
661
+ };
662
+ }
663
+
664
+ async verifyFinalResponse({
665
+ provider,
666
+ providerName,
667
+ model,
668
+ messages,
669
+ analysis,
670
+ tools,
671
+ toolExecutions,
672
+ finalReply,
673
+ options,
674
+ }) {
675
+ const evidenceSources = [...new Set(
676
+ toolExecutions
677
+ .map((item) => item.evidenceSource)
678
+ .filter(Boolean)
679
+ )];
680
+ const response = await this.requestStructuredJson({
681
+ provider,
682
+ providerName,
683
+ model,
684
+ messages,
685
+ prompt: buildVerifierPrompt({
686
+ analysis,
687
+ tools,
688
+ toolExecutionSummary: summarizeToolExecutions(toolExecutions),
689
+ evidenceSources,
690
+ finalReply,
691
+ }),
692
+ maxTokens: 1200,
693
+ normalize: (raw) => normalizeVerificationResult(raw, finalReply),
694
+ fallback: {
695
+ status: analysis.freshness_risk === 'none' ? 'verified' : 'insufficient_evidence',
696
+ final_reply: finalReply,
697
+ },
698
+ reasoningEffort: this.getReasoningEffort(providerName, options),
699
+ telemetry: options,
700
+ phase: 'verification',
701
+ });
702
+
703
+ return {
704
+ verification: response.value,
705
+ raw: response.raw,
706
+ usage: response.usage,
707
+ evidenceSources,
708
+ };
709
+ }
710
+
711
+ async decideLoopState({
712
+ provider,
713
+ providerName,
714
+ model,
715
+ messages,
716
+ analysis,
717
+ plan,
718
+ tools,
719
+ toolExecutions,
720
+ lastReply,
721
+ iteration,
722
+ maxIterations,
723
+ options,
724
+ messagingSent = false,
725
+ }) {
726
+ const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
727
+ const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
728
+ const response = await this.requestStructuredJson({
729
+ provider,
730
+ providerName,
731
+ model,
732
+ messages,
733
+ prompt: buildCompletionDecisionPrompt({
734
+ triggerSource: options?.triggerSource || 'web',
735
+ messagingSent,
736
+ goalContext,
737
+ parallelWork: analysis?.parallel_work === true,
738
+ tools,
739
+ toolExecutions,
740
+ lastReply,
741
+ iteration,
742
+ maxIterations,
743
+ }),
744
+ maxTokens: 500,
745
+ normalize: (raw) => normalizeCompletionDecision(raw, 'continue'),
746
+ fallback: { status: 'continue', reason: 'completion decision unavailable' },
747
+ reasoningEffort: this.getReasoningEffort(providerName, options),
748
+ telemetry: options,
749
+ phase: 'completion_decision',
750
+ });
751
+ return {
752
+ decision: response.value,
753
+ usage: response.usage,
754
+ raw: response.raw,
755
+ };
756
+ }
757
+
758
+ async evaluateTaskCompleteSignal({
759
+ provider,
760
+ providerName,
761
+ model,
762
+ messages,
763
+ analysis,
764
+ plan,
765
+ tools,
766
+ toolExecutions,
767
+ finalMessage,
768
+ confidence,
769
+ iteration,
770
+ maxIterations,
771
+ options,
772
+ messagingSent = false,
773
+ }) {
774
+ const goalContext = resolveRunGoalContext(this.getRunMeta(options?.runId), analysis, plan);
775
+ const confidenceDecision = shouldAcceptTaskComplete({
776
+ confidence,
777
+ requiredConfidence: goalContext.effectiveCompletionConfidence,
778
+ iteration,
779
+ maxIterations,
780
+ });
781
+ if (!confidenceDecision.accept) {
782
+ return {
783
+ accepted: false,
784
+ status: 'continue',
785
+ reason: confidenceDecision.reason,
786
+ usage: 0,
787
+ };
788
+ }
789
+
790
+ const judged = await this.decideLoopState({
791
+ provider,
792
+ providerName,
793
+ model,
794
+ messages,
795
+ analysis,
796
+ plan,
797
+ tools,
798
+ toolExecutions,
799
+ lastReply: finalMessage,
800
+ iteration,
801
+ maxIterations,
802
+ options,
803
+ messagingSent,
804
+ });
805
+ return {
806
+ accepted: judged.decision.status === 'complete' || judged.decision.status === 'blocked',
807
+ status: judged.decision.status,
808
+ reason: judged.decision.reason,
809
+ usage: judged.usage,
810
+ raw: judged.raw,
811
+ };
812
+ }
813
+
814
+ async refreshConversationState({
815
+ conversationId,
816
+ runId,
817
+ provider,
818
+ providerName,
819
+ model,
820
+ finalReply,
821
+ analysis,
822
+ verification,
823
+ historyWindow,
824
+ options,
825
+ }) {
826
+ if (!conversationId) return null;
827
+ const { MemoryManager } = require('../../memory/manager');
828
+ const memoryManager = this.memoryManager || new MemoryManager();
829
+ const context = getConversationContext(conversationId, Math.max(historyWindow, 8));
830
+ const existingState = memoryManager.getConversationState(conversationId);
831
+ const promptMessages = [
832
+ {
833
+ role: 'system',
834
+ content: [
835
+ 'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
836
+ 'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
837
+ buildMemoryConsolidationInstructions(new Date().toISOString()),
838
+ 'Schema:',
839
+ JSON.stringify({
840
+ summary: '',
841
+ open_commitments: [],
842
+ unresolved_questions: [],
843
+ referenced_entities: [],
844
+ last_verified_facts: [],
845
+ memory_candidates: [{
846
+ memory: 'Concise standalone fact for future context.',
847
+ subject: 'Canonical entity or person.',
848
+ predicate: 'Normalized relationship or attribute.',
849
+ object: 'Current atomic value.',
850
+ relation: 'new | updates | extends | derives',
851
+ category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
852
+ confidence: 0.9,
853
+ importance: 7,
854
+ is_static: false,
855
+ valid_from: null,
856
+ valid_to: null,
857
+ forget_after: null,
858
+ evidence: 'Short source-grounded evidence.',
859
+ }],
860
+ }, null, 2),
861
+ ].join('\n\n')
862
+ },
863
+ {
864
+ role: 'user',
865
+ content: [
866
+ existingState?.summary ? `Existing state:\n${JSON.stringify(existingState, null, 2)}` : 'Existing state: none',
867
+ context.summary ? `Conversation summary:\n${context.summary}` : 'Conversation summary: none',
868
+ `Recent thread messages:\n${JSON.stringify(context.recentMessages.slice(-8), null, 2)}`,
869
+ `Latest final reply:\n${finalReply || '(empty)'}`,
870
+ verification?.status ? `Verification status: ${verification.status}` : '',
871
+ verification?.final_reply && verification.final_reply !== finalReply ? `Verified reply:\n${verification.final_reply}` : '',
872
+ analysis?.goal ? `Thread goal: ${analysis.goal}` : '',
873
+ ].filter(Boolean).join('\n\n')
874
+ }
875
+ ];
876
+
877
+ const response = await withModelCallTimeout(
878
+ provider.chat(promptMessages, [], {
879
+ model,
880
+ maxTokens: 800,
881
+ reasoningEffort: this.getReasoningEffort(providerName, options),
882
+ }),
883
+ options,
884
+ 'Conversation state refresh',
885
+ );
886
+ const parsed = parseJsonObject(response.content || '') || {};
887
+ const nextState = {
888
+ summary: String(parsed.summary || existingState?.summary || '').trim(),
889
+ open_commitments: Array.isArray(parsed.open_commitments) ? parsed.open_commitments.slice(0, 8).map((item) => String(item || '').trim()).filter(Boolean) : [],
890
+ unresolved_questions: Array.isArray(parsed.unresolved_questions) ? parsed.unresolved_questions.slice(0, 8).map((item) => String(item || '').trim()).filter(Boolean) : [],
891
+ referenced_entities: Array.isArray(parsed.referenced_entities) ? parsed.referenced_entities.slice(0, 12).map((item) => String(item || '').trim()).filter(Boolean) : [],
892
+ last_verified_facts: Array.isArray(parsed.last_verified_facts) ? parsed.last_verified_facts.slice(0, 10).map((item) => String(item || '').trim()).filter(Boolean) : [],
893
+ };
894
+
895
+ if (verification?.status === 'verified' && String(finalReply || '').trim()) {
896
+ nextState.last_verified_facts = [...new Set([
897
+ ...nextState.last_verified_facts,
898
+ clampRunContext(verification.final_reply || finalReply, 280),
899
+ ])].slice(-10);
900
+ }
901
+
902
+ memoryManager.updateConversationState(conversationId, nextState);
903
+ const memoryCandidates = normalizeMemoryCandidates(parsed.memory_candidates);
904
+ if (memoryCandidates.length) {
905
+ await memoryManager.consolidateMemoryCandidates(
906
+ options.userId,
907
+ memoryCandidates,
908
+ {
909
+ agentId: options.agentId || null,
910
+ conversationId,
911
+ runId,
912
+ },
913
+ );
914
+ const { invalidateSystemPromptCache } = require('../systemPrompt');
915
+ invalidateSystemPromptCache(options.userId, options.agentId || null);
916
+ }
917
+ return nextState;
918
+ }
919
+
920
+ getAvailableTools(app, options = {}) {
921
+ return getAvailableToolsImpl(this, app, options);
922
+ }
923
+
924
+ async executeTool(toolName, args, context) {
925
+ return executeToolImpl(this, toolName, args, context);
926
+ }
927
+
928
+ isReadOnlyToolCall(toolCall) {
929
+ return isReadOnlyToolCallImpl(this, toolCall);
930
+ }
931
+
932
+ async executeReadOnlyBatch(toolCalls, context = {}) {
933
+ return executeReadOnlyBatchImpl(this, toolCalls, context);
934
+ }
935
+
936
+ async persistRunContext(userId, {
937
+ triggerSource,
938
+ runTitle,
939
+ userMessage,
940
+ lastContent,
941
+ stepIndex,
942
+ skipPersistence = false
943
+ }) {
944
+ if (skipPersistence) {
945
+ return;
946
+ }
947
+ void userId;
948
+ void triggerSource;
949
+ void runTitle;
950
+ void userMessage;
951
+ void lastContent;
952
+ void stepIndex;
953
+ // Run receipts belong in agent_runs/session history, not long-term memory.
954
+ // Long-term memory should only contain durable facts or explicitly saved context.
955
+ return;
956
+ }
957
+
958
+ getRunMeta(runId) {
959
+ return this.activeRuns.get(runId) || null;
960
+ }
961
+
962
+ initializeToolRuntime(runId, allTools, initialTools, options = {}) {
963
+ return initializeToolRuntimeImpl(this, runId, allTools, initialTools, options);
964
+ }
965
+
966
+ getActiveTools(runId) {
967
+ return getActiveToolsImpl(this, runId);
968
+ }
969
+
970
+ activateToolsForRun(runId, names = []) {
971
+ return activateToolsForRunImpl(this, runId, names);
972
+ }
973
+
974
+ findActiveRunForUser(userId, predicate = null) {
975
+ return findActiveRunForUserImpl(this, userId, predicate);
976
+ }
977
+
978
+ findSteerableRunForUser(userId, triggerSource = 'web') {
979
+ return findSteerableRunForUserImpl(this, userId, triggerSource);
980
+ }
981
+
982
+ enqueueSteering(runId, content, metadata = {}) {
983
+ return enqueueSteeringImpl(this, runId, content, metadata);
984
+ }
985
+
986
+ enqueueSystemSteering(runId, content, metadata = {}) {
987
+ return enqueueSystemSteeringImpl(this, runId, content, metadata);
988
+ }
989
+
990
+ applyQueuedSystemSteering(runId, messages) {
991
+ return applyQueuedSystemSteeringImpl(this, runId, messages);
992
+ }
993
+
994
+ applyQueuedSteering(runId, messages, { userId, conversationId }) {
995
+ return applyQueuedSteeringImpl(this, runId, messages, { userId, conversationId });
996
+ }
997
+
998
+ async sendRuntimeMessagingHeartbeat(runId, options = {}) {
999
+ return sendRuntimeMessagingHeartbeatImpl(this, runId, options);
1000
+ }
1001
+
1002
+ shouldSendMessagingFinalFallback(runMeta, content, platform = null) {
1003
+ return shouldSendMessagingFinalFallbackImpl(this, runMeta, content, platform);
1004
+ }
1005
+
1006
+ async deliverMessagingFinalFallback(args) {
1007
+ return deliverMessagingFinalFallbackImpl(this, args);
1008
+ }
1009
+
1010
+ async tickMessagingProgressSupervisor(runId) {
1011
+ return tickMessagingProgressSupervisorImpl(this, runId);
1012
+ }
1013
+
1014
+ startMessagingProgressSupervisor(runId) {
1015
+ return startMessagingProgressSupervisorImpl(this, runId);
1016
+ }
1017
+
1018
+ stopMessagingProgressSupervisor(runId) {
1019
+ return stopMessagingProgressSupervisorImpl(this, runId);
1020
+ }
1021
+
1022
+ isRunStopped(runId) {
1023
+ return isRunStoppedImpl(this, runId);
1024
+ }
1025
+
1026
+ attachProcessToRun(runId, pid) {
1027
+ return attachProcessToRunImpl(this, runId, pid);
1028
+ }
1029
+
1030
+ detachProcessFromRun(runId, pid) {
1031
+ return detachProcessFromRunImpl(this, runId, pid);
1032
+ }
1033
+
1034
+ // getIterationLimit() removed — use buildLoopPolicy() directly.
1035
+ // maxIterations is derived in runWithModel from loopPolicy.maxIterations.
1036
+
1037
+ getReasoningEffort(providerName, options = {}) {
1038
+ if (providerName === 'google') return undefined;
1039
+ if (options.latencyProfile === 'voice') {
1040
+ return 'low';
1041
+ }
1042
+ return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
1043
+ }
1044
+
1045
+ shouldFastCompleteVoiceReply({
1046
+ options = {},
1047
+ toolExecutions = [],
1048
+ failedStepCount = 0,
1049
+ messagingSent = false,
1050
+ lastReply = '',
1051
+ }) {
1052
+ return options.latencyProfile === 'voice'
1053
+ && toolExecutions.length === 0
1054
+ && failedStepCount === 0
1055
+ && !messagingSent
1056
+ && Boolean(String(lastReply || '').trim());
1057
+ }
1058
+
1059
+ getMessagingRetryLimit(maxIterations) {
1060
+ // Cap at 3: more than 3 autonomous messaging retries indicates a structural
1061
+ // problem (model unavailable, bad config) that more retries won't solve.
1062
+ return Math.min(3, Math.max(1, maxIterations));
1063
+ }
1064
+
1065
+ buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
1066
+ const messages = [];
1067
+ if (systemPrompt && typeof systemPrompt === 'object') {
1068
+ if (systemPrompt.stable) {
1069
+ messages.push({
1070
+ role: 'system',
1071
+ content: systemPrompt.stable,
1072
+ });
1073
+ }
1074
+ if (systemPrompt.dynamic) {
1075
+ messages.push({ role: 'system', content: systemPrompt.dynamic });
1076
+ }
1077
+ } else {
1078
+ messages.push({ role: 'system', content: systemPrompt });
1079
+ }
1080
+ if (summaryMessage) messages.push(summaryMessage);
1081
+ if (Array.isArray(historyMessages)) messages.push(...historyMessages);
1082
+ if (recallMsg) messages.push({ role: 'system', content: recallMsg });
1083
+ return messages;
1084
+ }
1085
+
1086
+ buildUserMessage(userMessage, options = {}) {
1087
+ if (!options.mediaAttachments || options.mediaAttachments.length === 0) {
1088
+ return { role: 'user', content: userMessage };
1089
+ }
1090
+
1091
+ const contentArr = [{ type: 'text', text: userMessage }];
1092
+ for (const att of options.mediaAttachments) {
1093
+ if ((att.type === 'image' || att.type === 'video') && att.path) {
1094
+ try {
1095
+ if (fs.existsSync(att.path)) {
1096
+ const b64 = fs.readFileSync(att.path).toString('base64');
1097
+ const mime = att.path.endsWith('.png') ? 'image/png' : att.path.endsWith('.gif') ? 'image/gif' : 'image/jpeg';
1098
+ contentArr.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${b64}` } });
1099
+ }
1100
+ } catch (err) {
1101
+ console.warn(`[AgentEngine] Failed to read attachment at ${att.path}:`, err?.message);
1102
+ }
1103
+ }
1104
+ }
1105
+
1106
+ return { role: 'user', content: contentArr.length > 1 ? contentArr : userMessage };
1107
+ }
1108
+
1109
+ estimatePromptMetrics(messages, tools) {
1110
+ const metrics = {
1111
+ systemPromptTokens: 0,
1112
+ toolSchemaTokens: estimateTokenValue(tools),
1113
+ historyTokens: 0,
1114
+ recalledMemoryTokens: 0,
1115
+ toolReplayTokens: 0,
1116
+ totalEstimatedTokens: 0
1117
+ };
1118
+
1119
+ messages.forEach((msg, index) => {
1120
+ const contentTokens = estimateTokenValue(msg.content);
1121
+ const callTokens = estimateTokenValue(msg.tool_calls);
1122
+ const total = contentTokens + callTokens;
1123
+
1124
+ if (msg.role === 'tool') {
1125
+ metrics.toolReplayTokens += total;
1126
+ } else if (msg.role === 'system' && index === 0) {
1127
+ metrics.systemPromptTokens += total;
1128
+ } else if (msg.role === 'system' && /^\[Recalled memory/.test(msg.content || '')) {
1129
+ metrics.recalledMemoryTokens += total;
1130
+ } else {
1131
+ metrics.historyTokens += total;
1132
+ }
1133
+ });
1134
+
1135
+ metrics.totalEstimatedTokens = metrics.systemPromptTokens
1136
+ + metrics.toolSchemaTokens
1137
+ + metrics.historyTokens
1138
+ + metrics.recalledMemoryTokens
1139
+ + metrics.toolReplayTokens;
1140
+
1141
+ return metrics;
1142
+ }
1143
+
1144
+ mergePromptMetrics(summary, metrics, iteration, toolCount) {
1145
+ return {
1146
+ iterationsObserved: Math.max(summary.iterationsObserved || 0, iteration),
1147
+ toolCount,
1148
+ maxEstimatedTokens: Math.max(summary.maxEstimatedTokens || 0, metrics.totalEstimatedTokens),
1149
+ maxSystemPromptTokens: Math.max(summary.maxSystemPromptTokens || 0, metrics.systemPromptTokens),
1150
+ maxToolSchemaTokens: Math.max(summary.maxToolSchemaTokens || 0, metrics.toolSchemaTokens),
1151
+ maxHistoryTokens: Math.max(summary.maxHistoryTokens || 0, metrics.historyTokens),
1152
+ maxRecalledMemoryTokens: Math.max(summary.maxRecalledMemoryTokens || 0, metrics.recalledMemoryTokens),
1153
+ maxToolReplayTokens: Math.max(summary.maxToolReplayTokens || 0, metrics.toolReplayTokens),
1154
+ lastEstimate: metrics
1155
+ };
1156
+ }
1157
+
1158
+ async persistPromptMetrics(runId, metrics) {
1159
+ db.prepare('UPDATE agent_runs SET prompt_metrics = ? WHERE id = ?')
1160
+ .run(JSON.stringify(metrics), runId);
1161
+ }
1162
+
1163
+ async run(userId, userMessage, options = {}) {
1164
+ return this.runWithModel(
1165
+ userId,
1166
+ userMessage,
1167
+ options,
1168
+ typeof options.model === 'string' && options.model.trim()
1169
+ ? options.model.trim()
1170
+ : null,
1171
+ );
1172
+ }
1173
+
1174
+ async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
1175
+ return runConversation(this, userId, userMessage, options, _modelOverride);
1176
+ }
1177
+
1178
+ async _runWithModelInternal(userId, userMessage, options = {}, _modelOverride = null) {
1179
+ return runConversation(this, userId, userMessage, options, _modelOverride);
1180
+ }
1181
+
1182
+ async spawnSubagent(userId, parentRunId, task, options = {}) {
1183
+ const handle = uuidv4();
1184
+ const childRunId = uuidv4();
1185
+ let relevantMemories = [];
1186
+ try {
1187
+ relevantMemories = this.memoryManager
1188
+ ? await this.memoryManager.recallMemory(userId, task, 4, {
1189
+ agentId: options.agentId || null,
1190
+ })
1191
+ : [];
1192
+ } catch {}
1193
+ const subEngine = new AgentEngine(this.io, {
1194
+ app: options.app || this.app,
1195
+ browserController: this.browserController,
1196
+ androidController: this.androidController,
1197
+ runtimeManager: this.runtimeManager,
1198
+ workspaceManager: this.workspaceManager,
1199
+ messagingManager: this.messagingManager,
1200
+ mcpManager: this.mcpManager,
1201
+ skillRunner: this.skillRunner,
1202
+ taskRuntime: this.taskRuntime,
1203
+ memoryManager: this.memoryManager,
1204
+ });
1205
+
1206
+ const subagentContract = [
1207
+ `Goal: ${String(task || '').trim()}`,
1208
+ options.context ? `Constraints and relevant context:\n${String(options.context).trim()}` : '',
1209
+ relevantMemories.length > 0
1210
+ ? `Top relevant memories: ${JSON.stringify(relevantMemories.map((memory) => ({
1211
+ content: memory.content,
1212
+ confidence: memory.confidence,
1213
+ })))}`
1214
+ : '',
1215
+ Array.isArray(options.requiredArtifacts) && options.requiredArtifacts.length > 0
1216
+ ? `Required artifacts: ${JSON.stringify(options.requiredArtifacts)}`
1217
+ : '',
1218
+ Array.isArray(options.selectedTools) && options.selectedTools.length > 0
1219
+ ? `Selected tools: ${JSON.stringify(options.selectedTools.slice(0, 20))}`
1220
+ : '',
1221
+ 'Return a single JSON object with exactly these top-level fields: status, findings, evidence, artifacts, confidence, remaining_blockers.',
1222
+ 'status must be completed, partial, or blocked. findings, evidence, artifacts, and remaining_blockers must be arrays. confidence must be low, medium, or high.',
1223
+ ].filter(Boolean).join('\n\n');
1224
+ const record = {
1225
+ handle,
1226
+ parentRunId,
1227
+ childRunId,
1228
+ userId,
1229
+ agentId: options.agentId || null,
1230
+ task: subagentContract,
1231
+ model: options.model || null,
1232
+ status: 'running',
1233
+ createdAt: new Date().toISOString(),
1234
+ result: null,
1235
+ error: null,
1236
+ engine: subEngine,
1237
+ promise: null,
1238
+ };
1239
+ this.subagents.set(handle, record);
1240
+ this.emit(userId, 'run:subagent', {
1241
+ runId: parentRunId,
1242
+ handle,
1243
+ childRunId,
1244
+ status: 'running',
1245
+ task: clampRunContext(task, 180),
1246
+ });
1247
+
1248
+ record.promise = (async () => {
1249
+ try {
1250
+ const result = await subEngine.runWithModel(
1251
+ userId,
1252
+ subagentContract,
1253
+ {
1254
+ app: options.app || this.app,
1255
+ triggerType: 'subagent',
1256
+ triggerSource: 'agent',
1257
+ runId: childRunId,
1258
+ agentId: options.agentId || null,
1259
+ },
1260
+ options.model || null
1261
+ );
1262
+ record.status = result.status || 'completed';
1263
+ let structured = null;
1264
+ try {
1265
+ const raw = String(result.content || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
1266
+ const parsed = JSON.parse(raw);
1267
+ if (parsed && typeof parsed === 'object') structured = parsed;
1268
+ } catch {}
1269
+ record.result = {
1270
+ runId: result.runId,
1271
+ status: structured?.status || result.status || 'completed',
1272
+ findings: Array.isArray(structured?.findings) ? structured.findings : [String(result.content || '').trim()].filter(Boolean),
1273
+ evidence: Array.isArray(structured?.evidence) ? structured.evidence : [],
1274
+ artifacts: Array.isArray(structured?.artifacts) ? structured.artifacts : [],
1275
+ confidence: ['low', 'medium', 'high'].includes(structured?.confidence) ? structured.confidence : 'medium',
1276
+ remainingBlockers: Array.isArray(structured?.remaining_blockers) ? structured.remaining_blockers : [],
1277
+ totalTokens: result.totalTokens,
1278
+ iterations: result.iterations,
1279
+ };
1280
+ this.emit(userId, 'run:subagent', {
1281
+ runId: parentRunId,
1282
+ handle,
1283
+ childRunId,
1284
+ status: record.status,
1285
+ result: record.result,
1286
+ });
1287
+ return record;
1288
+ } catch (err) {
1289
+ record.status = 'failed';
1290
+ record.error = err.message;
1291
+ this.emit(userId, 'run:subagent', {
1292
+ runId: parentRunId,
1293
+ handle,
1294
+ childRunId,
1295
+ status: 'failed',
1296
+ error: err.message,
1297
+ });
1298
+ throw err;
1299
+ }
1300
+ })();
1301
+
1302
+ return {
1303
+ handle,
1304
+ status: 'running',
1305
+ childRunId,
1306
+ task: clampRunContext(task, 180),
1307
+ };
1308
+ }
1309
+
1310
+ async delegateToAgent({
1311
+ userId,
1312
+ parentAgentId,
1313
+ parentRunId,
1314
+ target,
1315
+ task,
1316
+ context = '',
1317
+ app = null,
1318
+ allowExternalSideEffects = false,
1319
+ } = {}) {
1320
+ const { agentCanDelegateTo, getAgentById, getAgentBySlug, resolveAgentId } = require('../../agents/manager');
1321
+ const targetText = String(target || '').trim();
1322
+ const taskText = String(task || '').trim();
1323
+ if (!targetText || !taskText) {
1324
+ throw new Error('Target agent and task are required.');
1325
+ }
1326
+
1327
+ let targetAgent = getAgentById(userId, targetText) || getAgentBySlug(userId, targetText);
1328
+ if (!targetAgent) {
1329
+ targetAgent = db.prepare(
1330
+ "SELECT * FROM agents WHERE user_id = ? AND status = 'active' AND lower(display_name) = lower(?)"
1331
+ ).get(userId, targetText);
1332
+ }
1333
+ if (!targetAgent || targetAgent.status !== 'active') {
1334
+ throw new Error(`No active specialist agent matches "${targetText}".`);
1335
+ }
1336
+
1337
+ const scopedParentAgentId = resolveAgentId(userId, parentAgentId);
1338
+ const parentAgent = getAgentById(userId, scopedParentAgentId);
1339
+ if (targetAgent.id === scopedParentAgentId) {
1340
+ throw new Error('An agent cannot delegate to itself.');
1341
+ }
1342
+ if (!agentCanDelegateTo(parentAgent, targetAgent)) {
1343
+ throw new Error(`${parentAgent?.display_name || 'This agent'} is not allowed to delegate tasks to ${targetAgent.display_name}.`);
1344
+ }
1345
+
1346
+ const delegationId = uuidv4();
1347
+ const childRunId = uuidv4();
1348
+ db.prepare(
1349
+ `INSERT INTO agent_delegations (
1350
+ id, user_id, parent_agent_id, target_agent_id, parent_run_id, child_run_id, task, context, status
1351
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running')`
1352
+ ).run(
1353
+ delegationId,
1354
+ userId,
1355
+ scopedParentAgentId,
1356
+ targetAgent.id,
1357
+ parentRunId || null,
1358
+ childRunId,
1359
+ taskText,
1360
+ context || null,
1361
+ );
1362
+
1363
+ const delegatedPrompt = [
1364
+ '[SYSTEM: Delegated specialist-agent task]',
1365
+ `You are running as ${targetAgent.display_name} (${targetAgent.slug}).`,
1366
+ 'Complete this delegated task using only your own agent memory, settings, credentials, and available tools.',
1367
+ allowExternalSideEffects
1368
+ ? 'External side effects are allowed only when they directly satisfy the delegated task.'
1369
+ : 'Do not send external messages, make calls, or change external shared systems. Return findings and recommendations to the parent agent instead.',
1370
+ '',
1371
+ `Task:\n${taskText}`,
1372
+ context ? `\nContext from parent agent:\n${context}` : '',
1373
+ ].filter(Boolean).join('\n');
1374
+
1375
+ try {
1376
+ const result = await this.runWithModel(
1377
+ userId,
1378
+ delegatedPrompt,
1379
+ {
1380
+ app: app || this.app,
1381
+ runId: childRunId,
1382
+ agentId: targetAgent.id,
1383
+ triggerType: 'agent_delegation',
1384
+ triggerSource: 'agent_delegation',
1385
+ skipConversationHistory: true,
1386
+ skipConversationMaintenance: true,
1387
+ context: { additionalContext: `Parent run: ${parentRunId || 'unknown'}` },
1388
+ allowExternalSideEffects,
1389
+ },
1390
+ null,
1391
+ );
1392
+ const summary = String(result?.content || '').trim();
1393
+ db.prepare(
1394
+ `UPDATE agent_delegations
1395
+ SET status = ?, result_summary = ?, updated_at = datetime('now'), completed_at = datetime('now')
1396
+ WHERE id = ?`
1397
+ ).run(result?.status || 'completed', summary.slice(0, 20000), delegationId);
1398
+ return {
1399
+ delegationId,
1400
+ targetAgent: {
1401
+ id: targetAgent.id,
1402
+ slug: targetAgent.slug,
1403
+ name: targetAgent.display_name,
1404
+ },
1405
+ childRunId: result?.runId || childRunId,
1406
+ status: result?.status || 'completed',
1407
+ summary,
1408
+ totalTokens: result?.totalTokens || 0,
1409
+ };
1410
+ } catch (err) {
1411
+ db.prepare(
1412
+ `UPDATE agent_delegations
1413
+ SET status = 'failed', error = ?, updated_at = datetime('now'), completed_at = datetime('now')
1414
+ WHERE id = ?`
1415
+ ).run(String(err?.message || err).slice(0, 20000), delegationId);
1416
+ throw err;
1417
+ }
1418
+ }
1419
+
1420
+ listSubagents(parentRunId = null) {
1421
+ return Array.from(this.subagents.values())
1422
+ .filter((record) => !parentRunId || record.parentRunId === parentRunId)
1423
+ .map((record) => ({
1424
+ handle: record.handle,
1425
+ parentRunId: record.parentRunId,
1426
+ childRunId: record.childRunId,
1427
+ status: record.status,
1428
+ task: clampRunContext(record.task, 180),
1429
+ result: record.result,
1430
+ error: record.error,
1431
+ createdAt: record.createdAt,
1432
+ }));
1433
+ }
1434
+
1435
+ cleanupSubagentsForRun(parentRunId, options = {}) {
1436
+ if (!parentRunId) return;
1437
+ const cancelRunning = options.cancelRunning !== false;
1438
+ for (const [handle, record] of this.subagents.entries()) {
1439
+ if (record.parentRunId !== parentRunId) continue;
1440
+ if (cancelRunning && record.status === 'running') {
1441
+ try {
1442
+ record.engine?.abort(record.childRunId);
1443
+ record.status = 'cancelled';
1444
+ } catch (err) {
1445
+ console.warn(`[AgentEngine] Failed to abort subagent ${handle}:`, err?.message);
1446
+ }
1447
+ }
1448
+ this.subagents.delete(handle);
1449
+ }
1450
+ }
1451
+
1452
+ async waitForSubagent(handle, options = {}) {
1453
+ const record = this.subagents.get(handle);
1454
+ if (!record) {
1455
+ return { error: `Unknown sub-agent handle: ${handle}` };
1456
+ }
1457
+ if (options.parentRunId && record.parentRunId !== options.parentRunId) {
1458
+ return { error: 'That sub-agent does not belong to the current parent run.' };
1459
+ }
1460
+
1461
+ if (record.status !== 'running' || !record.promise) {
1462
+ return {
1463
+ handle,
1464
+ status: record.status,
1465
+ result: record.result,
1466
+ error: record.error,
1467
+ };
1468
+ }
1469
+
1470
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 30000);
1471
+ const timeout = new Promise((resolve) => {
1472
+ setTimeout(() => resolve(null), timeoutMs);
1473
+ });
1474
+ const settled = await Promise.race([
1475
+ record.promise.then(() => record).catch(() => record),
1476
+ timeout,
1477
+ ]);
1478
+
1479
+ if (!settled) {
1480
+ return {
1481
+ handle,
1482
+ status: 'running',
1483
+ timedOut: true,
1484
+ };
1485
+ }
1486
+
1487
+ return {
1488
+ handle,
1489
+ status: record.status,
1490
+ result: record.result,
1491
+ error: record.error,
1492
+ };
1493
+ }
1494
+
1495
+ async cancelSubagent(handle, options = {}) {
1496
+ const record = this.subagents.get(handle);
1497
+ if (!record) {
1498
+ return { error: `Unknown sub-agent handle: ${handle}` };
1499
+ }
1500
+ if (options.parentRunId && record.parentRunId !== options.parentRunId) {
1501
+ return { error: 'That sub-agent does not belong to the current parent run.' };
1502
+ }
1503
+ if (record.status !== 'running') {
1504
+ return {
1505
+ handle,
1506
+ status: record.status,
1507
+ result: record.result,
1508
+ error: record.error,
1509
+ };
1510
+ }
1511
+
1512
+ record.engine?.abort(record.childRunId);
1513
+ record.status = 'cancelled';
1514
+ this.emit(record.userId, 'run:subagent', {
1515
+ runId: record.parentRunId,
1516
+ handle,
1517
+ childRunId: record.childRunId,
1518
+ status: 'cancelled',
1519
+ });
1520
+
1521
+ return { handle, status: 'cancelled' };
1522
+ }
1523
+
1524
+ stopRun(runId) {
1525
+ const runMeta = this.activeRuns.get(runId);
1526
+ const delegatedChildren = db.prepare(
1527
+ "SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
1528
+ ).all(runId);
1529
+ if (runMeta) {
1530
+ runMeta.status = 'stopped';
1531
+ runMeta.aborted = true;
1532
+ this.emit(runMeta.userId, 'run:stopping', { runId });
1533
+ for (const pid of runMeta.toolPids) {
1534
+ if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
1535
+ void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
1536
+ }
1537
+ }
1538
+ runMeta.toolPids.clear();
1539
+ }
1540
+ for (const child of delegatedChildren) {
1541
+ if (child.child_run_id && child.child_run_id !== runId) {
1542
+ this.stopRun(child.child_run_id);
1543
+ }
1544
+ }
1545
+ db.prepare(
1546
+ "UPDATE agent_delegations SET status = 'stopped', updated_at = datetime('now'), completed_at = datetime('now') WHERE parent_run_id = ? AND status = 'running'"
1547
+ ).run(runId);
1548
+ db.prepare("UPDATE agent_runs SET status = 'stopped', updated_at = datetime('now') WHERE id = ?").run(runId);
1549
+ }
1550
+
1551
+ abort(runId, { userId } = {}) {
1552
+ if (!runId) return false;
1553
+ if (userId != null) {
1554
+ // Ownership gate: never let one user abort another user's active run.
1555
+ const runMeta = this.activeRuns.get(runId);
1556
+ if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
1557
+ }
1558
+ this.stopRun(runId);
1559
+ return true;
1560
+ }
1561
+
1562
+ abortAll(userId) {
1563
+ for (const [runId, run] of this.activeRuns) {
1564
+ if (run.userId === userId) this.stopRun(runId);
1565
+ }
1566
+ }
1567
+
1568
+ getStepType(toolName) {
1569
+ if (toolName.startsWith('browser_')) return 'browser';
1570
+ if (toolName.startsWith('android_')) return 'android';
1571
+ if (toolName === 'execute_command') return 'cli';
1572
+ if (toolName.startsWith('memory_')) return 'memory';
1573
+ if (toolName === 'send_interim_update') return 'note';
1574
+ if (toolName === 'send_message') return 'messaging';
1575
+ if (toolName === 'make_call') return 'messaging';
1576
+ if (toolName.startsWith('mcp_') || toolName.includes('mcp')) return 'mcp';
1577
+ if (toolName === 'create_task' || toolName === 'update_task' || toolName === 'delete_task' || toolName === 'list_tasks' || toolName.includes('widget')) return 'tasks';
1578
+ if (toolName.includes('subagent')) return 'subagent';
1579
+ if (toolName === 'think') return 'thinking';
1580
+ return 'tool';
1581
+ }
1582
+
1583
+ emit(userId, event, data) {
1584
+ if (this.io) {
1585
+ this.io.to(`user:${userId}`).emit(event, data);
1586
+ }
1587
+ }
1588
+ }
1589
+
1590
+ module.exports = { AgentEngine, buildInitialRunMetadata, getProviderForUser };