@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.
Files changed (117) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +61 -5
  3. package/dist/commands/web-agent-config.d.ts +23 -0
  4. package/dist/commands/web-agent-config.js +249 -0
  5. package/dist/extension.js +30 -66
  6. package/dist/orchestration/answer-synthesizer.d.ts +8 -0
  7. package/dist/orchestration/answer-synthesizer.js +17 -0
  8. package/dist/orchestration/candidate-selector.d.ts +6 -0
  9. package/dist/orchestration/candidate-selector.js +24 -0
  10. package/dist/orchestration/evidence-ranker.d.ts +4 -0
  11. package/dist/orchestration/evidence-ranker.js +36 -0
  12. package/dist/orchestration/index.d.ts +6 -21
  13. package/dist/orchestration/query-planner.d.ts +7 -0
  14. package/dist/orchestration/query-planner.js +37 -0
  15. package/dist/orchestration/research-orchestrator.d.ts +7 -22
  16. package/dist/orchestration/research-orchestrator.js +185 -73
  17. package/dist/orchestration/research-types.d.ts +6 -0
  18. package/dist/orchestration/research-worker.js +8 -1
  19. package/dist/orchestration/stop-decider.d.ts +19 -0
  20. package/dist/orchestration/stop-decider.js +14 -0
  21. package/dist/presentation/config-store.d.ts +23 -0
  22. package/dist/presentation/config-store.js +64 -0
  23. package/dist/presentation/config.d.ts +7 -0
  24. package/dist/presentation/config.js +44 -0
  25. package/dist/presentation/explore-presentation.d.ts +3 -0
  26. package/dist/presentation/explore-presentation.js +56 -0
  27. package/dist/presentation/fetch-presentation.d.ts +5 -0
  28. package/dist/presentation/fetch-presentation.js +40 -0
  29. package/dist/presentation/search-presentation.d.ts +3 -0
  30. package/dist/presentation/search-presentation.js +30 -0
  31. package/dist/presentation/select-view.d.ts +2 -0
  32. package/dist/presentation/select-view.js +12 -0
  33. package/dist/presentation/types.d.ts +50 -0
  34. package/dist/presentation/types.js +1 -0
  35. package/dist/search/duckduckgo.d.ts +6 -1
  36. package/dist/search/duckduckgo.js +11 -1
  37. package/dist/tools/web-explore.d.ts +16 -16
  38. package/dist/tools/web-explore.js +21 -29
  39. package/dist/tools/web-fetch-headless.js +11 -2
  40. package/dist/tools/web-fetch.js +11 -2
  41. package/dist/tools/web-search.js +99 -12
  42. package/dist/types.d.ts +22 -0
  43. package/package.json +75 -75
  44. package/dist/scripts/live-web-eval.d.ts +0 -1
  45. package/dist/scripts/live-web-eval.js +0 -411
  46. package/dist/src/cache/ttl-cache.d.ts +0 -8
  47. package/dist/src/cache/ttl-cache.js +0 -21
  48. package/dist/src/extension.d.ts +0 -2
  49. package/dist/src/extension.js +0 -155
  50. package/dist/src/extract/readability.d.ts +0 -8
  51. package/dist/src/extract/readability.js +0 -93
  52. package/dist/src/fetch/browser-resolution.d.ts +0 -15
  53. package/dist/src/fetch/browser-resolution.js +0 -55
  54. package/dist/src/fetch/headless-fetch.d.ts +0 -18
  55. package/dist/src/fetch/headless-fetch.js +0 -87
  56. package/dist/src/fetch/http-fetch.d.ts +0 -4
  57. package/dist/src/fetch/http-fetch.js +0 -50
  58. package/dist/src/orchestration/index.d.ts +0 -41
  59. package/dist/src/orchestration/index.js +0 -9
  60. package/dist/src/orchestration/research-orchestrator.d.ts +0 -43
  61. package/dist/src/orchestration/research-orchestrator.js +0 -87
  62. package/dist/src/orchestration/research-types.d.ts +0 -41
  63. package/dist/src/orchestration/research-types.js +0 -1
  64. package/dist/src/orchestration/research-worker.d.ts +0 -16
  65. package/dist/src/orchestration/research-worker.js +0 -131
  66. package/dist/src/search/duckduckgo.d.ts +0 -9
  67. package/dist/src/search/duckduckgo.js +0 -52
  68. package/dist/src/tools/web-explore.d.ts +0 -44
  69. package/dist/src/tools/web-explore.js +0 -50
  70. package/dist/src/tools/web-fetch-headless.d.ts +0 -6
  71. package/dist/src/tools/web-fetch-headless.js +0 -14
  72. package/dist/src/tools/web-fetch.d.ts +0 -6
  73. package/dist/src/tools/web-fetch.js +0 -14
  74. package/dist/src/tools/web-search.d.ts +0 -10
  75. package/dist/src/tools/web-search.js +0 -103
  76. package/dist/src/types.d.ts +0 -48
  77. package/dist/src/types.js +0 -7
  78. package/dist/tests/cache/ttl-cache.test.d.ts +0 -1
  79. package/dist/tests/cache/ttl-cache.test.js +0 -19
  80. package/dist/tests/contracts.test.d.ts +0 -1
  81. package/dist/tests/contracts.test.js +0 -65
  82. package/dist/tests/extension.test.d.ts +0 -1
  83. package/dist/tests/extension.test.js +0 -123
  84. package/dist/tests/extract/readability.test.d.ts +0 -1
  85. package/dist/tests/extract/readability.test.js +0 -79
  86. package/dist/tests/fetch/browser-resolution.test.d.ts +0 -1
  87. package/dist/tests/fetch/browser-resolution.test.js +0 -37
  88. package/dist/tests/fetch/headless-fetch.smoke.test.d.ts +0 -1
  89. package/dist/tests/fetch/headless-fetch.smoke.test.js +0 -17
  90. package/dist/tests/fetch/headless-fetch.test.d.ts +0 -1
  91. package/dist/tests/fetch/headless-fetch.test.js +0 -150
  92. package/dist/tests/fetch/http-fetch.test.d.ts +0 -1
  93. package/dist/tests/fetch/http-fetch.test.js +0 -129
  94. package/dist/tests/orchestration/research-orchestrator.test.d.ts +0 -1
  95. package/dist/tests/orchestration/research-orchestrator.test.js +0 -298
  96. package/dist/tests/orchestration/research-worker.test.d.ts +0 -1
  97. package/dist/tests/orchestration/research-worker.test.js +0 -171
  98. package/dist/tests/orchestration/research-workflow.test.d.ts +0 -1
  99. package/dist/tests/orchestration/research-workflow.test.js +0 -119
  100. package/dist/tests/package-manifest.test.d.ts +0 -1
  101. package/dist/tests/package-manifest.test.js +0 -29
  102. package/dist/tests/release-foundation.test.d.ts +0 -1
  103. package/dist/tests/release-foundation.test.js +0 -16
  104. package/dist/tests/release-script.test.d.ts +0 -1
  105. package/dist/tests/release-script.test.js +0 -72
  106. package/dist/tests/search/duckduckgo.test.d.ts +0 -1
  107. package/dist/tests/search/duckduckgo.test.js +0 -103
  108. package/dist/tests/tools/web-explore.test.d.ts +0 -1
  109. package/dist/tests/tools/web-explore.test.js +0 -163
  110. package/dist/tests/tools/web-fetch-headless.test.d.ts +0 -1
  111. package/dist/tests/tools/web-fetch-headless.test.js +0 -31
  112. package/dist/tests/tools/web-fetch.test.d.ts +0 -1
  113. package/dist/tests/tools/web-fetch.test.js +0 -27
  114. package/dist/tests/tools/web-search.test.d.ts +0 -1
  115. package/dist/tests/tools/web-search.test.js +0 -125
  116. package/dist/vitest.config.d.ts +0 -2
  117. 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
- decision: {
36
- action: "research-again";
37
- rationale: string;
38
- followupQuery: string;
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
- 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
- }
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 sortEvidence(evidence) {
20
- return [...evidence].sort((left, right) => sourceRank(left.sourceKind) - sourceRank(right.sourceKind));
22
+ function summarizeText(text, maxLength = 180) {
23
+ return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
21
24
  }
22
- function strongEvidence(evidence) {
23
- return evidence.filter((item) => item.sourceKind === 'official-docs' ||
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 hasOfficialDocsOrApi(evidence) {
28
- return evidence.some((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api');
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 hasBotCheck(outcomes) {
31
- return outcomes.some((outcome) => outcome.kind === 'bot-check');
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 isHeadlessWorthTrying(pass, approvedEvidence) {
34
- if (!pass.suggestedHeadlessUrl)
35
- return false;
36
- if (hasBotCheck(pass.lowValueOutcomes))
37
- return false;
38
- if (approvedEvidence.length >= 2 && hasOfficialDocsOrApi(approvedEvidence))
39
- return false;
40
- const candidate = pass.suggestedHeadlessUrl;
41
- return !candidate.includes('npmjs.com/package/');
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 pass = await worker.run({ query, maxSearchRounds: 1, maxFetches: 3 });
47
- const approvedEvidence = sortEvidence(pass.evidence.filter((item) => item.sourceKind !== 'package-page'));
48
- const strong = strongEvidence(approvedEvidence);
49
- const enoughEvidence = strong.length >= 2 && hasOfficialDocsOrApi(approvedEvidence);
50
- if (enoughEvidence) {
51
- const decision = {
52
- action: 'answer',
53
- rationale: 'Two strong sources with official support are enough to answer safely.',
54
- approvedEvidence
55
- };
56
- return { decision, evidence: approvedEvidence, workerPass: pass };
57
- }
58
- if (isHeadlessWorthTrying(pass, approvedEvidence)) {
59
- const url = pass.suggestedHeadlessUrl;
60
- await headlessFetch({ url });
61
- const decision = {
62
- action: 'escalate-headless',
63
- rationale: 'One high-value page is worth a single orchestrator-approved headless retry.',
64
- url,
65
- approvedEvidence
66
- };
67
- return { decision, evidence: approvedEvidence, workerPass: pass };
68
- }
69
- const hasConcreteGap = pass.gaps.length > 0;
70
- const onlyLowValueOutcomes = pass.lowValueOutcomes.length > 0 && pass.evidence.length === 0;
71
- if (!hasConcreteGap || onlyLowValueOutcomes) {
72
- const decision = {
73
- action: 'research-again',
74
- rationale: 'Current results did not justify more escalation; continue only with a more targeted pass.',
75
- followupQuery: query
76
- };
77
- return { decision, evidence: approvedEvidence, workerPass: pass };
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 decision = {
80
- action: 'research-again',
81
- rationale: 'The first pass did not gather enough strong evidence to answer safely.',
82
- followupQuery: query
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 = searchResult.results.slice(0, maxFetches);
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,3 @@
1
+ import type { WebExploreResponse } from '../types.js';
2
+ import type { PresentationEnvelope } from './types.js';
3
+ export declare function buildExplorePresentation(result: WebExploreResponse): PresentationEnvelope;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { WebSearchResponse } from '../types.js';
2
+ import type { PresentationEnvelope } from './types.js';
3
+ export declare function buildSearchPresentation(result: WebSearchResponse): PresentationEnvelope;