@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
package/README.md
CHANGED
|
@@ -6,12 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
`@demigodmode/pi-web-agent` is a Pi package for web access.
|
|
8
8
|
|
|
9
|
-
|
|
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
10
|
|
|
11
|
-
- `
|
|
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
|
|
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.
|
|
15
12
|
|
|
16
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.
|
|
17
14
|
|
|
@@ -45,6 +42,65 @@ Build the docs:
|
|
|
45
42
|
npm run docs:build
|
|
46
43
|
```
|
|
47
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
|
+
|
|
48
104
|
## Local development
|
|
49
105
|
|
|
50
106
|
```bash
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
|
|
3
|
+
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from '../presentation/types.js';
|
|
4
|
+
type CommandDeps = {
|
|
5
|
+
load?: () => ReturnType<typeof loadPresentationConfigLayers>;
|
|
6
|
+
save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
|
|
7
|
+
reset?: (scope: PresentationScope) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type SettingsDraftState = {
|
|
10
|
+
scope: PresentationScope;
|
|
11
|
+
drafts: Record<PresentationScope, PresentationConfig>;
|
|
12
|
+
config: PresentationConfig;
|
|
13
|
+
};
|
|
14
|
+
export declare function getInheritedConfigForScope(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
|
|
15
|
+
export declare function getScopeDisplayConfig(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
|
|
16
|
+
export declare function createSettingsDraftState(loaded: Awaited<LoadedPresentationConfig>, initialScope: PresentationScope): SettingsDraftState;
|
|
17
|
+
export declare function applySettingsValue(state: SettingsDraftState, id: string, newValue: string): SettingsDraftState;
|
|
18
|
+
export declare function collapsePresentationConfigToOverride(config: PresentationConfig, inheritedConfig: PresentationConfig): PresentationConfigOverride;
|
|
19
|
+
export declare function handleSettingsShortcut(data: string): {
|
|
20
|
+
action: 'cancel' | 'reset' | 'save';
|
|
21
|
+
} | undefined;
|
|
22
|
+
export declare function registerWebAgentConfigCommands(pi: ExtensionAPI, deps?: CommandDeps): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { getSettingsListTheme } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Container, SettingsList, Text } from '@mariozechner/pi-tui';
|
|
3
|
+
import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
|
|
4
|
+
import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
|
|
5
|
+
const PRESENTATION_TOOL_NAMES = ['web_explore'];
|
|
6
|
+
function parseScopeToken(token) {
|
|
7
|
+
return token === 'global' || token === 'project' ? token : undefined;
|
|
8
|
+
}
|
|
9
|
+
function clonePresentationConfig(config) {
|
|
10
|
+
return {
|
|
11
|
+
defaultMode: config.defaultMode,
|
|
12
|
+
tools: { ...config.tools }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function formatConfigSummary(config) {
|
|
16
|
+
const lines = [`defaultMode: ${config.defaultMode}`];
|
|
17
|
+
for (const toolName of PRESENTATION_TOOL_NAMES) {
|
|
18
|
+
lines.push(`${toolName}: ${config.tools[toolName]?.mode ?? 'inherit'}`);
|
|
19
|
+
}
|
|
20
|
+
return lines.join('\n');
|
|
21
|
+
}
|
|
22
|
+
function buildSettingsItems(scope, config) {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: 'scope',
|
|
26
|
+
label: 'Write scope',
|
|
27
|
+
currentValue: scope,
|
|
28
|
+
values: ['project', 'global']
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'defaultMode',
|
|
32
|
+
label: 'Default mode',
|
|
33
|
+
currentValue: config.defaultMode,
|
|
34
|
+
values: ['compact', 'preview', 'verbose']
|
|
35
|
+
},
|
|
36
|
+
...PRESENTATION_TOOL_NAMES.map((toolName) => ({
|
|
37
|
+
id: `tool:${toolName}`,
|
|
38
|
+
label: toolName,
|
|
39
|
+
currentValue: config.tools[toolName]?.mode ?? 'inherit',
|
|
40
|
+
values: ['inherit', 'compact', 'preview', 'verbose']
|
|
41
|
+
}))
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function isToolName(value) {
|
|
45
|
+
return PRESENTATION_TOOL_NAMES.includes(value);
|
|
46
|
+
}
|
|
47
|
+
function isModeOrInherit(value) {
|
|
48
|
+
return ['inherit', 'compact', 'preview', 'verbose'].includes(value);
|
|
49
|
+
}
|
|
50
|
+
export function getInheritedConfigForScope(loaded, scope) {
|
|
51
|
+
if (scope === 'global') {
|
|
52
|
+
return DEFAULT_PRESENTATION_CONFIG;
|
|
53
|
+
}
|
|
54
|
+
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
|
|
55
|
+
}
|
|
56
|
+
export function getScopeDisplayConfig(loaded, scope) {
|
|
57
|
+
if (scope === 'global') {
|
|
58
|
+
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
|
|
59
|
+
}
|
|
60
|
+
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig, loaded.project.rawConfig);
|
|
61
|
+
}
|
|
62
|
+
export function createSettingsDraftState(loaded, initialScope) {
|
|
63
|
+
const drafts = {
|
|
64
|
+
global: getScopeDisplayConfig(loaded, 'global'),
|
|
65
|
+
project: getScopeDisplayConfig(loaded, 'project')
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
scope: initialScope,
|
|
69
|
+
drafts,
|
|
70
|
+
config: clonePresentationConfig(drafts[initialScope])
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function applySettingsValue(state, id, newValue) {
|
|
74
|
+
const nextDrafts = {
|
|
75
|
+
global: clonePresentationConfig(state.drafts.global),
|
|
76
|
+
project: clonePresentationConfig(state.drafts.project)
|
|
77
|
+
};
|
|
78
|
+
let nextScope = state.scope;
|
|
79
|
+
if (id === 'scope' && (newValue === 'project' || newValue === 'global')) {
|
|
80
|
+
nextScope = newValue;
|
|
81
|
+
return {
|
|
82
|
+
scope: nextScope,
|
|
83
|
+
drafts: nextDrafts,
|
|
84
|
+
config: clonePresentationConfig(nextDrafts[nextScope])
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const currentDraft = clonePresentationConfig(nextDrafts[nextScope]);
|
|
88
|
+
if (id === 'defaultMode' && (newValue === 'compact' || newValue === 'preview' || newValue === 'verbose')) {
|
|
89
|
+
currentDraft.defaultMode = newValue;
|
|
90
|
+
}
|
|
91
|
+
if (id.startsWith('tool:')) {
|
|
92
|
+
const toolName = id.slice('tool:'.length);
|
|
93
|
+
const nextTools = { ...currentDraft.tools };
|
|
94
|
+
if (newValue === 'inherit') {
|
|
95
|
+
delete nextTools[toolName];
|
|
96
|
+
}
|
|
97
|
+
else if (newValue === 'compact' ||
|
|
98
|
+
newValue === 'preview' ||
|
|
99
|
+
newValue === 'verbose') {
|
|
100
|
+
nextTools[toolName] = { mode: newValue };
|
|
101
|
+
}
|
|
102
|
+
currentDraft.tools = nextTools;
|
|
103
|
+
}
|
|
104
|
+
nextDrafts[nextScope] = currentDraft;
|
|
105
|
+
return {
|
|
106
|
+
scope: nextScope,
|
|
107
|
+
drafts: nextDrafts,
|
|
108
|
+
config: clonePresentationConfig(nextDrafts[nextScope])
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function collapsePresentationConfigToOverride(config, inheritedConfig) {
|
|
112
|
+
const tools = Object.fromEntries(PRESENTATION_TOOL_NAMES.flatMap((toolName) => {
|
|
113
|
+
const configuredMode = config.tools[toolName]?.mode;
|
|
114
|
+
if (!configuredMode) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const inheritedMode = resolvePresentationMode(toolName, inheritedConfig);
|
|
118
|
+
if (configuredMode === inheritedMode) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return [[toolName, { mode: configuredMode }]];
|
|
122
|
+
}));
|
|
123
|
+
return {
|
|
124
|
+
defaultMode: config.defaultMode === inheritedConfig.defaultMode ? undefined : config.defaultMode,
|
|
125
|
+
tools
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function handleSettingsShortcut(data) {
|
|
129
|
+
if (data === '\u001b') {
|
|
130
|
+
return { action: 'cancel' };
|
|
131
|
+
}
|
|
132
|
+
if (data === '\u0012') {
|
|
133
|
+
return { action: 'reset' };
|
|
134
|
+
}
|
|
135
|
+
if (data === '\u0013') {
|
|
136
|
+
return { action: 'save' };
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
async function openSettingsUi(ctx, loaded, initialScope) {
|
|
141
|
+
return ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
142
|
+
let state = createSettingsDraftState(loaded, initialScope);
|
|
143
|
+
let settingsList;
|
|
144
|
+
const container = new Container();
|
|
145
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent settings')), 1, 1));
|
|
146
|
+
container.addChild(new Text(theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel'), 1, 2));
|
|
147
|
+
const rebuildSettingsList = () => {
|
|
148
|
+
if (settingsList) {
|
|
149
|
+
container.removeChild(settingsList);
|
|
150
|
+
}
|
|
151
|
+
settingsList = new SettingsList(buildSettingsItems(state.scope, state.config), Math.min(PRESENTATION_TOOL_NAMES.length + 8, 18), getSettingsListTheme(), (id, newValue) => {
|
|
152
|
+
state = applySettingsValue(state, id, newValue);
|
|
153
|
+
rebuildSettingsList();
|
|
154
|
+
container.invalidate();
|
|
155
|
+
}, () => done({ action: 'save', scope: state.scope, config: state.config }), { enableSearch: true });
|
|
156
|
+
container.addChild(settingsList);
|
|
157
|
+
};
|
|
158
|
+
rebuildSettingsList();
|
|
159
|
+
return {
|
|
160
|
+
render: (width) => container.render(width),
|
|
161
|
+
invalidate: () => container.invalidate(),
|
|
162
|
+
handleInput: (data) => {
|
|
163
|
+
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
164
|
+
if (shortcut?.action === 'cancel') {
|
|
165
|
+
done({ action: 'cancel' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (shortcut?.action === 'reset') {
|
|
169
|
+
done({ action: 'reset', scope: state.scope });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (shortcut?.action === 'save') {
|
|
173
|
+
done({ action: 'save', scope: state.scope, config: state.config });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
settingsList.handleInput?.(data);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
export function registerWebAgentConfigCommands(pi, deps = {}) {
|
|
182
|
+
const load = deps.load ?? (() => loadPresentationConfigLayers());
|
|
183
|
+
const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
|
|
184
|
+
const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
|
|
185
|
+
pi.registerCommand('web-agent', {
|
|
186
|
+
description: 'Open settings or manage pi-web-agent presentation config',
|
|
187
|
+
handler: async (args, ctx) => {
|
|
188
|
+
const [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
189
|
+
if (action === 'show') {
|
|
190
|
+
const loaded = await load();
|
|
191
|
+
ctx.ui.notify([
|
|
192
|
+
formatConfigSummary(loaded.effectiveConfig),
|
|
193
|
+
`global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
|
|
194
|
+
`project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
|
|
195
|
+
].join('\n'), 'info');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (action === 'reset') {
|
|
199
|
+
const scope = parseScopeToken(maybeScope) ?? 'project';
|
|
200
|
+
await reset(scope);
|
|
201
|
+
ctx.ui.notify(`Reset ${scope} config`, 'info');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (action === 'mode') {
|
|
205
|
+
const [, first, second] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
206
|
+
const loaded = await load();
|
|
207
|
+
const scope = 'project';
|
|
208
|
+
const baseConfig = getScopeDisplayConfig(loaded, scope);
|
|
209
|
+
const inheritedConfig = getInheritedConfigForScope(loaded, scope);
|
|
210
|
+
if (first && isModeOrInherit(first) && first !== 'inherit') {
|
|
211
|
+
await save(scope, collapsePresentationConfigToOverride({ ...baseConfig, defaultMode: first }, inheritedConfig));
|
|
212
|
+
ctx.ui.notify(`Saved project default mode = ${first}`, 'info');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (first && second && isToolName(first) && isModeOrInherit(second)) {
|
|
216
|
+
const nextTools = { ...baseConfig.tools };
|
|
217
|
+
if (second === 'inherit') {
|
|
218
|
+
delete nextTools[first];
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
nextTools[first] = { mode: second };
|
|
222
|
+
}
|
|
223
|
+
await save(scope, collapsePresentationConfigToOverride({ ...baseConfig, tools: nextTools }, inheritedConfig));
|
|
224
|
+
ctx.ui.notify(`Saved project ${first} = ${second}`, 'info');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
ctx.ui.notify('Usage: /web-agent mode <compact|preview|verbose> or /web-agent mode <tool> <inherit|compact|preview|verbose>', 'info');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!action || action === 'settings') {
|
|
231
|
+
const loaded = await load();
|
|
232
|
+
const initialScope = 'project';
|
|
233
|
+
const result = await openSettingsUi(ctx, loaded, initialScope);
|
|
234
|
+
if (!result || result.action === 'cancel') {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (result.action === 'reset') {
|
|
238
|
+
await reset(result.scope);
|
|
239
|
+
ctx.ui.notify(`Reset ${result.scope} config`, 'info');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await save(result.scope, collapsePresentationConfigToOverride(result.config, getInheritedConfigForScope(loaded, result.scope)));
|
|
243
|
+
ctx.ui.notify(`Saved ${result.scope} config`, 'info');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent reset project, or /web-agent settings', 'info');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
package/dist/extension.js
CHANGED
|
@@ -1,74 +1,38 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
|
|
3
|
+
import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
|
|
4
|
+
import { loadPresentationConfigLayers } from './presentation/config-store.js';
|
|
5
|
+
import { selectPresentationView } from './presentation/select-view.js';
|
|
2
6
|
import { createWebExploreTool } from './tools/web-explore.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
async function getEffectivePresentationConfig(pi) {
|
|
8
|
+
const store = pi.__presentationConfigStore;
|
|
9
|
+
try {
|
|
10
|
+
const loaded = await (store?.load?.() ?? loadPresentationConfigLayers());
|
|
11
|
+
return loaded.effectiveConfig;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return DEFAULT_PRESENTATION_CONFIG;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function renderToolText(pi, toolName, details) {
|
|
18
|
+
const config = await getEffectivePresentationConfig(pi);
|
|
19
|
+
const mode = resolvePresentationMode(toolName, config);
|
|
20
|
+
return selectPresentationView(details.presentation, mode) ?? JSON.stringify(details, null, 2);
|
|
21
|
+
}
|
|
6
22
|
export default function extension(pi) {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'After using web_explore, only call low-level web tools if there is a specific unresolved gap. ' +
|
|
17
|
-
'Do not keep searching or fetching just for extra confirmation.'
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
pi.registerTool({
|
|
21
|
-
name: 'web_search',
|
|
22
|
-
label: 'Web Search',
|
|
23
|
-
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.',
|
|
24
|
-
parameters: Type.Object({
|
|
25
|
-
query: Type.String({ description: 'Search query.' })
|
|
26
|
-
}),
|
|
27
|
-
async execute(_toolCallId, params) {
|
|
28
|
-
const result = await webSearch({ query: params.query });
|
|
29
|
-
return {
|
|
30
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
31
|
-
details: result,
|
|
32
|
-
isError: result.status === 'error'
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
pi.registerTool({
|
|
37
|
-
name: 'web_fetch',
|
|
38
|
-
label: 'Web Fetch',
|
|
39
|
-
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.',
|
|
40
|
-
parameters: Type.Object({
|
|
41
|
-
url: Type.String({ description: 'HTTP or HTTPS URL to fetch.' })
|
|
42
|
-
}),
|
|
43
|
-
async execute(_toolCallId, params) {
|
|
44
|
-
const result = await webFetch({ url: params.url });
|
|
45
|
-
return {
|
|
46
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
47
|
-
details: result,
|
|
48
|
-
isError: result.status === 'error'
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
pi.registerTool({
|
|
53
|
-
name: 'web_fetch_headless',
|
|
54
|
-
label: 'Web Fetch Headless',
|
|
55
|
-
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.',
|
|
56
|
-
parameters: Type.Object({
|
|
57
|
-
url: Type.String({ description: 'HTTP or HTTPS URL to fetch in headless mode.' })
|
|
58
|
-
}),
|
|
59
|
-
async execute(_toolCallId, params) {
|
|
60
|
-
const result = await webFetchHeadless({ url: params.url });
|
|
61
|
-
return {
|
|
62
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
63
|
-
details: result,
|
|
64
|
-
isError: result.status === 'error'
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
});
|
|
23
|
+
registerWebAgentConfigCommands(pi);
|
|
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
|
+
}));
|
|
68
32
|
pi.registerTool({
|
|
69
33
|
name: 'web_explore',
|
|
70
34
|
label: 'Web Explore',
|
|
71
|
-
description: 'Research a web question using bounded search/fetch passes, source ranking, and targeted headless escalation.
|
|
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.',
|
|
72
36
|
parameters: Type.Object({
|
|
73
37
|
query: Type.String({ description: 'Web research question to explore.' })
|
|
74
38
|
}),
|
|
@@ -78,7 +42,7 @@ export default function extension(pi) {
|
|
|
78
42
|
content: [
|
|
79
43
|
{
|
|
80
44
|
type: 'text',
|
|
81
|
-
text:
|
|
45
|
+
text: await renderToolText(pi, 'web_explore', result)
|
|
82
46
|
}
|
|
83
47
|
],
|
|
84
48
|
details: result,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function normalizeSummary(summary) {
|
|
2
|
+
return summary.replace(/\s+/g, ' ').trim();
|
|
3
|
+
}
|
|
4
|
+
export function synthesizeAnswer({ evidence, partial }) {
|
|
5
|
+
const findings = evidence.slice(0, 5).map((item) => {
|
|
6
|
+
const summary = normalizeSummary(item.summary);
|
|
7
|
+
return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
|
|
8
|
+
? `Community/practical context: ${summary}`
|
|
9
|
+
: summary;
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
findings,
|
|
13
|
+
caveat: partial
|
|
14
|
+
? 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.'
|
|
15
|
+
: undefined
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function candidateScore(result) {
|
|
2
|
+
const url = result.url.toLowerCase();
|
|
3
|
+
if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide') || url.includes('learn.microsoft.com'))
|
|
4
|
+
return 0;
|
|
5
|
+
if (url.includes('github.com/') && (url.includes('/issues/') || url.includes('/discussions/')))
|
|
6
|
+
return 1;
|
|
7
|
+
if (url.includes('github.com/'))
|
|
8
|
+
return 2;
|
|
9
|
+
if (url.includes('npmjs.com/package/'))
|
|
10
|
+
return 4;
|
|
11
|
+
return 3;
|
|
12
|
+
}
|
|
13
|
+
export function selectCandidates({ results, seenUrls, maxCandidates }) {
|
|
14
|
+
const deduped = new Map();
|
|
15
|
+
for (const result of results) {
|
|
16
|
+
if (seenUrls.has(result.url))
|
|
17
|
+
continue;
|
|
18
|
+
if (!deduped.has(result.url))
|
|
19
|
+
deduped.set(result.url, result);
|
|
20
|
+
}
|
|
21
|
+
return [...deduped.values()]
|
|
22
|
+
.sort((left, right) => candidateScore(left) - candidateScore(right))
|
|
23
|
+
.slice(0, maxCandidates);
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
+
export declare function rankEvidence(evidence: ResearchEvidence[]): ResearchEvidence[];
|
|
3
|
+
export declare function hasOfficialEvidence(evidence: ResearchEvidence[]): boolean;
|
|
4
|
+
export declare function strongEvidenceCount(evidence: ResearchEvidence[]): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function sourceRank(sourceKind) {
|
|
2
|
+
switch (sourceKind) {
|
|
3
|
+
case 'official-docs':
|
|
4
|
+
return 0;
|
|
5
|
+
case 'official-api':
|
|
6
|
+
return 1;
|
|
7
|
+
case 'official-discussion':
|
|
8
|
+
return 2;
|
|
9
|
+
case 'issue-thread':
|
|
10
|
+
return 3;
|
|
11
|
+
case 'community':
|
|
12
|
+
return 4;
|
|
13
|
+
case 'package-page':
|
|
14
|
+
return 5;
|
|
15
|
+
default:
|
|
16
|
+
return 6;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function rankEvidence(evidence) {
|
|
20
|
+
const bestByUrl = new Map();
|
|
21
|
+
for (const item of evidence) {
|
|
22
|
+
const current = bestByUrl.get(item.url);
|
|
23
|
+
if (!current || sourceRank(item.sourceKind) < sourceRank(current.sourceKind)) {
|
|
24
|
+
bestByUrl.set(item.url, item);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...bestByUrl.values()].sort((left, right) => sourceRank(left.sourceKind) - sourceRank(right.sourceKind));
|
|
28
|
+
}
|
|
29
|
+
export function hasOfficialEvidence(evidence) {
|
|
30
|
+
return evidence.some((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api');
|
|
31
|
+
}
|
|
32
|
+
export function strongEvidenceCount(evidence) {
|
|
33
|
+
return evidence.filter((item) => item.sourceKind === 'official-docs' ||
|
|
34
|
+
item.sourceKind === 'official-api' ||
|
|
35
|
+
item.sourceKind === 'official-discussion').length;
|
|
36
|
+
}
|
|
@@ -13,29 +13,14 @@ export declare function createResearchWorkflow({ search, fetchPage, headlessFetc
|
|
|
13
13
|
run({ query }: {
|
|
14
14
|
query: string;
|
|
15
15
|
}): Promise<{
|
|
16
|
-
decision:
|
|
17
|
-
action: "answer";
|
|
18
|
-
rationale: string;
|
|
19
|
-
approvedEvidence: import("./research-types.js").ResearchEvidence[];
|
|
20
|
-
};
|
|
21
|
-
evidence: import("./research-types.js").ResearchEvidence[];
|
|
22
|
-
workerPass: import("./research-types.js").ResearchWorkerResult;
|
|
23
|
-
} | {
|
|
24
|
-
decision: {
|
|
25
|
-
action: "escalate-headless";
|
|
26
|
-
rationale: string;
|
|
27
|
-
url: string;
|
|
28
|
-
approvedEvidence: import("./research-types.js").ResearchEvidence[];
|
|
29
|
-
};
|
|
16
|
+
decision: import("./research-types.js").ResearchOrchestratorDecision;
|
|
30
17
|
evidence: import("./research-types.js").ResearchEvidence[];
|
|
31
18
|
workerPass: import("./research-types.js").ResearchWorkerResult;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
metadata: {
|
|
20
|
+
searchPasses: number;
|
|
21
|
+
fetchedPages: number;
|
|
22
|
+
headlessAttempts: number;
|
|
23
|
+
exhaustedBudget: boolean;
|
|
37
24
|
};
|
|
38
|
-
evidence: import("./research-types.js").ResearchEvidence[];
|
|
39
|
-
workerPass: import("./research-types.js").ResearchWorkerResult;
|
|
40
25
|
}>;
|
|
41
26
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function normalizeWhitespace(value) {
|
|
2
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
3
|
+
}
|
|
4
|
+
function extractImportantWords(query) {
|
|
5
|
+
return normalizeWhitespace(query)
|
|
6
|
+
.replace(/\b(find|current|tell|me|how|to|the|and|with|for|in|a|an)\b/gi, ' ')
|
|
7
|
+
.replace(/\s+/g, ' ')
|
|
8
|
+
.trim();
|
|
9
|
+
}
|
|
10
|
+
function officialSiteQuery(query) {
|
|
11
|
+
const lower = query.toLowerCase();
|
|
12
|
+
const terms = extractImportantWords(query);
|
|
13
|
+
if (lower.includes('vitest'))
|
|
14
|
+
return `site:vitest.dev ${terms}`;
|
|
15
|
+
if (lower.includes('playwright'))
|
|
16
|
+
return `site:playwright.dev ${terms}`;
|
|
17
|
+
if (lower.includes('microsoft edge') || lower.includes('edge'))
|
|
18
|
+
return `site:learn.microsoft.com ${terms}`;
|
|
19
|
+
return `${terms} official docs`;
|
|
20
|
+
}
|
|
21
|
+
function implementationQuery(query) {
|
|
22
|
+
return `site:github.com ${extractImportantWords(query)}`;
|
|
23
|
+
}
|
|
24
|
+
export function planSearchQueries(input) {
|
|
25
|
+
const planned = input.passIndex === 0
|
|
26
|
+
? [input.originalQuery]
|
|
27
|
+
: [
|
|
28
|
+
officialSiteQuery(input.originalQuery),
|
|
29
|
+
implementationQuery(input.originalQuery),
|
|
30
|
+
`${extractImportantWords(input.originalQuery)} discussion`
|
|
31
|
+
];
|
|
32
|
+
const previous = new Set(input.previousQueries.map(normalizeWhitespace));
|
|
33
|
+
return planned
|
|
34
|
+
.map(normalizeWhitespace)
|
|
35
|
+
.filter((query) => query && !previous.has(query))
|
|
36
|
+
.slice(0, 3);
|
|
37
|
+
}
|