@demigodmode/pi-web-agent 1.3.0 → 1.4.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.
- package/CHANGELOG.md +14 -0
- package/dist/orchestration/answer-synthesizer.d.ts +3 -1
- package/dist/orchestration/answer-synthesizer.js +34 -4
- package/dist/orchestration/evidence-quality.d.ts +26 -0
- package/dist/orchestration/evidence-quality.js +62 -0
- package/dist/orchestration/index.d.ts +1 -0
- package/dist/orchestration/research-orchestrator.d.ts +2 -0
- package/dist/orchestration/research-orchestrator.js +45 -15
- package/dist/orchestration/research-worker.js +12 -0
- package/dist/orchestration/stop-decider.d.ts +3 -1
- package/dist/orchestration/stop-decider.js +31 -3
- package/dist/tools/web-explore.d.ts +1 -0
- package/dist/tools/web-explore.js +2 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,20 @@ The format is intentionally simple and release-oriented.
|
|
|
18
18
|
### Breaking
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [1.4.0] - 2026-06-09
|
|
22
|
+
### Added
|
|
23
|
+
- Added evidence quality analysis for `web_explore`, including source diversity, unreadable source, bot-check, and possible conflict signals.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `web_explore` now uses source quality signals before deciding whether to answer, search again, or answer with a caveat.
|
|
27
|
+
- Partial research caveats are now more specific when evidence is community-only, low-diversity, blocked, or cautionary.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Nothing yet.
|
|
31
|
+
|
|
32
|
+
### Breaking
|
|
33
|
+
- None.
|
|
34
|
+
|
|
21
35
|
## [1.3.0] - 2026-06-04
|
|
22
36
|
### Added
|
|
23
37
|
- Added direct URL handling in `web_explore` so linked pages are read before search results.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
-
|
|
2
|
+
import type { EvidenceCaveatReason } from './evidence-quality.js';
|
|
3
|
+
export declare function synthesizeAnswer({ evidence, partial, caveatReasons }: {
|
|
3
4
|
evidence: ResearchEvidence[];
|
|
4
5
|
partial: boolean;
|
|
6
|
+
caveatReasons?: EvidenceCaveatReason[];
|
|
5
7
|
}): {
|
|
6
8
|
findings: string[];
|
|
7
9
|
caveat: string | undefined;
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
function normalizeSummary(summary) {
|
|
2
2
|
return summary.replace(/\s+/g, ' ').trim();
|
|
3
3
|
}
|
|
4
|
-
|
|
4
|
+
function sentenceForReason(reason) {
|
|
5
|
+
switch (reason) {
|
|
6
|
+
case 'community-only':
|
|
7
|
+
return 'the strongest readable sources were mostly community/practical context';
|
|
8
|
+
case 'low-diversity':
|
|
9
|
+
return 'the source set was narrow';
|
|
10
|
+
case 'unreadable-direct-source':
|
|
11
|
+
return 'one or more linked sources could not be read reliably';
|
|
12
|
+
case 'unreadable-thread-source':
|
|
13
|
+
return 'one or more thread sources could not be read reliably';
|
|
14
|
+
case 'possible-conflict':
|
|
15
|
+
return 'readable sources include cautionary or possibly conflicting guidance';
|
|
16
|
+
case 'bot-check':
|
|
17
|
+
return 'some candidate sources showed bot-check or security verification pages';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function joinReasons(reasons) {
|
|
21
|
+
if (reasons.length === 0)
|
|
22
|
+
return '';
|
|
23
|
+
if (reasons.length === 1)
|
|
24
|
+
return reasons[0];
|
|
25
|
+
return `${reasons.slice(0, -1).join(', ')}, and ${reasons.at(-1)}`;
|
|
26
|
+
}
|
|
27
|
+
function caveatText(partial, caveatReasons = []) {
|
|
28
|
+
if (!partial)
|
|
29
|
+
return undefined;
|
|
30
|
+
const specificReasons = caveatReasons.map(sentenceForReason);
|
|
31
|
+
if (specificReasons.length > 0) {
|
|
32
|
+
return `Evidence is partial: ${joinReasons(specificReasons)}.`;
|
|
33
|
+
}
|
|
34
|
+
return 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.';
|
|
35
|
+
}
|
|
36
|
+
export function synthesizeAnswer({ evidence, partial, caveatReasons = [] }) {
|
|
5
37
|
const findings = evidence.slice(0, 5).map((item) => {
|
|
6
38
|
const summary = normalizeSummary(item.summary);
|
|
7
39
|
return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
|
|
@@ -10,8 +42,6 @@ export function synthesizeAnswer({ evidence, partial }) {
|
|
|
10
42
|
});
|
|
11
43
|
return {
|
|
12
44
|
findings,
|
|
13
|
-
caveat: partial
|
|
14
|
-
? 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.'
|
|
15
|
-
: undefined
|
|
45
|
+
caveat: caveatText(partial, caveatReasons)
|
|
16
46
|
};
|
|
17
47
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResearchEvidence, ResearchGap, ResearchLowValueOutcome } from './research-types.js';
|
|
2
|
+
export type EvidenceCaveatReason = 'community-only' | 'low-diversity' | 'unreadable-direct-source' | 'unreadable-thread-source' | 'possible-conflict' | 'bot-check';
|
|
3
|
+
export type EvidenceQualityReport = {
|
|
4
|
+
counts: {
|
|
5
|
+
total: number;
|
|
6
|
+
official: number;
|
|
7
|
+
community: number;
|
|
8
|
+
thread: number;
|
|
9
|
+
packagePage: number;
|
|
10
|
+
distinctHosts: number;
|
|
11
|
+
};
|
|
12
|
+
flags: {
|
|
13
|
+
hasOfficialEvidence: boolean;
|
|
14
|
+
hasOnlyCommunityEvidence: boolean;
|
|
15
|
+
hasLowDiversity: boolean;
|
|
16
|
+
hasUnreadableDirectSource: boolean;
|
|
17
|
+
hasUnreadableThreadSource: boolean;
|
|
18
|
+
hasPossibleConflict: boolean;
|
|
19
|
+
};
|
|
20
|
+
caveatReasons: EvidenceCaveatReason[];
|
|
21
|
+
};
|
|
22
|
+
export declare function analyzeEvidenceQuality({ evidence, gaps, lowValueOutcomes }: {
|
|
23
|
+
evidence: ResearchEvidence[];
|
|
24
|
+
gaps: ResearchGap[];
|
|
25
|
+
lowValueOutcomes: ResearchLowValueOutcome[];
|
|
26
|
+
}): EvidenceQualityReport;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function hostname(url) {
|
|
2
|
+
try {
|
|
3
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, '');
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return url.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function hasConflictMarkers(evidence) {
|
|
10
|
+
const combined = evidence
|
|
11
|
+
.flatMap((item) => [item.summary, ...item.supports])
|
|
12
|
+
.join('\n')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
const caution = /\bdeprecated\b|not recommended|use at your own risk|should not/.test(combined);
|
|
15
|
+
const positiveText = combined.replace(/not recommended/g, '');
|
|
16
|
+
const positive = /\brecommended\b/.test(positiveText);
|
|
17
|
+
return positive && caution;
|
|
18
|
+
}
|
|
19
|
+
function addReason(reasons, reason, enabled) {
|
|
20
|
+
if (enabled && !reasons.includes(reason))
|
|
21
|
+
reasons.push(reason);
|
|
22
|
+
}
|
|
23
|
+
export function analyzeEvidenceQuality({ evidence, gaps, lowValueOutcomes }) {
|
|
24
|
+
const official = evidence.filter((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api').length;
|
|
25
|
+
const community = evidence.filter((item) => item.sourceKind === 'community').length;
|
|
26
|
+
const thread = evidence.filter((item) => item.sourceKind === 'issue-thread' || item.sourceKind === 'official-discussion').length;
|
|
27
|
+
const packagePage = evidence.filter((item) => item.sourceKind === 'package-page').length;
|
|
28
|
+
const distinctHosts = new Set(evidence.map((item) => hostname(item.url))).size;
|
|
29
|
+
const hasOfficialEvidence = official > 0;
|
|
30
|
+
const hasOnlyCommunityEvidence = evidence.length > 0 && official === 0;
|
|
31
|
+
const hasLowDiversity = evidence.length > 1 && distinctHosts <= 1;
|
|
32
|
+
const hasUnreadableDirectSource = gaps.some((gap) => /Direct URL could not be read reliably/i.test(gap.message));
|
|
33
|
+
const hasUnreadableThreadSource = gaps.some((gap) => /Thread source could not be read reliably/i.test(gap.message));
|
|
34
|
+
const hasPossibleConflict = hasConflictMarkers(evidence);
|
|
35
|
+
const hasBotCheck = lowValueOutcomes.some((outcome) => outcome.kind === 'bot-check');
|
|
36
|
+
const caveatReasons = [];
|
|
37
|
+
addReason(caveatReasons, 'community-only', hasOnlyCommunityEvidence);
|
|
38
|
+
addReason(caveatReasons, 'low-diversity', hasLowDiversity);
|
|
39
|
+
addReason(caveatReasons, 'unreadable-direct-source', hasUnreadableDirectSource);
|
|
40
|
+
addReason(caveatReasons, 'unreadable-thread-source', hasUnreadableThreadSource);
|
|
41
|
+
addReason(caveatReasons, 'possible-conflict', hasPossibleConflict);
|
|
42
|
+
addReason(caveatReasons, 'bot-check', hasBotCheck);
|
|
43
|
+
return {
|
|
44
|
+
counts: {
|
|
45
|
+
total: evidence.length,
|
|
46
|
+
official,
|
|
47
|
+
community,
|
|
48
|
+
thread,
|
|
49
|
+
packagePage,
|
|
50
|
+
distinctHosts
|
|
51
|
+
},
|
|
52
|
+
flags: {
|
|
53
|
+
hasOfficialEvidence,
|
|
54
|
+
hasOnlyCommunityEvidence,
|
|
55
|
+
hasLowDiversity,
|
|
56
|
+
hasUnreadableDirectSource,
|
|
57
|
+
hasUnreadableThreadSource,
|
|
58
|
+
hasPossibleConflict
|
|
59
|
+
},
|
|
60
|
+
caveatReasons
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
2
|
import type { ResearchEvidence, ResearchOrchestratorDecision, ResearchWorkerResult } from './research-types.js';
|
|
3
|
+
import { type EvidenceCaveatReason } from './evidence-quality.js';
|
|
3
4
|
export declare function createResearchOrchestrator({ worker, fetchDirect, headlessFetch }: {
|
|
4
5
|
worker: {
|
|
5
6
|
run: (input: {
|
|
@@ -26,6 +27,7 @@ export declare function createResearchOrchestrator({ worker, fetchDirect, headle
|
|
|
26
27
|
fetchedPages: number;
|
|
27
28
|
headlessAttempts: number;
|
|
28
29
|
exhaustedBudget: boolean;
|
|
30
|
+
caveatReasons: EvidenceCaveatReason[];
|
|
29
31
|
};
|
|
30
32
|
}>;
|
|
31
33
|
};
|
|
@@ -3,6 +3,7 @@ import { planSearchQueries } from './query-planner.js';
|
|
|
3
3
|
import { classifySourceProfile } from './source-profile.js';
|
|
4
4
|
import { extractDirectUrls } from './direct-url.js';
|
|
5
5
|
import { decideNextResearchStep } from './stop-decider.js';
|
|
6
|
+
import { analyzeEvidenceQuality } from './evidence-quality.js';
|
|
6
7
|
const DEFAULT_MAX_PASSES = 3;
|
|
7
8
|
const DEFAULT_MAX_FETCHES_PER_PASS = 4;
|
|
8
9
|
const DEFAULT_MAX_HEADLESS_ATTEMPTS = 2;
|
|
@@ -65,15 +66,16 @@ function shouldRetryDirectWithHeadless(result, evidence) {
|
|
|
65
66
|
return false;
|
|
66
67
|
return classifySourceProfile(result.url).shouldPreferHeadlessWhenWeak;
|
|
67
68
|
}
|
|
68
|
-
function buildMetadata({ previousQueries, allEvidence, allGaps, allLowValueOutcomes, headlessAttempts, exhaustedBudget }) {
|
|
69
|
+
function buildMetadata({ previousQueries, allEvidence, allGaps, allLowValueOutcomes, headlessAttempts, exhaustedBudget, caveatReasons = [] }) {
|
|
69
70
|
return {
|
|
70
71
|
searchPasses: previousQueries.length,
|
|
71
72
|
fetchedPages: allEvidence.length + allGaps.length + allLowValueOutcomes.length,
|
|
72
73
|
headlessAttempts,
|
|
73
|
-
exhaustedBudget
|
|
74
|
+
exhaustedBudget,
|
|
75
|
+
caveatReasons
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
|
-
function decisionForAnswer(action, query, ranked) {
|
|
78
|
+
function decisionForAnswer({ action, query, ranked, exhaustedBudget }) {
|
|
77
79
|
if (action === 'answer') {
|
|
78
80
|
return {
|
|
79
81
|
action: 'answer',
|
|
@@ -83,7 +85,7 @@ function decisionForAnswer(action, query, ranked) {
|
|
|
83
85
|
}
|
|
84
86
|
return {
|
|
85
87
|
action: 'research-again',
|
|
86
|
-
rationale: 'Research budget exhausted; answer with caveat.',
|
|
88
|
+
rationale: exhaustedBudget ? 'Research budget exhausted; answer with caveat.' : 'Evidence has quality caveats; answer with caveat.',
|
|
87
89
|
followupQuery: query
|
|
88
90
|
};
|
|
89
91
|
}
|
|
@@ -153,13 +155,19 @@ export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch
|
|
|
153
155
|
if (pass.suggestedHeadlessUrl)
|
|
154
156
|
suggestedHeadlessUrls.push(pass.suggestedHeadlessUrl);
|
|
155
157
|
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
158
|
+
const quality = analyzeEvidenceQuality({
|
|
159
|
+
evidence: ranked,
|
|
160
|
+
gaps: allGaps,
|
|
161
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
162
|
+
});
|
|
156
163
|
const decision = decideNextResearchStep({
|
|
157
164
|
evidence: ranked,
|
|
158
165
|
suggestedHeadlessUrls,
|
|
159
166
|
passIndex,
|
|
160
167
|
maxPasses: DEFAULT_MAX_PASSES,
|
|
161
168
|
headlessAttempts,
|
|
162
|
-
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS
|
|
169
|
+
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS,
|
|
170
|
+
quality
|
|
163
171
|
});
|
|
164
172
|
if (decision.action === 'headless') {
|
|
165
173
|
headlessAttempts++;
|
|
@@ -168,23 +176,35 @@ export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch
|
|
|
168
176
|
if (headlessEvidence) {
|
|
169
177
|
allEvidence.push(headlessEvidence);
|
|
170
178
|
const updatedRanked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
179
|
+
const updatedQuality = analyzeEvidenceQuality({
|
|
180
|
+
evidence: updatedRanked,
|
|
181
|
+
gaps: allGaps,
|
|
182
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
183
|
+
});
|
|
171
184
|
const updatedDecision = decideNextResearchStep({
|
|
172
185
|
evidence: updatedRanked,
|
|
173
186
|
suggestedHeadlessUrls: [],
|
|
174
187
|
passIndex,
|
|
175
188
|
maxPasses: DEFAULT_MAX_PASSES,
|
|
176
189
|
headlessAttempts,
|
|
177
|
-
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS
|
|
190
|
+
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS,
|
|
191
|
+
quality: updatedQuality
|
|
178
192
|
});
|
|
193
|
+
const exhaustedBudget = updatedDecision.action !== 'answer' && passIndex + 1 >= DEFAULT_MAX_PASSES;
|
|
179
194
|
return {
|
|
180
|
-
decision: decisionForAnswer(
|
|
195
|
+
decision: decisionForAnswer({
|
|
196
|
+
action: updatedDecision.action === 'answer' ? 'answer' : 'answer-with-caveat',
|
|
197
|
+
query,
|
|
198
|
+
ranked: updatedRanked,
|
|
199
|
+
exhaustedBudget
|
|
200
|
+
}),
|
|
181
201
|
evidence: updatedRanked,
|
|
182
202
|
workerPass: combinedWorkerPass({
|
|
183
203
|
lastPass,
|
|
184
204
|
previousQueries,
|
|
185
205
|
allGaps,
|
|
186
206
|
allLowValueOutcomes,
|
|
187
|
-
exhaustedBudget
|
|
207
|
+
exhaustedBudget
|
|
188
208
|
}),
|
|
189
209
|
metadata: buildMetadata({
|
|
190
210
|
previousQueries,
|
|
@@ -192,7 +212,8 @@ export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch
|
|
|
192
212
|
allGaps,
|
|
193
213
|
allLowValueOutcomes,
|
|
194
214
|
headlessAttempts,
|
|
195
|
-
exhaustedBudget
|
|
215
|
+
exhaustedBudget,
|
|
216
|
+
caveatReasons: updatedQuality.caveatReasons
|
|
196
217
|
})
|
|
197
218
|
};
|
|
198
219
|
}
|
|
@@ -217,20 +238,22 @@ export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch
|
|
|
217
238
|
allGaps,
|
|
218
239
|
allLowValueOutcomes,
|
|
219
240
|
headlessAttempts,
|
|
220
|
-
exhaustedBudget: false
|
|
241
|
+
exhaustedBudget: false,
|
|
242
|
+
caveatReasons: quality.caveatReasons
|
|
221
243
|
})
|
|
222
244
|
};
|
|
223
245
|
}
|
|
224
246
|
if (decision.action === 'answer' || decision.action === 'answer-with-caveat') {
|
|
247
|
+
const exhaustedBudget = decision.action === 'answer-with-caveat' && passIndex + 1 >= DEFAULT_MAX_PASSES;
|
|
225
248
|
return {
|
|
226
|
-
decision: decisionForAnswer(decision.action, query, ranked),
|
|
249
|
+
decision: decisionForAnswer({ action: decision.action, query, ranked, exhaustedBudget }),
|
|
227
250
|
evidence: ranked,
|
|
228
251
|
workerPass: combinedWorkerPass({
|
|
229
252
|
lastPass,
|
|
230
253
|
previousQueries,
|
|
231
254
|
allGaps,
|
|
232
255
|
allLowValueOutcomes,
|
|
233
|
-
exhaustedBudget
|
|
256
|
+
exhaustedBudget
|
|
234
257
|
}),
|
|
235
258
|
metadata: buildMetadata({
|
|
236
259
|
previousQueries,
|
|
@@ -238,15 +261,21 @@ export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch
|
|
|
238
261
|
allGaps,
|
|
239
262
|
allLowValueOutcomes,
|
|
240
263
|
headlessAttempts,
|
|
241
|
-
exhaustedBudget
|
|
264
|
+
exhaustedBudget,
|
|
265
|
+
caveatReasons: quality.caveatReasons
|
|
242
266
|
})
|
|
243
267
|
};
|
|
244
268
|
}
|
|
245
269
|
}
|
|
246
270
|
}
|
|
247
271
|
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
272
|
+
const quality = analyzeEvidenceQuality({
|
|
273
|
+
evidence: ranked,
|
|
274
|
+
gaps: allGaps,
|
|
275
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
276
|
+
});
|
|
248
277
|
return {
|
|
249
|
-
decision: decisionForAnswer('answer-with-caveat', query, ranked),
|
|
278
|
+
decision: decisionForAnswer({ action: 'answer-with-caveat', query, ranked, exhaustedBudget: true }),
|
|
250
279
|
evidence: ranked,
|
|
251
280
|
workerPass: combinedWorkerPass({
|
|
252
281
|
lastPass,
|
|
@@ -261,7 +290,8 @@ export function createResearchOrchestrator({ worker, fetchDirect, headlessFetch
|
|
|
261
290
|
allGaps,
|
|
262
291
|
allLowValueOutcomes,
|
|
263
292
|
headlessAttempts,
|
|
264
|
-
exhaustedBudget: true
|
|
293
|
+
exhaustedBudget: true,
|
|
294
|
+
caveatReasons: quality.caveatReasons
|
|
265
295
|
})
|
|
266
296
|
};
|
|
267
297
|
}
|
|
@@ -6,10 +6,15 @@ function classifySource(url) {
|
|
|
6
6
|
function summarizeText(text, maxLength = 180) {
|
|
7
7
|
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
8
8
|
}
|
|
9
|
+
function isBotCheckContent({ title = '', text }) {
|
|
10
|
+
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(`${title}\n${text}`);
|
|
11
|
+
}
|
|
9
12
|
function evidenceFromFetch(fetched, fallbackTitle) {
|
|
10
13
|
const content = fetched.content;
|
|
11
14
|
if (fetched.status !== 'ok' || !content)
|
|
12
15
|
return null;
|
|
16
|
+
if (isBotCheckContent({ title: content.title, text: content.text }))
|
|
17
|
+
return null;
|
|
13
18
|
const sourceKind = classifySource(fetched.url);
|
|
14
19
|
if (sourceKind === 'package-page') {
|
|
15
20
|
return null;
|
|
@@ -26,6 +31,13 @@ function evidenceFromFetch(fetched, fallbackTitle) {
|
|
|
26
31
|
function lowValueOutcomeFromFetch(fetched) {
|
|
27
32
|
if (fetched.status !== 'ok' || !fetched.content)
|
|
28
33
|
return null;
|
|
34
|
+
if (isBotCheckContent({ title: fetched.content.title, text: fetched.content.text })) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'bot-check',
|
|
37
|
+
url: fetched.url,
|
|
38
|
+
message: 'Fetched page showed a bot-check or security verification page.'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
29
41
|
if (classifySource(fetched.url) !== 'package-page')
|
|
30
42
|
return null;
|
|
31
43
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
+
import type { EvidenceQualityReport } from './evidence-quality.js';
|
|
2
3
|
export type ResearchStepDecision = {
|
|
3
4
|
action: 'answer';
|
|
4
5
|
} | {
|
|
@@ -9,11 +10,12 @@ export type ResearchStepDecision = {
|
|
|
9
10
|
action: 'headless';
|
|
10
11
|
url: string;
|
|
11
12
|
};
|
|
12
|
-
export declare function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts }: {
|
|
13
|
+
export declare function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts, quality }: {
|
|
13
14
|
evidence: ResearchEvidence[];
|
|
14
15
|
suggestedHeadlessUrls: string[];
|
|
15
16
|
passIndex: number;
|
|
16
17
|
maxPasses: number;
|
|
17
18
|
headlessAttempts: number;
|
|
18
19
|
maxHeadlessAttempts: number;
|
|
20
|
+
quality?: EvidenceQualityReport;
|
|
19
21
|
}): ResearchStepDecision;
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import { hasOfficialEvidence, strongEvidenceCount } from './evidence-ranker.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
function activeCaveatReasons(evidence, quality) {
|
|
3
|
+
const reasons = quality?.caveatReasons ?? [];
|
|
4
|
+
if (!hasOfficialDocsAndApi(evidence))
|
|
5
|
+
return reasons;
|
|
6
|
+
return reasons.filter((reason) => reason !== 'low-diversity');
|
|
7
|
+
}
|
|
8
|
+
function hasQualityConcern(evidence, quality) {
|
|
9
|
+
return activeCaveatReasons(evidence, quality).length > 0;
|
|
10
|
+
}
|
|
11
|
+
function hasOfficialDocsAndApi(evidence) {
|
|
12
|
+
return evidence.some((item) => item.sourceKind === 'official-docs') &&
|
|
13
|
+
evidence.some((item) => item.sourceKind === 'official-api');
|
|
14
|
+
}
|
|
15
|
+
function shouldSearchForBetterQuality({ evidence, quality, passIndex, maxPasses }) {
|
|
16
|
+
if (!quality)
|
|
17
|
+
return false;
|
|
18
|
+
if (passIndex + 1 >= maxPasses)
|
|
19
|
+
return false;
|
|
20
|
+
if (quality.flags.hasOnlyCommunityEvidence)
|
|
21
|
+
return true;
|
|
22
|
+
if (quality.flags.hasLowDiversity && !hasOfficialDocsAndApi(evidence))
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts, quality }) {
|
|
27
|
+
const strongEnough = strongEvidenceCount(evidence) >= 2 && hasOfficialEvidence(evidence);
|
|
28
|
+
if (strongEnough && shouldSearchForBetterQuality({ evidence, quality, passIndex, maxPasses })) {
|
|
29
|
+
return { action: 'search-again' };
|
|
30
|
+
}
|
|
31
|
+
if (strongEnough) {
|
|
32
|
+
return hasQualityConcern(evidence, quality) ? { action: 'answer-with-caveat' } : { action: 'answer' };
|
|
5
33
|
}
|
|
6
34
|
const headlessUrl = suggestedHeadlessUrls.find((url) => !url.includes('npmjs.com/package/'));
|
|
7
35
|
if (headlessUrl && headlessAttempts < maxHeadlessAttempts) {
|
|
@@ -25,7 +25,8 @@ export function createWebExploreTool({ explore = createResearchWorkflow() } = {}
|
|
|
25
25
|
}));
|
|
26
26
|
const synthesized = synthesizeAnswer({
|
|
27
27
|
evidence: result.evidence,
|
|
28
|
-
partial: result.decision.action !== 'answer'
|
|
28
|
+
partial: result.decision.action !== 'answer',
|
|
29
|
+
caveatReasons: result.metadata?.caveatReasons
|
|
29
30
|
});
|
|
30
31
|
const shaped = {
|
|
31
32
|
status: 'ok',
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED