neoagent 3.2.1-beta.8 → 3.2.1-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.2.1-beta.8",
3
+ "version": "3.2.1-beta.9",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 38fc05bea2f5bf14dda4c0f58b31ff9a
1
+ 1a325740c55a320441979a83b65c758d
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"0cd610717bde95fd88343c64f81c11ba4e5c00
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "222530411" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "393614478" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -144506,7 +144506,7 @@ if(r){r=s.d
144506
144506
  r===$&&A.b()
144507
144507
  p.push(A.ju(q,A.jx(!1,new A.Y(B.wP,A.cU(new A.cH(B.kE,new A.acU(r,q),q),q,q),q),!1,B.J,!0),q,q,0,0,0,q))}if(!s.ay){r=s.e
144508
144508
  r===$&&A.b()
144509
- r=B.b.t("mryrwx0x-27df61b").length!==0&&r.b}else r=!1
144509
+ r=B.b.t("ms0ggj2e-2c398a2").length!==0&&r.b}else r=!1
144510
144510
  if(r){r=s.d
144511
144511
  r===$&&A.b()
144512
144512
  r=r.aU&&!r.ag?84:0
@@ -150393,7 +150393,7 @@ $S:0}
150393
150393
  A.a3j.prototype={}
150394
150394
  A.WC.prototype={
150395
150395
  r9(a){var s=this
150396
- if(B.b.t("mryrwx0x-27df61b").length===0||s.a!=null)return
150396
+ if(B.b.t("ms0ggj2e-2c398a2").length===0||s.a!=null)return
150397
150397
  s.Co()
150398
150398
  s.a=A.mL(B.Yh,new A.bmK(s))},
150399
150399
  Co(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -150411,7 +150411,7 @@ if(!t.f.b(k)){s=1
150411
150411
  break}i=J.a0(k,"buildId")
150412
150412
  h=i==null?null:B.b.t(J.q(i))
150413
150413
  j=h==null?"":h
150414
- if(J.bv(j)===0||J.e(j,"mryrwx0x-27df61b")){s=1
150414
+ if(J.bv(j)===0||J.e(j,"ms0ggj2e-2c398a2")){s=1
150415
150415
  break}n.b=!0
150416
150416
  n.H()
150417
150417
  p=2
@@ -150428,7 +150428,7 @@ case 2:return A.i(o.at(-1),r)}})
150428
150428
  return A.k($async$Co,r)},
150429
150429
  wV(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
150430
150430
  var $async$wV=A.h(function(a2,a3){if(a2===1){o.push(a3)
150431
- s=p}for(;;)switch(s){case 0:if(B.b.t("mryrwx0x-27df61b").length===0||n.c){s=1
150431
+ s=p}for(;;)switch(s){case 0:if(B.b.t("ms0ggj2e-2c398a2").length===0||n.c){s=1
150432
150432
  break}n.c=!0
150433
150433
  n.H()
150434
150434
  p=4
@@ -87,6 +87,7 @@ const {
87
87
  } = require('../messagingFallback');
88
88
  const { isDeferredWorkReply } = require('../terminal_reply');
89
89
  const {
90
+ assessResearchAdequacy,
90
91
  summarizeToolExecutions,
91
92
  } = require('../toolEvidence');
92
93
  const {
@@ -793,6 +794,11 @@ class AgentEngine {
793
794
  }) {
794
795
  const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
795
796
  const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
797
+ const researchAdequacy = assessResearchAdequacy({
798
+ analysis,
799
+ goalContext,
800
+ toolExecutions,
801
+ });
796
802
  const response = await this.requestStructuredJson({
797
803
  provider,
798
804
  providerName,
@@ -808,6 +814,8 @@ class AgentEngine {
808
814
  lastReply,
809
815
  iteration,
810
816
  maxIterations,
817
+ analysis,
818
+ researchAdequacy,
811
819
  }),
812
820
  maxTokens: 500,
813
821
  normalize: (raw) => normalizeCompletionDecision(raw, 'continue'),
@@ -817,9 +825,15 @@ class AgentEngine {
817
825
  phase: 'completion_decision',
818
826
  });
819
827
  return {
820
- decision: enforceTerminalReplyDecision(response.value, lastReply),
828
+ decision: enforceTerminalReplyDecision(response.value, lastReply, {
829
+ analysis,
830
+ goalContext,
831
+ toolExecutions,
832
+ researchAdequacy,
833
+ }),
821
834
  usage: response.usage,
822
835
  raw: response.raw,
836
+ researchAdequacy,
823
837
  };
824
838
  }
825
839
 
@@ -905,6 +919,12 @@ class AgentEngine {
905
919
  goalContext,
906
920
  toolExecutions,
907
921
  iteration,
922
+ analysis,
923
+ researchAdequacy: assessResearchAdequacy({
924
+ analysis,
925
+ goalContext,
926
+ toolExecutions,
927
+ }),
908
928
  }),
909
929
  maxTokens: 200,
910
930
  normalize: normalizeChurnAssessment,
@@ -7,6 +7,8 @@ const {
7
7
  isTerminalQuestionOrBlockerReply,
8
8
  } = require('../terminal_reply');
9
9
  const {
10
+ assessResearchAdequacy,
11
+ formatResearchAdequacyGuidance,
10
12
  summarizeAvailableTools,
11
13
  summarizeToolExecutions,
12
14
  } = require('../toolEvidence');
@@ -198,8 +200,15 @@ function buildCompletionDecisionPrompt({
198
200
  lastReply,
199
201
  iteration,
200
202
  maxIterations,
203
+ analysis = null,
204
+ researchAdequacy = null,
201
205
  }) {
202
206
  const draftReply = normalizeOutgoingMessage(lastReply) || '';
207
+ const adequacy = researchAdequacy || assessResearchAdequacy({
208
+ analysis,
209
+ goalContext,
210
+ toolExecutions,
211
+ });
203
212
  const lines = [
204
213
  'Return JSON only.',
205
214
  'Decide whether this run should continue autonomously or stop now.',
@@ -214,13 +223,29 @@ function buildCompletionDecisionPrompt({
214
223
  '- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
215
224
  '- Repeated read-only inspection that has already established the relevant object is absent or unchanged is not progress. Accept a concise complete/blocker reply instead of requiring more searching.',
216
225
  `- If completion_confidence_required is ${goalContext.effectiveCompletionConfidence} and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.`,
217
- triggerSource === 'messaging' && messagingSent
218
- ? '- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.'
219
- : triggerSource === 'messaging'
220
- ? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.'
221
- : '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
226
+ '- When research intensity is light or deep, use "continue" until the required primary/source coverage and target coverage are met, or the draft is an explicit blocker naming exactly what could not be verified.',
227
+ '- Search snippets, memory, and model priors are leads, not completion evidence. Prefer opened/fetched primary sources before "complete".',
222
228
  ];
223
229
 
230
+ if (adequacy.intensity !== 'none') {
231
+ lines.push(
232
+ `- Research intensity for this run is ${adequacy.intensity}. Current coverage: primary=${adequacy.primarySourceCount}/${adequacy.requiredPrimarySources}, secondary=${adequacy.secondarySourceCount}, targets_covered=${adequacy.coveredTargets.length}/${Math.max(adequacy.requiredTargetCoverage, adequacy.targets.length)}.`,
233
+ );
234
+ }
235
+ if (adequacy.adequate === false) {
236
+ lines.push(
237
+ `- Research is still incomplete (${adequacy.reason}). Use "continue" unless the latest draft is an explicit blocker naming the exact missing evidence.`,
238
+ );
239
+ }
240
+
241
+ if (triggerSource === 'messaging' && messagingSent) {
242
+ lines.push('- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.');
243
+ } else if (triggerSource === 'messaging') {
244
+ lines.push('- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.');
245
+ } else {
246
+ lines.push('- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.');
247
+ }
248
+
224
249
  lines.push(
225
250
  goalContext.effectiveGoal ? `Goal: ${goalContext.effectiveGoal}` : '',
226
251
  goalContext.persistedGoalPrompt,
@@ -231,6 +256,16 @@ function buildCompletionDecisionPrompt({
231
256
  `Current iteration: ${iteration} of ${maxIterations}.`,
232
257
  `Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
233
258
  `Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 8) || 'none'}`,
259
+ adequacy.intensity !== 'none'
260
+ ? `Research adequacy: intensity=${adequacy.intensity}; adequate=${adequacy.adequate}; reason=${adequacy.reason}`
261
+ : '',
262
+ adequacy.targets.length
263
+ ? `Research targets: ${adequacy.targets.join('; ')}`
264
+ : '',
265
+ adequacy.uncoveredTargets.length
266
+ ? `Uncovered research targets: ${adequacy.uncoveredTargets.join('; ')}`
267
+ : '',
268
+ formatResearchAdequacyGuidance(adequacy),
234
269
  `Latest draft reply:\n${draftReply || '(empty)'}`,
235
270
  );
236
271
  return lines.filter(Boolean).join('\n');
@@ -245,7 +280,7 @@ function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
245
280
  };
246
281
  }
247
282
 
248
- function enforceTerminalReplyDecision(decision, lastReply) {
283
+ function enforceTerminalReplyDecision(decision, lastReply, options = {}) {
249
284
  if (isDeferredWorkReply(lastReply)) {
250
285
  return {
251
286
  status: 'continue',
@@ -258,6 +293,25 @@ function enforceTerminalReplyDecision(decision, lastReply) {
258
293
  reason: 'The latest reply asks for user input or states a concrete blocker, so the run must wait instead of repeating it.',
259
294
  };
260
295
  }
296
+
297
+ const researchAdequacy = options.researchAdequacy
298
+ || assessResearchAdequacy({
299
+ analysis: options.analysis || null,
300
+ goalContext: options.goalContext || null,
301
+ toolExecutions: options.toolExecutions || [],
302
+ });
303
+ if (
304
+ researchAdequacy
305
+ && researchAdequacy.adequate === false
306
+ && (decision?.status === 'complete' || decision?.status === 'blocked')
307
+ && !isTerminalQuestionOrBlockerReply(lastReply)
308
+ ) {
309
+ return {
310
+ status: 'continue',
311
+ reason: researchAdequacy.reason
312
+ || 'Research evidence is still incomplete for the requested targets; continue gathering sources before finishing.',
313
+ };
314
+ }
261
315
  return decision;
262
316
  }
263
317
 
@@ -269,7 +323,14 @@ function buildChurnAssessmentPrompt({
269
323
  goalContext,
270
324
  toolExecutions,
271
325
  iteration,
326
+ analysis = null,
327
+ researchAdequacy = null,
272
328
  }) {
329
+ const adequacy = researchAdequacy || assessResearchAdequacy({
330
+ analysis,
331
+ goalContext,
332
+ toolExecutions,
333
+ });
273
334
  const lines = [
274
335
  'Return JSON only.',
275
336
  'Self-assess your current loop state — are you making genuine progress or spinning?',
@@ -283,11 +344,19 @@ function buildChurnAssessmentPrompt({
283
344
  : '',
284
345
  `Iteration: ${iteration}`,
285
346
  `Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 6) || 'none'}`,
347
+ adequacy.intensity !== 'none'
348
+ ? `Research adequacy: intensity=${adequacy.intensity}; adequate=${adequacy.adequate}; covered=${adequacy.coveredTargets.length}/${Math.max(adequacy.requiredTargetCoverage, adequacy.targets.length)}; primary=${adequacy.primarySourceCount}/${adequacy.requiredPrimarySources}.`
349
+ : '',
350
+ adequacy.uncoveredTargets.length
351
+ ? `Still uncovered research targets: ${adequacy.uncoveredTargets.join('; ')}.`
352
+ : '',
286
353
  '',
287
354
  'Assessment rules:',
288
355
  '- "progressing": You are systematically gathering necessary context and the next concrete action is already determined — you know exactly what to do next.',
289
356
  '- "churn": You are re-reading/re-searching information already in context, or exploring without a clear next concrete step. Accept the nudge and act.',
290
357
  '- "blocked": No concrete action is available in this run. You have all the evidence needed to deliver a truthful final answer or a specific external blocker.',
358
+ '- For multi-target research, keep "progressing" while uncovered targets remain and a fresh primary source can still be opened. Do not mark "blocked" just because you have partial notes.',
359
+ '- Re-querying the same snippet source for an already covered target is "churn". Opening a different primary source for an uncovered target is "progressing".',
291
360
  ];
292
361
  return lines.filter(Boolean).join('\n');
293
362
  }
@@ -24,6 +24,7 @@ const ANALYSIS_SCHEMA_EXAMPLE = {
24
24
  draft_reply: '',
25
25
  goal: 'Answer the user accurately.',
26
26
  success_criteria: ['Final reply is correct and specific.'],
27
+ research_targets: [],
27
28
  suggested_tools: ['web_search', 'browser_navigate'],
28
29
  complexity: 'standard',
29
30
  autonomy_level: 'normal',
@@ -59,6 +60,8 @@ const ANALYSIS_PROMPT_INSTRUCTIONS = [
59
60
  '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
61
  'Set parallel_work=true when independent tool calls or subagents can materially reduce latency.',
61
62
  'Set completion_confidence_required="high" when wrong completion would be costly, state-changing, user-visible, or hard to recover.',
63
+ '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".',
64
+ '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
65
  ];
63
66
  const PLAN_PROMPT_INSTRUCTIONS = [
64
67
  'Create a concise execution plan for the current task.',
@@ -83,9 +86,12 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
83
86
  'A successful create_task or update_task tool call is required before claiming a task schedule changed.',
84
87
  'If external evidence conflicts with memory, history, or another tool result, preserve the uncertainty instead of flattening it into a single confident claim.',
85
88
  'When the draft reply is already correct and fully supported by the evidence, return it unchanged. Do not rewrite for style.',
89
+ '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.',
90
+ '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.',
86
91
  ];
87
92
  const EXECUTION_GUIDANCE_ACTION_LINES = [
88
93
  '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.',
94
+ '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
95
  '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
96
  '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
97
  '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.',
@@ -317,6 +323,13 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
317
323
  'successCriteria',
318
324
  TASK_ANALYSIS_SUCCESS_CRITERIA_LIMIT,
319
325
  );
326
+ const researchTargets = resolveAliasedStringList(
327
+ raw,
328
+ fallback,
329
+ 'research_targets',
330
+ 'researchTargets',
331
+ TASK_ANALYSIS_SUCCESS_CRITERIA_LIMIT,
332
+ );
320
333
 
321
334
  const draftReply = resolveAliasedText(raw, fallback, 'draft_reply', 'draftReply', '');
322
335
  const initialMode = pickEnum(
@@ -398,6 +411,7 @@ function normalizeTaskAnalysis(raw = {}, fallback = {}) {
398
411
  draft_reply: draftReply,
399
412
  goal: resolveAliasedText(raw, fallback, 'goal', 'goal', ''),
400
413
  success_criteria: successCriteria,
414
+ research_targets: researchTargets,
401
415
  complexity: mode === 'plan_execute' ? 'complex' : normalizedComplexity,
402
416
  autonomy_level: mode === 'plan_execute' ? 'high' : normalizedAutonomyLevel,
403
417
  progress_update_policy: pickEnum(
@@ -567,6 +581,7 @@ function buildExecutionGuidance({ analysis, plan = null, capabilityHealth }) {
567
581
  `Execution mode: ${analysis.mode}.`,
568
582
  analysis.goal ? `Goal: ${analysis.goal}` : '',
569
583
  formatBulletSection('Success criteria', analysis.success_criteria),
584
+ formatBulletSection('Research targets', analysis.research_targets),
570
585
  analysis.suggested_tools?.length
571
586
  ? `Advisory tool suggestions: ${analysis.suggested_tools.join(', ')}`
572
587
  : '',
@@ -200,6 +200,356 @@ 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
+ function clampResearchText(value, maxChars = 220) {
239
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
240
+ if (!text) return '';
241
+ if (text.length <= maxChars) return text;
242
+ return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
243
+ }
244
+
245
+ function uniqueResearchTokens(values = [], { limit = 12 } = {}) {
246
+ const seen = new Set();
247
+ const tokens = [];
248
+ for (const value of values) {
249
+ const token = String(value || '').replace(/\s+/g, ' ').trim();
250
+ if (!token) continue;
251
+ const key = token.toLowerCase();
252
+ if (seen.has(key)) continue;
253
+ seen.add(key);
254
+ tokens.push(token);
255
+ if (tokens.length >= limit) break;
256
+ }
257
+ return tokens;
258
+ }
259
+
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
+
310
+ function collectResearchTargets(analysis = null, goalContext = null) {
311
+ const explicit = uniqueResearchTokens([
312
+ ...(Array.isArray(analysis?.research_targets) ? analysis.research_targets : []),
313
+ ...(Array.isArray(goalContext?.researchTargets) ? goalContext.researchTargets : []),
314
+ ], { 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
+ }
340
+
341
+ function resolveResearchIntensity(analysis = null, goalContext = null) {
342
+ const mode = String(analysis?.mode || '').trim().toLowerCase();
343
+ const complexity = String(
344
+ goalContext?.effectiveComplexity
345
+ || analysis?.complexity
346
+ || '',
347
+ ).trim().toLowerCase();
348
+ const autonomyLevel = String(
349
+ goalContext?.effectiveAutonomyLevel
350
+ || analysis?.autonomy_level
351
+ || '',
352
+ ).trim().toLowerCase();
353
+ const completionConfidence = String(
354
+ goalContext?.effectiveCompletionConfidence
355
+ || analysis?.completion_confidence_required
356
+ || '',
357
+ ).trim().toLowerCase();
358
+ const verificationNeed = String(analysis?.verification_need || '').trim().toLowerCase();
359
+ const freshnessRisk = String(analysis?.freshness_risk || '').trim().toLowerCase();
360
+ const planningDepth = String(analysis?.planning_depth || '').trim().toLowerCase();
361
+ const targets = collectResearchTargets(analysis, goalContext);
362
+ const researchToolHint = hasResearchToolHint(analysis);
363
+
364
+ if (mode === 'direct_answer' && verificationNeed === 'none' && freshnessRisk === 'none') {
365
+ return 'none';
366
+ }
367
+
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
+ const needsExternalEvidence = (
371
+ targets.length > 0
372
+ || researchToolHint
373
+ || freshnessRisk === 'possible'
374
+ || freshnessRisk === 'high'
375
+ || verificationNeed === 'required'
376
+ );
377
+ if (!needsExternalEvidence) {
378
+ return 'none';
379
+ }
380
+
381
+ const deepSignals = [
382
+ mode === 'plan_execute',
383
+ complexity === 'complex',
384
+ autonomyLevel === 'high',
385
+ completionConfidence === 'high',
386
+ verificationNeed === 'required',
387
+ freshnessRisk === 'high',
388
+ planningDepth === 'deep',
389
+ targets.length >= 2,
390
+ ].filter(Boolean).length;
391
+
392
+ if (deepSignals >= 2 || targets.length >= 2 || verificationNeed === 'required' || freshnessRisk === 'high') {
393
+ return 'deep';
394
+ }
395
+
396
+ return 'light';
397
+ }
398
+
399
+ function isSuccessfulResearchExecution(item = {}) {
400
+ if (!item || item.ok !== true) return false;
401
+ if (!isSubstantiveProgressToolName(item.toolName)) return false;
402
+ if (item.evidenceSource === 'messaging') return false;
403
+ return Boolean(item.evidenceRelevant || item.dependsOnOutput || item.stateChanged);
404
+ }
405
+
406
+ function isPrimaryResearchSource(source = '') {
407
+ return PRIMARY_RESEARCH_SOURCES.has(String(source || '').trim().toLowerCase());
408
+ }
409
+
410
+ function isSecondaryResearchSource(source = '') {
411
+ return SECONDARY_RESEARCH_SOURCES.has(String(source || '').trim().toLowerCase());
412
+ }
413
+
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);
432
+ }
433
+
434
+ function assessResearchAdequacy({
435
+ analysis = null,
436
+ goalContext = null,
437
+ toolExecutions = [],
438
+ } = {}) {
439
+ const intensity = resolveResearchIntensity(analysis, goalContext);
440
+ const targets = collectResearchTargets(analysis, goalContext);
441
+
442
+ const successful = (Array.isArray(toolExecutions) ? toolExecutions : [])
443
+ .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;
497
+ 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
+ }
514
+ }
515
+
516
+ return {
517
+ intensity,
518
+ adequate,
519
+ requiredPrimarySources,
520
+ requiredSecondarySources,
521
+ requiredTargetCoverage,
522
+ requiredPrimaryTargetCoverage,
523
+ primarySourceCount: primary.length,
524
+ secondarySourceCount: secondary.length,
525
+ targets,
526
+ coveredTargets,
527
+ primaryCoveredTargets,
528
+ uncoveredTargets,
529
+ primaryUncoveredTargets,
530
+ missing,
531
+ nextActions,
532
+ reason: adequate
533
+ ? (intensity === 'none'
534
+ ? 'No research burden for this run.'
535
+ : 'Research evidence covers the requested targets.')
536
+ : clampResearchText(missing.join(' ') || 'Research evidence is still incomplete.', 320),
537
+ };
538
+ }
539
+
540
+ function formatResearchAdequacyGuidance(assessment = null) {
541
+ if (!assessment || assessment.adequate !== false) return '';
542
+ const lines = [
543
+ 'Research self-check: evidence is still incomplete for this run.',
544
+ assessment.reason ? `Gap: ${assessment.reason}` : '',
545
+ assessment.nextActions?.length
546
+ ? `Next safe steps:\n- ${assessment.nextActions.join('\n- ')}`
547
+ : '',
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.',
549
+ ];
550
+ return lines.filter(Boolean).join('\n');
551
+ }
552
+
203
553
  function isSubstantiveProgressEvidence(item = {}) {
204
554
  if (!isSubstantiveProgressToolName(item.toolName)) return false;
205
555
  if (item.evidenceSource === 'messaging') return false;
@@ -274,9 +624,13 @@ function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [],
274
624
  module.exports = {
275
625
  classifyToolExecution,
276
626
  deriveEvidenceSource,
627
+ assessResearchAdequacy,
628
+ extractResearchTargets,
629
+ formatResearchAdequacyGuidance,
277
630
  gatheredNewEvidence,
278
631
  isSubstantiveProgressEvidence,
279
632
  isSubstantiveProgressToolName,
633
+ resolveResearchIntensity,
280
634
  summarizeProgressToolExecutions,
281
635
  summarizeToolExecutions,
282
636
  summarizeAvailableTools,