@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,298 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import * as researchTypes from '../../src/orchestration/research-types.js';
|
|
3
|
-
import { createResearchOrchestrator } from '../../src/orchestration/research-orchestrator.js';
|
|
4
|
-
describe('research orchestrator types', () => {
|
|
5
|
-
it('exports a runtime module for orchestration types', () => {
|
|
6
|
-
expect(researchTypes).toBeTypeOf('object');
|
|
7
|
-
});
|
|
8
|
-
it('supports evidence, gaps, and decisions for one bounded pass', () => {
|
|
9
|
-
const evidence = {
|
|
10
|
-
title: 'BrowserType | Playwright',
|
|
11
|
-
url: 'https://playwright.dev/docs/api/class-browsertype',
|
|
12
|
-
sourceKind: 'official-docs',
|
|
13
|
-
method: 'http',
|
|
14
|
-
summary: 'The API docs describe channel and executablePath.',
|
|
15
|
-
supports: ['Use channel for branded browsers', 'executablePath exists but is risky']
|
|
16
|
-
};
|
|
17
|
-
const gap = {
|
|
18
|
-
kind: 'needs-more-evidence',
|
|
19
|
-
message: 'Need one source showing the recommended Edge usage pattern.'
|
|
20
|
-
};
|
|
21
|
-
const result = {
|
|
22
|
-
searchQueries: ['playwright installed edge channel executablePath'],
|
|
23
|
-
evidence: [evidence],
|
|
24
|
-
gaps: [gap],
|
|
25
|
-
lowValueOutcomes: [],
|
|
26
|
-
suggestedHeadlessUrl: undefined,
|
|
27
|
-
exhaustedBudget: false
|
|
28
|
-
};
|
|
29
|
-
const decision = {
|
|
30
|
-
action: 'answer',
|
|
31
|
-
rationale: 'One official source is enough for this smoke test.',
|
|
32
|
-
approvedEvidence: result.evidence
|
|
33
|
-
};
|
|
34
|
-
expect(result.evidence[0].sourceKind).toBe('official-docs');
|
|
35
|
-
expect(decision.action).toBe('answer');
|
|
36
|
-
});
|
|
37
|
-
it('supports explicit low-value outcomes in worker results', () => {
|
|
38
|
-
const result = {
|
|
39
|
-
searchQueries: ['duckduckgo scraping node'],
|
|
40
|
-
evidence: [],
|
|
41
|
-
gaps: [{ kind: 'fetch-failed', message: 'Primary fetch failed.' }],
|
|
42
|
-
lowValueOutcomes: [
|
|
43
|
-
{
|
|
44
|
-
kind: 'bot-check',
|
|
45
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
46
|
-
message: 'Headless hit a security verification page.'
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
suggestedHeadlessUrl: undefined,
|
|
50
|
-
exhaustedBudget: false
|
|
51
|
-
};
|
|
52
|
-
expect(result.lowValueOutcomes[0]?.kind).toBe('bot-check');
|
|
53
|
-
expect(result.suggestedHeadlessUrl).toBeUndefined();
|
|
54
|
-
});
|
|
55
|
-
it('answers when one bounded pass returns enough official evidence', async () => {
|
|
56
|
-
const orchestrator = createResearchOrchestrator({
|
|
57
|
-
worker: {
|
|
58
|
-
run: vi.fn().mockResolvedValue({
|
|
59
|
-
searchQueries: ['playwright edge channel'],
|
|
60
|
-
evidence: [
|
|
61
|
-
{
|
|
62
|
-
title: 'Browsers | Playwright',
|
|
63
|
-
url: 'https://playwright.dev/docs/browsers',
|
|
64
|
-
sourceKind: 'official-docs',
|
|
65
|
-
method: 'http',
|
|
66
|
-
summary: 'Use branded browsers with channel values like msedge.',
|
|
67
|
-
supports: ['Use msedge for Edge']
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
title: 'BrowserType | Playwright',
|
|
71
|
-
url: 'https://playwright.dev/docs/api/class-browsertype',
|
|
72
|
-
sourceKind: 'official-api',
|
|
73
|
-
method: 'http',
|
|
74
|
-
summary: 'executablePath exists but is use-at-your-own-risk.',
|
|
75
|
-
supports: ['executablePath is risky']
|
|
76
|
-
}
|
|
77
|
-
],
|
|
78
|
-
gaps: [],
|
|
79
|
-
lowValueOutcomes: [],
|
|
80
|
-
suggestedHeadlessUrl: undefined,
|
|
81
|
-
exhaustedBudget: false
|
|
82
|
-
})
|
|
83
|
-
},
|
|
84
|
-
headlessFetch: vi.fn()
|
|
85
|
-
});
|
|
86
|
-
const result = await orchestrator.run({ query: 'playwright installed edge executablePath vs channel' });
|
|
87
|
-
expect(result.decision.action).toBe('answer');
|
|
88
|
-
expect(result.evidence).toHaveLength(2);
|
|
89
|
-
});
|
|
90
|
-
it('requests another pass when evidence is too thin', async () => {
|
|
91
|
-
const orchestrator = createResearchOrchestrator({
|
|
92
|
-
worker: {
|
|
93
|
-
run: vi.fn().mockResolvedValue({
|
|
94
|
-
searchQueries: ['ambiguous query'],
|
|
95
|
-
evidence: [
|
|
96
|
-
{
|
|
97
|
-
title: 'Some blog',
|
|
98
|
-
url: 'https://example.com/post',
|
|
99
|
-
sourceKind: 'community',
|
|
100
|
-
method: 'http',
|
|
101
|
-
summary: 'A single community source only.',
|
|
102
|
-
supports: ['One weak source']
|
|
103
|
-
}
|
|
104
|
-
],
|
|
105
|
-
gaps: [{ kind: 'needs-more-evidence', message: 'Need at least one official source.' }],
|
|
106
|
-
lowValueOutcomes: [],
|
|
107
|
-
suggestedHeadlessUrl: undefined,
|
|
108
|
-
exhaustedBudget: false
|
|
109
|
-
})
|
|
110
|
-
},
|
|
111
|
-
headlessFetch: vi.fn()
|
|
112
|
-
});
|
|
113
|
-
const result = await orchestrator.run({ query: 'ambiguous query' });
|
|
114
|
-
expect(result.decision.action).toBe('research-again');
|
|
115
|
-
});
|
|
116
|
-
it('escalates one specific page to headless only when approved by the orchestrator', async () => {
|
|
117
|
-
const headlessFetch = vi.fn().mockResolvedValue({
|
|
118
|
-
status: 'ok',
|
|
119
|
-
url: 'https://example.com/app',
|
|
120
|
-
content: { title: 'Dynamic App', text: 'Rendered content with enough detail.' },
|
|
121
|
-
metadata: {
|
|
122
|
-
method: 'headless',
|
|
123
|
-
cacheHit: false,
|
|
124
|
-
browser: 'edge',
|
|
125
|
-
navigationMs: 1200,
|
|
126
|
-
truncated: false
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
const orchestrator = createResearchOrchestrator({
|
|
130
|
-
worker: {
|
|
131
|
-
run: vi.fn().mockResolvedValue({
|
|
132
|
-
searchQueries: ['dynamic app'],
|
|
133
|
-
evidence: [],
|
|
134
|
-
gaps: [{ kind: 'fetch-failed', message: 'HTTP was weak.' }],
|
|
135
|
-
lowValueOutcomes: [],
|
|
136
|
-
suggestedHeadlessUrl: 'https://example.com/app',
|
|
137
|
-
exhaustedBudget: false
|
|
138
|
-
})
|
|
139
|
-
},
|
|
140
|
-
headlessFetch
|
|
141
|
-
});
|
|
142
|
-
const result = await orchestrator.run({ query: 'dynamic app' });
|
|
143
|
-
expect(result.decision.action).toBe('escalate-headless');
|
|
144
|
-
expect(headlessFetch).toHaveBeenCalledWith({ url: 'https://example.com/app' });
|
|
145
|
-
});
|
|
146
|
-
it('answers once two strong sources exist and one is official', async () => {
|
|
147
|
-
const orchestrator = createResearchOrchestrator({
|
|
148
|
-
worker: {
|
|
149
|
-
run: vi.fn().mockResolvedValue({
|
|
150
|
-
searchQueries: ['playwright edge channel'],
|
|
151
|
-
evidence: [
|
|
152
|
-
{
|
|
153
|
-
title: 'Browsers | Playwright',
|
|
154
|
-
url: 'https://playwright.dev/docs/browsers',
|
|
155
|
-
sourceKind: 'official-docs',
|
|
156
|
-
method: 'http',
|
|
157
|
-
summary: 'Official docs',
|
|
158
|
-
supports: ['Use channel']
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
title: 'Edge docs',
|
|
162
|
-
url: 'https://learn.microsoft.com/en-us/microsoft-edge/playwright/',
|
|
163
|
-
sourceKind: 'official-discussion',
|
|
164
|
-
method: 'http',
|
|
165
|
-
summary: 'Vendor guidance',
|
|
166
|
-
supports: ['Use msedge']
|
|
167
|
-
}
|
|
168
|
-
],
|
|
169
|
-
gaps: [],
|
|
170
|
-
lowValueOutcomes: [],
|
|
171
|
-
suggestedHeadlessUrl: undefined,
|
|
172
|
-
exhaustedBudget: false
|
|
173
|
-
})
|
|
174
|
-
},
|
|
175
|
-
headlessFetch: vi.fn()
|
|
176
|
-
});
|
|
177
|
-
const result = await orchestrator.run({ query: 'playwright edge channel' });
|
|
178
|
-
expect(result.decision.action).toBe('answer');
|
|
179
|
-
});
|
|
180
|
-
it('does not escalate to headless when strong http evidence already answers the question', async () => {
|
|
181
|
-
const headlessFetch = vi.fn();
|
|
182
|
-
const orchestrator = createResearchOrchestrator({
|
|
183
|
-
worker: {
|
|
184
|
-
run: vi.fn().mockResolvedValue({
|
|
185
|
-
searchQueries: ['vitest coverage docs'],
|
|
186
|
-
evidence: [
|
|
187
|
-
{
|
|
188
|
-
title: 'Coverage | Guide | Vitest',
|
|
189
|
-
url: 'https://vitest.dev/guide/coverage.html',
|
|
190
|
-
sourceKind: 'official-docs',
|
|
191
|
-
method: 'http',
|
|
192
|
-
summary: 'Coverage docs',
|
|
193
|
-
supports: ['provider v8']
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
title: 'Coverage package',
|
|
197
|
-
url: 'https://vitest.dev/guide/coverage.html#provider',
|
|
198
|
-
sourceKind: 'official-api',
|
|
199
|
-
method: 'http',
|
|
200
|
-
summary: 'Install @vitest/coverage-v8',
|
|
201
|
-
supports: ['install package']
|
|
202
|
-
}
|
|
203
|
-
],
|
|
204
|
-
gaps: [{ kind: 'fetch-failed', message: 'One page was weak over HTTP.' }],
|
|
205
|
-
lowValueOutcomes: [],
|
|
206
|
-
suggestedHeadlessUrl: 'https://vitest.dev/guide/coverage.html',
|
|
207
|
-
exhaustedBudget: false
|
|
208
|
-
})
|
|
209
|
-
},
|
|
210
|
-
headlessFetch
|
|
211
|
-
});
|
|
212
|
-
const result = await orchestrator.run({ query: 'vitest coverage docs' });
|
|
213
|
-
expect(result.decision.action).toBe('answer');
|
|
214
|
-
expect(headlessFetch).not.toHaveBeenCalled();
|
|
215
|
-
});
|
|
216
|
-
it('prefers stronger sources over package pages in approved evidence', async () => {
|
|
217
|
-
const orchestrator = createResearchOrchestrator({
|
|
218
|
-
worker: {
|
|
219
|
-
run: vi.fn().mockResolvedValue({
|
|
220
|
-
searchQueries: ['duckduckgo scraping node'],
|
|
221
|
-
evidence: [
|
|
222
|
-
{
|
|
223
|
-
title: 'npm package',
|
|
224
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
225
|
-
sourceKind: 'package-page',
|
|
226
|
-
method: 'http',
|
|
227
|
-
summary: 'Package page',
|
|
228
|
-
supports: ['Install command']
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
title: 'SearXNG docs',
|
|
232
|
-
url: 'https://docs.searxng.org/dev/engines/online/duckduckgo.html',
|
|
233
|
-
sourceKind: 'community',
|
|
234
|
-
method: 'http',
|
|
235
|
-
summary: 'vqd and pagination details',
|
|
236
|
-
supports: ['vqd matters']
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
title: 'ddg-search',
|
|
240
|
-
url: 'https://github.com/camohiddendj/ddg-search',
|
|
241
|
-
sourceKind: 'community',
|
|
242
|
-
method: 'http',
|
|
243
|
-
summary: 'Node implementation',
|
|
244
|
-
supports: ['bot detection note']
|
|
245
|
-
}
|
|
246
|
-
],
|
|
247
|
-
gaps: [],
|
|
248
|
-
lowValueOutcomes: [],
|
|
249
|
-
suggestedHeadlessUrl: undefined,
|
|
250
|
-
exhaustedBudget: false
|
|
251
|
-
})
|
|
252
|
-
},
|
|
253
|
-
headlessFetch: vi.fn()
|
|
254
|
-
});
|
|
255
|
-
const result = await orchestrator.run({ query: 'duckduckgo scraping node' });
|
|
256
|
-
expect(result.decision.action).toBe('research-again');
|
|
257
|
-
expect(result.evidence[0]?.sourceKind).not.toBe('package-page');
|
|
258
|
-
});
|
|
259
|
-
it('records low-value bot-check outcomes as a reason not to escalate again', async () => {
|
|
260
|
-
const headlessFetch = vi.fn().mockResolvedValue({
|
|
261
|
-
status: 'ok',
|
|
262
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
263
|
-
content: {
|
|
264
|
-
title: 'Just a moment...',
|
|
265
|
-
text: 'Performing security verification'
|
|
266
|
-
},
|
|
267
|
-
metadata: {
|
|
268
|
-
method: 'headless',
|
|
269
|
-
cacheHit: false,
|
|
270
|
-
browser: 'edge',
|
|
271
|
-
navigationMs: 5000,
|
|
272
|
-
truncated: false
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
const orchestrator = createResearchOrchestrator({
|
|
276
|
-
worker: {
|
|
277
|
-
run: vi.fn().mockResolvedValue({
|
|
278
|
-
searchQueries: ['duckduckgo scraping node'],
|
|
279
|
-
evidence: [],
|
|
280
|
-
gaps: [{ kind: 'needs-more-evidence', message: 'Need one more technical source.' }],
|
|
281
|
-
lowValueOutcomes: [
|
|
282
|
-
{
|
|
283
|
-
kind: 'bot-check',
|
|
284
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
285
|
-
message: 'Security verification wall.'
|
|
286
|
-
}
|
|
287
|
-
],
|
|
288
|
-
suggestedHeadlessUrl: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
289
|
-
exhaustedBudget: false
|
|
290
|
-
})
|
|
291
|
-
},
|
|
292
|
-
headlessFetch
|
|
293
|
-
});
|
|
294
|
-
const result = await orchestrator.run({ query: 'duckduckgo scraping node' });
|
|
295
|
-
expect(result.decision.action).toBe('research-again');
|
|
296
|
-
expect(headlessFetch).not.toHaveBeenCalled();
|
|
297
|
-
});
|
|
298
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { createResearchWorker } from '../../src/orchestration/research-worker.js';
|
|
3
|
-
describe('research worker', () => {
|
|
4
|
-
it('runs one bounded search/fetch pass and summarizes evidence', async () => {
|
|
5
|
-
const worker = createResearchWorker({
|
|
6
|
-
search: vi.fn().mockResolvedValue({
|
|
7
|
-
status: 'ok',
|
|
8
|
-
results: [
|
|
9
|
-
{
|
|
10
|
-
title: 'Browsers | Playwright',
|
|
11
|
-
url: 'https://playwright.dev/docs/browsers',
|
|
12
|
-
snippet: 'Playwright can operate against branded Chrome and Edge browsers.'
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
title: 'Use Playwright to automate and test in Microsoft Edge',
|
|
16
|
-
url: 'https://learn.microsoft.com/en-us/microsoft-edge/playwright/',
|
|
17
|
-
snippet: 'Use channel msedge to run in Edge.'
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
21
|
-
}),
|
|
22
|
-
fetchPage: vi.fn()
|
|
23
|
-
.mockResolvedValueOnce({
|
|
24
|
-
status: 'ok',
|
|
25
|
-
url: 'https://playwright.dev/docs/browsers',
|
|
26
|
-
content: {
|
|
27
|
-
title: 'Browsers | Playwright',
|
|
28
|
-
text: 'Playwright can operate against branded Google Chrome and Microsoft Edge browsers available on the machine.'
|
|
29
|
-
},
|
|
30
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html', truncated: false }
|
|
31
|
-
})
|
|
32
|
-
.mockResolvedValueOnce({
|
|
33
|
-
status: 'ok',
|
|
34
|
-
url: 'https://learn.microsoft.com/en-us/microsoft-edge/playwright/',
|
|
35
|
-
content: {
|
|
36
|
-
title: 'Use Playwright to automate and test in Microsoft Edge',
|
|
37
|
-
text: 'Use channel: msedge to run tests in Microsoft Edge.'
|
|
38
|
-
},
|
|
39
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html', truncated: false }
|
|
40
|
-
})
|
|
41
|
-
});
|
|
42
|
-
const result = await worker.run({
|
|
43
|
-
query: 'playwright installed edge executablePath vs channel',
|
|
44
|
-
maxSearchRounds: 1,
|
|
45
|
-
maxFetches: 2
|
|
46
|
-
});
|
|
47
|
-
expect(result.searchQueries).toEqual(['playwright installed edge executablePath vs channel']);
|
|
48
|
-
expect(result.evidence).toHaveLength(2);
|
|
49
|
-
expect(result.evidence[0].summary.length).toBeGreaterThan(0);
|
|
50
|
-
expect(result.exhaustedBudget).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
it('flags a likely headless candidate when http fetch is weak', async () => {
|
|
53
|
-
const worker = createResearchWorker({
|
|
54
|
-
search: vi.fn().mockResolvedValue({
|
|
55
|
-
status: 'ok',
|
|
56
|
-
results: [
|
|
57
|
-
{
|
|
58
|
-
title: 'Dynamic docs site',
|
|
59
|
-
url: 'https://example.com/app',
|
|
60
|
-
snippet: 'JS-heavy docs app'
|
|
61
|
-
}
|
|
62
|
-
],
|
|
63
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
64
|
-
}),
|
|
65
|
-
fetchPage: vi.fn().mockResolvedValue({
|
|
66
|
-
status: 'needs_headless',
|
|
67
|
-
url: 'https://example.com/app',
|
|
68
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html' },
|
|
69
|
-
error: { code: 'WEAK_EXTRACTION', message: 'HTTP extraction was not reliable enough.' }
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
const result = await worker.run({
|
|
73
|
-
query: 'dynamic docs app',
|
|
74
|
-
maxSearchRounds: 1,
|
|
75
|
-
maxFetches: 1
|
|
76
|
-
});
|
|
77
|
-
expect(result.suggestedHeadlessUrl).toBe('https://example.com/app');
|
|
78
|
-
expect(result.gaps[0]?.kind).toBe('fetch-failed');
|
|
79
|
-
});
|
|
80
|
-
it('records empty search results as a low-value outcome', async () => {
|
|
81
|
-
const worker = createResearchWorker({
|
|
82
|
-
search: vi.fn().mockResolvedValue({
|
|
83
|
-
status: 'ok',
|
|
84
|
-
results: [],
|
|
85
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
86
|
-
}),
|
|
87
|
-
fetchPage: vi.fn()
|
|
88
|
-
});
|
|
89
|
-
const result = await worker.run({
|
|
90
|
-
query: 'vitest coverage docs',
|
|
91
|
-
maxSearchRounds: 1,
|
|
92
|
-
maxFetches: 2
|
|
93
|
-
});
|
|
94
|
-
expect(result.lowValueOutcomes).toEqual([
|
|
95
|
-
{
|
|
96
|
-
kind: 'empty-search',
|
|
97
|
-
message: 'Search returned no results for this pass.'
|
|
98
|
-
}
|
|
99
|
-
]);
|
|
100
|
-
expect(result.evidence).toHaveLength(0);
|
|
101
|
-
});
|
|
102
|
-
it('limits headless suggestion to one flagged url even if multiple fetches are weak', async () => {
|
|
103
|
-
const worker = createResearchWorker({
|
|
104
|
-
search: vi.fn().mockResolvedValue({
|
|
105
|
-
status: 'ok',
|
|
106
|
-
results: [
|
|
107
|
-
{ title: 'Page A', url: 'https://example.com/a', snippet: 'A' },
|
|
108
|
-
{ title: 'Page B', url: 'https://example.com/b', snippet: 'B' }
|
|
109
|
-
],
|
|
110
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
111
|
-
}),
|
|
112
|
-
fetchPage: vi.fn()
|
|
113
|
-
.mockResolvedValueOnce({
|
|
114
|
-
status: 'needs_headless',
|
|
115
|
-
url: 'https://example.com/a',
|
|
116
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html' },
|
|
117
|
-
error: { code: 'WEAK_EXTRACTION', message: 'Weak A' }
|
|
118
|
-
})
|
|
119
|
-
.mockResolvedValueOnce({
|
|
120
|
-
status: 'needs_headless',
|
|
121
|
-
url: 'https://example.com/b',
|
|
122
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html' },
|
|
123
|
-
error: { code: 'WEAK_EXTRACTION', message: 'Weak B' }
|
|
124
|
-
})
|
|
125
|
-
});
|
|
126
|
-
const result = await worker.run({
|
|
127
|
-
query: 'two weak pages',
|
|
128
|
-
maxSearchRounds: 1,
|
|
129
|
-
maxFetches: 2
|
|
130
|
-
});
|
|
131
|
-
expect(result.suggestedHeadlessUrl).toBe('https://example.com/a');
|
|
132
|
-
expect(result.gaps).toHaveLength(2);
|
|
133
|
-
});
|
|
134
|
-
it('classifies npm package pages as low-value when they do not add useful evidence', async () => {
|
|
135
|
-
const worker = createResearchWorker({
|
|
136
|
-
search: vi.fn().mockResolvedValue({
|
|
137
|
-
status: 'ok',
|
|
138
|
-
results: [
|
|
139
|
-
{
|
|
140
|
-
title: 'duck-duck-scrape - npm',
|
|
141
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
142
|
-
snippet: 'Package page'
|
|
143
|
-
}
|
|
144
|
-
],
|
|
145
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
146
|
-
}),
|
|
147
|
-
fetchPage: vi.fn().mockResolvedValue({
|
|
148
|
-
status: 'ok',
|
|
149
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
150
|
-
content: {
|
|
151
|
-
title: 'duck-duck-scrape - npm',
|
|
152
|
-
text: 'Package page, install instructions, version history.'
|
|
153
|
-
},
|
|
154
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html', truncated: false }
|
|
155
|
-
})
|
|
156
|
-
});
|
|
157
|
-
const result = await worker.run({
|
|
158
|
-
query: 'duckduckgo scraping node',
|
|
159
|
-
maxSearchRounds: 1,
|
|
160
|
-
maxFetches: 1
|
|
161
|
-
});
|
|
162
|
-
expect(result.evidence).toHaveLength(0);
|
|
163
|
-
expect(result.lowValueOutcomes).toEqual([
|
|
164
|
-
{
|
|
165
|
-
kind: 'low-value-page',
|
|
166
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
167
|
-
message: 'Fetched page did not add strong research evidence.'
|
|
168
|
-
}
|
|
169
|
-
]);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createResearchWorkflow } from '../../src/orchestration/index.js';
|
|
3
|
-
describe('research workflow composition', () => {
|
|
4
|
-
it('can compose the orchestrator from existing search and fetch capabilities', async () => {
|
|
5
|
-
const workflow = createResearchWorkflow({
|
|
6
|
-
search: async () => ({
|
|
7
|
-
status: 'ok',
|
|
8
|
-
results: [],
|
|
9
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
10
|
-
}),
|
|
11
|
-
fetchPage: async () => ({
|
|
12
|
-
status: 'unsupported',
|
|
13
|
-
url: 'https://example.com',
|
|
14
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html' }
|
|
15
|
-
}),
|
|
16
|
-
headlessFetch: async () => ({
|
|
17
|
-
status: 'error',
|
|
18
|
-
url: 'https://example.com',
|
|
19
|
-
metadata: { method: 'headless', cacheHit: false },
|
|
20
|
-
error: { code: 'BROWSER_NOT_FOUND', message: 'No browser found.' }
|
|
21
|
-
})
|
|
22
|
-
});
|
|
23
|
-
const result = await workflow.run({ query: 'example query' });
|
|
24
|
-
expect(result.decision.action).toBeDefined();
|
|
25
|
-
});
|
|
26
|
-
it('does not spend headless on a low-value npm package page when other technical sources exist', async () => {
|
|
27
|
-
const workflow = createResearchWorkflow({
|
|
28
|
-
search: async () => ({
|
|
29
|
-
status: 'ok',
|
|
30
|
-
results: [
|
|
31
|
-
{
|
|
32
|
-
title: 'ddg-search',
|
|
33
|
-
url: 'https://github.com/camohiddendj/ddg-search',
|
|
34
|
-
snippet: 'Node scraper'
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
title: 'duck-duck-scrape - npm',
|
|
38
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
39
|
-
snippet: 'Package page'
|
|
40
|
-
}
|
|
41
|
-
],
|
|
42
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
43
|
-
}),
|
|
44
|
-
fetchPage: async ({ url }) => {
|
|
45
|
-
if (url.includes('github.com/camohiddendj/ddg-search')) {
|
|
46
|
-
return {
|
|
47
|
-
status: 'ok',
|
|
48
|
-
url,
|
|
49
|
-
content: {
|
|
50
|
-
title: 'ddg-search',
|
|
51
|
-
text: 'DuckDuckGo HTML search scraper with bot-detection and pagination notes.'
|
|
52
|
-
},
|
|
53
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html', truncated: false }
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
status: 'needs_headless',
|
|
58
|
-
url,
|
|
59
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html' },
|
|
60
|
-
error: { code: 'WEAK_EXTRACTION', message: 'Weak extraction.' }
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
headlessFetch: async () => ({
|
|
64
|
-
status: 'ok',
|
|
65
|
-
url: 'https://www.npmjs.com/package/duck-duck-scrape',
|
|
66
|
-
content: { title: 'Just a moment...', text: 'Security verification' },
|
|
67
|
-
metadata: {
|
|
68
|
-
method: 'headless',
|
|
69
|
-
cacheHit: false,
|
|
70
|
-
browser: 'edge',
|
|
71
|
-
navigationMs: 4000,
|
|
72
|
-
truncated: false
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
});
|
|
76
|
-
const result = await workflow.run({ query: 'duckduckgo scraping node pitfalls' });
|
|
77
|
-
expect(result.decision.action).toBe('research-again');
|
|
78
|
-
});
|
|
79
|
-
it('produces enough approved evidence for web_explore to format a compact research result', async () => {
|
|
80
|
-
const workflow = createResearchWorkflow({
|
|
81
|
-
search: async () => ({
|
|
82
|
-
status: 'ok',
|
|
83
|
-
results: [
|
|
84
|
-
{
|
|
85
|
-
title: 'Coverage | Guide | Vitest',
|
|
86
|
-
url: 'https://vitest.dev/guide/coverage.html',
|
|
87
|
-
snippet: 'Coverage docs'
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
title: 'coverage | Config | Vitest',
|
|
91
|
-
url: 'https://vitest.dev/config/coverage',
|
|
92
|
-
snippet: 'Coverage config'
|
|
93
|
-
}
|
|
94
|
-
],
|
|
95
|
-
metadata: { backend: 'duckduckgo', cacheHit: false }
|
|
96
|
-
}),
|
|
97
|
-
fetchPage: async ({ url }) => ({
|
|
98
|
-
status: 'ok',
|
|
99
|
-
url,
|
|
100
|
-
content: {
|
|
101
|
-
title: url.includes('/config/') ? 'coverage | Config | Vitest' : 'Coverage | Guide | Vitest',
|
|
102
|
-
text: url.includes('/config/')
|
|
103
|
-
? 'coverage.enabled and coverage.provider can be configured here.'
|
|
104
|
-
: 'Set coverage.provider to v8 and install @vitest/coverage-v8.'
|
|
105
|
-
},
|
|
106
|
-
metadata: { method: 'http', cacheHit: false, contentType: 'text/html', truncated: false }
|
|
107
|
-
}),
|
|
108
|
-
headlessFetch: async () => ({
|
|
109
|
-
status: 'error',
|
|
110
|
-
url: 'https://example.com',
|
|
111
|
-
metadata: { method: 'headless', cacheHit: false },
|
|
112
|
-
error: { code: 'BROWSER_NOT_FOUND', message: 'No browser found.' }
|
|
113
|
-
})
|
|
114
|
-
});
|
|
115
|
-
const result = await workflow.run({ query: 'vitest coverage docs' });
|
|
116
|
-
expect(result.decision.action).toBe('answer');
|
|
117
|
-
expect(result.evidence.length).toBeGreaterThanOrEqual(2);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
5
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
6
|
-
describe('package manifest', () => {
|
|
7
|
-
it('is configured for publishing as a public pi package', () => {
|
|
8
|
-
expect(packageJson.private).not.toBe(true);
|
|
9
|
-
expect(packageJson.name).toBe('@demigodmode/pi-web-agent');
|
|
10
|
-
expect(packageJson.keywords).toContain('pi-package');
|
|
11
|
-
expect(packageJson.publishConfig).toEqual({ access: 'public' });
|
|
12
|
-
});
|
|
13
|
-
it('points pi to the packaged runtime entrypoint', () => {
|
|
14
|
-
expect(packageJson.pi).toEqual({
|
|
15
|
-
extensions: ['./dist/extension.js']
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
it('declares a clean package surface', () => {
|
|
19
|
-
expect(packageJson.main).toBe('./dist/extension.js');
|
|
20
|
-
expect(packageJson.types).toBe('./dist/extension.d.ts');
|
|
21
|
-
expect(packageJson.exports).toEqual({
|
|
22
|
-
'.': {
|
|
23
|
-
types: './dist/extension.d.ts',
|
|
24
|
-
import: './dist/extension.js'
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
expect(packageJson.files).toEqual(['dist', 'README.md']);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
const root = process.cwd();
|
|
5
|
-
const packageJson = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
6
|
-
describe('release foundation', () => {
|
|
7
|
-
it('has a changelog file checked into the repo', () => {
|
|
8
|
-
expect(existsSync(path.join(root, 'CHANGELOG.md'))).toBe(true);
|
|
9
|
-
});
|
|
10
|
-
it('has a license file checked into the repo', () => {
|
|
11
|
-
expect(existsSync(path.join(root, 'LICENSE'))).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
it('keeps package metadata compatible with open source publishing', () => {
|
|
14
|
-
expect(packageJson.license).toBe('AGPL-3.0-only');
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|