@demigodmode/pi-web-agent 0.2.2 → 0.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/LICENSE +661 -661
- package/README.md +61 -5
- package/dist/commands/web-agent-config.d.ts +23 -0
- package/dist/commands/web-agent-config.js +249 -0
- package/dist/extension.js +30 -66
- 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/config-store.d.ts +23 -0
- package/dist/presentation/config-store.js +64 -0
- package/dist/presentation/config.d.ts +7 -0
- package/dist/presentation/config.js +44 -0
- package/dist/presentation/explore-presentation.d.ts +3 -0
- package/dist/presentation/explore-presentation.js +56 -0
- package/dist/presentation/fetch-presentation.d.ts +5 -0
- package/dist/presentation/fetch-presentation.js +40 -0
- package/dist/presentation/search-presentation.d.ts +3 -0
- package/dist/presentation/search-presentation.js +30 -0
- package/dist/presentation/select-view.d.ts +2 -0
- package/dist/presentation/select-view.js +12 -0
- package/dist/presentation/types.d.ts +50 -0
- package/dist/presentation/types.js +1 -0
- package/dist/search/duckduckgo.d.ts +6 -1
- package/dist/search/duckduckgo.js +11 -1
- package/dist/tools/web-explore.d.ts +16 -16
- package/dist/tools/web-explore.js +21 -29
- package/dist/tools/web-fetch-headless.js +11 -2
- package/dist/tools/web-fetch.js +11 -2
- package/dist/tools/web-search.js +99 -12
- package/dist/types.d.ts +22 -0
- package/package.json +75 -75
- package/dist/scripts/live-web-eval.d.ts +0 -1
- package/dist/scripts/live-web-eval.js +0 -411
- package/dist/src/cache/ttl-cache.d.ts +0 -8
- package/dist/src/cache/ttl-cache.js +0 -21
- package/dist/src/extension.d.ts +0 -2
- package/dist/src/extension.js +0 -155
- package/dist/src/extract/readability.d.ts +0 -8
- package/dist/src/extract/readability.js +0 -93
- package/dist/src/fetch/browser-resolution.d.ts +0 -15
- package/dist/src/fetch/browser-resolution.js +0 -55
- package/dist/src/fetch/headless-fetch.d.ts +0 -18
- package/dist/src/fetch/headless-fetch.js +0 -87
- package/dist/src/fetch/http-fetch.d.ts +0 -4
- package/dist/src/fetch/http-fetch.js +0 -50
- package/dist/src/orchestration/index.d.ts +0 -41
- package/dist/src/orchestration/index.js +0 -9
- package/dist/src/orchestration/research-orchestrator.d.ts +0 -43
- package/dist/src/orchestration/research-orchestrator.js +0 -87
- package/dist/src/orchestration/research-types.d.ts +0 -41
- package/dist/src/orchestration/research-types.js +0 -1
- package/dist/src/orchestration/research-worker.d.ts +0 -16
- package/dist/src/orchestration/research-worker.js +0 -131
- package/dist/src/search/duckduckgo.d.ts +0 -9
- package/dist/src/search/duckduckgo.js +0 -52
- package/dist/src/tools/web-explore.d.ts +0 -44
- package/dist/src/tools/web-explore.js +0 -50
- package/dist/src/tools/web-fetch-headless.d.ts +0 -6
- package/dist/src/tools/web-fetch-headless.js +0 -14
- package/dist/src/tools/web-fetch.d.ts +0 -6
- package/dist/src/tools/web-fetch.js +0 -14
- package/dist/src/tools/web-search.d.ts +0 -10
- package/dist/src/tools/web-search.js +0 -103
- package/dist/src/types.d.ts +0 -48
- package/dist/src/types.js +0 -7
- package/dist/tests/cache/ttl-cache.test.d.ts +0 -1
- package/dist/tests/cache/ttl-cache.test.js +0 -19
- package/dist/tests/contracts.test.d.ts +0 -1
- package/dist/tests/contracts.test.js +0 -65
- package/dist/tests/extension.test.d.ts +0 -1
- package/dist/tests/extension.test.js +0 -123
- package/dist/tests/extract/readability.test.d.ts +0 -1
- package/dist/tests/extract/readability.test.js +0 -79
- package/dist/tests/fetch/browser-resolution.test.d.ts +0 -1
- package/dist/tests/fetch/browser-resolution.test.js +0 -37
- package/dist/tests/fetch/headless-fetch.smoke.test.d.ts +0 -1
- package/dist/tests/fetch/headless-fetch.smoke.test.js +0 -17
- package/dist/tests/fetch/headless-fetch.test.d.ts +0 -1
- package/dist/tests/fetch/headless-fetch.test.js +0 -150
- package/dist/tests/fetch/http-fetch.test.d.ts +0 -1
- package/dist/tests/fetch/http-fetch.test.js +0 -129
- package/dist/tests/orchestration/research-orchestrator.test.d.ts +0 -1
- package/dist/tests/orchestration/research-orchestrator.test.js +0 -298
- package/dist/tests/orchestration/research-worker.test.d.ts +0 -1
- package/dist/tests/orchestration/research-worker.test.js +0 -171
- package/dist/tests/orchestration/research-workflow.test.d.ts +0 -1
- package/dist/tests/orchestration/research-workflow.test.js +0 -119
- package/dist/tests/package-manifest.test.d.ts +0 -1
- package/dist/tests/package-manifest.test.js +0 -29
- package/dist/tests/release-foundation.test.d.ts +0 -1
- package/dist/tests/release-foundation.test.js +0 -16
- package/dist/tests/release-script.test.d.ts +0 -1
- package/dist/tests/release-script.test.js +0 -72
- package/dist/tests/search/duckduckgo.test.d.ts +0 -1
- package/dist/tests/search/duckduckgo.test.js +0 -103
- package/dist/tests/tools/web-explore.test.d.ts +0 -1
- package/dist/tests/tools/web-explore.test.js +0 -163
- package/dist/tests/tools/web-fetch-headless.test.d.ts +0 -1
- package/dist/tests/tools/web-fetch-headless.test.js +0 -31
- package/dist/tests/tools/web-fetch.test.d.ts +0 -1
- package/dist/tests/tools/web-fetch.test.js +0 -27
- package/dist/tests/tools/web-search.test.d.ts +0 -1
- package/dist/tests/tools/web-search.test.js +0 -125
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -13
|
@@ -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
|
}
|
|
@@ -25,6 +25,12 @@ export type ResearchWorkerResult = {
|
|
|
25
25
|
suggestedHeadlessUrl?: string;
|
|
26
26
|
exhaustedBudget: boolean;
|
|
27
27
|
};
|
|
28
|
+
export type ResearchRunMetadata = {
|
|
29
|
+
searchPasses: number;
|
|
30
|
+
fetchedPages: number;
|
|
31
|
+
headlessAttempts: number;
|
|
32
|
+
exhaustedBudget: boolean;
|
|
33
|
+
};
|
|
28
34
|
export type ResearchOrchestratorDecision = {
|
|
29
35
|
action: 'answer';
|
|
30
36
|
rationale: string;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { selectCandidates } from './candidate-selector.js';
|
|
1
2
|
function classifySource(url) {
|
|
2
3
|
if (url.includes('/docs/api/') || url.includes('/config/'))
|
|
3
4
|
return 'official-api';
|
|
4
5
|
if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide/'))
|
|
5
6
|
return 'official-docs';
|
|
7
|
+
if (url.includes('github.com/vitest-dev/vitest') && url.includes('/docs/'))
|
|
8
|
+
return 'official-docs';
|
|
6
9
|
if (url.includes('learn.microsoft.com'))
|
|
7
10
|
return 'official-docs';
|
|
8
11
|
if (url.includes('github.com/') && url.includes('/issues/'))
|
|
@@ -91,7 +94,11 @@ export function createResearchWorker({ search, fetchPage }) {
|
|
|
91
94
|
exhaustedBudget: false
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
|
-
const candidates =
|
|
97
|
+
const candidates = selectCandidates({
|
|
98
|
+
results: searchResult.results,
|
|
99
|
+
seenUrls: new Set(evidence.map((item) => item.url)),
|
|
100
|
+
maxCandidates: maxFetches
|
|
101
|
+
});
|
|
95
102
|
for (const candidate of candidates) {
|
|
96
103
|
const fetched = await fetchPage({ url: candidate.url });
|
|
97
104
|
if (fetched.status === 'ok') {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
+
export type ResearchStepDecision = {
|
|
3
|
+
action: 'answer';
|
|
4
|
+
} | {
|
|
5
|
+
action: 'answer-with-caveat';
|
|
6
|
+
} | {
|
|
7
|
+
action: 'search-again';
|
|
8
|
+
} | {
|
|
9
|
+
action: 'headless';
|
|
10
|
+
url: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts }: {
|
|
13
|
+
evidence: ResearchEvidence[];
|
|
14
|
+
suggestedHeadlessUrls: string[];
|
|
15
|
+
passIndex: number;
|
|
16
|
+
maxPasses: number;
|
|
17
|
+
headlessAttempts: number;
|
|
18
|
+
maxHeadlessAttempts: number;
|
|
19
|
+
}): ResearchStepDecision;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { hasOfficialEvidence, strongEvidenceCount } from './evidence-ranker.js';
|
|
2
|
+
export function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts }) {
|
|
3
|
+
if (strongEvidenceCount(evidence) >= 2 && hasOfficialEvidence(evidence)) {
|
|
4
|
+
return { action: 'answer' };
|
|
5
|
+
}
|
|
6
|
+
const headlessUrl = suggestedHeadlessUrls.find((url) => !url.includes('npmjs.com/package/'));
|
|
7
|
+
if (headlessUrl && headlessAttempts < maxHeadlessAttempts) {
|
|
8
|
+
return { action: 'headless', url: headlessUrl };
|
|
9
|
+
}
|
|
10
|
+
if (passIndex + 1 < maxPasses) {
|
|
11
|
+
return { action: 'search-again' };
|
|
12
|
+
}
|
|
13
|
+
return { action: 'answer-with-caveat' };
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from './types.js';
|
|
2
|
+
export type PresentationConfigStoreOptions = {
|
|
3
|
+
homeDir?: string;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
};
|
|
6
|
+
export type PresentationConfigLayer = {
|
|
7
|
+
path: string;
|
|
8
|
+
exists: boolean;
|
|
9
|
+
rawConfig?: PresentationConfigOverride;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
export type LoadedPresentationConfig = {
|
|
13
|
+
global: PresentationConfigLayer;
|
|
14
|
+
project: PresentationConfigLayer;
|
|
15
|
+
effectiveConfig: PresentationConfig;
|
|
16
|
+
};
|
|
17
|
+
export declare function getPresentationConfigPaths(options?: PresentationConfigStoreOptions): {
|
|
18
|
+
globalPath: string;
|
|
19
|
+
projectPath: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function loadPresentationConfigLayers(options?: PresentationConfigStoreOptions): Promise<LoadedPresentationConfig>;
|
|
22
|
+
export declare function savePresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope, config: PresentationConfigOverride): Promise<void>;
|
|
23
|
+
export declare function resetPresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_PRESENTATION_CONFIG, extractPresentationConfigOverride, mergePresentationConfigLayers } from './config.js';
|
|
4
|
+
export function getPresentationConfigPaths(options = {}) {
|
|
5
|
+
const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
6
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
7
|
+
return {
|
|
8
|
+
globalPath: path.join(homeDir, '.pi', 'agent', 'extensions', 'pi-web-agent', 'config.json'),
|
|
9
|
+
projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function readPresentationConfigFile(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const rawText = await readFile(filePath, 'utf8');
|
|
15
|
+
const parsed = JSON.parse(rawText);
|
|
16
|
+
return {
|
|
17
|
+
path: filePath,
|
|
18
|
+
exists: true,
|
|
19
|
+
rawConfig: extractPresentationConfigOverride(parsed)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error?.code === 'ENOENT') {
|
|
24
|
+
return { path: filePath, exists: false };
|
|
25
|
+
}
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
return {
|
|
28
|
+
path: filePath,
|
|
29
|
+
exists: true,
|
|
30
|
+
error: message
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function serializePresentationConfigOverride(config) {
|
|
35
|
+
const presentation = {};
|
|
36
|
+
if (config.defaultMode) {
|
|
37
|
+
presentation.defaultMode = config.defaultMode;
|
|
38
|
+
}
|
|
39
|
+
if (Object.keys(config.tools).length > 0) {
|
|
40
|
+
presentation.tools = config.tools;
|
|
41
|
+
}
|
|
42
|
+
return { presentation };
|
|
43
|
+
}
|
|
44
|
+
export async function loadPresentationConfigLayers(options = {}) {
|
|
45
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
46
|
+
const global = await readPresentationConfigFile(globalPath);
|
|
47
|
+
const project = await readPresentationConfigFile(projectPath);
|
|
48
|
+
return {
|
|
49
|
+
global,
|
|
50
|
+
project,
|
|
51
|
+
effectiveConfig: mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, global.rawConfig, project.rawConfig)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function savePresentationConfigScope(options, scope, config) {
|
|
55
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
56
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
57
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
58
|
+
await writeFile(filePath, JSON.stringify(serializePresentationConfigOverride(config), null, 2) + '\n', 'utf8');
|
|
59
|
+
}
|
|
60
|
+
export async function resetPresentationConfigScope(options, scope) {
|
|
61
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
62
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
63
|
+
await rm(filePath, { force: true });
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type PresentationConfig, type PresentationConfigFile, type PresentationConfigOverride, type PresentationMode, type PresentationToolName } from './types.js';
|
|
2
|
+
export declare const DEFAULT_PRESENTATION_CONFIG: PresentationConfig;
|
|
3
|
+
export declare function isPresentationMode(value: unknown): value is PresentationMode;
|
|
4
|
+
export declare function extractPresentationConfigOverride(file: PresentationConfigFile | null | undefined): PresentationConfigOverride;
|
|
5
|
+
export declare function normalizePresentationConfigFile(file: PresentationConfigFile | null | undefined): PresentationConfig;
|
|
6
|
+
export declare function mergePresentationConfigLayers(defaults: PresentationConfig, globalConfig?: PresentationConfigOverride, projectConfig?: PresentationConfigOverride): PresentationConfig;
|
|
7
|
+
export declare function resolvePresentationMode(toolName: PresentationToolName, config?: PresentationConfig): PresentationMode;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PRESENTATION_MODES } from './types.js';
|
|
2
|
+
const PRESENTATION_MODE_SET = new Set(PRESENTATION_MODES);
|
|
3
|
+
export const DEFAULT_PRESENTATION_CONFIG = {
|
|
4
|
+
defaultMode: 'compact',
|
|
5
|
+
tools: {}
|
|
6
|
+
};
|
|
7
|
+
export function isPresentationMode(value) {
|
|
8
|
+
return typeof value === 'string' && PRESENTATION_MODE_SET.has(value);
|
|
9
|
+
}
|
|
10
|
+
export function extractPresentationConfigOverride(file) {
|
|
11
|
+
const presentation = file?.presentation;
|
|
12
|
+
const tools = Object.fromEntries(Object.entries(presentation?.tools ?? {}).flatMap(([toolName, value]) => {
|
|
13
|
+
if (!value || !isPresentationMode(value.mode)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return [[toolName, { mode: value.mode }]];
|
|
17
|
+
}));
|
|
18
|
+
return {
|
|
19
|
+
defaultMode: isPresentationMode(presentation?.defaultMode)
|
|
20
|
+
? presentation.defaultMode
|
|
21
|
+
: undefined,
|
|
22
|
+
tools
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function normalizePresentationConfigFile(file) {
|
|
26
|
+
const override = extractPresentationConfigOverride(file);
|
|
27
|
+
return {
|
|
28
|
+
defaultMode: override.defaultMode ?? DEFAULT_PRESENTATION_CONFIG.defaultMode,
|
|
29
|
+
tools: override.tools
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function mergePresentationConfigLayers(defaults, globalConfig, projectConfig) {
|
|
33
|
+
return {
|
|
34
|
+
defaultMode: projectConfig?.defaultMode ?? globalConfig?.defaultMode ?? defaults.defaultMode,
|
|
35
|
+
tools: {
|
|
36
|
+
...defaults.tools,
|
|
37
|
+
...globalConfig?.tools,
|
|
38
|
+
...projectConfig?.tools
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function resolvePresentationMode(toolName, config = DEFAULT_PRESENTATION_CONFIG) {
|
|
43
|
+
return config.tools[toolName]?.mode ?? config.defaultMode;
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function internalReaderLabel(method) {
|
|
2
|
+
if (method === 'headless')
|
|
3
|
+
return 'web_fetch_headless';
|
|
4
|
+
if (method === 'http')
|
|
5
|
+
return 'web_fetch';
|
|
6
|
+
return 'web_explore';
|
|
7
|
+
}
|
|
8
|
+
export function buildExplorePresentation(result) {
|
|
9
|
+
if (result.status === 'error') {
|
|
10
|
+
return {
|
|
11
|
+
mode: 'compact',
|
|
12
|
+
views: {
|
|
13
|
+
compact: `Research failed: ${result.error?.message ?? 'Unknown research failure.'}`
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const internalSummary = result.metadata
|
|
18
|
+
? `Internal research: web_search ×${result.metadata.searchPasses}, web_fetch ×${result.metadata.fetchedPages}, web_fetch_headless ×${result.metadata.headlessAttempts}`
|
|
19
|
+
: undefined;
|
|
20
|
+
const hasEvidence = result.findings.length > 0 || result.sources.length > 0;
|
|
21
|
+
const evidenceLines = hasEvidence
|
|
22
|
+
? result.findings.map((finding, index) => `- [${internalReaderLabel(result.sources[index]?.method)}] ${finding}`)
|
|
23
|
+
: ['No usable evidence found.'];
|
|
24
|
+
const preview = [
|
|
25
|
+
...evidenceLines,
|
|
26
|
+
internalSummary ? `\n${internalSummary}` : undefined
|
|
27
|
+
]
|
|
28
|
+
.filter((line) => line !== undefined)
|
|
29
|
+
.join('\n');
|
|
30
|
+
const verbose = [
|
|
31
|
+
'Findings',
|
|
32
|
+
...evidenceLines,
|
|
33
|
+
'',
|
|
34
|
+
'Sources',
|
|
35
|
+
...result.sources.map((source) => `- [${internalReaderLabel(source.method)}] ${source.title}: ${source.url}`),
|
|
36
|
+
internalSummary ? `\nInternal tools\n${internalSummary}` : undefined,
|
|
37
|
+
result.caveat ? `\nCaveat\n${result.caveat}` : undefined
|
|
38
|
+
]
|
|
39
|
+
.filter((line) => line !== undefined)
|
|
40
|
+
.join('\n');
|
|
41
|
+
return {
|
|
42
|
+
mode: 'compact',
|
|
43
|
+
views: {
|
|
44
|
+
compact: hasEvidence
|
|
45
|
+
? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
|
|
46
|
+
: 'No usable evidence found',
|
|
47
|
+
preview,
|
|
48
|
+
verbose
|
|
49
|
+
},
|
|
50
|
+
metrics: {
|
|
51
|
+
sourceCount: result.sources.length,
|
|
52
|
+
resultCount: result.findings.length
|
|
53
|
+
},
|
|
54
|
+
sources: result.sources
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
|
+
import type { PresentationEnvelope } from './types.js';
|
|
3
|
+
type FetchLike = WebFetchResponse | WebFetchHeadlessResponse;
|
|
4
|
+
export declare function buildFetchPresentation(result: FetchLike): PresentationEnvelope;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function countWords(text) {
|
|
2
|
+
return text?.trim() ? text.trim().split(/\s+/).length : undefined;
|
|
3
|
+
}
|
|
4
|
+
function firstExcerpt(text, maxChars = 240) {
|
|
5
|
+
if (!text) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars).trimEnd()}…`;
|
|
9
|
+
}
|
|
10
|
+
export function buildFetchPresentation(result) {
|
|
11
|
+
const wordCount = countWords(result.content?.text);
|
|
12
|
+
const compact = result.status === 'ok'
|
|
13
|
+
? `Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
|
|
14
|
+
: result.status === 'needs_headless'
|
|
15
|
+
? `Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
|
|
16
|
+
: `Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
|
|
17
|
+
return {
|
|
18
|
+
mode: 'compact',
|
|
19
|
+
views: {
|
|
20
|
+
compact,
|
|
21
|
+
preview: result.content?.title
|
|
22
|
+
? `${result.content.title}\n${firstExcerpt(result.content.text) ?? ''}`.trim()
|
|
23
|
+
: firstExcerpt(result.content?.text),
|
|
24
|
+
verbose: result.status === 'ok'
|
|
25
|
+
? [
|
|
26
|
+
`URL: ${result.url}`,
|
|
27
|
+
result.content?.title ? `Title: ${result.content.title}` : undefined,
|
|
28
|
+
firstExcerpt(result.content?.text, 500)
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join('\n')
|
|
32
|
+
: undefined
|
|
33
|
+
},
|
|
34
|
+
metrics: {
|
|
35
|
+
wordCount,
|
|
36
|
+
cacheHit: result.metadata.cacheHit
|
|
37
|
+
},
|
|
38
|
+
sources: [{ title: result.content?.title ?? result.url, url: result.url }]
|
|
39
|
+
};
|
|
40
|
+
}
|