@demigodmode/pi-web-agent 0.3.1 → 0.5.1
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/LICENSE +661 -661
- package/README.md +124 -119
- package/dist/commands/web-agent-config.d.ts +8 -0
- package/dist/commands/web-agent-config.js +90 -10
- package/dist/extension.js +10 -155
- package/dist/fetch/browser-resolution.d.ts +7 -2
- package/dist/fetch/browser-resolution.js +111 -17
- package/dist/orchestration/answer-synthesizer.d.ts +8 -0
- package/dist/orchestration/answer-synthesizer.js +17 -0
- package/dist/orchestration/candidate-selector.d.ts +6 -0
- package/dist/orchestration/candidate-selector.js +24 -0
- package/dist/orchestration/evidence-ranker.d.ts +4 -0
- package/dist/orchestration/evidence-ranker.js +36 -0
- package/dist/orchestration/index.d.ts +6 -21
- package/dist/orchestration/query-planner.d.ts +7 -0
- package/dist/orchestration/query-planner.js +37 -0
- package/dist/orchestration/research-orchestrator.d.ts +7 -22
- package/dist/orchestration/research-orchestrator.js +185 -73
- package/dist/orchestration/research-types.d.ts +6 -0
- package/dist/orchestration/research-worker.js +8 -1
- package/dist/orchestration/stop-decider.d.ts +19 -0
- package/dist/orchestration/stop-decider.js +14 -0
- package/dist/presentation/explore-presentation.js +26 -4
- package/dist/tools/web-explore.d.ts +10 -0
- package/dist/tools/web-explore.js +10 -20
- package/dist/types.d.ts +8 -1
- package/package.json +74 -75
|
@@ -1,14 +1,82 @@
|
|
|
1
|
-
const WINDOWS_CANDIDATES =
|
|
2
|
-
|
|
3
|
-
'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
const WINDOWS_CANDIDATES = [
|
|
2
|
+
{
|
|
3
|
+
browser: 'chrome',
|
|
4
|
+
paths: [
|
|
5
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
6
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
|
|
7
|
+
],
|
|
8
|
+
commands: ['chrome.exe', 'chrome', 'google-chrome']
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
browser: 'edge',
|
|
12
|
+
paths: [
|
|
13
|
+
'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
|
|
14
|
+
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
|
|
15
|
+
],
|
|
16
|
+
commands: ['msedge.exe', 'msedge', 'microsoft-edge']
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
browser: 'brave',
|
|
20
|
+
paths: [
|
|
21
|
+
'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe',
|
|
22
|
+
'C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe'
|
|
23
|
+
],
|
|
24
|
+
commands: ['brave.exe', 'brave']
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
browser: 'chromium',
|
|
28
|
+
paths: [
|
|
29
|
+
'C:/Program Files/Chromium/Application/chrome.exe',
|
|
30
|
+
'C:/Program Files (x86)/Chromium/Application/chrome.exe'
|
|
31
|
+
],
|
|
32
|
+
commands: ['chromium.exe', 'chromium']
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
const MACOS_CANDIDATES = [
|
|
36
|
+
{
|
|
37
|
+
browser: 'chrome',
|
|
38
|
+
paths: ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],
|
|
39
|
+
commands: ['google-chrome', 'chrome']
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
browser: 'edge',
|
|
43
|
+
paths: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
44
|
+
commands: ['microsoft-edge', 'msedge']
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
browser: 'brave',
|
|
48
|
+
paths: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
|
|
49
|
+
commands: ['brave-browser', 'brave']
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
browser: 'chromium',
|
|
53
|
+
paths: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
|
|
54
|
+
commands: ['chromium', 'chromium-browser']
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
const LINUX_CANDIDATES = [
|
|
58
|
+
{
|
|
59
|
+
browser: 'chrome',
|
|
60
|
+
paths: ['/usr/bin/google-chrome', '/usr/local/bin/google-chrome', '/opt/google/chrome/chrome'],
|
|
61
|
+
commands: ['google-chrome', 'google-chrome-stable', 'chrome']
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
browser: 'edge',
|
|
65
|
+
paths: ['/usr/bin/microsoft-edge', '/opt/microsoft/msedge/msedge'],
|
|
66
|
+
commands: ['microsoft-edge', 'microsoft-edge-stable', 'msedge']
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
browser: 'brave',
|
|
70
|
+
paths: ['/usr/bin/brave-browser', '/usr/local/bin/brave-browser', '/opt/brave.com/brave/brave'],
|
|
71
|
+
commands: ['brave-browser', 'brave']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
browser: 'chromium',
|
|
75
|
+
paths: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
|
|
76
|
+
commands: ['chromium', 'chromium-browser']
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
export async function resolveBrowserExecutable({ configuredPath, platform = process.platform, env = process.env, fileExists = defaultFileExists }) {
|
|
12
80
|
if (configuredPath) {
|
|
13
81
|
if (await fileExists(configuredPath)) {
|
|
14
82
|
return {
|
|
@@ -25,14 +93,19 @@ export async function resolveBrowserExecutable({ configuredPath, fileExists = de
|
|
|
25
93
|
}
|
|
26
94
|
};
|
|
27
95
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
96
|
+
const candidates = getCandidatesForPlatform(platform);
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
for (const path of candidate.paths) {
|
|
99
|
+
if (await fileExists(path)) {
|
|
100
|
+
return { ok: true, executablePath: path, browser: candidate.browser };
|
|
101
|
+
}
|
|
31
102
|
}
|
|
32
103
|
}
|
|
33
|
-
for (const
|
|
34
|
-
|
|
35
|
-
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
for (const path of getPathCommandCandidates(candidate.commands, platform, env)) {
|
|
106
|
+
if (await fileExists(path)) {
|
|
107
|
+
return { ok: true, executablePath: path, browser: candidate.browser };
|
|
108
|
+
}
|
|
36
109
|
}
|
|
37
110
|
}
|
|
38
111
|
return {
|
|
@@ -43,6 +116,27 @@ export async function resolveBrowserExecutable({ configuredPath, fileExists = de
|
|
|
43
116
|
}
|
|
44
117
|
};
|
|
45
118
|
}
|
|
119
|
+
function getCandidatesForPlatform(platform) {
|
|
120
|
+
if (platform === 'win32')
|
|
121
|
+
return WINDOWS_CANDIDATES;
|
|
122
|
+
if (platform === 'darwin')
|
|
123
|
+
return MACOS_CANDIDATES;
|
|
124
|
+
if (platform === 'linux')
|
|
125
|
+
return LINUX_CANDIDATES;
|
|
126
|
+
return [...WINDOWS_CANDIDATES, ...MACOS_CANDIDATES, ...LINUX_CANDIDATES];
|
|
127
|
+
}
|
|
128
|
+
function getPathCommandCandidates(commands, platform, env) {
|
|
129
|
+
const pathValue = env.PATH ?? env.Path ?? env.path;
|
|
130
|
+
if (!pathValue)
|
|
131
|
+
return [];
|
|
132
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
133
|
+
const dirs = pathValue.split(delimiter).filter(Boolean);
|
|
134
|
+
const extensions = platform === 'win32' ? ['', '.exe'] : [''];
|
|
135
|
+
return dirs.flatMap((dir) => commands.flatMap((command) => extensions.map((extension) => {
|
|
136
|
+
const normalizedCommand = command.toLowerCase().endsWith(extension) ? command : `${command}${extension}`;
|
|
137
|
+
return `${dir.replace(/[\\/]$/, '')}/${normalizedCommand}`;
|
|
138
|
+
})));
|
|
139
|
+
}
|
|
46
140
|
async function defaultFileExists(path) {
|
|
47
141
|
try {
|
|
48
142
|
const { access } = await import('node:fs/promises');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function normalizeSummary(summary) {
|
|
2
|
+
return summary.replace(/\s+/g, ' ').trim();
|
|
3
|
+
}
|
|
4
|
+
export function synthesizeAnswer({ evidence, partial }) {
|
|
5
|
+
const findings = evidence.slice(0, 5).map((item) => {
|
|
6
|
+
const summary = normalizeSummary(item.summary);
|
|
7
|
+
return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
|
|
8
|
+
? `Community/practical context: ${summary}`
|
|
9
|
+
: summary;
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
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
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function candidateScore(result) {
|
|
2
|
+
const url = result.url.toLowerCase();
|
|
3
|
+
if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide') || url.includes('learn.microsoft.com'))
|
|
4
|
+
return 0;
|
|
5
|
+
if (url.includes('github.com/') && (url.includes('/issues/') || url.includes('/discussions/')))
|
|
6
|
+
return 1;
|
|
7
|
+
if (url.includes('github.com/'))
|
|
8
|
+
return 2;
|
|
9
|
+
if (url.includes('npmjs.com/package/'))
|
|
10
|
+
return 4;
|
|
11
|
+
return 3;
|
|
12
|
+
}
|
|
13
|
+
export function selectCandidates({ results, seenUrls, maxCandidates }) {
|
|
14
|
+
const deduped = new Map();
|
|
15
|
+
for (const result of results) {
|
|
16
|
+
if (seenUrls.has(result.url))
|
|
17
|
+
continue;
|
|
18
|
+
if (!deduped.has(result.url))
|
|
19
|
+
deduped.set(result.url, result);
|
|
20
|
+
}
|
|
21
|
+
return [...deduped.values()]
|
|
22
|
+
.sort((left, right) => candidateScore(left) - candidateScore(right))
|
|
23
|
+
.slice(0, maxCandidates);
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
+
export declare function rankEvidence(evidence: ResearchEvidence[]): ResearchEvidence[];
|
|
3
|
+
export declare function hasOfficialEvidence(evidence: ResearchEvidence[]): boolean;
|
|
4
|
+
export declare function strongEvidenceCount(evidence: ResearchEvidence[]): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function sourceRank(sourceKind) {
|
|
2
|
+
switch (sourceKind) {
|
|
3
|
+
case 'official-docs':
|
|
4
|
+
return 0;
|
|
5
|
+
case 'official-api':
|
|
6
|
+
return 1;
|
|
7
|
+
case 'official-discussion':
|
|
8
|
+
return 2;
|
|
9
|
+
case 'issue-thread':
|
|
10
|
+
return 3;
|
|
11
|
+
case 'community':
|
|
12
|
+
return 4;
|
|
13
|
+
case 'package-page':
|
|
14
|
+
return 5;
|
|
15
|
+
default:
|
|
16
|
+
return 6;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function rankEvidence(evidence) {
|
|
20
|
+
const bestByUrl = new Map();
|
|
21
|
+
for (const item of evidence) {
|
|
22
|
+
const current = bestByUrl.get(item.url);
|
|
23
|
+
if (!current || sourceRank(item.sourceKind) < sourceRank(current.sourceKind)) {
|
|
24
|
+
bestByUrl.set(item.url, item);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...bestByUrl.values()].sort((left, right) => sourceRank(left.sourceKind) - sourceRank(right.sourceKind));
|
|
28
|
+
}
|
|
29
|
+
export function hasOfficialEvidence(evidence) {
|
|
30
|
+
return evidence.some((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api');
|
|
31
|
+
}
|
|
32
|
+
export function strongEvidenceCount(evidence) {
|
|
33
|
+
return evidence.filter((item) => item.sourceKind === 'official-docs' ||
|
|
34
|
+
item.sourceKind === 'official-api' ||
|
|
35
|
+
item.sourceKind === 'official-discussion').length;
|
|
36
|
+
}
|
|
@@ -13,29 +13,14 @@ export declare function createResearchWorkflow({ search, fetchPage, headlessFetc
|
|
|
13
13
|
run({ query }: {
|
|
14
14
|
query: string;
|
|
15
15
|
}): Promise<{
|
|
16
|
-
decision:
|
|
17
|
-
action: "answer";
|
|
18
|
-
rationale: string;
|
|
19
|
-
approvedEvidence: import("./research-types.js").ResearchEvidence[];
|
|
20
|
-
};
|
|
21
|
-
evidence: import("./research-types.js").ResearchEvidence[];
|
|
22
|
-
workerPass: import("./research-types.js").ResearchWorkerResult;
|
|
23
|
-
} | {
|
|
24
|
-
decision: {
|
|
25
|
-
action: "escalate-headless";
|
|
26
|
-
rationale: string;
|
|
27
|
-
url: string;
|
|
28
|
-
approvedEvidence: import("./research-types.js").ResearchEvidence[];
|
|
29
|
-
};
|
|
16
|
+
decision: import("./research-types.js").ResearchOrchestratorDecision;
|
|
30
17
|
evidence: import("./research-types.js").ResearchEvidence[];
|
|
31
18
|
workerPass: import("./research-types.js").ResearchWorkerResult;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
metadata: {
|
|
20
|
+
searchPasses: number;
|
|
21
|
+
fetchedPages: number;
|
|
22
|
+
headlessAttempts: number;
|
|
23
|
+
exhaustedBudget: boolean;
|
|
37
24
|
};
|
|
38
|
-
evidence: import("./research-types.js").ResearchEvidence[];
|
|
39
|
-
workerPass: import("./research-types.js").ResearchWorkerResult;
|
|
40
25
|
}>;
|
|
41
26
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function normalizeWhitespace(value) {
|
|
2
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
3
|
+
}
|
|
4
|
+
function extractImportantWords(query) {
|
|
5
|
+
return normalizeWhitespace(query)
|
|
6
|
+
.replace(/\b(find|current|tell|me|how|to|the|and|with|for|in|a|an)\b/gi, ' ')
|
|
7
|
+
.replace(/\s+/g, ' ')
|
|
8
|
+
.trim();
|
|
9
|
+
}
|
|
10
|
+
function officialSiteQuery(query) {
|
|
11
|
+
const lower = query.toLowerCase();
|
|
12
|
+
const terms = extractImportantWords(query);
|
|
13
|
+
if (lower.includes('vitest'))
|
|
14
|
+
return `site:vitest.dev ${terms}`;
|
|
15
|
+
if (lower.includes('playwright'))
|
|
16
|
+
return `site:playwright.dev ${terms}`;
|
|
17
|
+
if (lower.includes('microsoft edge') || lower.includes('edge'))
|
|
18
|
+
return `site:learn.microsoft.com ${terms}`;
|
|
19
|
+
return `${terms} official docs`;
|
|
20
|
+
}
|
|
21
|
+
function implementationQuery(query) {
|
|
22
|
+
return `site:github.com ${extractImportantWords(query)}`;
|
|
23
|
+
}
|
|
24
|
+
export function planSearchQueries(input) {
|
|
25
|
+
const planned = input.passIndex === 0
|
|
26
|
+
? [input.originalQuery]
|
|
27
|
+
: [
|
|
28
|
+
officialSiteQuery(input.originalQuery),
|
|
29
|
+
implementationQuery(input.originalQuery),
|
|
30
|
+
`${extractImportantWords(input.originalQuery)} discussion`
|
|
31
|
+
];
|
|
32
|
+
const previous = new Set(input.previousQueries.map(normalizeWhitespace));
|
|
33
|
+
return planned
|
|
34
|
+
.map(normalizeWhitespace)
|
|
35
|
+
.filter((query) => query && !previous.has(query))
|
|
36
|
+
.slice(0, 3);
|
|
37
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WebFetchHeadlessResponse } from '../types.js';
|
|
2
|
-
import type { ResearchEvidence, ResearchWorkerResult } from './research-types.js';
|
|
2
|
+
import type { ResearchEvidence, ResearchOrchestratorDecision, ResearchWorkerResult } from './research-types.js';
|
|
3
3
|
export declare function createResearchOrchestrator({ worker, headlessFetch }: {
|
|
4
4
|
worker: {
|
|
5
5
|
run: (input: {
|
|
@@ -15,29 +15,14 @@ export declare function createResearchOrchestrator({ worker, headlessFetch }: {
|
|
|
15
15
|
run({ query }: {
|
|
16
16
|
query: string;
|
|
17
17
|
}): Promise<{
|
|
18
|
-
decision:
|
|
19
|
-
action: "answer";
|
|
20
|
-
rationale: string;
|
|
21
|
-
approvedEvidence: ResearchEvidence[];
|
|
22
|
-
};
|
|
23
|
-
evidence: ResearchEvidence[];
|
|
24
|
-
workerPass: ResearchWorkerResult;
|
|
25
|
-
} | {
|
|
26
|
-
decision: {
|
|
27
|
-
action: "escalate-headless";
|
|
28
|
-
rationale: string;
|
|
29
|
-
url: string;
|
|
30
|
-
approvedEvidence: ResearchEvidence[];
|
|
31
|
-
};
|
|
18
|
+
decision: ResearchOrchestratorDecision;
|
|
32
19
|
evidence: ResearchEvidence[];
|
|
33
20
|
workerPass: ResearchWorkerResult;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
metadata: {
|
|
22
|
+
searchPasses: number;
|
|
23
|
+
fetchedPages: number;
|
|
24
|
+
headlessAttempts: number;
|
|
25
|
+
exhaustedBudget: boolean;
|
|
39
26
|
};
|
|
40
|
-
evidence: ResearchEvidence[];
|
|
41
|
-
workerPass: ResearchWorkerResult;
|
|
42
27
|
}>;
|
|
43
28
|
};
|
|
@@ -1,87 +1,199 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { rankEvidence } from './evidence-ranker.js';
|
|
2
|
+
import { planSearchQueries } from './query-planner.js';
|
|
3
|
+
import { decideNextResearchStep } from './stop-decider.js';
|
|
4
|
+
const DEFAULT_MAX_PASSES = 3;
|
|
5
|
+
const DEFAULT_MAX_FETCHES_PER_PASS = 4;
|
|
6
|
+
const DEFAULT_MAX_HEADLESS_ATTEMPTS = 2;
|
|
7
|
+
function classifyEvidenceUrl(url) {
|
|
8
|
+
if (url.includes('/docs/api/') || url.includes('/config/'))
|
|
9
|
+
return 'official-api';
|
|
10
|
+
if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide/'))
|
|
11
|
+
return 'official-docs';
|
|
12
|
+
if (url.includes('github.com/vitest-dev/vitest') && url.includes('/docs/'))
|
|
13
|
+
return 'official-docs';
|
|
14
|
+
if (url.includes('learn.microsoft.com'))
|
|
15
|
+
return 'official-docs';
|
|
16
|
+
if (url.includes('github.com/') && url.includes('/issues/'))
|
|
17
|
+
return 'issue-thread';
|
|
18
|
+
if (url.includes('npmjs.com/package/'))
|
|
19
|
+
return 'package-page';
|
|
20
|
+
return 'community';
|
|
18
21
|
}
|
|
19
|
-
function
|
|
20
|
-
return
|
|
22
|
+
function summarizeText(text, maxLength = 180) {
|
|
23
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
21
24
|
}
|
|
22
|
-
function
|
|
23
|
-
return
|
|
24
|
-
item.sourceKind === 'official-api' ||
|
|
25
|
-
item.sourceKind === 'official-discussion');
|
|
25
|
+
function isBotCheckContent({ title = '', text }) {
|
|
26
|
+
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(`${title}\n${text}`);
|
|
26
27
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
28
|
+
function evidenceFromHeadless(result) {
|
|
29
|
+
if (result.status !== 'ok' || !result.content?.text.trim())
|
|
30
|
+
return null;
|
|
31
|
+
if (isBotCheckContent({ title: result.content.title, text: result.content.text }))
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
title: result.content.title ?? result.url,
|
|
35
|
+
url: result.url,
|
|
36
|
+
sourceKind: classifyEvidenceUrl(result.url),
|
|
37
|
+
method: 'headless',
|
|
38
|
+
summary: summarizeText(result.content.text),
|
|
39
|
+
supports: [summarizeText(result.content.text, 120)]
|
|
40
|
+
};
|
|
29
41
|
}
|
|
30
|
-
function
|
|
31
|
-
return
|
|
42
|
+
function fallbackWorkerPass({ previousQueries, allGaps, allLowValueOutcomes, exhaustedBudget }) {
|
|
43
|
+
return {
|
|
44
|
+
searchQueries: previousQueries,
|
|
45
|
+
evidence: [],
|
|
46
|
+
gaps: allGaps,
|
|
47
|
+
lowValueOutcomes: allLowValueOutcomes,
|
|
48
|
+
exhaustedBudget
|
|
49
|
+
};
|
|
32
50
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
function buildMetadata({ previousQueries, allEvidence, allGaps, allLowValueOutcomes, headlessAttempts, exhaustedBudget }) {
|
|
52
|
+
return {
|
|
53
|
+
searchPasses: previousQueries.length,
|
|
54
|
+
fetchedPages: allEvidence.length + allGaps.length + allLowValueOutcomes.length,
|
|
55
|
+
headlessAttempts,
|
|
56
|
+
exhaustedBudget
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function decisionForAnswer(action, query, ranked) {
|
|
60
|
+
if (action === 'answer') {
|
|
61
|
+
return {
|
|
62
|
+
action: 'answer',
|
|
63
|
+
rationale: 'Adaptive research gathered enough strong evidence.',
|
|
64
|
+
approvedEvidence: ranked
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
action: 'research-again',
|
|
69
|
+
rationale: 'Research budget exhausted; answer with caveat.',
|
|
70
|
+
followupQuery: query
|
|
71
|
+
};
|
|
42
72
|
}
|
|
43
73
|
export function createResearchOrchestrator({ worker, headlessFetch }) {
|
|
44
74
|
return {
|
|
45
75
|
async run({ query }) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const allEvidence = [];
|
|
77
|
+
const allGaps = [];
|
|
78
|
+
const allLowValueOutcomes = [];
|
|
79
|
+
const previousQueries = [];
|
|
80
|
+
const suggestedHeadlessUrls = [];
|
|
81
|
+
let headlessAttempts = 0;
|
|
82
|
+
let lastPass;
|
|
83
|
+
for (let passIndex = 0; passIndex < DEFAULT_MAX_PASSES; passIndex++) {
|
|
84
|
+
const queries = planSearchQueries({
|
|
85
|
+
originalQuery: query,
|
|
86
|
+
passIndex,
|
|
87
|
+
previousQueries,
|
|
88
|
+
gaps: allGaps.map((gap) => gap.message)
|
|
89
|
+
});
|
|
90
|
+
for (const plannedQuery of queries) {
|
|
91
|
+
previousQueries.push(plannedQuery);
|
|
92
|
+
const pass = await worker.run({
|
|
93
|
+
query: plannedQuery,
|
|
94
|
+
maxSearchRounds: 1,
|
|
95
|
+
maxFetches: DEFAULT_MAX_FETCHES_PER_PASS
|
|
96
|
+
});
|
|
97
|
+
lastPass = pass;
|
|
98
|
+
allEvidence.push(...pass.evidence);
|
|
99
|
+
allGaps.push(...pass.gaps);
|
|
100
|
+
allLowValueOutcomes.push(...pass.lowValueOutcomes);
|
|
101
|
+
if (pass.suggestedHeadlessUrl)
|
|
102
|
+
suggestedHeadlessUrls.push(pass.suggestedHeadlessUrl);
|
|
103
|
+
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
104
|
+
const decision = decideNextResearchStep({
|
|
105
|
+
evidence: ranked,
|
|
106
|
+
suggestedHeadlessUrls,
|
|
107
|
+
passIndex,
|
|
108
|
+
maxPasses: DEFAULT_MAX_PASSES,
|
|
109
|
+
headlessAttempts,
|
|
110
|
+
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS
|
|
111
|
+
});
|
|
112
|
+
if (decision.action === 'headless') {
|
|
113
|
+
headlessAttempts++;
|
|
114
|
+
const headlessResult = await headlessFetch({ url: decision.url });
|
|
115
|
+
const headlessEvidence = evidenceFromHeadless(headlessResult);
|
|
116
|
+
if (headlessEvidence) {
|
|
117
|
+
allEvidence.push(headlessEvidence);
|
|
118
|
+
const updatedRanked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
119
|
+
const updatedDecision = decideNextResearchStep({
|
|
120
|
+
evidence: updatedRanked,
|
|
121
|
+
suggestedHeadlessUrls: [],
|
|
122
|
+
passIndex,
|
|
123
|
+
maxPasses: DEFAULT_MAX_PASSES,
|
|
124
|
+
headlessAttempts,
|
|
125
|
+
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
decision: decisionForAnswer(updatedDecision.action === 'answer' ? 'answer' : 'answer-with-caveat', query, updatedRanked),
|
|
129
|
+
evidence: updatedRanked,
|
|
130
|
+
workerPass: lastPass,
|
|
131
|
+
metadata: buildMetadata({
|
|
132
|
+
previousQueries,
|
|
133
|
+
allEvidence,
|
|
134
|
+
allGaps,
|
|
135
|
+
allLowValueOutcomes,
|
|
136
|
+
headlessAttempts,
|
|
137
|
+
exhaustedBudget: updatedDecision.action !== 'answer'
|
|
138
|
+
})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
decision: {
|
|
143
|
+
action: 'escalate-headless',
|
|
144
|
+
rationale: 'One high-value page is worth a single orchestrator-approved headless retry.',
|
|
145
|
+
url: decision.url,
|
|
146
|
+
approvedEvidence: ranked
|
|
147
|
+
},
|
|
148
|
+
evidence: ranked,
|
|
149
|
+
workerPass: lastPass,
|
|
150
|
+
metadata: buildMetadata({
|
|
151
|
+
previousQueries,
|
|
152
|
+
allEvidence,
|
|
153
|
+
allGaps,
|
|
154
|
+
allLowValueOutcomes,
|
|
155
|
+
headlessAttempts,
|
|
156
|
+
exhaustedBudget: false
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (decision.action === 'answer' || decision.action === 'answer-with-caveat') {
|
|
161
|
+
return {
|
|
162
|
+
decision: decisionForAnswer(decision.action, query, ranked),
|
|
163
|
+
evidence: ranked,
|
|
164
|
+
workerPass: lastPass,
|
|
165
|
+
metadata: buildMetadata({
|
|
166
|
+
previousQueries,
|
|
167
|
+
allEvidence,
|
|
168
|
+
allGaps,
|
|
169
|
+
allLowValueOutcomes,
|
|
170
|
+
headlessAttempts,
|
|
171
|
+
exhaustedBudget: decision.action === 'answer-with-caveat'
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
78
176
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
177
|
+
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
178
|
+
return {
|
|
179
|
+
decision: decisionForAnswer('answer-with-caveat', query, ranked),
|
|
180
|
+
evidence: ranked,
|
|
181
|
+
workerPass: lastPass ??
|
|
182
|
+
fallbackWorkerPass({
|
|
183
|
+
previousQueries,
|
|
184
|
+
allGaps,
|
|
185
|
+
allLowValueOutcomes,
|
|
186
|
+
exhaustedBudget: true
|
|
187
|
+
}),
|
|
188
|
+
metadata: buildMetadata({
|
|
189
|
+
previousQueries,
|
|
190
|
+
allEvidence,
|
|
191
|
+
allGaps,
|
|
192
|
+
allLowValueOutcomes,
|
|
193
|
+
headlessAttempts,
|
|
194
|
+
exhaustedBudget: true
|
|
195
|
+
})
|
|
83
196
|
};
|
|
84
|
-
return { decision, evidence: approvedEvidence, workerPass: pass };
|
|
85
197
|
}
|
|
86
198
|
};
|
|
87
199
|
}
|