neoagent 3.2.1-beta.7 → 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.
@@ -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,