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 +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/ai/loop/agent_engine_core.js +21 -1
- package/server/services/ai/loop/completion_judge.js +75 -6
- package/server/services/ai/taskAnalysis.js +15 -0
- package/server/services/ai/toolEvidence.js +354 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1a325740c55a320441979a83b65c758d
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"0cd610717bde95fd88343c64f81c11ba4e5c00
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
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("
|
|
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("
|
|
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,"
|
|
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("
|
|
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
|
-
|
|
218
|
-
|
|
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,
|