neoagent 3.2.1-beta.8 → 3.3.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 (70) hide show
  1. package/flutter_app/lib/main_app_shell.dart +178 -34
  2. package/flutter_app/lib/main_chat.dart +542 -80
  3. package/flutter_app/lib/main_controller.dart +59 -4
  4. package/flutter_app/lib/main_models.dart +171 -22
  5. package/flutter_app/lib/main_operations.dart +0 -49
  6. package/flutter_app/lib/main_settings.dart +338 -0
  7. package/flutter_app/lib/src/backend_client.dart +19 -0
  8. package/package.json +1 -1
  9. package/server/db/database.js +2 -2
  10. package/server/http/routes.js +1 -0
  11. package/server/public/.last_build_id +1 -1
  12. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  13. package/server/public/flutter_bootstrap.js +1 -1
  14. package/server/public/main.dart.js +85079 -83904
  15. package/server/routes/behavior.js +80 -0
  16. package/server/routes/settings.js +4 -0
  17. package/server/services/agents/manager.js +1 -1
  18. package/server/services/ai/history.js +1 -1
  19. package/server/services/ai/loop/agent_engine_core.js +131 -18
  20. package/server/services/ai/loop/blank_recovery.js +5 -4
  21. package/server/services/ai/loop/completion_judge.js +280 -18
  22. package/server/services/ai/loop/conversation_loop.js +92 -34
  23. package/server/services/ai/loop/messaging_delivery.js +47 -0
  24. package/server/services/ai/loop/progress_classification.js +2 -0
  25. package/server/services/ai/loopPolicy.js +24 -2
  26. package/server/services/ai/messagingFallback.js +17 -17
  27. package/server/services/ai/model_failure_cache.js +7 -0
  28. package/server/services/ai/systemPrompt.js +31 -122
  29. package/server/services/ai/taskAnalysis.js +101 -12
  30. package/server/services/ai/toolEvidence.js +198 -0
  31. package/server/services/ai/tools.js +60 -19
  32. package/server/services/behavior/config.js +251 -0
  33. package/server/services/behavior/defaults.js +68 -0
  34. package/server/services/behavior/delivery.js +176 -0
  35. package/server/services/behavior/index.js +43 -0
  36. package/server/services/behavior/model_client.js +35 -0
  37. package/server/services/behavior/modules/agent_identity.js +37 -0
  38. package/server/services/behavior/modules/channel_style.js +28 -0
  39. package/server/services/behavior/modules/index.js +29 -0
  40. package/server/services/behavior/modules/norms.js +101 -0
  41. package/server/services/behavior/modules/persona.js +172 -0
  42. package/server/services/behavior/modules/persona_prompt.js +238 -0
  43. package/server/services/behavior/modules/social_memory.js +86 -0
  44. package/server/services/behavior/modules/social_observability.js +94 -0
  45. package/server/services/behavior/modules/social_signals.js +41 -0
  46. package/server/services/behavior/modules/theory_of_mind.js +118 -0
  47. package/server/services/behavior/modules/turn_taking.js +238 -0
  48. package/server/services/behavior/pipeline.js +341 -0
  49. package/server/services/behavior/registry.js +78 -0
  50. package/server/services/behavior/signals.js +107 -0
  51. package/server/services/behavior/state.js +99 -0
  52. package/server/services/behavior/system_prompt.js +75 -0
  53. package/server/services/memory/manager.js +14 -33
  54. package/server/services/messaging/access_policy.js +269 -74
  55. package/server/services/messaging/automation.js +158 -27
  56. package/server/services/messaging/discord.js +11 -1
  57. package/server/services/messaging/formatting_guides.js +5 -4
  58. package/server/services/messaging/http_platforms.js +73 -28
  59. package/server/services/messaging/inbound_queue.js +74 -13
  60. package/server/services/messaging/inbound_store.js +33 -0
  61. package/server/services/messaging/manager.js +57 -5
  62. package/server/services/messaging/telegram.js +10 -1
  63. package/server/services/messaging/whatsapp.js +11 -0
  64. package/server/services/social_reach/channels/social_video.js +1 -1
  65. package/server/services/social_video/captions.js +2 -2
  66. package/server/services/social_video/service.js +194 -29
  67. package/server/services/voice/message.js +1 -1
  68. package/server/services/voice/runtime.js +2 -2
  69. package/server/utils/logger.js +19 -0
  70. package/server/services/ai/terminal_reply.js +0 -57
@@ -4,6 +4,7 @@ const COMPLEXITY_LEVELS = ['simple', 'standard', 'complex'];
4
4
  const AUTONOMY_LEVELS = ['minimal', 'normal', 'high'];
5
5
  const PROGRESS_UPDATE_POLICIES = ['none', 'optional', 'required'];
6
6
  const COMPLETION_CONFIDENCE_LEVELS = ['medium', 'high'];
7
+ const DRAFT_STATUSES = ['final', 'needs_execution', 'needs_user_input'];
7
8
  const TASK_ANALYSIS_SUGGESTED_TOOLS_LIMIT = 12;
8
9
  const TASK_ANALYSIS_SUCCESS_CRITERIA_LIMIT = 8;
9
10
  const PLAN_STEP_SUGGESTED_TOOLS_LIMIT = 8;
@@ -17,13 +18,15 @@ const TASK_ANALYSIS_CONFIDENCE_DEFAULT = 0.55;
17
18
  const VERIFICATION_CONFIDENCE_VERIFIED = 0.85;
18
19
  const VERIFICATION_CONFIDENCE_DEFAULT = 0.5;
19
20
  const JSON_ONLY_RESPONSE_RULE = 'Return JSON only. No markdown, no prose, no code fences.';
20
- const { isDeferredWorkReply } = require('./terminal_reply');
21
21
  const ANALYSIS_SCHEMA_EXAMPLE = {
22
22
  mode: 'execute',
23
23
  needs_verification: true,
24
24
  draft_reply: '',
25
+ draft_status: 'needs_execution',
25
26
  goal: 'Answer the user accurately.',
26
27
  success_criteria: ['Final reply is correct and specific.'],
28
+ research_targets: [],
29
+ research_depth: 'none',
27
30
  suggested_tools: ['web_search', 'browser_navigate'],
28
31
  complexity: 'standard',
29
32
  autonomy_level: 'normal',
@@ -44,11 +47,13 @@ const PLAN_SCHEMA_EXAMPLE = {
44
47
  verification_focus: ['Confirm the most time-sensitive claim before replying.'],
45
48
  };
46
49
  const ANALYSIS_PROMPT_INSTRUCTIONS = [
47
- 'Choose the lightest routing mode that still handles the task well.',
48
- 'Use mode="direct_answer" only when a final user-facing reply can be given immediately without tool work.',
49
- 'For short immediate questions, greetings, small explanations, or quick conversational replies, prefer mode="direct_answer" with progress_update_policy="none"; speed matters more than creating plans or tasks.',
50
+ 'Choose the lightest routing mode that still handles the task well. Prefer speed for simple work and depth only when the request needs tools or multi-step evidence.',
51
+ 'Use mode="direct_answer" only when a final user-facing reply can be given immediately without tool work. The draft_reply must be the actual answer, not a promise to check later.',
52
+ 'Set draft_status="final" only when draft_reply fully answers the request now. Use "needs_execution" when any autonomous work remains, and "needs_user_input" only when the draft asks for information that is genuinely required before work can continue.',
53
+ 'For short immediate questions, greetings, small explanations, quick conversational replies, or pure opinion/advice that does not need live lookup, prefer mode="direct_answer" with progress_update_policy="none", complexity="simple", autonomy_level="minimal", and an empty research_targets list. Speed matters more than creating plans or tasks.',
50
54
  'Use mode="execute" for normal tool-driven work without a separate planning step.',
51
55
  'Use mode="plan_execute" only when the task is genuinely multi-step, broad, or coordination-heavy.',
56
+ 'Never invent entities, products, people, files, or outcomes to fill gaps. If the user refers to multiple targets but only one is known, ask once or research only the known ones and mark the rest unknown.',
52
57
  'Set needs_verification=true when the final answer should be checked against tool evidence before it is sent.',
53
58
  'Set goal to a concise restatement of what the user is asking for in this message. Never leave goal empty.',
54
59
  'Keep goal and success_criteria short and practical.',
@@ -59,6 +64,9 @@ const ANALYSIS_PROMPT_INSTRUCTIONS = [
59
64
  'Do not suggest create_task, update_task, delete_task, or list_tasks for ordinary immediate work. Use task tools only when the user asks for future, recurring, scheduled, monitored, background, or existing-task management behavior.',
60
65
  'Set parallel_work=true when independent tool calls or subagents can materially reduce latency.',
61
66
  'Set completion_confidence_required="high" when wrong completion would be costly, state-changing, user-visible, or hard to recover.',
67
+ 'For research, product/device comparisons, reviews, fact-checks, or multi-entity look-into requests, use mode="execute" or mode="plan_execute", never direct_answer. Set needs_verification=true, freshness_risk="possible" or higher, and completion_confidence_required="high".',
68
+ 'Set research_depth="none" when external source research is not needed, "light" for a focused lookup, and "deep" for comparisons, multi-source synthesis, disputed claims, or broad investigation. This is the authoritative research-routing field; do not infer it from tool names.',
69
+ 'When the user asks to look into multiple devices, products, options, or entities, put each named target into research_targets and success_criteria and keep them exact. Prefer suggested_tools that can open primary sources (web_search plus browser_navigate/http_request) rather than answering from memory.',
62
70
  ];
63
71
  const PLAN_PROMPT_INSTRUCTIONS = [
64
72
  'Create a concise execution plan for the current task.',
@@ -83,9 +91,14 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
83
91
  'A successful create_task or update_task tool call is required before claiming a task schedule changed.',
84
92
  'If external evidence conflicts with memory, history, or another tool result, preserve the uncertainty instead of flattening it into a single confident claim.',
85
93
  'When the draft reply is already correct and fully supported by the evidence, return it unchanged. Do not rewrite for style.',
94
+ 'For multi-entity research or comparison replies, every compared target needs supporting tool evidence. If a target was never searched or opened, mark insufficient_evidence and rewrite the reply so it does not invent that target\'s specs.',
95
+ 'Do not invent missing targets, products, people, or outcomes. If the draft invents an entity that is not in the task and not in tool evidence, rewrite it out or mark insufficient_evidence.',
96
+ 'Search-result snippets alone are weak evidence for concrete product claims. Prefer opened pages, fetched docs, or other primary tool output. If only snippets exist, either keep the claim clearly provisional or mark missing_evidence.',
97
+ 'Set safe_to_deliver=true only when final_reply is fully supported or has been rewritten into a truthful, clearly limited partial answer with every unsupported claim removed. Otherwise set it false.',
86
98
  ];
87
99
  const EXECUTION_GUIDANCE_ACTION_LINES = [
88
100
  'Act end-to-end. Run independent searches or inspections in parallel when possible. Prefer native integration tools and structured APIs over browser automation or shell scraping. Use exact IDs and required parameters; list or search first when you do not have them.',
101
+ 'For research and multi-entity comparisons, cover each requested target with its own search/open path. Do not stop after one partial lead or invent the remaining devices from memory. Open primary sources before stating concrete specs.',
89
102
  'For GitHub issue implementation or PR work, fetch the issue once, then establish or reuse a writable local checkout, create a task branch, inspect/edit/test locally, and push/open the PR. Use direct GitHub file mutation tools only as a fallback when a local checkout is unavailable.',
90
103
  'Prefer the highest-level available tool for the job. If a tool accepts normal text, JSON, file paths, or line ranges, pass those directly instead of reconstructing equivalent data through shell commands.',
91
104
  'Your shell (execute_command) starts in your workspace, and the file tools (read_file, read_files, write_file, edit_file, replace_file_range, list_directory, search_files) operate on that same workspace. Keep source checkouts and generated files in the shared workspace, then prefer file tools for inspection and edits instead of shell snippets. Clone a repo once and reuse it; do not re-clone or re-list the same tree.',
@@ -105,6 +118,7 @@ function buildVerifierSchemaExample(finalReply) {
105
118
  evidence_sources: ['web_search'],
106
119
  missing_evidence: [],
107
120
  final_reply: finalReply || '',
121
+ safe_to_deliver: true,
108
122
  confidence: 0.83,
109
123
  };
110
124
  }
@@ -250,6 +264,10 @@ function promoteAnalysisMode(initialMode, { verificationNeed, freshnessRisk, dra
250
264
  function isDirectAnswerEligibleAnalysis(analysis) {
251
265
  if (!analysis || typeof analysis !== 'object') return false;
252
266
  const draftReply = String(analysis.draft_reply || '').trim();
267
+ const draftStatus = String(analysis.draft_status || '').trim().toLowerCase();
268
+ const researchTargets = Array.isArray(analysis.research_targets)
269
+ ? analysis.research_targets.filter(Boolean)
270
+ : [];
253
271
  const promotedMode = promoteAnalysisMode(analysis.mode, {
254
272
  verificationNeed: analysis.verification_need,
255
273
  freshnessRisk: analysis.freshness_risk,
@@ -257,10 +275,16 @@ function isDirectAnswerEligibleAnalysis(analysis) {
257
275
  planningDepth: analysis.planning_depth,
258
276
  });
259
277
 
278
+ // Direct answers are for zero-tool replies only. Any research targets or
279
+ // freshness/verification burden must enter the tool loop.
260
280
  return promotedMode === 'direct_answer'
261
281
  && !analysis.needs_subagents
262
282
  && Boolean(draftReply)
263
- && !isDeferredWorkReply(draftReply);
283
+ && (draftStatus === 'final' || draftStatus === 'needs_user_input')
284
+ && researchTargets.length === 0
285
+ && String(analysis.research_depth || 'none') === 'none'
286
+ && String(analysis.verification_need || 'none') === 'none'
287
+ && String(analysis.freshness_risk || 'none') === 'none';
264
288
  }
265
289
 
266
290
  function extractJsonCandidate(text) {
@@ -317,8 +341,25 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
317
341
  'successCriteria',
318
342
  TASK_ANALYSIS_SUCCESS_CRITERIA_LIMIT,
319
343
  );
344
+ const researchTargets = resolveAliasedStringList(
345
+ raw,
346
+ fallback,
347
+ 'research_targets',
348
+ 'researchTargets',
349
+ TASK_ANALYSIS_SUCCESS_CRITERIA_LIMIT,
350
+ );
351
+ const declaredResearchDepth = pickEnum(
352
+ resolveAliasedValue(raw, fallback, 'research_depth', 'researchDepth', ''),
353
+ ['none', 'light', 'deep'],
354
+ '',
355
+ );
320
356
 
321
357
  const draftReply = resolveAliasedText(raw, fallback, 'draft_reply', 'draftReply', '');
358
+ const draftStatus = pickEnum(
359
+ resolveAliasedValue(raw, fallback, 'draft_status', 'draftStatus', ''),
360
+ DRAFT_STATUSES,
361
+ 'needs_execution',
362
+ );
322
363
  const initialMode = pickEnum(
323
364
  raw.mode || fallback.mode,
324
365
  ANALYSIS_MODES,
@@ -364,13 +405,52 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
364
405
  ['none', 'possible', 'high'],
365
406
  freshnessRiskFor({ verificationNeed })
366
407
  );
408
+ let researchDepth = declaredResearchDepth;
409
+ if (researchTargets.length >= 2) {
410
+ researchDepth = 'deep';
411
+ } else if (researchTargets.length === 1 && researchDepth === 'none') {
412
+ researchDepth = 'light';
413
+ } else if (!researchDepth) {
414
+ if (freshnessRisk === 'high') {
415
+ researchDepth = 'deep';
416
+ } else if (freshnessRisk === 'possible') {
417
+ researchDepth = 'light';
418
+ } else {
419
+ researchDepth = 'none';
420
+ }
421
+ }
367
422
 
368
- const mode = promoteAnalysisMode(initialMode, {
423
+ let mode = promoteAnalysisMode(initialMode, {
369
424
  verificationNeed,
370
425
  freshnessRisk,
371
426
  draftReply,
372
427
  planningDepth,
373
428
  });
429
+ if (mode === 'direct_answer' && draftStatus === 'needs_execution') {
430
+ mode = 'execute';
431
+ }
432
+ if (mode === 'direct_answer' && researchDepth !== 'none') {
433
+ mode = 'execute';
434
+ }
435
+
436
+ // Structural promotion: explicit research targets require tool work.
437
+ if (researchTargets.length > 0 && mode === 'direct_answer') {
438
+ mode = planningDepth === 'deep' || researchTargets.length >= 2 ? 'plan_execute' : 'execute';
439
+ }
440
+
441
+ let effectiveVerificationNeed = verificationNeed;
442
+ let effectiveFreshnessRisk = freshnessRisk;
443
+ if (researchTargets.length > 0) {
444
+ if (effectiveVerificationNeed === 'none') {
445
+ effectiveVerificationNeed = researchTargets.length >= 2 ? 'required' : 'light';
446
+ }
447
+ if (effectiveFreshnessRisk === 'none') {
448
+ effectiveFreshnessRisk = researchTargets.length >= 2 ? 'possible' : 'possible';
449
+ }
450
+ if (mode === 'execute' && researchTargets.length >= 2) {
451
+ mode = 'plan_execute';
452
+ }
453
+ }
374
454
 
375
455
  const normalizedComplexity = pickEnum(
376
456
  resolveAliasedValue(raw, fallback, 'complexity', 'complexity', ''),
@@ -385,19 +465,22 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
385
465
 
386
466
  return {
387
467
  mode,
388
- freshness_risk: freshnessRisk,
389
- verification_need: verificationNeed,
390
- planning_depth: planningDepth,
468
+ freshness_risk: effectiveFreshnessRisk,
469
+ verification_need: effectiveVerificationNeed,
470
+ planning_depth: mode === 'plan_execute' ? 'deep' : mode === 'direct_answer' ? 'none' : 'light',
391
471
  confidence: clampConfidence(
392
472
  raw.confidence ?? fallback.confidence,
393
473
  draftReply ? TASK_ANALYSIS_CONFIDENCE_WITH_DRAFT : TASK_ANALYSIS_CONFIDENCE_DEFAULT,
394
474
  ),
395
475
  suggested_tools: suggestedTools,
396
476
  needs_subagents: resolveAliasedBoolean(raw, fallback, 'needs_subagents', 'needsSubagents'),
397
- needs_verification: verificationNeed !== 'none',
477
+ needs_verification: effectiveVerificationNeed !== 'none',
398
478
  draft_reply: draftReply,
479
+ draft_status: draftStatus,
399
480
  goal: resolveAliasedText(raw, fallback, 'goal', 'goal', ''),
400
481
  success_criteria: successCriteria,
482
+ research_targets: researchTargets,
483
+ research_depth: researchDepth,
401
484
  complexity: mode === 'plan_execute' ? 'complex' : normalizedComplexity,
402
485
  autonomy_level: mode === 'plan_execute' ? 'high' : normalizedAutonomyLevel,
403
486
  progress_update_policy: pickEnum(
@@ -409,7 +492,9 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
409
492
  completion_confidence_required: pickEnum(
410
493
  resolveAliasedValue(raw, fallback, 'completion_confidence_required', 'completionConfidenceRequired', ''),
411
494
  COMPLETION_CONFIDENCE_LEVELS,
412
- verificationNeed === 'required' || mode === 'plan_execute' ? 'high' : 'medium',
495
+ effectiveVerificationNeed === 'required' || mode === 'plan_execute' || researchTargets.length >= 2
496
+ ? 'high'
497
+ : 'medium',
413
498
  ),
414
499
  };
415
500
  }
@@ -493,6 +578,8 @@ function normalizeVerificationResult(raw = {}, fallbackReply = '') {
493
578
  if (status === 'verified' && missingEvidence.length > 0) {
494
579
  status = 'insufficient_evidence';
495
580
  }
581
+ const safeToDeliver = status === 'verified'
582
+ || resolveAliasedBoolean(raw, null, 'safe_to_deliver', 'safeToDeliver', false);
496
583
 
497
584
  return {
498
585
  status,
@@ -505,7 +592,8 @@ function normalizeVerificationResult(raw = {}, fallbackReply = '') {
505
592
  VERIFICATION_EVIDENCE_SOURCES_LIMIT,
506
593
  ),
507
594
  missing_evidence: missingEvidence,
508
- final_reply: finalReply,
595
+ final_reply: safeToDeliver ? finalReply : '',
596
+ safe_to_deliver: safeToDeliver,
509
597
  confidence: clampConfidence(
510
598
  raw.confidence,
511
599
  status === 'verified' ? VERIFICATION_CONFIDENCE_VERIFIED : VERIFICATION_CONFIDENCE_DEFAULT,
@@ -567,6 +655,7 @@ function buildExecutionGuidance({ analysis, plan = null, capabilityHealth }) {
567
655
  `Execution mode: ${analysis.mode}.`,
568
656
  analysis.goal ? `Goal: ${analysis.goal}` : '',
569
657
  formatBulletSection('Success criteria', analysis.success_criteria),
658
+ formatBulletSection('Research targets', analysis.research_targets),
570
659
  analysis.suggested_tools?.length
571
660
  ? `Advisory tool suggestions: ${analysis.suggested_tools.join(', ')}`
572
661
  : '',
@@ -200,6 +200,199 @@ function isSubstantiveProgressToolName(toolName = '') {
200
200
  return true;
201
201
  }
202
202
 
203
+ function clampResearchText(value, maxChars = 220) {
204
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
205
+ if (!text) return '';
206
+ if (text.length <= maxChars) return text;
207
+ return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
208
+ }
209
+
210
+ function uniqueResearchTokens(values = [], { limit = 12 } = {}) {
211
+ const seen = new Set();
212
+ const tokens = [];
213
+ for (const value of values) {
214
+ const token = String(value || '').replace(/\s+/g, ' ').trim();
215
+ if (!token) continue;
216
+ const key = token.toLowerCase();
217
+ if (seen.has(key)) continue;
218
+ seen.add(key);
219
+ tokens.push(token);
220
+ if (tokens.length >= limit) break;
221
+ }
222
+ return tokens;
223
+ }
224
+
225
+ // Research targets come from structured task analysis / goal contract only.
226
+ // Do not NLP-extract entity names from free text.
227
+ function collectResearchTargets(analysis = null, goalContext = null) {
228
+ return uniqueResearchTokens([
229
+ ...(Array.isArray(analysis?.research_targets) ? analysis.research_targets : []),
230
+ ...(Array.isArray(goalContext?.researchTargets) ? goalContext.researchTargets : []),
231
+ ], { limit: 8 });
232
+ }
233
+
234
+ function resolveResearchIntensity(analysis = null, goalContext = null) {
235
+ const declaredDepth = String(
236
+ analysis?.research_depth
237
+ || analysis?.researchDepth
238
+ || '',
239
+ ).trim().toLowerCase();
240
+ const declaredTargets = collectResearchTargets(analysis, goalContext);
241
+ if (
242
+ ['none', 'light', 'deep'].includes(declaredDepth)
243
+ && !(declaredDepth === 'none' && declaredTargets.length > 0)
244
+ ) {
245
+ return declaredDepth;
246
+ }
247
+
248
+ const mode = String(analysis?.mode || '').trim().toLowerCase();
249
+ const complexity = String(
250
+ goalContext?.effectiveComplexity
251
+ || analysis?.complexity
252
+ || '',
253
+ ).trim().toLowerCase();
254
+ const autonomyLevel = String(
255
+ goalContext?.effectiveAutonomyLevel
256
+ || analysis?.autonomy_level
257
+ || '',
258
+ ).trim().toLowerCase();
259
+ const completionConfidence = String(
260
+ goalContext?.effectiveCompletionConfidence
261
+ || analysis?.completion_confidence_required
262
+ || '',
263
+ ).trim().toLowerCase();
264
+ const verificationNeed = String(analysis?.verification_need || '').trim().toLowerCase();
265
+ const freshnessRisk = String(analysis?.freshness_risk || '').trim().toLowerCase();
266
+ const planningDepth = String(analysis?.planning_depth || '').trim().toLowerCase();
267
+ const targets = declaredTargets;
268
+
269
+ if (mode === 'direct_answer' && verificationNeed === 'none' && freshnessRisk === 'none') {
270
+ return 'none';
271
+ }
272
+
273
+ const needsExternalEvidence = (
274
+ targets.length > 0
275
+ || freshnessRisk === 'possible'
276
+ || freshnessRisk === 'high'
277
+ );
278
+ if (!needsExternalEvidence) {
279
+ return 'none';
280
+ }
281
+
282
+ const deepSignals = [
283
+ mode === 'plan_execute',
284
+ complexity === 'complex',
285
+ autonomyLevel === 'high',
286
+ completionConfidence === 'high',
287
+ verificationNeed === 'required',
288
+ freshnessRisk === 'high',
289
+ planningDepth === 'deep',
290
+ targets.length >= 2,
291
+ ].filter(Boolean).length;
292
+
293
+ if (deepSignals >= 2 || targets.length >= 2 || verificationNeed === 'required' || freshnessRisk === 'high') {
294
+ return 'deep';
295
+ }
296
+
297
+ return 'light';
298
+ }
299
+
300
+ function isSuccessfulResearchExecution(item = {}) {
301
+ if (!item || item.ok !== true) return false;
302
+ if (!isSubstantiveProgressToolName(item.toolName)) return false;
303
+ if (item.evidenceSource === 'messaging') return false;
304
+ return Boolean(item.evidenceRelevant || item.dependsOnOutput || item.stateChanged);
305
+ }
306
+
307
+ function selectResearchEvidenceCandidates(toolExecutions = [], maxItems = 80) {
308
+ const candidates = (Array.isArray(toolExecutions) ? toolExecutions : [])
309
+ .map((execution, index) => ({ execution, evidenceIndex: index + 1 }))
310
+ .filter(({ execution }) => isSuccessfulResearchExecution(execution));
311
+ if (candidates.length <= maxItems) return candidates;
312
+
313
+ const firstCount = Math.floor(maxItems / 4);
314
+ return [
315
+ ...candidates.slice(0, firstCount),
316
+ ...candidates.slice(-(maxItems - firstCount)),
317
+ ];
318
+ }
319
+
320
+ function summarizeResearchEvidenceCatalog(toolExecutions = [], maxItems = 80) {
321
+ return selectResearchEvidenceCandidates(toolExecutions, maxItems)
322
+ .map(({ execution, evidenceIndex }) => (
323
+ `E${evidenceIndex}. ${execution.toolName} [${execution.evidenceSource || 'tool'}] :: ${clampRunContext(execution.summary || '', 160)}`
324
+ ))
325
+ .join('\n');
326
+ }
327
+
328
+ function researchExecutionSignature(execution = {}) {
329
+ return JSON.stringify([
330
+ String(execution.toolName || '').trim(),
331
+ execution.input && typeof execution.input === 'object' ? execution.input : {},
332
+ ]);
333
+ }
334
+
335
+ function assessResearchAdequacy({
336
+ analysis = null,
337
+ goalContext = null,
338
+ toolExecutions = [],
339
+ } = {}) {
340
+ const intensity = resolveResearchIntensity(analysis, goalContext);
341
+ const targets = collectResearchTargets(analysis, goalContext);
342
+
343
+ const successful = (Array.isArray(toolExecutions) ? toolExecutions : [])
344
+ .filter(isSuccessfulResearchExecution);
345
+ const uniqueEvidenceCandidateCount = new Set(
346
+ successful.map((item) => researchExecutionSignature(item)),
347
+ ).size;
348
+ const structurallyReady = intensity === 'none' || uniqueEvidenceCandidateCount > 0;
349
+ const missing = structurallyReady
350
+ ? []
351
+ : ['No successful source-bearing tool evidence is available for semantic review.'];
352
+ const nextActions = [];
353
+ if (!structurallyReady) {
354
+ nextActions.push('Gather source-backed evidence before requesting completion.');
355
+ } else if (intensity !== 'none') {
356
+ nextActions.push('Have the completion judge map each requested target to concrete evidence entries and assess source quality.');
357
+ }
358
+
359
+ return {
360
+ intensity,
361
+ adequate: structurallyReady,
362
+ structurallyReady,
363
+ semanticReviewRequired: intensity !== 'none',
364
+ evidenceCandidateCount: successful.length,
365
+ uniqueEvidenceCandidateCount,
366
+ targets,
367
+ coveredTargets: [],
368
+ primaryCoveredTargets: [],
369
+ uncoveredTargets: intensity === 'none' ? [] : targets,
370
+ primaryUncoveredTargets: intensity === 'none' ? [] : targets,
371
+ missing,
372
+ nextActions,
373
+ reason: structurallyReady
374
+ ? (intensity === 'none'
375
+ ? 'No research burden for this run.'
376
+ : 'Source candidates exist; the AI completion judge must still validate target coverage and source quality.')
377
+ : clampResearchText(missing.join(' ') || 'Research evidence is still incomplete.', 320),
378
+ };
379
+ }
380
+
381
+ function formatResearchAdequacyGuidance(assessment = null) {
382
+ if (!assessment || assessment.intensity === 'none') return '';
383
+ const lines = [
384
+ assessment.structurallyReady
385
+ ? 'Research self-check: source candidates exist, but semantic target coverage is not inferred from tool names, arguments, or token overlap.'
386
+ : 'Research self-check: no successful source evidence is available yet.',
387
+ assessment.reason ? `Gap: ${assessment.reason}` : '',
388
+ assessment.nextActions?.length
389
+ ? `Next safe steps:\n- ${assessment.nextActions.join('\n- ')}`
390
+ : '',
391
+ 'The completion judge must explicitly map every requested target to supporting evidence and judge whether that evidence is primary, secondary, or merely contextual.',
392
+ ];
393
+ return lines.filter(Boolean).join('\n');
394
+ }
395
+
203
396
  function isSubstantiveProgressEvidence(item = {}) {
204
397
  if (!isSubstantiveProgressToolName(item.toolName)) return false;
205
398
  if (item.evidenceSource === 'messaging') return false;
@@ -274,9 +467,14 @@ function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [],
274
467
  module.exports = {
275
468
  classifyToolExecution,
276
469
  deriveEvidenceSource,
470
+ assessResearchAdequacy,
471
+ formatResearchAdequacyGuidance,
277
472
  gatheredNewEvidence,
278
473
  isSubstantiveProgressEvidence,
279
474
  isSubstantiveProgressToolName,
475
+ resolveResearchIntensity,
476
+ selectResearchEvidenceCandidates,
477
+ summarizeResearchEvidenceCatalog,
280
478
  summarizeProgressToolExecutions,
281
479
  summarizeToolExecutions,
282
480
  summarizeAvailableTools,
@@ -12,7 +12,6 @@ const {
12
12
  normalizeOutgoingMessageForPlatform,
13
13
  } = require('../messaging/formatting_guides');
14
14
  const { INTERIM_KINDS, normalizeInterimKind } = require('./interim');
15
- const { isDeferredWorkReply } = require('./terminal_reply');
16
15
  const { normalizeWhatsAppId } = require('../../utils/whatsapp');
17
16
  const {
18
17
  executeIntegratedTool,
@@ -2401,6 +2400,13 @@ async function executeTool(toolName, args, context, engine) {
2401
2400
  if (!engine || !runId) {
2402
2401
  return { error: 'Interim updates require an active run.' };
2403
2402
  }
2403
+ if (engine.getRunMeta(runId)?.messagingContext?.behavior?.isGroup === true) {
2404
+ return {
2405
+ sent: false,
2406
+ skipped: true,
2407
+ reason: 'Interim progress updates are suppressed in shared rooms.',
2408
+ };
2409
+ }
2404
2410
  const interimContent = typeof args.content === 'string' ? args.content : '';
2405
2411
  const expectsReply = args.expects_reply === true;
2406
2412
  const deferFollowUp = args.defer_follow_up === true;
@@ -2438,16 +2444,6 @@ async function executeTool(toolName, args, context, engine) {
2438
2444
  platform: args.platform,
2439
2445
  to: args.to,
2440
2446
  });
2441
- if (
2442
- !suppressReply
2443
- && triggerSource === 'messaging'
2444
- && originDelivery
2445
- && isDeferredWorkReply(normalizedMessage)
2446
- ) {
2447
- return {
2448
- error: 'send_message cannot end the run with a promise or progress-only reply. Continue the work, or use send_interim_update for a factual interim update.',
2449
- };
2450
- }
2451
2447
  if (isProactiveTrigger(triggerSource)) {
2452
2448
  const proactiveValidation = validateProactiveSendMessageArgs({
2453
2449
  purpose: args.purpose,
@@ -2506,13 +2502,58 @@ async function executeTool(toolName, args, context, engine) {
2506
2502
  if (triggerSource === 'messaging' && originDelivery) {
2507
2503
  await engine?.stopMessagingProgressSupervisor?.(runId);
2508
2504
  }
2509
- const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
2510
- agentId,
2511
- mediaPath: args.media_path,
2512
- runId,
2513
- persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks',
2514
- signal,
2515
- });
2505
+ const behavior = runState?.messagingContext?.behavior;
2506
+ const behaviorPipeline = app?.locals?.behaviorPipeline;
2507
+ let deliveredContent = normalizedMessage;
2508
+ let sendResult;
2509
+ if (
2510
+ originDelivery
2511
+ && behavior
2512
+ && behavior.enabled !== false
2513
+ && behaviorPipeline
2514
+ && typeof behaviorPipeline.refineAndMaybeDeliver === 'function'
2515
+ ) {
2516
+ const behaviorResult = await behaviorPipeline.refineAndMaybeDeliver({
2517
+ userId,
2518
+ agentId,
2519
+ msg: behavior.message,
2520
+ config: behavior.config,
2521
+ draft: normalizedMessage,
2522
+ messagingManager: manager,
2523
+ runId,
2524
+ signal,
2525
+ mediaPath: args.media_path,
2526
+ turnEpoch: behavior.turnEpoch,
2527
+ deliver: true,
2528
+ });
2529
+ deliveredContent = behaviorResult.content;
2530
+ if (behaviorResult.delivered) {
2531
+ sendResult = { success: true, behavior: true, result: behaviorResult.delivery };
2532
+ } else if (behaviorResult.suppressed) {
2533
+ markProactiveNoResponse({ runState, deliveryState });
2534
+ sendResult = {
2535
+ success: true,
2536
+ sent: false,
2537
+ suppressed: true,
2538
+ reason: behaviorResult.reasonCodes?.[0] || 'behavior_suppressed',
2539
+ };
2540
+ } else {
2541
+ sendResult = {
2542
+ success: false,
2543
+ error: behaviorResult.delivery?.error
2544
+ || behaviorResult.delivery?.reason
2545
+ || 'Behavior delivery was not confirmed.',
2546
+ };
2547
+ }
2548
+ } else {
2549
+ sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
2550
+ agentId,
2551
+ mediaPath: args.media_path,
2552
+ runId,
2553
+ persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks',
2554
+ signal,
2555
+ });
2556
+ }
2516
2557
  // Track that the agent explicitly sent a message during this run
2517
2558
  if (
2518
2559
  !suppressReply
@@ -2520,7 +2561,7 @@ async function executeTool(toolName, args, context, engine) {
2520
2561
  && sendResult?.suppressed !== true
2521
2562
  && originDelivery
2522
2563
  ) {
2523
- markProactiveMessageSent({ runState, deliveryState, content: normalizedMessage });
2564
+ markProactiveMessageSent({ runState, deliveryState, content: deliveredContent });
2524
2565
  if (runState && triggerSource === 'messaging') {
2525
2566
  runState.explicitMessageSent = true;
2526
2567
  }