@demigodmode/pi-web-agent 0.3.1 → 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/README.md CHANGED
@@ -1,119 +1,119 @@
1
- # pi-web-agent
2
-
3
- [![CI](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/@demigodmode/pi-web-agent)](https://www.npmjs.com/package/@demigodmode/pi-web-agent)
5
- [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://demigodmode.github.io/pi-web-agent/)
6
-
7
- `@demigodmode/pi-web-agent` is a Pi package for web access.
8
-
9
- The whole point is keeping the boundaries straight:
10
-
11
- - `web_search` is for discovery
12
- - `web_fetch` is for plain HTTP reads
13
- - `web_fetch_headless` is the explicit browser path
14
- - `web_explore` is the bounded research path
15
-
16
- That sounds obvious, but a lot of agent tooling gets fuzzy right there. This package is meant to be stricter about what it actually did and more willing to say when a read was not good enough to trust.
17
-
18
- ## Install
19
-
20
- ```bash
21
- pi install npm:@demigodmode/pi-web-agent
22
- ```
23
-
24
- Later on, update installed packages with:
25
-
26
- ```bash
27
- pi update
28
- ```
29
-
30
- ## Docs
31
-
32
- Docs site:
33
-
34
- - https://demigodmode.github.io/pi-web-agent/
35
-
36
- Work on the docs locally:
37
-
38
- ```bash
39
- npm run docs:dev
40
- ```
41
-
42
- Build the docs:
43
-
44
- ```bash
45
- npm run docs:build
46
- ```
47
-
48
- ## Presentation modes
49
-
50
- `pi-web-agent` renders web tool output in one visible mode at a time:
51
-
52
- - `compact` — short summary, default everywhere
53
- - `preview` slightly richer bounded view
54
- - `verbose` — fuller bounded view
55
-
56
- ## Settings
57
-
58
- Primary UI:
59
-
60
- ```text
61
- /web-agent settings
62
- ```
63
-
64
- Helper commands:
65
-
66
- ```text
67
- /web-agent show
68
- /web-agent reset project
69
- /web-agent reset global
70
- /web-agent mode preview
71
- /web-agent mode web_search verbose
72
- /web-agent mode web_search inherit
73
- ```
74
-
75
- Config files:
76
-
77
- ```text
78
- Global: ~/.pi/agent/extensions/pi-web-agent/config.json
79
- Project: .pi/extensions/pi-web-agent/config.json
80
- ```
81
-
82
- Precedence:
83
-
84
- - built-in defaults
85
- - global config
86
- - project config
87
-
88
- Project config overrides global config.
89
-
90
- Example:
91
-
92
- ```json
93
- {
94
- "presentation": {
95
- "defaultMode": "compact",
96
- "tools": {
97
- "web_search": { "mode": "preview" },
98
- "web_explore": { "mode": "verbose" }
99
- }
100
- }
101
- }
102
- ```
103
-
104
- ## Local development
105
-
106
- ```bash
107
- npm install
108
- npm test
109
- npm run lint
110
- npm run build
111
- ```
112
-
113
- For local Pi work, this repo includes `.pi/extensions/pi-web-agent.ts`.
114
-
115
- If Pi is already running, use `/reload` after changes.
116
-
117
- ## License
118
-
119
- AGPL-3.0-only. See `LICENSE`.
1
+ # pi-web-agent
2
+
3
+ [![CI](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@demigodmode/pi-web-agent)](https://www.npmjs.com/package/@demigodmode/pi-web-agent)
5
+ [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://demigodmode.github.io/pi-web-agent/)
6
+
7
+ `@demigodmode/pi-web-agent` is a Pi package for web access.
8
+
9
+ Most agent web tools blur search, fetch, browser rendering, and research into one vague thing. `pi-web-agent` exposes one public research tool, `web_explore`, and keeps search/fetch/headless work inside that bounded workflow.
10
+
11
+ The point is keeping the model-facing boundary simple: ask `web_explore` to research a question, and it handles discovery, HTTP reads, targeted browser rendering, source ranking, and caveats internally.
12
+
13
+ That sounds obvious, but a lot of agent tooling gets fuzzy right there. This package is meant to be stricter about what it actually did and more willing to say when a read was not good enough to trust.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pi install npm:@demigodmode/pi-web-agent
19
+ ```
20
+
21
+ Later on, update installed packages with:
22
+
23
+ ```bash
24
+ pi update
25
+ ```
26
+
27
+ ## Docs
28
+
29
+ Docs site:
30
+
31
+ - https://demigodmode.github.io/pi-web-agent/
32
+
33
+ Work on the docs locally:
34
+
35
+ ```bash
36
+ npm run docs:dev
37
+ ```
38
+
39
+ Build the docs:
40
+
41
+ ```bash
42
+ npm run docs:build
43
+ ```
44
+
45
+ ## Presentation modes
46
+
47
+ `pi-web-agent` renders web tool output in one visible mode at a time:
48
+
49
+ - `compact` — short summary, default everywhere
50
+ - `preview` slightly richer bounded view
51
+ - `verbose` — fuller bounded view
52
+
53
+ See the `v0.3.0` release notes for a before/after of the transcript cleanup:
54
+
55
+ - https://github.com/demigodmode/pi-web-agent/releases/tag/v0.3.0
56
+
57
+ ## Settings
58
+
59
+ Primary UI:
60
+
61
+ ```text
62
+ /web-agent settings
63
+ ```
64
+
65
+ Helper commands:
66
+
67
+ ```text
68
+ /web-agent show
69
+ /web-agent reset project
70
+ /web-agent reset global
71
+ /web-agent mode preview
72
+ /web-agent mode web_explore verbose
73
+ /web-agent mode web_explore inherit
74
+ ```
75
+
76
+ Config files:
77
+
78
+ ```text
79
+ Global: ~/.pi/agent/extensions/pi-web-agent/config.json
80
+ Project: .pi/extensions/pi-web-agent/config.json
81
+ ```
82
+
83
+ Precedence:
84
+
85
+ - built-in defaults
86
+ - global config
87
+ - project config
88
+
89
+ Project config overrides global config.
90
+
91
+ Example:
92
+
93
+ ```json
94
+ {
95
+ "presentation": {
96
+ "defaultMode": "compact",
97
+ "tools": {
98
+ "web_explore": { "mode": "verbose" }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Local development
105
+
106
+ ```bash
107
+ npm install
108
+ npm test
109
+ npm run lint
110
+ npm run build
111
+ ```
112
+
113
+ For local Pi work, this repo includes `.pi/extensions/pi-web-agent.ts`.
114
+
115
+ If Pi is already running, use `/reload` after changes.
116
+
117
+ ## License
118
+
119
+ AGPL-3.0-only. See `LICENSE`.
@@ -2,12 +2,7 @@ import { getSettingsListTheme } from '@mariozechner/pi-coding-agent';
2
2
  import { Container, SettingsList, Text } from '@mariozechner/pi-tui';
3
3
  import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
4
4
  import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
5
- const PRESENTATION_TOOL_NAMES = [
6
- 'web_search',
7
- 'web_fetch',
8
- 'web_fetch_headless',
9
- 'web_explore'
10
- ];
5
+ const PRESENTATION_TOOL_NAMES = ['web_explore'];
11
6
  function parseScopeToken(token) {
12
7
  return token === 'global' || token === 'project' ? token : undefined;
13
8
  }
package/dist/extension.js CHANGED
@@ -4,9 +4,6 @@ import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presenta
4
4
  import { loadPresentationConfigLayers } from './presentation/config-store.js';
5
5
  import { selectPresentationView } from './presentation/select-view.js';
6
6
  import { createWebExploreTool } from './tools/web-explore.js';
7
- import { createWebFetchTool } from './tools/web-fetch.js';
8
- import { createWebFetchHeadlessTool } from './tools/web-fetch-headless.js';
9
- import { createWebSearchTool } from './tools/web-search.js';
10
7
  async function getEffectivePresentationConfig(pi) {
11
8
  const store = pi.__presentationConfigStore;
12
9
  try {
@@ -24,165 +21,23 @@ async function renderToolText(pi, toolName, details) {
24
21
  }
25
22
  export default function extension(pi) {
26
23
  registerWebAgentConfigCommands(pi);
27
- const webSearch = createWebSearchTool();
28
- const webFetch = createWebFetchTool();
29
- const webFetchHeadless = createWebFetchHeadlessTool();
30
- const webExplore = createWebExploreTool();
31
- let webExploreUsedInCurrentFlow = false;
32
- const postWebExploreGuardError = {
33
- code: 'POST_WEB_EXPLORE_GUARD',
34
- message: 'web_explore already ran for this research task. Only use low-level web tools if there is a specific unresolved gap.'
35
- };
36
- async function guardSearchResponse() {
37
- const result = {
38
- status: 'error',
39
- results: [],
40
- metadata: {
41
- backend: 'duckduckgo',
42
- cacheHit: false
43
- },
44
- error: postWebExploreGuardError,
45
- presentation: {
46
- mode: 'compact',
47
- views: {
48
- compact: `Search failed: ${postWebExploreGuardError.message}`
49
- }
50
- }
51
- };
52
- return {
53
- content: [{ type: 'text', text: await renderToolText(pi, 'web_search', result) }],
54
- details: result,
55
- isError: true
56
- };
57
- }
58
- async function guardFetchResponse(url) {
59
- const result = {
60
- status: 'error',
61
- url,
62
- metadata: {
63
- method: 'http',
64
- cacheHit: false
65
- },
66
- error: postWebExploreGuardError,
67
- presentation: {
68
- mode: 'compact',
69
- views: {
70
- compact: `Fetch failed: ${postWebExploreGuardError.message}`
71
- }
72
- }
73
- };
74
- return {
75
- content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch', result) }],
76
- details: result,
77
- isError: true
78
- };
79
- }
80
- async function guardHeadlessResponse(url) {
81
- const result = {
82
- status: 'error',
83
- url,
84
- metadata: {
85
- method: 'headless',
86
- cacheHit: false
87
- },
88
- error: postWebExploreGuardError,
89
- presentation: {
90
- mode: 'compact',
91
- views: {
92
- compact: `Fetch failed: ${postWebExploreGuardError.message}`
93
- }
94
- }
95
- };
96
- return {
97
- content: [
98
- {
99
- type: 'text',
100
- text: await renderToolText(pi, 'web_fetch_headless', result)
101
- }
102
- ],
103
- details: result,
104
- isError: true
105
- };
106
- }
107
- pi.on('before_agent_start', async (event) => {
108
- webExploreUsedInCurrentFlow = false;
109
- return {
110
- systemPrompt: `${event.systemPrompt}\n\n` +
111
- 'For web research questions that require finding and comparing multiple sources, prefer web_explore. ' +
112
- 'Use web_search, web_fetch, and web_fetch_headless for direct/manual operations like explicit search calls, specific URL reads, or debugging. ' +
113
- 'After using web_explore, only call low-level web tools if there is a specific unresolved gap. ' +
114
- 'Do not keep searching or fetching just for extra confirmation.'
115
- };
116
- });
117
- pi.registerTool({
118
- name: 'web_search',
119
- label: 'Web Search',
120
- description: 'Direct search tool for manual discovery of links and snippets. Use for explicit search requests or when the user wants raw search results. Prefer web_explore for broader research questions.',
121
- parameters: Type.Object({
122
- query: Type.String({ description: 'Search query.' })
123
- }),
124
- async execute(_toolCallId, params) {
125
- if (webExploreUsedInCurrentFlow) {
126
- return guardSearchResponse();
127
- }
128
- const result = await webSearch({ query: params.query });
129
- return {
130
- content: [{ type: 'text', text: await renderToolText(pi, 'web_search', result) }],
131
- details: result,
132
- isError: result.status === 'error'
133
- };
134
- }
135
- });
136
- pi.registerTool({
137
- name: 'web_fetch',
138
- label: 'Web Fetch',
139
- description: 'Direct HTTP page fetch for a specific URL. Use when the user wants one page read directly. Prefer web_explore for broader research across multiple sources.',
140
- parameters: Type.Object({
141
- url: Type.String({ description: 'HTTP or HTTPS URL to fetch.' })
142
- }),
143
- async execute(_toolCallId, params) {
144
- if (webExploreUsedInCurrentFlow) {
145
- return guardFetchResponse(params.url);
146
- }
147
- const result = await webFetch({ url: params.url });
148
- return {
149
- content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch', result) }],
150
- details: result,
151
- isError: result.status === 'error'
152
- };
153
- }
154
- });
155
- pi.registerTool({
156
- name: 'web_fetch_headless',
157
- label: 'Web Fetch Headless',
158
- description: 'Direct headless page fetch for a specific URL when browser rendering is explicitly needed. Prefer web_explore for research tasks; it decides headless escalation internally.',
159
- parameters: Type.Object({
160
- url: Type.String({ description: 'HTTP or HTTPS URL to fetch in headless mode.' })
161
- }),
162
- async execute(_toolCallId, params) {
163
- if (webExploreUsedInCurrentFlow) {
164
- return guardHeadlessResponse(params.url);
165
- }
166
- const result = await webFetchHeadless({ url: params.url });
167
- return {
168
- content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch_headless', result) }],
169
- details: result,
170
- isError: result.status === 'error'
171
- };
172
- }
173
- });
24
+ const webExplore = pi.__webExploreTool ??
25
+ createWebExploreTool();
26
+ pi.on('before_agent_start', async (event) => ({
27
+ systemPrompt: `${event.systemPrompt}\n\n` +
28
+ 'For web research questions that require finding and comparing sources, use web_explore. ' +
29
+ 'web_explore handles search, fetch, source ranking, and headless escalation internally. ' +
30
+ 'If more web evidence is needed after web_explore, call web_explore again with a narrower query; do not use shell/network commands such as curl, Invoke-WebRequest, npm view/search/pack, or direct HTTP URLs for web research.'
31
+ }));
174
32
  pi.registerTool({
175
33
  name: 'web_explore',
176
34
  label: 'Web Explore',
177
- description: 'Research a web question using bounded search/fetch passes, source ranking, and targeted headless escalation. Prefer this for multi-source web research, current docs/discussion lookups, and recommendation summaries. Use this instead of chaining low-level web tools for the same research task.',
35
+ description: 'Research a web question using bounded search/fetch passes, source ranking, and targeted headless escalation. Use this for web research, current docs/discussion lookups, and recommendation summaries.',
178
36
  parameters: Type.Object({
179
37
  query: Type.String({ description: 'Web research question to explore.' })
180
38
  }),
181
39
  async execute(_toolCallId, params) {
182
40
  const result = await webExplore({ query: params.query });
183
- if (result.status === 'ok') {
184
- webExploreUsedInCurrentFlow = true;
185
- }
186
41
  return {
187
42
  content: [
188
43
  {
@@ -0,0 +1,8 @@
1
+ import type { ResearchEvidence } from './research-types.js';
2
+ export declare function synthesizeAnswer({ evidence, partial }: {
3
+ evidence: ResearchEvidence[];
4
+ partial: boolean;
5
+ }): {
6
+ findings: string[];
7
+ caveat: string | undefined;
8
+ };
@@ -0,0 +1,17 @@
1
+ function normalizeSummary(summary) {
2
+ return summary.replace(/\s+/g, ' ').trim();
3
+ }
4
+ export function synthesizeAnswer({ evidence, partial }) {
5
+ const findings = evidence.slice(0, 5).map((item) => {
6
+ const summary = normalizeSummary(item.summary);
7
+ return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
8
+ ? `Community/practical context: ${summary}`
9
+ : summary;
10
+ });
11
+ return {
12
+ findings,
13
+ caveat: partial
14
+ ? 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.'
15
+ : undefined
16
+ };
17
+ }
@@ -0,0 +1,6 @@
1
+ import type { SearchResult } from '../types.js';
2
+ export declare function selectCandidates({ results, seenUrls, maxCandidates }: {
3
+ results: SearchResult[];
4
+ seenUrls: Set<string>;
5
+ maxCandidates: number;
6
+ }): SearchResult[];
@@ -0,0 +1,24 @@
1
+ function candidateScore(result) {
2
+ const url = result.url.toLowerCase();
3
+ if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide') || url.includes('learn.microsoft.com'))
4
+ return 0;
5
+ if (url.includes('github.com/') && (url.includes('/issues/') || url.includes('/discussions/')))
6
+ return 1;
7
+ if (url.includes('github.com/'))
8
+ return 2;
9
+ if (url.includes('npmjs.com/package/'))
10
+ return 4;
11
+ return 3;
12
+ }
13
+ export function selectCandidates({ results, seenUrls, maxCandidates }) {
14
+ const deduped = new Map();
15
+ for (const result of results) {
16
+ if (seenUrls.has(result.url))
17
+ continue;
18
+ if (!deduped.has(result.url))
19
+ deduped.set(result.url, result);
20
+ }
21
+ return [...deduped.values()]
22
+ .sort((left, right) => candidateScore(left) - candidateScore(right))
23
+ .slice(0, maxCandidates);
24
+ }
@@ -0,0 +1,4 @@
1
+ import type { ResearchEvidence } from './research-types.js';
2
+ export declare function rankEvidence(evidence: ResearchEvidence[]): ResearchEvidence[];
3
+ export declare function hasOfficialEvidence(evidence: ResearchEvidence[]): boolean;
4
+ export declare function strongEvidenceCount(evidence: ResearchEvidence[]): number;
@@ -0,0 +1,36 @@
1
+ function sourceRank(sourceKind) {
2
+ switch (sourceKind) {
3
+ case 'official-docs':
4
+ return 0;
5
+ case 'official-api':
6
+ return 1;
7
+ case 'official-discussion':
8
+ return 2;
9
+ case 'issue-thread':
10
+ return 3;
11
+ case 'community':
12
+ return 4;
13
+ case 'package-page':
14
+ return 5;
15
+ default:
16
+ return 6;
17
+ }
18
+ }
19
+ export function rankEvidence(evidence) {
20
+ const bestByUrl = new Map();
21
+ for (const item of evidence) {
22
+ const current = bestByUrl.get(item.url);
23
+ if (!current || sourceRank(item.sourceKind) < sourceRank(current.sourceKind)) {
24
+ bestByUrl.set(item.url, item);
25
+ }
26
+ }
27
+ return [...bestByUrl.values()].sort((left, right) => sourceRank(left.sourceKind) - sourceRank(right.sourceKind));
28
+ }
29
+ export function hasOfficialEvidence(evidence) {
30
+ return evidence.some((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api');
31
+ }
32
+ export function strongEvidenceCount(evidence) {
33
+ return evidence.filter((item) => item.sourceKind === 'official-docs' ||
34
+ item.sourceKind === 'official-api' ||
35
+ item.sourceKind === 'official-discussion').length;
36
+ }
@@ -13,29 +13,14 @@ export declare function createResearchWorkflow({ search, fetchPage, headlessFetc
13
13
  run({ query }: {
14
14
  query: string;
15
15
  }): Promise<{
16
- decision: {
17
- action: "answer";
18
- rationale: string;
19
- approvedEvidence: import("./research-types.js").ResearchEvidence[];
20
- };
21
- evidence: import("./research-types.js").ResearchEvidence[];
22
- workerPass: import("./research-types.js").ResearchWorkerResult;
23
- } | {
24
- decision: {
25
- action: "escalate-headless";
26
- rationale: string;
27
- url: string;
28
- approvedEvidence: import("./research-types.js").ResearchEvidence[];
29
- };
16
+ decision: import("./research-types.js").ResearchOrchestratorDecision;
30
17
  evidence: import("./research-types.js").ResearchEvidence[];
31
18
  workerPass: import("./research-types.js").ResearchWorkerResult;
32
- } | {
33
- decision: {
34
- action: "research-again";
35
- rationale: string;
36
- followupQuery: string;
19
+ metadata: {
20
+ searchPasses: number;
21
+ fetchedPages: number;
22
+ headlessAttempts: number;
23
+ exhaustedBudget: boolean;
37
24
  };
38
- evidence: import("./research-types.js").ResearchEvidence[];
39
- workerPass: import("./research-types.js").ResearchWorkerResult;
40
25
  }>;
41
26
  };
@@ -0,0 +1,7 @@
1
+ export type QueryPlanInput = {
2
+ originalQuery: string;
3
+ passIndex: number;
4
+ previousQueries: string[];
5
+ gaps: string[];
6
+ };
7
+ export declare function planSearchQueries(input: QueryPlanInput): string[];
@@ -0,0 +1,37 @@
1
+ function normalizeWhitespace(value) {
2
+ return value.replace(/\s+/g, ' ').trim();
3
+ }
4
+ function extractImportantWords(query) {
5
+ return normalizeWhitespace(query)
6
+ .replace(/\b(find|current|tell|me|how|to|the|and|with|for|in|a|an)\b/gi, ' ')
7
+ .replace(/\s+/g, ' ')
8
+ .trim();
9
+ }
10
+ function officialSiteQuery(query) {
11
+ const lower = query.toLowerCase();
12
+ const terms = extractImportantWords(query);
13
+ if (lower.includes('vitest'))
14
+ return `site:vitest.dev ${terms}`;
15
+ if (lower.includes('playwright'))
16
+ return `site:playwright.dev ${terms}`;
17
+ if (lower.includes('microsoft edge') || lower.includes('edge'))
18
+ return `site:learn.microsoft.com ${terms}`;
19
+ return `${terms} official docs`;
20
+ }
21
+ function implementationQuery(query) {
22
+ return `site:github.com ${extractImportantWords(query)}`;
23
+ }
24
+ export function planSearchQueries(input) {
25
+ const planned = input.passIndex === 0
26
+ ? [input.originalQuery]
27
+ : [
28
+ officialSiteQuery(input.originalQuery),
29
+ implementationQuery(input.originalQuery),
30
+ `${extractImportantWords(input.originalQuery)} discussion`
31
+ ];
32
+ const previous = new Set(input.previousQueries.map(normalizeWhitespace));
33
+ return planned
34
+ .map(normalizeWhitespace)
35
+ .filter((query) => query && !previous.has(query))
36
+ .slice(0, 3);
37
+ }
@@ -1,5 +1,5 @@
1
1
  import type { WebFetchHeadlessResponse } from '../types.js';
2
- import type { ResearchEvidence, ResearchWorkerResult } from './research-types.js';
2
+ import type { ResearchEvidence, ResearchOrchestratorDecision, ResearchWorkerResult } from './research-types.js';
3
3
  export declare function createResearchOrchestrator({ worker, headlessFetch }: {
4
4
  worker: {
5
5
  run: (input: {
@@ -15,29 +15,14 @@ export declare function createResearchOrchestrator({ worker, headlessFetch }: {
15
15
  run({ query }: {
16
16
  query: string;
17
17
  }): Promise<{
18
- decision: {
19
- action: "answer";
20
- rationale: string;
21
- approvedEvidence: ResearchEvidence[];
22
- };
23
- evidence: ResearchEvidence[];
24
- workerPass: ResearchWorkerResult;
25
- } | {
26
- decision: {
27
- action: "escalate-headless";
28
- rationale: string;
29
- url: string;
30
- approvedEvidence: ResearchEvidence[];
31
- };
18
+ decision: ResearchOrchestratorDecision;
32
19
  evidence: ResearchEvidence[];
33
20
  workerPass: ResearchWorkerResult;
34
- } | {
35
- 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
  };