neoagent 3.2.1-beta.9 → 3.3.1-beta.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 (78) hide show
  1. package/LICENSE +67 -80
  2. package/README.md +7 -1
  3. package/docs/licensing.md +44 -0
  4. package/flutter_app/lib/main.dart +1 -0
  5. package/flutter_app/lib/main_app_shell.dart +186 -40
  6. package/flutter_app/lib/main_chat.dart +542 -80
  7. package/flutter_app/lib/main_controller.dart +66 -5
  8. package/flutter_app/lib/main_install.dart +9 -7
  9. package/flutter_app/lib/main_models.dart +171 -22
  10. package/flutter_app/lib/main_operations.dart +0 -49
  11. package/flutter_app/lib/main_settings.dart +338 -0
  12. package/flutter_app/lib/src/backend_client.dart +19 -0
  13. package/flutter_app/lib/src/local_runtime_paths.dart +60 -0
  14. package/lib/manager.js +10 -2
  15. package/package.json +1 -1
  16. package/runtime/paths.js +70 -0
  17. package/server/db/database.js +2 -2
  18. package/server/http/routes.js +1 -0
  19. package/server/public/.last_build_id +1 -1
  20. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  21. package/server/public/flutter_bootstrap.js +1 -1
  22. package/server/public/main.dart.js +84940 -83737
  23. package/server/routes/behavior.js +80 -0
  24. package/server/routes/settings.js +4 -0
  25. package/server/services/agents/manager.js +1 -1
  26. package/server/services/ai/history.js +1 -1
  27. package/server/services/ai/loop/agent_engine_core.js +110 -17
  28. package/server/services/ai/loop/blank_recovery.js +5 -4
  29. package/server/services/ai/loop/completion_judge.js +226 -33
  30. package/server/services/ai/loop/conversation_loop.js +92 -34
  31. package/server/services/ai/loop/messaging_delivery.js +47 -0
  32. package/server/services/ai/loop/progress_classification.js +2 -0
  33. package/server/services/ai/loopPolicy.js +24 -2
  34. package/server/services/ai/messagingFallback.js +17 -17
  35. package/server/services/ai/model_failure_cache.js +7 -0
  36. package/server/services/ai/systemPrompt.js +31 -122
  37. package/server/services/ai/taskAnalysis.js +86 -12
  38. package/server/services/ai/toolEvidence.js +68 -224
  39. package/server/services/ai/tools.js +60 -19
  40. package/server/services/behavior/config.js +251 -0
  41. package/server/services/behavior/defaults.js +68 -0
  42. package/server/services/behavior/delivery.js +176 -0
  43. package/server/services/behavior/index.js +43 -0
  44. package/server/services/behavior/model_client.js +35 -0
  45. package/server/services/behavior/modules/agent_identity.js +37 -0
  46. package/server/services/behavior/modules/channel_style.js +28 -0
  47. package/server/services/behavior/modules/index.js +29 -0
  48. package/server/services/behavior/modules/norms.js +101 -0
  49. package/server/services/behavior/modules/persona.js +172 -0
  50. package/server/services/behavior/modules/persona_prompt.js +238 -0
  51. package/server/services/behavior/modules/social_memory.js +86 -0
  52. package/server/services/behavior/modules/social_observability.js +94 -0
  53. package/server/services/behavior/modules/social_signals.js +41 -0
  54. package/server/services/behavior/modules/theory_of_mind.js +118 -0
  55. package/server/services/behavior/modules/turn_taking.js +238 -0
  56. package/server/services/behavior/pipeline.js +341 -0
  57. package/server/services/behavior/registry.js +78 -0
  58. package/server/services/behavior/signals.js +107 -0
  59. package/server/services/behavior/state.js +99 -0
  60. package/server/services/behavior/system_prompt.js +75 -0
  61. package/server/services/memory/manager.js +14 -33
  62. package/server/services/messaging/access_policy.js +269 -74
  63. package/server/services/messaging/automation.js +158 -27
  64. package/server/services/messaging/discord.js +11 -1
  65. package/server/services/messaging/formatting_guides.js +5 -4
  66. package/server/services/messaging/http_platforms.js +73 -28
  67. package/server/services/messaging/inbound_queue.js +74 -13
  68. package/server/services/messaging/inbound_store.js +33 -0
  69. package/server/services/messaging/manager.js +57 -5
  70. package/server/services/messaging/telegram.js +10 -1
  71. package/server/services/messaging/whatsapp.js +11 -0
  72. package/server/services/social_reach/channels/social_video.js +1 -1
  73. package/server/services/social_video/captions.js +2 -2
  74. package/server/services/social_video/service.js +194 -29
  75. package/server/services/voice/message.js +1 -1
  76. package/server/services/voice/runtime.js +2 -2
  77. package/server/utils/logger.js +19 -0
  78. 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,14 +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.'],
27
28
  research_targets: [],
29
+ research_depth: 'none',
28
30
  suggested_tools: ['web_search', 'browser_navigate'],
29
31
  complexity: 'standard',
30
32
  autonomy_level: 'normal',
@@ -45,11 +47,13 @@ const PLAN_SCHEMA_EXAMPLE = {
45
47
  verification_focus: ['Confirm the most time-sensitive claim before replying.'],
46
48
  };
47
49
  const ANALYSIS_PROMPT_INSTRUCTIONS = [
48
- 'Choose the lightest routing mode that still handles the task well.',
49
- 'Use mode="direct_answer" only when a final user-facing reply can be given immediately without tool work.',
50
- '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.',
51
54
  'Use mode="execute" for normal tool-driven work without a separate planning step.',
52
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.',
53
57
  'Set needs_verification=true when the final answer should be checked against tool evidence before it is sent.',
54
58
  'Set goal to a concise restatement of what the user is asking for in this message. Never leave goal empty.',
55
59
  'Keep goal and success_criteria short and practical.',
@@ -61,6 +65,7 @@ const ANALYSIS_PROMPT_INSTRUCTIONS = [
61
65
  'Set parallel_work=true when independent tool calls or subagents can materially reduce latency.',
62
66
  'Set completion_confidence_required="high" when wrong completion would be costly, state-changing, user-visible, or hard to recover.',
63
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.',
64
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.',
65
70
  ];
66
71
  const PLAN_PROMPT_INSTRUCTIONS = [
@@ -87,7 +92,9 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
87
92
  'If external evidence conflicts with memory, history, or another tool result, preserve the uncertainty instead of flattening it into a single confident claim.',
88
93
  'When the draft reply is already correct and fully supported by the evidence, return it unchanged. Do not rewrite for style.',
89
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.',
90
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.',
91
98
  ];
92
99
  const EXECUTION_GUIDANCE_ACTION_LINES = [
93
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.',
@@ -111,6 +118,7 @@ function buildVerifierSchemaExample(finalReply) {
111
118
  evidence_sources: ['web_search'],
112
119
  missing_evidence: [],
113
120
  final_reply: finalReply || '',
121
+ safe_to_deliver: true,
114
122
  confidence: 0.83,
115
123
  };
116
124
  }
@@ -256,6 +264,10 @@ function promoteAnalysisMode(initialMode, { verificationNeed, freshnessRisk, dra
256
264
  function isDirectAnswerEligibleAnalysis(analysis) {
257
265
  if (!analysis || typeof analysis !== 'object') return false;
258
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
+ : [];
259
271
  const promotedMode = promoteAnalysisMode(analysis.mode, {
260
272
  verificationNeed: analysis.verification_need,
261
273
  freshnessRisk: analysis.freshness_risk,
@@ -263,10 +275,16 @@ function isDirectAnswerEligibleAnalysis(analysis) {
263
275
  planningDepth: analysis.planning_depth,
264
276
  });
265
277
 
278
+ // Direct answers are for zero-tool replies only. Any research targets or
279
+ // freshness/verification burden must enter the tool loop.
266
280
  return promotedMode === 'direct_answer'
267
281
  && !analysis.needs_subagents
268
282
  && Boolean(draftReply)
269
- && !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';
270
288
  }
271
289
 
272
290
  function extractJsonCandidate(text) {
@@ -330,8 +348,18 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
330
348
  'researchTargets',
331
349
  TASK_ANALYSIS_SUCCESS_CRITERIA_LIMIT,
332
350
  );
351
+ const declaredResearchDepth = pickEnum(
352
+ resolveAliasedValue(raw, fallback, 'research_depth', 'researchDepth', ''),
353
+ ['none', 'light', 'deep'],
354
+ '',
355
+ );
333
356
 
334
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
+ );
335
363
  const initialMode = pickEnum(
336
364
  raw.mode || fallback.mode,
337
365
  ANALYSIS_MODES,
@@ -377,13 +405,52 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
377
405
  ['none', 'possible', 'high'],
378
406
  freshnessRiskFor({ verificationNeed })
379
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
+ }
380
422
 
381
- const mode = promoteAnalysisMode(initialMode, {
423
+ let mode = promoteAnalysisMode(initialMode, {
382
424
  verificationNeed,
383
425
  freshnessRisk,
384
426
  draftReply,
385
427
  planningDepth,
386
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
+ }
387
454
 
388
455
  const normalizedComplexity = pickEnum(
389
456
  resolveAliasedValue(raw, fallback, 'complexity', 'complexity', ''),
@@ -398,20 +465,22 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
398
465
 
399
466
  return {
400
467
  mode,
401
- freshness_risk: freshnessRisk,
402
- verification_need: verificationNeed,
403
- planning_depth: planningDepth,
468
+ freshness_risk: effectiveFreshnessRisk,
469
+ verification_need: effectiveVerificationNeed,
470
+ planning_depth: mode === 'plan_execute' ? 'deep' : mode === 'direct_answer' ? 'none' : 'light',
404
471
  confidence: clampConfidence(
405
472
  raw.confidence ?? fallback.confidence,
406
473
  draftReply ? TASK_ANALYSIS_CONFIDENCE_WITH_DRAFT : TASK_ANALYSIS_CONFIDENCE_DEFAULT,
407
474
  ),
408
475
  suggested_tools: suggestedTools,
409
476
  needs_subagents: resolveAliasedBoolean(raw, fallback, 'needs_subagents', 'needsSubagents'),
410
- needs_verification: verificationNeed !== 'none',
477
+ needs_verification: effectiveVerificationNeed !== 'none',
411
478
  draft_reply: draftReply,
479
+ draft_status: draftStatus,
412
480
  goal: resolveAliasedText(raw, fallback, 'goal', 'goal', ''),
413
481
  success_criteria: successCriteria,
414
482
  research_targets: researchTargets,
483
+ research_depth: researchDepth,
415
484
  complexity: mode === 'plan_execute' ? 'complex' : normalizedComplexity,
416
485
  autonomy_level: mode === 'plan_execute' ? 'high' : normalizedAutonomyLevel,
417
486
  progress_update_policy: pickEnum(
@@ -423,7 +492,9 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
423
492
  completion_confidence_required: pickEnum(
424
493
  resolveAliasedValue(raw, fallback, 'completion_confidence_required', 'completionConfidenceRequired', ''),
425
494
  COMPLETION_CONFIDENCE_LEVELS,
426
- verificationNeed === 'required' || mode === 'plan_execute' ? 'high' : 'medium',
495
+ effectiveVerificationNeed === 'required' || mode === 'plan_execute' || researchTargets.length >= 2
496
+ ? 'high'
497
+ : 'medium',
427
498
  ),
428
499
  };
429
500
  }
@@ -507,6 +578,8 @@ function normalizeVerificationResult(raw = {}, fallbackReply = '') {
507
578
  if (status === 'verified' && missingEvidence.length > 0) {
508
579
  status = 'insufficient_evidence';
509
580
  }
581
+ const safeToDeliver = status === 'verified'
582
+ || resolveAliasedBoolean(raw, null, 'safe_to_deliver', 'safeToDeliver', false);
510
583
 
511
584
  return {
512
585
  status,
@@ -519,7 +592,8 @@ function normalizeVerificationResult(raw = {}, fallbackReply = '') {
519
592
  VERIFICATION_EVIDENCE_SOURCES_LIMIT,
520
593
  ),
521
594
  missing_evidence: missingEvidence,
522
- final_reply: finalReply,
595
+ final_reply: safeToDeliver ? finalReply : '',
596
+ safe_to_deliver: safeToDeliver,
523
597
  confidence: clampConfidence(
524
598
  raw.confidence,
525
599
  status === 'verified' ? VERIFICATION_CONFIDENCE_VERIFIED : VERIFICATION_CONFIDENCE_DEFAULT,
@@ -200,41 +200,6 @@ function isSubstantiveProgressToolName(toolName = '') {
200
200
  return true;
201
201
  }
202
202
 
203
- const PRIMARY_RESEARCH_SOURCES = new Set([
204
- 'browser',
205
- 'http',
206
- 'integration',
207
- 'files',
208
- 'command',
209
- 'mcp',
210
- 'android',
211
- 'data',
212
- 'vision',
213
- 'skills',
214
- 'subagent',
215
- ]);
216
-
217
- const SECONDARY_RESEARCH_SOURCES = new Set([
218
- 'search',
219
- 'memory',
220
- ]);
221
-
222
- // External/source-backed tools only. Local file/edit/shell work must not
223
- // inherit a research burden just because inspection tools are available.
224
- const RESEARCH_TOOL_HINTS = new Set([
225
- 'web_search',
226
- 'http_request',
227
- 'browser_navigate',
228
- 'browser_open',
229
- 'browser_click',
230
- 'browser_type',
231
- 'browser_snapshot',
232
- 'browser_evaluate',
233
- 'analyze_image',
234
- 'spawn_subagent',
235
- 'delegate_to_agent',
236
- ]);
237
-
238
203
  function clampResearchText(value, maxChars = 220) {
239
204
  const text = String(value || '').replace(/\s+/g, ' ').trim();
240
205
  if (!text) return '';
@@ -257,88 +222,29 @@ function uniqueResearchTokens(values = [], { limit = 12 } = {}) {
257
222
  return tokens;
258
223
  }
259
224
 
260
- // Structural target extraction only: quoted spans and product-like proper names.
261
- // No phrase-based intent filters.
262
- function isProductLikeToken(token = '') {
263
- const value = String(token || '').trim();
264
- if (!value) return false;
265
- if (/[0-9]/.test(value)) return true;
266
- if (/[-_/]/.test(value)) return true;
267
- if (/[a-z][A-Z]/.test(value)) return true;
268
- return false;
269
- }
270
-
271
- function extractResearchTargets(text = '') {
272
- const raw = String(text || '');
273
- if (!raw.trim()) return [];
274
-
275
- const targets = [];
276
- const quoted = raw.match(/["“”'‘’`]([^"“”'‘’`]{2,80})["“”'‘’`]/g) || [];
277
- for (const match of quoted) {
278
- const cleaned = match.replace(/^["“”'‘’`]|["“”'‘’`]$/g, '').trim();
279
- if (cleaned) targets.push(cleaned);
280
- }
281
-
282
- // Require each subsequent token to start with a capital letter or digit so
283
- // ordinary sentence fragments ("Implement the pause...") do not become targets.
284
- const productish = raw.match(
285
- /\b[A-Z][A-Za-z0-9+./-]*(?:[ -][A-Z0-9][A-Za-z0-9+./-]*){0,5}\b/g,
286
- ) || [];
287
- for (const match of productish) {
288
- const cleaned = match.replace(/\s+/g, ' ').trim();
289
- const parts = cleaned.split(/\s+/).filter(Boolean);
290
- if (parts.length === 0) continue;
291
- if (parts.length === 1) {
292
- if (!isProductLikeToken(parts[0]) || parts[0].length < 4) continue;
293
- } else if (!parts.some(isProductLikeToken) && parts.length < 2) {
294
- continue;
295
- } else if (!parts.some(isProductLikeToken) && parts.every((part) => part.length <= 3)) {
296
- continue;
297
- }
298
- // Drop pure sentence openers with no product-like signal.
299
- if (parts.length >= 2 && !parts.some(isProductLikeToken)) {
300
- // Keep multi-token proper names such as brand + model family only when
301
- // at least one token is long enough to be a meaningful entity name.
302
- if (!parts.some((part) => part.length >= 5)) continue;
303
- }
304
- targets.push(cleaned);
305
- }
306
-
307
- return uniqueResearchTokens(targets, { limit: 8 });
308
- }
309
-
225
+ // Research targets come from structured task analysis / goal contract only.
226
+ // Do not NLP-extract entity names from free text.
310
227
  function collectResearchTargets(analysis = null, goalContext = null) {
311
- const explicit = uniqueResearchTokens([
228
+ return uniqueResearchTokens([
312
229
  ...(Array.isArray(analysis?.research_targets) ? analysis.research_targets : []),
313
230
  ...(Array.isArray(goalContext?.researchTargets) ? goalContext.researchTargets : []),
314
231
  ], { limit: 8 });
315
- if (explicit.length > 0) return explicit;
316
-
317
- const goalText = [
318
- goalContext?.effectiveGoal,
319
- analysis?.goal,
320
- ...(Array.isArray(goalContext?.successCriteria) ? goalContext.successCriteria : []),
321
- ...(Array.isArray(analysis?.success_criteria) ? analysis.success_criteria : []),
322
- ].filter(Boolean).join(' ');
323
-
324
- return extractResearchTargets(goalText);
325
- }
326
-
327
- function hasResearchToolHint(analysis = null) {
328
- const tools = Array.isArray(analysis?.suggested_tools) ? analysis.suggested_tools : [];
329
- return tools.some((name) => {
330
- const tool = String(name || '').trim().toLowerCase();
331
- if (!tool) return false;
332
- if (RESEARCH_TOOL_HINTS.has(tool)) return true;
333
- return tool.startsWith('browser_')
334
- || tool.startsWith('mcp_')
335
- || tool === 'web_search'
336
- || tool.includes('web_search')
337
- || tool.includes('http_request');
338
- });
339
232
  }
340
233
 
341
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
+
342
248
  const mode = String(analysis?.mode || '').trim().toLowerCase();
343
249
  const complexity = String(
344
250
  goalContext?.effectiveComplexity
@@ -358,21 +264,16 @@ function resolveResearchIntensity(analysis = null, goalContext = null) {
358
264
  const verificationNeed = String(analysis?.verification_need || '').trim().toLowerCase();
359
265
  const freshnessRisk = String(analysis?.freshness_risk || '').trim().toLowerCase();
360
266
  const planningDepth = String(analysis?.planning_depth || '').trim().toLowerCase();
361
- const targets = collectResearchTargets(analysis, goalContext);
362
- const researchToolHint = hasResearchToolHint(analysis);
267
+ const targets = declaredTargets;
363
268
 
364
269
  if (mode === 'direct_answer' && verificationNeed === 'none' && freshnessRisk === 'none') {
365
270
  return 'none';
366
271
  }
367
272
 
368
- // External/source-backed work only. Pure local implementation tasks must not
369
- // inherit a research burden just because they are complex or multi-step.
370
273
  const needsExternalEvidence = (
371
274
  targets.length > 0
372
- || researchToolHint
373
275
  || freshnessRisk === 'possible'
374
276
  || freshnessRisk === 'high'
375
- || verificationNeed === 'required'
376
277
  );
377
278
  if (!needsExternalEvidence) {
378
279
  return 'none';
@@ -403,32 +304,32 @@ function isSuccessfulResearchExecution(item = {}) {
403
304
  return Boolean(item.evidenceRelevant || item.dependsOnOutput || item.stateChanged);
404
305
  }
405
306
 
406
- function isPrimaryResearchSource(source = '') {
407
- return PRIMARY_RESEARCH_SOURCES.has(String(source || '').trim().toLowerCase());
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
+ ];
408
318
  }
409
319
 
410
- function isSecondaryResearchSource(source = '') {
411
- return SECONDARY_RESEARCH_SOURCES.has(String(source || '').trim().toLowerCase());
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');
412
326
  }
413
327
 
414
- function executionMentionsTarget(execution = {}, target = '') {
415
- const needle = String(target || '').trim().toLowerCase();
416
- if (!needle) return false;
417
- const haystack = [
418
- execution.summary,
419
- execution.toolName,
420
- execution.evidenceSource,
421
- JSON.stringify(execution.input || {}),
422
- ].join(' ').toLowerCase();
423
- if (haystack.includes(needle)) return true;
424
-
425
- const tokens = needle
426
- .split(/[^a-z0-9+]+/i)
427
- .map((token) => token.trim())
428
- .filter((token) => token.length >= 3);
429
- if (tokens.length === 0) return false;
430
- const matched = tokens.filter((token) => haystack.includes(token)).length;
431
- return matched >= Math.min(2, tokens.length);
328
+ function researchExecutionSignature(execution = {}) {
329
+ return JSON.stringify([
330
+ String(execution.toolName || '').trim(),
331
+ execution.input && typeof execution.input === 'object' ? execution.input : {},
332
+ ]);
432
333
  }
433
334
 
434
335
  function assessResearchAdequacy({
@@ -441,111 +342,53 @@ function assessResearchAdequacy({
441
342
 
442
343
  const successful = (Array.isArray(toolExecutions) ? toolExecutions : [])
443
344
  .filter(isSuccessfulResearchExecution);
444
- const primary = successful.filter((item) => isPrimaryResearchSource(item.evidenceSource));
445
- const secondary = successful.filter((item) => isSecondaryResearchSource(item.evidenceSource));
446
- const coveredTargets = targets.filter((target) => (
447
- successful.some((item) => executionMentionsTarget(item, target))
448
- ));
449
- const primaryCoveredTargets = targets.filter((target) => (
450
- primary.some((item) => executionMentionsTarget(item, target))
451
- ));
452
- const uncoveredTargets = targets.filter((target) => !coveredTargets.includes(target));
453
- const primaryUncoveredTargets = targets.filter((target) => !primaryCoveredTargets.includes(target));
454
-
455
- const requiredPrimarySources = intensity === 'deep'
456
- ? (targets.length > 0 ? Math.min(4, targets.length) : 2)
457
- : intensity === 'light'
458
- ? 1
459
- : 0;
460
- const requiredSecondarySources = intensity === 'deep' ? 1 : 0;
461
- const requiredTargetCoverage = intensity === 'deep'
462
- ? targets.length
463
- : intensity === 'light'
464
- ? Math.min(targets.length, 1)
465
- : 0;
466
- const requiredPrimaryTargetCoverage = intensity === 'deep'
467
- ? targets.length
468
- : 0;
469
-
470
- const missing = [];
471
- if (primary.length < requiredPrimarySources) {
472
- missing.push(
473
- `Need ${requiredPrimarySources} primary source open/fetch/inspect step(s); have ${primary.length}.`,
474
- );
475
- }
476
- if (secondary.length < requiredSecondarySources && primary.length < requiredPrimarySources) {
477
- missing.push('Need at least one search lead before finishing deep research.');
478
- }
479
- if (targets.length > 0 && coveredTargets.length < requiredTargetCoverage) {
480
- missing.push(
481
- `Need evidence for: ${uncoveredTargets.slice(0, 4).join('; ') || targets.slice(0, 4).join('; ')}.`,
482
- );
483
- }
484
- if (targets.length > 0 && primaryCoveredTargets.length < requiredPrimaryTargetCoverage) {
485
- missing.push(
486
- `Need primary-source evidence for: ${primaryUncoveredTargets.slice(0, 4).join('; ') || targets.slice(0, 4).join('; ')}.`,
487
- );
488
- }
489
- if (intensity === 'deep' && primary.length === 0 && secondary.length > 0) {
490
- missing.push('Search snippets alone are not enough; open primary sources for the key claims.');
491
- }
492
- if (intensity === 'light' && primary.length === 0 && secondary.length === 0) {
493
- missing.push('Need at least one successful search or primary-source check before finishing.');
494
- }
495
-
496
- const adequate = intensity === 'none' ? true : missing.length === 0;
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.'];
497
352
  const nextActions = [];
498
- if (!adequate) {
499
- if (primaryUncoveredTargets.length > 0) {
500
- nextActions.push(
501
- `Open or fetch primary sources for each remaining target: ${primaryUncoveredTargets.slice(0, 4).join('; ')}.`,
502
- );
503
- } else if (uncoveredTargets.length > 0) {
504
- nextActions.push(
505
- `Research each remaining target separately: ${uncoveredTargets.slice(0, 4).join('; ')}.`,
506
- );
507
- }
508
- if (primary.length < requiredPrimarySources) {
509
- nextActions.push('Open or fetch primary pages/docs for the remaining claims instead of guessing from memory or snippets.');
510
- }
511
- if (secondary.length === 0 && intensity === 'deep') {
512
- nextActions.push('Run targeted searches, then open the strongest sources.');
513
- }
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.');
514
357
  }
515
358
 
516
359
  return {
517
360
  intensity,
518
- adequate,
519
- requiredPrimarySources,
520
- requiredSecondarySources,
521
- requiredTargetCoverage,
522
- requiredPrimaryTargetCoverage,
523
- primarySourceCount: primary.length,
524
- secondarySourceCount: secondary.length,
361
+ adequate: structurallyReady,
362
+ structurallyReady,
363
+ semanticReviewRequired: intensity !== 'none',
364
+ evidenceCandidateCount: successful.length,
365
+ uniqueEvidenceCandidateCount,
525
366
  targets,
526
- coveredTargets,
527
- primaryCoveredTargets,
528
- uncoveredTargets,
529
- primaryUncoveredTargets,
367
+ coveredTargets: [],
368
+ primaryCoveredTargets: [],
369
+ uncoveredTargets: intensity === 'none' ? [] : targets,
370
+ primaryUncoveredTargets: intensity === 'none' ? [] : targets,
530
371
  missing,
531
372
  nextActions,
532
- reason: adequate
373
+ reason: structurallyReady
533
374
  ? (intensity === 'none'
534
375
  ? 'No research burden for this run.'
535
- : 'Research evidence covers the requested targets.')
376
+ : 'Source candidates exist; the AI completion judge must still validate target coverage and source quality.')
536
377
  : clampResearchText(missing.join(' ') || 'Research evidence is still incomplete.', 320),
537
378
  };
538
379
  }
539
380
 
540
381
  function formatResearchAdequacyGuidance(assessment = null) {
541
- if (!assessment || assessment.adequate !== false) return '';
382
+ if (!assessment || assessment.intensity === 'none') return '';
542
383
  const lines = [
543
- 'Research self-check: evidence is still incomplete for this run.',
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.',
544
387
  assessment.reason ? `Gap: ${assessment.reason}` : '',
545
388
  assessment.nextActions?.length
546
389
  ? `Next safe steps:\n- ${assessment.nextActions.join('\n- ')}`
547
390
  : '',
548
- 'Do not complete with memory, guesses, or a partial comparison while these gaps remain. Gather the missing evidence first, or return a blocker that names exactly what could not be verified.',
391
+ 'The completion judge must explicitly map every requested target to supporting evidence and judge whether that evidence is primary, secondary, or merely contextual.',
549
392
  ];
550
393
  return lines.filter(Boolean).join('\n');
551
394
  }
@@ -625,12 +468,13 @@ module.exports = {
625
468
  classifyToolExecution,
626
469
  deriveEvidenceSource,
627
470
  assessResearchAdequacy,
628
- extractResearchTargets,
629
471
  formatResearchAdequacyGuidance,
630
472
  gatheredNewEvidence,
631
473
  isSubstantiveProgressEvidence,
632
474
  isSubstantiveProgressToolName,
633
475
  resolveResearchIntensity,
476
+ selectResearchEvidenceCandidates,
477
+ summarizeResearchEvidenceCatalog,
634
478
  summarizeProgressToolExecutions,
635
479
  summarizeToolExecutions,
636
480
  summarizeAvailableTools,