neoagent 2.4.2-beta.4 → 2.4.2-beta.6

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 (62) hide show
  1. package/.env.example +0 -9
  2. package/README.md +48 -18
  3. package/docs/configuration.md +0 -10
  4. package/flutter_app/lib/main.dart +0 -1
  5. package/flutter_app/lib/main_controller.dart +0 -160
  6. package/flutter_app/lib/main_models.dart +28 -0
  7. package/flutter_app/lib/main_operations.dart +17 -1
  8. package/flutter_app/lib/main_runtime.dart +0 -22
  9. package/flutter_app/lib/main_settings.dart +9 -0
  10. package/flutter_app/lib/main_shared.dart +0 -161
  11. package/flutter_app/lib/src/backend_client.dart +8 -0
  12. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  13. package/flutter_app/pubspec.lock +0 -8
  14. package/flutter_app/pubspec.yaml +0 -1
  15. package/package.json +1 -1
  16. package/server/admin/admin.css +23 -0
  17. package/server/admin/admin.js +17 -19
  18. package/server/admin/analytics.js +254 -48
  19. package/server/admin/index.html +9 -2
  20. package/server/db/database.js +112 -0
  21. package/server/http/routes.js +1 -0
  22. package/server/public/.last_build_id +1 -1
  23. package/server/public/assets/AssetManifest.bin +1 -1
  24. package/server/public/assets/AssetManifest.bin.json +1 -1
  25. package/server/public/assets/NOTICES +0 -183
  26. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  27. package/server/public/flutter_bootstrap.js +2 -2
  28. package/server/public/main.dart.js +77723 -78469
  29. package/server/routes/admin.js +73 -24
  30. package/server/routes/mcp.js +9 -0
  31. package/server/routes/memory.js +35 -2
  32. package/server/routes/runtime.js +5 -2
  33. package/server/routes/settings.js +34 -1
  34. package/server/routes/skills.js +42 -0
  35. package/server/routes/task_webhooks.js +65 -0
  36. package/server/services/ai/engine.js +525 -24
  37. package/server/services/ai/learning.js +56 -8
  38. package/server/services/ai/models.js +4 -4
  39. package/server/services/ai/providers/anthropic.js +40 -8
  40. package/server/services/ai/providers/google.js +10 -0
  41. package/server/services/ai/providers/openai.js +3 -15
  42. package/server/services/ai/providers/openaiCompatible.js +11 -0
  43. package/server/services/ai/repetitionGuard.js +60 -0
  44. package/server/services/ai/systemPrompt.js +52 -16
  45. package/server/services/ai/taskAnalysis.js +15 -6
  46. package/server/services/ai/toolEvidence.js +3 -1
  47. package/server/services/ai/toolRunner.js +43 -1
  48. package/server/services/ai/toolSelector.js +92 -46
  49. package/server/services/ai/tools.js +89 -9
  50. package/server/services/ai/usage.js +114 -0
  51. package/server/services/manager.js +34 -0
  52. package/server/services/memory/manager.js +170 -4
  53. package/server/services/security/capability_audit.js +107 -0
  54. package/server/services/tasks/adapters/index.js +1 -0
  55. package/server/services/tasks/adapters/webhook.js +14 -0
  56. package/server/services/tasks/runtime.js +1 -1
  57. package/server/services/tasks/webhooks.js +146 -0
  58. package/server/services/workspace/code_navigation.js +194 -0
  59. package/server/services/workspace/structured_data.js +93 -0
  60. package/server/utils/cloud-security.js +24 -2
  61. package/flutter_app/lib/src/analytics_service.dart +0 -294
  62. package/server/config/analytics.js +0 -30
@@ -10,7 +10,12 @@ const {
10
10
  sanitizeConversationMessages
11
11
  } = require('./history');
12
12
  const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
13
- const { selectToolsForTask } = require('./toolSelector');
13
+ const {
14
+ activateTools,
15
+ buildToolCatalog,
16
+ selectInitialTools,
17
+ selectToolsForTask,
18
+ } = require('./toolSelector');
14
19
  const { compactToolResult } = require('./toolResult');
15
20
  const { salvageTextToolCalls } = require('./toolCallSalvage');
16
21
  const { sanitizeModelOutput } = require('./outputSanitizer');
@@ -50,6 +55,8 @@ const { buildLoopPolicy, resolveToolResultLimits } = require('./loopPolicy');
50
55
  const { globalHooks } = require('./hooks');
51
56
  const { withProviderRetry, isTransientError } = require('./providerRetry');
52
57
  const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('./completion');
58
+ const { normalizeUsage, recordModelUsage } = require('./usage');
59
+ const { ToolRepetitionGuard } = require('./repetitionGuard');
53
60
  const { shortenRunId, summarizeForLog, parseMaybeJson } = require('./logFormat');
54
61
  const {
55
62
  normalizeOutgoingMessage,
@@ -209,18 +216,34 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
209
216
  : {};
210
217
  const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
211
218
  const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
219
+ const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
220
+ const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
212
221
  const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
213
222
  ? preferredPurpose
214
223
  : '';
224
+ const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
225
+ const chooseForPurpose = (purpose) => {
226
+ const candidates = availableModels.filter((model) => model.purpose === purpose);
227
+ if (candidates.length === 0) return null;
228
+ if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
229
+ return [...candidates].sort((left, right) => (
230
+ (priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
231
+ ))[0];
232
+ }
233
+ if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
234
+ return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
235
+ }
236
+ return candidates[0];
237
+ };
215
238
 
216
239
  if (smarterSelection && requestedPurpose) {
217
- selectedModelDef = availableModels.find((m) => m.purpose === requestedPurpose) || fallbackModel;
240
+ selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
218
241
  } else if (smarterSelection && highAutonomy) {
219
- selectedModelDef = availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
242
+ selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
220
243
  } else if (isSubagent) {
221
- selectedModelDef = availableModels.find((m) => m.purpose === 'fast') || fallbackModel;
244
+ selectedModelDef = chooseForPurpose('fast') || fallbackModel;
222
245
  } else {
223
- selectedModelDef = availableModels.find((m) => m.purpose === 'general') || fallbackModel;
246
+ selectedModelDef = chooseForPurpose('general') || fallbackModel;
224
247
  }
225
248
  }
226
249
 
@@ -302,17 +325,20 @@ class AgentEngine {
302
325
  }
303
326
 
304
327
  async buildSystemPrompt(userId, context = {}) {
305
- const { buildSystemPrompt } = require('./systemPrompt');
328
+ const { buildSystemPromptSections } = require('./systemPrompt');
306
329
  const { MemoryManager } = require('../memory/manager');
307
330
  const memoryManager = this.memoryManager || new MemoryManager();
308
- const basePrompt = await buildSystemPrompt(userId, context, memoryManager);
331
+ const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
309
332
  const skillRunner = context.skillRunner || this.skillRunner || null;
310
333
  const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
311
334
  maxTotalChars: 9000,
312
335
  maxDescriptionChars: 180,
313
336
  maxTriggerChars: 100,
314
337
  }) || '';
315
- return [basePrompt, skillsPrompt].filter(Boolean).join('\n\n');
338
+ return {
339
+ stable: [promptSections.stable, skillsPrompt].filter(Boolean).join('\n\n'),
340
+ dynamic: promptSections.dynamic,
341
+ };
316
342
  }
317
343
 
318
344
  persistRunMetadata(runId, patch = {}) {
@@ -516,7 +542,10 @@ class AgentEngine {
516
542
  normalize,
517
543
  fallback = {},
518
544
  reasoningEffort,
545
+ telemetry = null,
546
+ phase = 'structured',
519
547
  }) {
548
+ const startedAt = Date.now();
520
549
  const response = await withProviderRetry(
521
550
  () => provider.chat(
522
551
  sanitizeConversationMessages([
@@ -532,12 +561,26 @@ class AgentEngine {
532
561
  ),
533
562
  { label: `Engine ${model} (structured)` }
534
563
  );
564
+ if (telemetry?.runId && telemetry?.userId) {
565
+ recordModelUsage({
566
+ runId: telemetry.runId,
567
+ stepId: telemetry.stepId || null,
568
+ userId: telemetry.userId,
569
+ agentId: telemetry.agentId || null,
570
+ provider: providerName,
571
+ model,
572
+ phase,
573
+ usage: response.usage,
574
+ latencyMs: Date.now() - startedAt,
575
+ });
576
+ }
535
577
 
536
578
  const parsed = parseJsonObject(response.content || '');
579
+ const normalizedUsage = normalizeUsage(response.usage);
537
580
  return {
538
581
  value: normalize(parsed || {}, fallback),
539
582
  raw: response.content || '',
540
- usage: response.usage?.totalTokens || 0,
583
+ usage: normalizedUsage?.totalTokens || 0,
541
584
  };
542
585
  }
543
586
 
@@ -551,6 +594,7 @@ class AgentEngine {
551
594
  runId,
552
595
  iteration,
553
596
  }) {
597
+ const startedAt = Date.now();
554
598
  const requestMessages = sanitizeConversationMessages(messages);
555
599
  const callOptions = {
556
600
  model,
@@ -627,6 +671,20 @@ class AgentEngine {
627
671
  error.code = 'MODEL_EMPTY_RESPONSE';
628
672
  throw error;
629
673
  }
674
+ if (options.runId && options.userId) {
675
+ recordModelUsage({
676
+ runId: options.runId,
677
+ stepId: options.stepId || null,
678
+ userId: options.userId,
679
+ agentId: options.agentId || null,
680
+ provider: providerName,
681
+ model,
682
+ phase: options.phase || 'model_turn',
683
+ usage: resolvedResponse.usage,
684
+ latencyMs: Date.now() - startedAt,
685
+ metadata: { iteration },
686
+ });
687
+ }
630
688
 
631
689
  return {
632
690
  response: resolvedResponse,
@@ -661,6 +719,8 @@ class AgentEngine {
661
719
  normalize: normalizeTaskAnalysis,
662
720
  fallback: buildAnalyzeTaskFallback(forceMode, userMessage),
663
721
  reasoningEffort: this.getReasoningEffort(providerName, options),
722
+ telemetry: options,
723
+ phase: 'task_analysis',
664
724
  });
665
725
 
666
726
  return {
@@ -692,6 +752,8 @@ class AgentEngine {
692
752
  success_criteria: analysis.success_criteria,
693
753
  },
694
754
  reasoningEffort: this.getReasoningEffort(providerName, options),
755
+ telemetry: options,
756
+ phase: 'execution_plan',
695
757
  });
696
758
 
697
759
  return {
@@ -767,6 +829,8 @@ class AgentEngine {
767
829
  },
768
830
  fallback: { status: fallbackStatus },
769
831
  reasoningEffort: this.getReasoningEffort(providerName, options),
832
+ telemetry: options,
833
+ phase: 'loop_decision',
770
834
  });
771
835
 
772
836
  return {
@@ -810,6 +874,8 @@ class AgentEngine {
810
874
  final_reply: finalReply,
811
875
  },
812
876
  reasoningEffort: this.getReasoningEffort(providerName, options),
877
+ telemetry: options,
878
+ phase: 'verification',
813
879
  });
814
880
 
815
881
  return {
@@ -953,6 +1019,212 @@ class AgentEngine {
953
1019
  return executeTool(toolName, args, context, this);
954
1020
  }
955
1021
 
1022
+ isReadOnlyToolCall(toolCall) {
1023
+ const name = String(toolCall?.function?.name || '');
1024
+ const readOnly = new Set([
1025
+ 'read_file',
1026
+ 'list_directory',
1027
+ 'search_files',
1028
+ 'code_navigate',
1029
+ 'query_structured_data',
1030
+ 'memory_recall',
1031
+ 'memory_read',
1032
+ 'session_search',
1033
+ 'web_search',
1034
+ 'list_tasks',
1035
+ 'list_skills',
1036
+ 'list_subagents',
1037
+ 'recordings_list',
1038
+ 'recordings_get',
1039
+ 'recordings_search',
1040
+ 'read_health_data',
1041
+ ]);
1042
+ if (name === 'http_request') {
1043
+ try {
1044
+ const args = JSON.parse(toolCall.function.arguments || '{}');
1045
+ return String(args.method || 'GET').toUpperCase() === 'GET';
1046
+ } catch {
1047
+ return false;
1048
+ }
1049
+ }
1050
+ return readOnly.has(name);
1051
+ }
1052
+
1053
+ async executeReadOnlyBatch(toolCalls, context = {}) {
1054
+ const {
1055
+ userId,
1056
+ runId,
1057
+ agentId,
1058
+ app,
1059
+ triggerType,
1060
+ triggerSource,
1061
+ conversationId,
1062
+ startingStepIndex,
1063
+ options = {},
1064
+ } = context;
1065
+ const prepared = [];
1066
+ let nextStepIndex = startingStepIndex;
1067
+ for (const toolCall of toolCalls) {
1068
+ nextStepIndex += 1;
1069
+ let toolArgs = {};
1070
+ try { toolArgs = JSON.parse(toolCall.function.arguments || '{}'); } catch {}
1071
+ const toolName = toolCall.function.name;
1072
+ const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
1073
+ if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
1074
+ const result = {
1075
+ status: 'blocked',
1076
+ reason: 'The same read-only call already returned an unchanged result twice.',
1077
+ };
1078
+ prepared.push({
1079
+ toolCall,
1080
+ toolName,
1081
+ toolArgs,
1082
+ stepIndex: nextStepIndex,
1083
+ blocked: true,
1084
+ result,
1085
+ error: result.reason,
1086
+ });
1087
+ this.recordRunEvent(userId, runId, 'repetition_blocked', {
1088
+ toolName,
1089
+ toolArgs,
1090
+ parallel: true,
1091
+ }, { agentId });
1092
+ continue;
1093
+ }
1094
+ if (globalHooks.has('before_tool_call')) {
1095
+ const hookResult = await globalHooks.run('before_tool_call', {
1096
+ toolName,
1097
+ toolArgs,
1098
+ runId,
1099
+ userId,
1100
+ agentId,
1101
+ iteration: context.iteration,
1102
+ });
1103
+ if (hookResult.block) {
1104
+ prepared.push({
1105
+ toolCall,
1106
+ toolName,
1107
+ toolArgs,
1108
+ stepIndex: nextStepIndex,
1109
+ blocked: true,
1110
+ result: { status: 'skipped', reason: 'Blocked by policy.' },
1111
+ });
1112
+ continue;
1113
+ }
1114
+ if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
1115
+ }
1116
+ const stepId = uuidv4();
1117
+ db.prepare(
1118
+ `INSERT INTO agent_steps (
1119
+ id, run_id, step_index, type, description, status, tool_name, tool_input, started_at
1120
+ ) VALUES (?, ?, ?, ?, ?, 'running', ?, ?, datetime('now'))`
1121
+ ).run(
1122
+ stepId,
1123
+ runId,
1124
+ nextStepIndex,
1125
+ this.getStepType(toolName),
1126
+ `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)}`,
1127
+ toolName,
1128
+ JSON.stringify(toolArgs),
1129
+ );
1130
+ this.emit(userId, 'run:tool_start', {
1131
+ runId,
1132
+ stepId,
1133
+ stepIndex: nextStepIndex,
1134
+ toolName,
1135
+ toolArgs,
1136
+ type: this.getStepType(toolName),
1137
+ });
1138
+ this.recordRunEvent(userId, runId, 'tool_started', {
1139
+ stepIndex: nextStepIndex,
1140
+ toolName,
1141
+ toolArgs,
1142
+ type: this.getStepType(toolName),
1143
+ parallel: true,
1144
+ }, { agentId, stepId });
1145
+ prepared.push({ toolCall, toolName, toolArgs, stepId, stepIndex: nextStepIndex });
1146
+ }
1147
+ this.recordRunEvent(userId, runId, 'parallel_batch_started', {
1148
+ toolNames: prepared.map((item) => item.toolName),
1149
+ count: prepared.length,
1150
+ }, { agentId });
1151
+ const results = await Promise.all(prepared.map(async (item) => {
1152
+ if (item.blocked) return item;
1153
+ const startedAt = Date.now();
1154
+ try {
1155
+ const result = await this.executeTool(item.toolName, item.toolArgs, {
1156
+ userId,
1157
+ runId,
1158
+ agentId,
1159
+ app,
1160
+ triggerType,
1161
+ triggerSource,
1162
+ conversationId,
1163
+ source: options.source || null,
1164
+ chatId: options.chatId || null,
1165
+ taskId: options.taskId || null,
1166
+ widgetId: options.widgetId || null,
1167
+ deliveryState: options.deliveryState || null,
1168
+ allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
1169
+ allowExternalSideEffects: false,
1170
+ });
1171
+ const error = inferToolFailureMessage(item.toolName, result);
1172
+ const status = error ? 'failed' : 'completed';
1173
+ db.prepare(
1174
+ `UPDATE agent_steps
1175
+ SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime('now')
1176
+ WHERE id = ?`
1177
+ ).run(
1178
+ status,
1179
+ JSON.stringify(result).slice(0, 20000),
1180
+ error || null,
1181
+ result?.screenshotPath || null,
1182
+ item.stepId,
1183
+ );
1184
+ this.emit(userId, 'run:tool_end', {
1185
+ runId,
1186
+ stepId: item.stepId,
1187
+ toolName: item.toolName,
1188
+ result,
1189
+ error: error || undefined,
1190
+ status,
1191
+ });
1192
+ this.recordRunEvent(userId, runId, error ? 'tool_failed' : 'tool_completed', {
1193
+ toolName: item.toolName,
1194
+ status,
1195
+ durationMs: Date.now() - startedAt,
1196
+ resultPreview: summarizeForLog(result),
1197
+ parallel: true,
1198
+ }, { agentId, stepId: item.stepId });
1199
+ return { ...item, result, error };
1200
+ } catch (err) {
1201
+ db.prepare(
1202
+ `UPDATE agent_steps SET status = 'failed', error = ?, completed_at = datetime('now') WHERE id = ?`
1203
+ ).run(err.message, item.stepId);
1204
+ this.emit(userId, 'run:tool_end', {
1205
+ runId,
1206
+ stepId: item.stepId,
1207
+ toolName: item.toolName,
1208
+ error: err.message,
1209
+ status: 'failed',
1210
+ });
1211
+ this.recordRunEvent(userId, runId, 'tool_failed', {
1212
+ toolName: item.toolName,
1213
+ status: 'failed',
1214
+ error: err.message,
1215
+ durationMs: Date.now() - startedAt,
1216
+ parallel: true,
1217
+ }, { agentId, stepId: item.stepId });
1218
+ return { ...item, result: { error: err.message }, error: err.message };
1219
+ }
1220
+ }));
1221
+ this.recordRunEvent(userId, runId, 'parallel_batch_completed', {
1222
+ toolNames: results.map((item) => item.toolName),
1223
+ failedCount: results.filter((item) => item.error).length,
1224
+ }, { agentId });
1225
+ return { results, endingStepIndex: nextStepIndex };
1226
+ }
1227
+
956
1228
  async persistRunContext(userId, {
957
1229
  triggerSource,
958
1230
  runTitle,
@@ -979,6 +1251,47 @@ class AgentEngine {
979
1251
  return this.activeRuns.get(runId) || null;
980
1252
  }
981
1253
 
1254
+ initializeToolRuntime(runId, allTools, initialTools, options = {}) {
1255
+ const runMeta = this.getRunMeta(runId);
1256
+ if (!runMeta) return;
1257
+ runMeta.toolCatalog = Array.isArray(allTools) ? allTools : [];
1258
+ runMeta.activeTools = Array.isArray(initialTools) ? initialTools : [];
1259
+ runMeta.toolSelectionOptions = {
1260
+ widgetId: options.widgetId || null,
1261
+ };
1262
+ }
1263
+
1264
+ getActiveTools(runId) {
1265
+ return this.getRunMeta(runId)?.activeTools || [];
1266
+ }
1267
+
1268
+ activateToolsForRun(runId, names = []) {
1269
+ const runMeta = this.getRunMeta(runId);
1270
+ if (!runMeta) throw new Error('Run is not active.');
1271
+ const result = activateTools(
1272
+ runMeta.activeTools,
1273
+ runMeta.toolCatalog,
1274
+ names,
1275
+ runMeta.toolSelectionOptions,
1276
+ );
1277
+ runMeta.activeTools = result.tools;
1278
+ this.recordRunEvent(runMeta.userId, runId, 'tools_activated', {
1279
+ activated: result.activated,
1280
+ evicted: result.evicted,
1281
+ unknown: result.unknown,
1282
+ notActivated: result.notActivated,
1283
+ activeToolNames: result.tools.map((tool) => tool.name),
1284
+ }, { agentId: runMeta.agentId });
1285
+ return {
1286
+ success: result.unknown.length === 0 && result.notActivated.length === 0,
1287
+ activated: result.activated,
1288
+ evicted: result.evicted,
1289
+ unknown: result.unknown,
1290
+ not_activated: result.notActivated,
1291
+ active_tools: result.tools.map((tool) => tool.name),
1292
+ };
1293
+ }
1294
+
982
1295
  findActiveRunForUser(userId, predicate = null) {
983
1296
  let candidate = null;
984
1297
  for (const [runId, runMeta] of this.activeRuns.entries()) {
@@ -1112,7 +1425,20 @@ class AgentEngine {
1112
1425
  }
1113
1426
 
1114
1427
  buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
1115
- const messages = [{ role: 'system', content: systemPrompt }];
1428
+ const messages = [];
1429
+ if (systemPrompt && typeof systemPrompt === 'object') {
1430
+ if (systemPrompt.stable) {
1431
+ messages.push({
1432
+ role: 'system',
1433
+ content: systemPrompt.stable,
1434
+ });
1435
+ }
1436
+ if (systemPrompt.dynamic) {
1437
+ messages.push({ role: 'system', content: systemPrompt.dynamic });
1438
+ }
1439
+ } else {
1440
+ messages.push({ role: 'system', content: systemPrompt });
1441
+ }
1116
1442
  if (summaryMessage) messages.push(summaryMessage);
1117
1443
  if (Array.isArray(historyMessages)) messages.push(...historyMessages);
1118
1444
  if (recallMsg) messages.push({ role: 'system', content: recallMsg });
@@ -1340,6 +1666,7 @@ class AgentEngine {
1340
1666
  voiceSessionId: options.voiceSessionId || null,
1341
1667
  steeringQueue: [],
1342
1668
  toolPids: new Set(),
1669
+ repetitionGuard: new ToolRepetitionGuard(),
1343
1670
  });
1344
1671
  this.emit(userId, 'run:start', { runId, agentId, title: runTitle, model, triggerType, triggerSource });
1345
1672
  this.recordRunEvent(userId, runId, 'run_started', {
@@ -1371,8 +1698,9 @@ class AgentEngine {
1371
1698
  const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || this.mcpManager;
1372
1699
  const integrationManager = app?.locals?.integrationManager || null;
1373
1700
  const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
1374
- const tools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
1375
- const toolNames = tools.map((tool) => tool.name).filter(Boolean);
1701
+ const allTools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
1702
+ let tools = allTools;
1703
+ const toolNames = allTools.map((tool) => tool.name).filter(Boolean);
1376
1704
  const coreToolStatus = {
1377
1705
  send_message: toolNames.includes('send_message'),
1378
1706
  create_task: toolNames.includes('create_task'),
@@ -1470,7 +1798,7 @@ class AgentEngine {
1470
1798
  capabilityHealth,
1471
1799
  forceMode: options.forceMode || null,
1472
1800
  userMessage,
1473
- options: { ...options, triggerSource },
1801
+ options: { ...options, triggerSource, runId, userId, agentId },
1474
1802
  }));
1475
1803
  analysisUsage = analysisResult.usage || 0;
1476
1804
  totalTokens += analysisUsage;
@@ -1511,6 +1839,25 @@ class AgentEngine {
1511
1839
 
1512
1840
  }
1513
1841
 
1842
+ tools = selectInitialTools(allTools, analysis.suggested_tools, {
1843
+ widgetId: options.widgetId || null,
1844
+ });
1845
+ this.initializeToolRuntime(runId, allTools, tools, options);
1846
+ messages.push({
1847
+ role: 'system',
1848
+ content: [
1849
+ '[Available tool catalog]',
1850
+ buildToolCatalog(allTools),
1851
+ '',
1852
+ `Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
1853
+ 'Use activate_tools with exact catalog names when another schema is required.',
1854
+ ].join('\n'),
1855
+ });
1856
+ this.recordRunEvent(userId, runId, 'tool_selection_applied', {
1857
+ activeToolNames: tools.map((tool) => tool.name),
1858
+ catalogSize: allTools.length,
1859
+ }, { agentId });
1860
+
1514
1861
  const activeDefaultModelSetting = triggerType === 'subagent'
1515
1862
  ? aiSettings.default_subagent_model
1516
1863
  : aiSettings.default_chat_model;
@@ -1532,6 +1879,8 @@ class AgentEngine {
1532
1879
  purpose: requestedPurpose,
1533
1880
  complexity: analysis?.complexity,
1534
1881
  autonomyLevel: analysis?.autonomy_level,
1882
+ requiredConfidence: analysis?.completion_confidence_required,
1883
+ costMode: aiSettings.cost_mode,
1535
1884
  },
1536
1885
  }
1537
1886
  );
@@ -1568,7 +1917,7 @@ class AgentEngine {
1568
1917
  model,
1569
1918
  messages,
1570
1919
  tools,
1571
- options,
1920
+ options: { ...options, runId, userId, agentId },
1572
1921
  });
1573
1922
  totalTokens += deliverableSelectionResult.usage || 0;
1574
1923
  const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
@@ -1589,6 +1938,7 @@ class AgentEngine {
1589
1938
  await selectedWorkflow.run(deliverablePlan, {
1590
1939
  engine: this,
1591
1940
  userId,
1941
+ agentId,
1592
1942
  runId,
1593
1943
  agentId,
1594
1944
  app,
@@ -1616,7 +1966,7 @@ class AgentEngine {
1616
1966
  messages,
1617
1967
  analysis,
1618
1968
  capabilitySummary,
1619
- options,
1969
+ options: { ...options, runId, userId, agentId },
1620
1970
  }));
1621
1971
  totalTokens += planResult.usage || 0;
1622
1972
  plan = planResult.plan;
@@ -1723,7 +2073,7 @@ class AgentEngine {
1723
2073
  model,
1724
2074
  messages,
1725
2075
  tools,
1726
- options: { ...options, userId },
2076
+ options: { ...options, userId, agentId, runId, phase: 'model_turn' },
1727
2077
  runId,
1728
2078
  iteration,
1729
2079
  });
@@ -1899,7 +2249,7 @@ class AgentEngine {
1899
2249
  messagingSent,
1900
2250
  iteration,
1901
2251
  maxIterations,
1902
- options,
2252
+ options: { ...options, runId, userId, agentId },
1903
2253
  fallbackStatus,
1904
2254
  }));
1905
2255
  totalTokens += loopState.usage || 0;
@@ -1920,6 +2270,65 @@ class AgentEngine {
1920
2270
  break;
1921
2271
  }
1922
2272
 
2273
+ const canRunParallelBatch = (
2274
+ response.toolCalls.length > 1
2275
+ && response.toolCalls.every((toolCall) => this.isReadOnlyToolCall(toolCall))
2276
+ );
2277
+ if (canRunParallelBatch) {
2278
+ const batch = await this.executeReadOnlyBatch(response.toolCalls, {
2279
+ userId,
2280
+ runId,
2281
+ agentId,
2282
+ app,
2283
+ triggerType,
2284
+ triggerSource,
2285
+ conversationId,
2286
+ startingStepIndex: stepIndex,
2287
+ iteration,
2288
+ options,
2289
+ });
2290
+ stepIndex = batch.endingStepIndex;
2291
+ for (const item of batch.results) {
2292
+ const execution = classifyToolExecution(
2293
+ item.toolName,
2294
+ item.toolArgs,
2295
+ item.result,
2296
+ item.error || '',
2297
+ );
2298
+ execution.input = item.toolArgs;
2299
+ execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
2300
+ toolExecutions.push(execution);
2301
+ this.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
2302
+ if (item.error) failedStepCount += 1;
2303
+ const modelPayload = compactPayloadForModel(item.toolName, item.result);
2304
+ const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
2305
+ const toolMessage = {
2306
+ role: 'tool',
2307
+ name: item.toolName,
2308
+ tool_call_id: item.toolCall.id,
2309
+ content: compactToolResult(item.toolName, item.toolArgs, modelPayload.result, {
2310
+ softLimit: toolResultLimits.softLimit,
2311
+ hardLimit: toolResultLimits.hardLimit,
2312
+ }),
2313
+ };
2314
+ messages.push(toolMessage);
2315
+ if (conversationId) {
2316
+ db.prepare(
2317
+ `INSERT INTO conversation_messages (
2318
+ conversation_id, role, content, tool_call_id, name
2319
+ ) VALUES (?, 'tool', ?, ?, ?)`
2320
+ ).run(conversationId, toolMessage.content, item.toolCall.id, item.toolName);
2321
+ }
2322
+ }
2323
+ this.persistRunMetadata(runId, {
2324
+ evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
2325
+ subagentState: this.listSubagents(runId),
2326
+ deliverableArtifacts,
2327
+ compactionMetrics: compactionMetrics.slice(-20),
2328
+ });
2329
+ continue;
2330
+ }
2331
+
1923
2332
  for (const toolCall of response.toolCalls) {
1924
2333
  if (this.isRunStopped(runId)) break;
1925
2334
  stepIndex++;
@@ -1983,6 +2392,36 @@ class AgentEngine {
1983
2392
  break; // exit the for-loop; the while condition will also exit
1984
2393
  }
1985
2394
 
2395
+ const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
2396
+ if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
2397
+ const blockedResult = {
2398
+ tool: toolName,
2399
+ status: 'blocked',
2400
+ reason: 'The same tool call already returned an unchanged result twice. Change the approach or complete with the available evidence.',
2401
+ };
2402
+ messages.push({
2403
+ role: 'tool',
2404
+ name: toolName,
2405
+ tool_call_id: toolCall.id,
2406
+ content: JSON.stringify(blockedResult),
2407
+ });
2408
+ this.recordRunEvent(userId, runId, 'repetition_blocked', {
2409
+ toolName,
2410
+ toolArgs,
2411
+ }, { agentId });
2412
+ this.emit(userId, 'run:tool_end', {
2413
+ runId,
2414
+ toolName,
2415
+ status: 'blocked',
2416
+ result: blockedResult,
2417
+ });
2418
+ messages.push({
2419
+ role: 'system',
2420
+ content: 'The repeated call was blocked because it made no progress. Use a different tool, change the arguments, or finish with a truthful result.',
2421
+ });
2422
+ continue;
2423
+ }
2424
+
1986
2425
  // ── before_tool_call hook ──
1987
2426
  // Plugins can block a tool call (e.g. security policy) or mutate args.
1988
2427
  if (globalHooks.has('before_tool_call')) {
@@ -2091,6 +2530,8 @@ class AgentEngine {
2091
2530
  }
2092
2531
 
2093
2532
  const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
2533
+ execution.input = toolArgs;
2534
+ repetitionGuard?.observe(toolName, toolArgs, toolResult);
2094
2535
  execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
2095
2536
  toolExecutions.push(execution);
2096
2537
  if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
@@ -2114,7 +2555,6 @@ class AgentEngine {
2114
2555
  deliverableArtifacts,
2115
2556
  compactionMetrics: compactionMetrics.slice(-20),
2116
2557
  });
2117
-
2118
2558
  const modelPayload = compactPayloadForModel(toolName, toolResult);
2119
2559
  if (modelPayload.metrics?.applied) {
2120
2560
  const metric = {
@@ -2144,6 +2584,9 @@ class AgentEngine {
2144
2584
  })
2145
2585
  };
2146
2586
  messages.push(toolMessage);
2587
+ if (toolName === 'activate_tools' && !toolErrorMessage) {
2588
+ tools = this.getActiveTools(runId);
2589
+ }
2147
2590
 
2148
2591
  if (toolErrorMessage) {
2149
2592
  consecutiveToolFailures += 1;
@@ -2259,7 +2702,7 @@ class AgentEngine {
2259
2702
  provider,
2260
2703
  model,
2261
2704
  providerName,
2262
- options,
2705
+ options: { ...options, runId, userId, agentId },
2263
2706
  stepIndex,
2264
2707
  failedStepCount,
2265
2708
  toolExecutions,
@@ -2330,7 +2773,7 @@ class AgentEngine {
2330
2773
  tools,
2331
2774
  toolExecutions,
2332
2775
  finalReply: finalResponseText,
2333
- options,
2776
+ options: { ...options, runId, userId, agentId },
2334
2777
  }));
2335
2778
  totalTokens += verificationResult.usage || 0;
2336
2779
  verification = verificationResult.verification;
@@ -2502,7 +2945,6 @@ class AgentEngine {
2502
2945
  executionMode: analysis?.mode || 'execute',
2503
2946
  verificationStatus: verification?.status || 'skipped',
2504
2947
  }, { agentId });
2505
-
2506
2948
  // ── on_loop_end hook ──
2507
2949
  // Fire-and-forget: plugins can use this for self-improvement, memory
2508
2950
  // consolidation, analytics, or other post-run housekeeping.
@@ -2514,6 +2956,27 @@ class AgentEngine {
2514
2956
  finalContent: finalResponseText,
2515
2957
  }).catch(() => {});
2516
2958
  }
2959
+ if (this.learningManager) {
2960
+ try {
2961
+ const learningSteps = db.prepare(
2962
+ `SELECT tool_name, tool_input, result, status
2963
+ FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC`
2964
+ ).all(runId);
2965
+ this.learningManager.maybeCaptureDraft({
2966
+ userId,
2967
+ agentId,
2968
+ runId,
2969
+ triggerSource,
2970
+ triggerType,
2971
+ task: userMessage,
2972
+ title: runTitle,
2973
+ finalContent: finalResponseText || lastContent,
2974
+ steps: learningSteps,
2975
+ });
2976
+ } catch (learningError) {
2977
+ console.warn('[Engine] Skill reflection failed:', learningError.message);
2978
+ }
2979
+ }
2517
2980
 
2518
2981
  return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
2519
2982
  } catch (err) {
@@ -2700,6 +3163,15 @@ class AgentEngine {
2700
3163
  async spawnSubagent(userId, parentRunId, task, options = {}) {
2701
3164
  const handle = uuidv4();
2702
3165
  const childRunId = uuidv4();
3166
+ let relevantMemories = [];
3167
+ try {
3168
+ relevantMemories = this.memoryManager
3169
+ ? await this.memoryManager.recallMemory(userId, task, {
3170
+ agentId: options.agentId || null,
3171
+ limit: 4,
3172
+ })
3173
+ : [];
3174
+ } catch {}
2703
3175
  const subEngine = new AgentEngine(this.io, {
2704
3176
  app: options.app || this.app,
2705
3177
  browserController: this.browserController,
@@ -2713,13 +3185,31 @@ class AgentEngine {
2713
3185
  memoryManager: this.memoryManager,
2714
3186
  });
2715
3187
 
3188
+ const subagentContract = [
3189
+ `Goal: ${String(task || '').trim()}`,
3190
+ options.context ? `Constraints and relevant context:\n${String(options.context).trim()}` : '',
3191
+ relevantMemories.length > 0
3192
+ ? `Top relevant memories: ${JSON.stringify(relevantMemories.map((memory) => ({
3193
+ content: memory.content,
3194
+ confidence: memory.confidence,
3195
+ })))}`
3196
+ : '',
3197
+ Array.isArray(options.requiredArtifacts) && options.requiredArtifacts.length > 0
3198
+ ? `Required artifacts: ${JSON.stringify(options.requiredArtifacts)}`
3199
+ : '',
3200
+ Array.isArray(options.selectedTools) && options.selectedTools.length > 0
3201
+ ? `Selected tools: ${JSON.stringify(options.selectedTools.slice(0, 20))}`
3202
+ : '',
3203
+ 'Return a single JSON object with exactly these top-level fields: status, findings, evidence, artifacts, confidence, remaining_blockers.',
3204
+ 'status must be completed, partial, or blocked. findings, evidence, artifacts, and remaining_blockers must be arrays. confidence must be low, medium, or high.',
3205
+ ].filter(Boolean).join('\n\n');
2716
3206
  const record = {
2717
3207
  handle,
2718
3208
  parentRunId,
2719
3209
  childRunId,
2720
3210
  userId,
2721
3211
  agentId: options.agentId || null,
2722
- task,
3212
+ task: subagentContract,
2723
3213
  model: options.model || null,
2724
3214
  status: 'running',
2725
3215
  createdAt: new Date().toISOString(),
@@ -2741,7 +3231,7 @@ class AgentEngine {
2741
3231
  try {
2742
3232
  const result = await subEngine.runWithModel(
2743
3233
  userId,
2744
- task,
3234
+ subagentContract,
2745
3235
  {
2746
3236
  app: options.app || this.app,
2747
3237
  triggerType: 'subagent',
@@ -2752,9 +3242,20 @@ class AgentEngine {
2752
3242
  options.model || null
2753
3243
  );
2754
3244
  record.status = result.status || 'completed';
3245
+ let structured = null;
3246
+ try {
3247
+ const raw = String(result.content || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
3248
+ const parsed = JSON.parse(raw);
3249
+ if (parsed && typeof parsed === 'object') structured = parsed;
3250
+ } catch {}
2755
3251
  record.result = {
2756
3252
  runId: result.runId,
2757
- content: result.content,
3253
+ status: structured?.status || result.status || 'completed',
3254
+ findings: Array.isArray(structured?.findings) ? structured.findings : [String(result.content || '').trim()].filter(Boolean),
3255
+ evidence: Array.isArray(structured?.evidence) ? structured.evidence : [],
3256
+ artifacts: Array.isArray(structured?.artifacts) ? structured.artifacts : [],
3257
+ confidence: ['low', 'medium', 'high'].includes(structured?.confidence) ? structured.confidence : 'medium',
3258
+ remainingBlockers: Array.isArray(structured?.remaining_blockers) ? structured.remaining_blockers : [],
2758
3259
  totalTokens: result.totalTokens,
2759
3260
  iterations: result.iterations,
2760
3261
  };