@mrclrchtr/supi-web 11.0.0 → 11.1.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 +39 -2
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +41 -1
- package/package.json +3 -2
- package/src/api.ts +4 -0
- package/src/context7-client.ts +28 -113
- package/src/tool/result-schema.ts +7 -0
- package/src/tool/result.ts +10 -0
- package/src/tool/tool-specs.ts +9 -0
- package/src/tool/web_docs_fetch/execute.ts +4 -8
- package/src/tool/web_docs_fetch/result-schema.ts +32 -0
- package/src/tool/web_docs_fetch/result.ts +28 -6
- package/src/tool/web_docs_fetch/spec.ts +3 -1
- package/src/tool/web_docs_search/execute.ts +3 -7
- package/src/tool/web_docs_search/result-schema.ts +28 -0
- package/src/tool/web_docs_search/result.ts +30 -3
- package/src/tool/web_docs_search/spec.ts +2 -0
- package/src/tool/web_fetch_md/execute.ts +4 -8
- package/src/tool/web_fetch_md/result-schema.ts +26 -0
- package/src/tool/web_fetch_md/result.ts +26 -7
- package/src/tool/web_fetch_md/spec.ts +2 -0
- package/src/tool/web_search/execute.ts +3 -7
- package/src/tool/web_search/result-schema.ts +22 -0
- package/src/tool/web_search/result.ts +23 -4
- package/src/tool/web_search/spec.ts +2 -0
package/README.md
CHANGED
|
@@ -46,6 +46,39 @@ The package adds four tools that Pi selects as needed:
|
|
|
46
46
|
|
|
47
47
|
Pi searches Context7 first when the library ID is unknown, then fetches docs with the selected ID. Narrow questions produce more useful documentation context than broad requests. Documentation returns as Markdown by default; the agent can request structured JSON snippets when needed.
|
|
48
48
|
|
|
49
|
+
### Structured results in codemode
|
|
50
|
+
|
|
51
|
+
All four tools declare PI-native `outputSchema` contracts. Direct calls still return readable, bounded text; `await tools.<name>(args)` in codemode resolves to structured data, not that text or UI `details`.
|
|
52
|
+
|
|
53
|
+
| Tool | Structured value |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `web_fetch_md` | `mode: "inline"` with complete `markdown`, or `mode: "file"` with `filePath`; both include resolved `url`, `chars`, and `lines` |
|
|
56
|
+
| `web_search` | Complete `sources` (`title`, `url`, `excerpts`, optional `sourceDate`), `sourceCount`, and `excerptCount` |
|
|
57
|
+
| `web_docs_search` | All `results` with IDs, names, complete descriptions/versions, scores, and snippet counts; `count` is the number returned by Context7 |
|
|
58
|
+
| `web_docs_fetch` | `libraryId`, `chars`, `lines`, and either `format: "markdown"` with complete `markdown`, or `format: "snippets"` with SDK-normalized `snippets` (`title`, `content`, `source`) for `raw: true` |
|
|
59
|
+
|
|
60
|
+
Inline/data results include `modelOutput.truncated` and, when text is truncated, `modelOutput.fullOutputPath`. These describe the **direct-call text**, not missing structured data. Library search also reports `modelOutput.omittedResults`: matches excluded by the table's top-10 row limit before line/byte truncation. Structured descriptions and version lists are not shortened. File-mode page fetches retain the existing disk delivery instead of duplicating the body in structured data.
|
|
61
|
+
|
|
62
|
+
Results describe the evidence returned by the provider, not exhaustive coverage of the web or a library. Empty search arrays and empty snippet arrays are successful outcomes. Authentication, network, malformed web-search data, and cancellation failures still throw; codemode calls reject rather than returning a successful empty result. No tool currently returns a schema-defined failure or paginated result.
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const libraries = await tools.web_docs_search({
|
|
66
|
+
library_name: "react",
|
|
67
|
+
query: "effect cleanup",
|
|
68
|
+
});
|
|
69
|
+
if (libraries.results.length > 0) {
|
|
70
|
+
const docs = await tools.web_docs_fetch({
|
|
71
|
+
library_id: libraries.results[0].id,
|
|
72
|
+
query: "effect cleanup",
|
|
73
|
+
raw: true,
|
|
74
|
+
});
|
|
75
|
+
return docs.snippets.map(({ title, source }) => ({ title, source }));
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Structured values can be larger than their text previews. Select the fields you need before printing them. The package's `./api` export provides `WebFetchMdOutput`, `WebSearchOutput`, `WebDocsSearchOutput`, and `WebDocsFetchOutput` types. Active Pi sessions need `/reload` or a restart to load extension changes.
|
|
81
|
+
|
|
49
82
|
## Web Search
|
|
50
83
|
|
|
51
84
|
`web_search` uses the installed `bx` executable. The tool is enabled by default, but it is available only when `bx` is on `PATH`. Search freshness accepts `pd`, `pw`, `pm`, `py`, or an ordered `YYYY-MM-DDtoYYYY-MM-DD` range.
|
|
@@ -91,14 +124,18 @@ pi -e npm:@mrclrchtr/supi-web
|
|
|
91
124
|
|
|
92
125
|
## Context7 API key
|
|
93
126
|
|
|
94
|
-
The documentation tools
|
|
127
|
+
The documentation tools use the official [Context7 TypeScript SDK](https://github.com/upstash/context7/tree/master/packages/sdk). Create a free key at <https://context7.com/dashboard>, export it before starting Pi, and keep it out of repository files:
|
|
95
128
|
|
|
96
129
|
```bash
|
|
97
130
|
export CONTEXT7_API_KEY="ctx7sk-..."
|
|
98
131
|
pi
|
|
99
132
|
```
|
|
100
133
|
|
|
101
|
-
`web_fetch_md` does not use Context7 and needs no API key.
|
|
134
|
+
`web_fetch_md` does not use Context7 and needs no API key. Credentials are resolved when a documentation tool is called, not while the extension loads.
|
|
135
|
+
|
|
136
|
+
`raw: true` requests JSON and normalizes both provider code snippets and information snippets into `{ title, content, source }` objects. It does not expose the untouched REST envelope or provider ranking metadata. Default mode retains Context7's formatted text.
|
|
137
|
+
|
|
138
|
+
Requests make one attempt, forward cancellation, and add no SDK timeout. The explicit `no_libraries_found` HTTP 404 response is a successful empty library search; other API errors remain failures. Use a library ID returned by search: stale IDs can produce redirect errors instead of documentation.
|
|
102
139
|
|
|
103
140
|
## Boundaries
|
|
104
141
|
|
|
@@ -14,6 +14,8 @@ export interface DebugRegistryConfig {
|
|
|
14
14
|
enabled: boolean;
|
|
15
15
|
/** What the agent-callable debug tool may return. */
|
|
16
16
|
agentAccess: DebugAgentAccess;
|
|
17
|
+
/** Whether Agent Run retrospective capture is explicitly enabled. */
|
|
18
|
+
agentRunRetrospectivesEnabled: boolean;
|
|
17
19
|
/** Maximum number of session-local events to keep in memory. */
|
|
18
20
|
maxEvents: number;
|
|
19
21
|
}
|
|
@@ -21,6 +23,7 @@ export interface DebugRegistryConfig {
|
|
|
21
23
|
export const DEBUG_REGISTRY_DEFAULTS: DebugRegistryConfig = {
|
|
22
24
|
enabled: false,
|
|
23
25
|
agentAccess: "sanitized",
|
|
26
|
+
agentRunRetrospectivesEnabled: false,
|
|
24
27
|
maxEvents: 100,
|
|
25
28
|
};
|
|
26
29
|
|
|
@@ -75,6 +78,9 @@ export interface DebugEventQueryResult {
|
|
|
75
78
|
/** Receives a sanitized event whenever the registry records one. */
|
|
76
79
|
export type DebugEventListener = (event: DebugEventView) => void;
|
|
77
80
|
|
|
81
|
+
/** Receives sanitized events for persistence; separate from ordinary event subscriptions. */
|
|
82
|
+
export type DebugPersistenceSink = (event: DebugEventView) => void;
|
|
83
|
+
|
|
78
84
|
export interface DebugSummary {
|
|
79
85
|
total: number;
|
|
80
86
|
byLevel: Partial<Record<DebugLevel, number>>;
|
|
@@ -85,6 +91,7 @@ interface DebugRegistryState {
|
|
|
85
91
|
config: DebugRegistryConfig;
|
|
86
92
|
events: DebugEvent[];
|
|
87
93
|
listeners: Set<DebugEventListener>;
|
|
94
|
+
persistenceSinks: Set<DebugPersistenceSink>;
|
|
88
95
|
nextId: number;
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -111,13 +118,15 @@ function getState(): DebugRegistryState {
|
|
|
111
118
|
config: cloneConfig(DEBUG_REGISTRY_DEFAULTS),
|
|
112
119
|
events: [],
|
|
113
120
|
listeners: new Set(),
|
|
121
|
+
persistenceSinks: new Set(),
|
|
114
122
|
nextId: 1,
|
|
115
123
|
};
|
|
116
124
|
(globalThis as Record<symbol, unknown>)[REGISTRY_KEY] = state;
|
|
117
125
|
}
|
|
118
126
|
// Keep the shared registry compatible with extension reloads that reuse an
|
|
119
|
-
// instance created before
|
|
127
|
+
// instance created before listener sets existed.
|
|
120
128
|
if (!state.listeners) state.listeners = new Set();
|
|
129
|
+
if (!state.persistenceSinks) state.persistenceSinks = new Set();
|
|
121
130
|
return state;
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -200,6 +209,19 @@ export function isDebugRegistryEnabled(): boolean {
|
|
|
200
209
|
return getState().config.enabled;
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Return whether a retrospective can be captured: Debug and its explicit opt-in
|
|
214
|
+
* must be enabled, and at least one persistence sink must be registered.
|
|
215
|
+
*/
|
|
216
|
+
export function canRecordAgentRunRetrospective(): boolean {
|
|
217
|
+
const state = getState();
|
|
218
|
+
return (
|
|
219
|
+
state.config.enabled &&
|
|
220
|
+
state.config.agentRunRetrospectivesEnabled &&
|
|
221
|
+
state.persistenceSinks.size > 0
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
/** Best-effort redaction helper for data exposed through sanitized debug views. */
|
|
204
226
|
export function redactDebugData<T>(value: T): T {
|
|
205
227
|
return redactValue(value, 8) as T;
|
|
@@ -226,6 +248,16 @@ export function subscribeDebugEvents(listener: DebugEventListener): () => void {
|
|
|
226
248
|
return () => state.listeners.delete(listener);
|
|
227
249
|
}
|
|
228
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Register a sanitized event persistence sink. Sinks are tracked separately from
|
|
253
|
+
* ordinary listeners so eligibility checks only succeed when logging is active.
|
|
254
|
+
*/
|
|
255
|
+
export function subscribeDebugPersistence(sink: DebugPersistenceSink): () => void {
|
|
256
|
+
const state = getState();
|
|
257
|
+
state.persistenceSinks.add(sink);
|
|
258
|
+
return () => state.persistenceSinks.delete(sink);
|
|
259
|
+
}
|
|
260
|
+
|
|
229
261
|
/** Record a session-local debug event if debugging is enabled. */
|
|
230
262
|
export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
231
263
|
const state = getState();
|
|
@@ -251,6 +283,13 @@ export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
|
251
283
|
// Debug-event consumers must not alter producer behavior.
|
|
252
284
|
}
|
|
253
285
|
}
|
|
286
|
+
for (const sink of state.persistenceSinks) {
|
|
287
|
+
try {
|
|
288
|
+
sink(toSanitizedView(event));
|
|
289
|
+
} catch {
|
|
290
|
+
// Persistence failures must not alter producer behavior.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
254
293
|
return {
|
|
255
294
|
...event,
|
|
256
295
|
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
@@ -309,5 +348,6 @@ export function resetDebugRegistry(): void {
|
|
|
309
348
|
state.config = cloneConfig(DEBUG_REGISTRY_DEFAULTS);
|
|
310
349
|
state.events = [];
|
|
311
350
|
state.listeners.clear();
|
|
351
|
+
state.persistenceSinks.clear();
|
|
312
352
|
state.nextId = 1;
|
|
313
353
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-web",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "Public web pages, web search sources, and focused Context7 docs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@upstash/context7-sdk": "0.6.0",
|
|
36
37
|
"jsdom": "^30.0.0",
|
|
37
38
|
"@mozilla/readability": "^0.6.0",
|
|
38
39
|
"turndown": "^7.2.0",
|
|
39
40
|
"turndown-plugin-gfm": "^1.0.2",
|
|
40
|
-
"@mrclrchtr/supi-core": "11.
|
|
41
|
+
"@mrclrchtr/supi-core": "11.1.0"
|
|
41
42
|
},
|
|
42
43
|
"bundledDependencies": [
|
|
43
44
|
"@mrclrchtr/supi-core"
|
package/src/api.ts
CHANGED
|
@@ -21,3 +21,7 @@ export {
|
|
|
21
21
|
type WebSearchInput,
|
|
22
22
|
type WebToolName,
|
|
23
23
|
} from "./tool/tool-specs.ts";
|
|
24
|
+
export type { WebDocsFetchOutput } from "./tool/web_docs_fetch/result-schema.ts";
|
|
25
|
+
export type { WebDocsSearchOutput } from "./tool/web_docs_search/result-schema.ts";
|
|
26
|
+
export type { WebFetchMdOutput } from "./tool/web_fetch_md/result-schema.ts";
|
|
27
|
+
export type { WebSearchOutput } from "./tool/web_search/result-schema.ts";
|
package/src/context7-client.ts
CHANGED
|
@@ -1,135 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context7 API client — calls the Context7 REST API directly via fetch(),
|
|
3
|
-
* matching the official Context7 pi extension pattern.
|
|
4
|
-
*
|
|
5
|
-
* API key is read from CONTEXT7_API_KEY env var. Without a key, requests are
|
|
6
|
-
* sent without an Authorization header; the API will return 401.
|
|
7
|
-
*/
|
|
1
|
+
/** Context7 SDK adapter: request policy and SuPi's successful empty-search outcome. */
|
|
8
2
|
|
|
9
|
-
|
|
3
|
+
import { Context7, Context7Error, type Documentation, type Library } from "@upstash/context7-sdk";
|
|
10
4
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = "Context7Error";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface SearchResult {
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
description: string;
|
|
22
|
-
totalSnippets: number;
|
|
23
|
-
trustScore: number;
|
|
24
|
-
benchmarkScore: number;
|
|
25
|
-
versions?: string[];
|
|
26
|
-
}
|
|
5
|
+
export type { Documentation as DocSnippet, Library as SearchResult } from "@upstash/context7-sdk";
|
|
6
|
+
export { Context7Error } from "@upstash/context7-sdk";
|
|
27
7
|
|
|
28
|
-
|
|
29
|
-
title: string;
|
|
30
|
-
content: string;
|
|
31
|
-
source: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Request options shared by Context7 REST calls. */
|
|
8
|
+
/** Request options shared by Context7 calls. */
|
|
35
9
|
export interface Context7RequestOptions {
|
|
36
10
|
signal?: AbortSignal;
|
|
37
11
|
}
|
|
38
12
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function parseErrorResponse(response: Response): Promise<string> {
|
|
45
|
-
try {
|
|
46
|
-
const json = (await response.json()) as { message?: string };
|
|
47
|
-
if (json.message) return json.message;
|
|
48
|
-
} catch {
|
|
49
|
-
// JSON parsing failed, fall through to status-based message
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const hasKey = Boolean(process.env.CONTEXT7_API_KEY);
|
|
53
|
-
if (response.status === 429) {
|
|
54
|
-
return hasKey
|
|
55
|
-
? "Rate limited or quota exceeded. Upgrade your plan at https://context7.com/plans for higher limits."
|
|
56
|
-
: "Rate limited or quota exceeded. Create a free API key at https://context7.com/dashboard for higher limits.";
|
|
57
|
-
}
|
|
58
|
-
if (response.status === 404) {
|
|
59
|
-
return "The library you are trying to access does not exist. Please try with a different library ID.";
|
|
60
|
-
}
|
|
61
|
-
if (response.status === 401) {
|
|
62
|
-
return "Invalid API key. Please check your API key. API keys should start with 'ctx7sk' prefix.";
|
|
63
|
-
}
|
|
64
|
-
return `Request failed with status ${response.status}. Please try again later.`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface ApiSearchResult {
|
|
68
|
-
id: string;
|
|
69
|
-
title: string;
|
|
70
|
-
description: string;
|
|
71
|
-
totalSnippets: number;
|
|
72
|
-
trustScore: number;
|
|
73
|
-
benchmarkScore: number;
|
|
74
|
-
versions?: string[];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface ApiSearchResponse {
|
|
78
|
-
error?: string;
|
|
79
|
-
results: ApiSearchResult[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function mapSearchResult(r: ApiSearchResult): SearchResult {
|
|
83
|
-
return {
|
|
84
|
-
id: r.id,
|
|
85
|
-
name: r.title,
|
|
86
|
-
description: r.description,
|
|
87
|
-
totalSnippets: r.totalSnippets,
|
|
88
|
-
trustScore: r.trustScore,
|
|
89
|
-
benchmarkScore: r.benchmarkScore,
|
|
90
|
-
versions: r.versions,
|
|
91
|
-
};
|
|
13
|
+
function createClient(): Context7 {
|
|
14
|
+
// Resolve credentials per call, never during extension loading. Preserve single-attempt,
|
|
15
|
+
// signal-driven requests rather than inheriting the SDK's retries and timeout.
|
|
16
|
+
return new Context7({ retry: false, timeout: false });
|
|
92
17
|
}
|
|
93
18
|
|
|
19
|
+
/** Search libraries; only the provider's explicit no-match response is a successful empty result. */
|
|
94
20
|
export async function searchLibrary(
|
|
95
21
|
query: string,
|
|
96
22
|
libraryName: string,
|
|
97
23
|
options: Context7RequestOptions = {},
|
|
98
|
-
): Promise<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
24
|
+
): Promise<Library[]> {
|
|
25
|
+
try {
|
|
26
|
+
return await createClient().searchLibrary(query, libraryName, options);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (
|
|
29
|
+
error instanceof Context7Error &&
|
|
30
|
+
error.status === 404 &&
|
|
31
|
+
error.code === "no_libraries_found"
|
|
32
|
+
) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
107
36
|
}
|
|
108
|
-
|
|
109
|
-
const data = (await response.json()) as ApiSearchResponse;
|
|
110
|
-
return (data.results ?? []).map(mapSearchResult);
|
|
111
37
|
}
|
|
112
38
|
|
|
39
|
+
/** Fetch text or SDK-normalized JSON snippets, preserving code and information snippets. */
|
|
113
40
|
export async function getContext(
|
|
114
41
|
query: string,
|
|
115
42
|
libraryId: string,
|
|
116
43
|
raw?: boolean,
|
|
117
44
|
options: Context7RequestOptions = {},
|
|
118
|
-
): Promise<string |
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const response = await fetch(url, { headers: authHeaders(), signal: options.signal });
|
|
124
|
-
|
|
125
|
-
if (!response.ok) {
|
|
126
|
-
throw new Context7Error(await parseErrorResponse(response));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (raw) {
|
|
130
|
-
const json = (await response.json()) as DocSnippet[];
|
|
131
|
-
return json;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return response.text();
|
|
45
|
+
): Promise<string | Documentation[]> {
|
|
46
|
+
return createClient().getContext(query, libraryId, {
|
|
47
|
+
...options,
|
|
48
|
+
type: raw ? "json" : "txt",
|
|
49
|
+
});
|
|
135
50
|
}
|
package/src/tool/result.ts
CHANGED
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
type TruncationResult,
|
|
6
6
|
truncateHead,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { Static } from "typebox";
|
|
8
9
|
import { writeTempFile } from "../temp-file.ts";
|
|
10
|
+
import type { modelOutputSchema } from "./result-schema.ts";
|
|
9
11
|
|
|
10
12
|
/** Result of preparing content for a tool response visible to the model. */
|
|
11
13
|
export interface ModelVisibleOutput {
|
|
@@ -17,6 +19,14 @@ export interface ModelVisibleOutput {
|
|
|
17
19
|
fullOutputPath?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/** Project only public truncation metadata, without copying preview text or internal state. */
|
|
23
|
+
export function modelOutputMetadata(output: ModelVisibleOutput): Static<typeof modelOutputSchema> {
|
|
24
|
+
return {
|
|
25
|
+
truncated: output.truncation?.truncated === true,
|
|
26
|
+
...(output.fullOutputPath ? { fullOutputPath: output.fullOutputPath } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
/** Prepare content for model-visible output, preserving full output in a temp file if truncated. */
|
|
21
31
|
export async function limitModelVisibleOutput(
|
|
22
32
|
content: string,
|
package/src/tool/tool-specs.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
WEB_DOCS_FETCH_TOOL_LABEL,
|
|
11
11
|
WEB_DOCS_FETCH_TOOL_NAME,
|
|
12
12
|
webDocsFetchParameters,
|
|
13
|
+
webDocsFetchSpec,
|
|
13
14
|
} from "./web_docs_fetch/spec.ts";
|
|
14
15
|
import {
|
|
15
16
|
toolDescription as searchDescription,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
WEB_DOCS_SEARCH_TOOL_LABEL,
|
|
21
22
|
WEB_DOCS_SEARCH_TOOL_NAME,
|
|
22
23
|
webDocsSearchParameters,
|
|
24
|
+
webDocsSearchSpec,
|
|
23
25
|
} from "./web_docs_search/spec.ts";
|
|
24
26
|
import {
|
|
25
27
|
toolDescription as fetchDescription,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
WEB_FETCH_MD_TOOL_LABEL,
|
|
32
34
|
WEB_FETCH_MD_TOOL_NAME,
|
|
33
35
|
webFetchMdParameters,
|
|
36
|
+
webFetchMdSpec,
|
|
34
37
|
} from "./web_fetch_md/spec.ts";
|
|
35
38
|
import {
|
|
36
39
|
toolDescription as webSearchDescription,
|
|
@@ -41,6 +44,7 @@ import {
|
|
|
41
44
|
WEB_SEARCH_TOOL_LABEL,
|
|
42
45
|
WEB_SEARCH_TOOL_NAME,
|
|
43
46
|
webSearchParameters,
|
|
47
|
+
webSearchSpec,
|
|
44
48
|
} from "./web_search/spec.ts";
|
|
45
49
|
|
|
46
50
|
export type { WebDocsFetchInput } from "./web_docs_fetch/spec.ts";
|
|
@@ -74,6 +78,7 @@ export interface WebToolSpec {
|
|
|
74
78
|
promptSnippet: string;
|
|
75
79
|
promptGuidelines: readonly string[];
|
|
76
80
|
parameters: TSchema;
|
|
81
|
+
outputSchema: TSchema;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
export const WEB_TOOL_SPECS = [
|
|
@@ -84,6 +89,7 @@ export const WEB_TOOL_SPECS = [
|
|
|
84
89
|
promptSnippet: fetchSnippet,
|
|
85
90
|
promptGuidelines: fetchGuidelines,
|
|
86
91
|
parameters: webFetchMdParameters,
|
|
92
|
+
outputSchema: webFetchMdSpec.outputSchema,
|
|
87
93
|
},
|
|
88
94
|
{
|
|
89
95
|
name: WEB_DOCS_SEARCH_TOOL_NAME,
|
|
@@ -92,6 +98,7 @@ export const WEB_TOOL_SPECS = [
|
|
|
92
98
|
promptSnippet: searchSnippet,
|
|
93
99
|
promptGuidelines: searchGuidelines,
|
|
94
100
|
parameters: webDocsSearchParameters,
|
|
101
|
+
outputSchema: webDocsSearchSpec.outputSchema,
|
|
95
102
|
},
|
|
96
103
|
{
|
|
97
104
|
name: WEB_DOCS_FETCH_TOOL_NAME,
|
|
@@ -100,6 +107,7 @@ export const WEB_TOOL_SPECS = [
|
|
|
100
107
|
promptSnippet: docsFetchSnippet,
|
|
101
108
|
promptGuidelines: docsFetchGuidelines,
|
|
102
109
|
parameters: webDocsFetchParameters,
|
|
110
|
+
outputSchema: webDocsFetchSpec.outputSchema,
|
|
103
111
|
},
|
|
104
112
|
{
|
|
105
113
|
name: WEB_SEARCH_TOOL_NAME,
|
|
@@ -108,6 +116,7 @@ export const WEB_TOOL_SPECS = [
|
|
|
108
116
|
promptSnippet: webSearchSnippet,
|
|
109
117
|
promptGuidelines: webSearchGuidelines,
|
|
110
118
|
parameters: webSearchParameters,
|
|
119
|
+
outputSchema: webSearchSpec.outputSchema,
|
|
111
120
|
},
|
|
112
121
|
] as const satisfies readonly WebToolSpec[];
|
|
113
122
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolResult,
|
|
3
|
-
AgentToolUpdateCallback,
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { getContext } from "../../context7-client.ts";
|
|
7
3
|
import { limitModelVisibleOutput } from "../result.ts";
|
|
8
|
-
import { buildFetchResult, type
|
|
4
|
+
import { buildFetchResult, type FetchResult } from "./result.ts";
|
|
9
5
|
import type { WebDocsFetchInput } from "./spec.ts";
|
|
10
6
|
|
|
11
7
|
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
@@ -15,7 +11,7 @@ export async function runFetch(
|
|
|
15
11
|
signal: AbortSignal | undefined,
|
|
16
12
|
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
17
13
|
_ctx: ExtensionContext,
|
|
18
|
-
): Promise<
|
|
14
|
+
): Promise<FetchResult> {
|
|
19
15
|
const input = (params ?? {}) as WebDocsFetchInput;
|
|
20
16
|
const libraryId = input.library_id?.trim();
|
|
21
17
|
const query = input.query?.trim();
|
|
@@ -37,5 +33,5 @@ export async function runFetch(
|
|
|
37
33
|
suffix: raw ? ".json" : ".md",
|
|
38
34
|
});
|
|
39
35
|
|
|
40
|
-
return buildFetchResult(libraryId, raw, textContent, output);
|
|
36
|
+
return buildFetchResult(libraryId, { raw, content, textContent }, output);
|
|
41
37
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { modelOutputSchema } from "../result-schema.ts";
|
|
3
|
+
|
|
4
|
+
const context = {
|
|
5
|
+
libraryId: Type.String(),
|
|
6
|
+
chars: Type.Integer({ minimum: 0 }),
|
|
7
|
+
lines: Type.Integer({ minimum: 0 }),
|
|
8
|
+
modelOutput: modelOutputSchema,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Context7's text payload or parsed snippet data, selected by the existing raw input. */
|
|
12
|
+
export const webDocsFetchOutputSchema = Type.Union([
|
|
13
|
+
Type.Object({
|
|
14
|
+
...context,
|
|
15
|
+
format: Type.Literal("markdown"),
|
|
16
|
+
markdown: Type.String(),
|
|
17
|
+
}),
|
|
18
|
+
Type.Object({
|
|
19
|
+
...context,
|
|
20
|
+
format: Type.Literal("snippets"),
|
|
21
|
+
snippets: Type.Array(
|
|
22
|
+
Type.Object({
|
|
23
|
+
title: Type.String(),
|
|
24
|
+
content: Type.String(),
|
|
25
|
+
source: Type.String(),
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** Complete documentation payload, unaffected by model-text truncation. */
|
|
32
|
+
export type WebDocsFetchOutput = Static<typeof webDocsFetchOutputSchema>;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AgentToolResult, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { DocSnippet } from "../../context7-client.ts";
|
|
3
|
+
import { type ModelVisibleOutput, modelOutputMetadata } from "../result.ts";
|
|
4
|
+
import type { WebDocsFetchOutput } from "./result-schema.ts";
|
|
5
|
+
|
|
6
|
+
/** Final documentation result preserves the API payload for programmatic callers. */
|
|
7
|
+
export type FetchResult = AgentToolResult<FetchDetails> & {
|
|
8
|
+
structuredContent: WebDocsFetchOutput;
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
export interface FetchDetails extends Record<string, unknown> {
|
|
5
12
|
libraryId: string;
|
|
@@ -13,17 +20,32 @@ export interface FetchDetails extends Record<string, unknown> {
|
|
|
13
20
|
/** Assemble the model-facing result for one docs fetch. */
|
|
14
21
|
export function buildFetchResult(
|
|
15
22
|
libraryId: string,
|
|
16
|
-
raw: boolean,
|
|
17
|
-
textContent: string,
|
|
23
|
+
payload: { raw: boolean; content: string | DocSnippet[]; textContent: string },
|
|
18
24
|
output: ModelVisibleOutput,
|
|
19
|
-
):
|
|
25
|
+
): FetchResult {
|
|
26
|
+
const { raw, content, textContent } = payload;
|
|
27
|
+
const chars = textContent.length;
|
|
28
|
+
const lines = textContent.split("\n").length;
|
|
29
|
+
const context = { libraryId, chars, lines, modelOutput: modelOutputMetadata(output) };
|
|
20
30
|
return {
|
|
21
31
|
content: [{ type: "text", text: output.text }],
|
|
32
|
+
structuredContent:
|
|
33
|
+
typeof content === "string"
|
|
34
|
+
? { ...context, format: "markdown", markdown: content }
|
|
35
|
+
: {
|
|
36
|
+
...context,
|
|
37
|
+
format: "snippets",
|
|
38
|
+
snippets: content.map(({ title, content: body, source }) => ({
|
|
39
|
+
title,
|
|
40
|
+
content: body,
|
|
41
|
+
source,
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
22
44
|
details: {
|
|
23
45
|
libraryId,
|
|
24
46
|
raw,
|
|
25
|
-
chars
|
|
26
|
-
lines
|
|
47
|
+
chars,
|
|
48
|
+
lines,
|
|
27
49
|
truncation: output.truncation,
|
|
28
50
|
fullOutputPath: output.fullOutputPath,
|
|
29
51
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Static } from "typebox";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { runFetch } from "./execute.ts";
|
|
4
|
+
import { webDocsFetchOutputSchema } from "./result-schema.ts";
|
|
4
5
|
|
|
5
6
|
export const WEB_DOCS_FETCH_TOOL_NAME = "web_docs_fetch";
|
|
6
7
|
export const WEB_DOCS_FETCH_TOOL_LABEL = "Web Docs Fetch";
|
|
@@ -8,7 +9,7 @@ export const WEB_DOCS_FETCH_TOOL_LABEL = "Web Docs Fetch";
|
|
|
8
9
|
export const webDocsFetchParameters = Type.Object(
|
|
9
10
|
{
|
|
10
11
|
library_id: Type.String({
|
|
11
|
-
description: "Context7 ID, for example /
|
|
12
|
+
description: "Context7 ID, for example /react/react.",
|
|
12
13
|
}),
|
|
13
14
|
query: Type.String({ description: "Specific docs question" }),
|
|
14
15
|
raw: Type.Optional(
|
|
@@ -28,5 +29,6 @@ export const webDocsFetchSpec = {
|
|
|
28
29
|
name: WEB_DOCS_FETCH_TOOL_NAME,
|
|
29
30
|
label: WEB_DOCS_FETCH_TOOL_LABEL,
|
|
30
31
|
parameters: webDocsFetchParameters,
|
|
32
|
+
outputSchema: webDocsFetchOutputSchema,
|
|
31
33
|
execute: runFetch,
|
|
32
34
|
} as const;
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolResult,
|
|
3
|
-
AgentToolUpdateCallback,
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { searchLibrary } from "../../context7-client.ts";
|
|
7
3
|
import { limitModelVisibleOutput } from "../result.ts";
|
|
8
4
|
import {
|
|
9
5
|
buildNoResultsResult,
|
|
10
6
|
buildSearchResult,
|
|
11
7
|
formatSearchResults,
|
|
12
|
-
type
|
|
8
|
+
type SearchResult,
|
|
13
9
|
} from "./result.ts";
|
|
14
10
|
import type { WebDocsSearchInput } from "./spec.ts";
|
|
15
11
|
|
|
@@ -20,7 +16,7 @@ export async function runSearch(
|
|
|
20
16
|
signal: AbortSignal | undefined,
|
|
21
17
|
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
22
18
|
_ctx: ExtensionContext,
|
|
23
|
-
): Promise<
|
|
19
|
+
): Promise<SearchResult> {
|
|
24
20
|
const input = (params ?? {}) as WebDocsSearchInput;
|
|
25
21
|
const libraryName = input.library_name?.trim();
|
|
26
22
|
const query = input.query?.trim();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { modelOutputSchema } from "../result-schema.ts";
|
|
3
|
+
|
|
4
|
+
/** Complete Context7 library matches, independent of the compact table's limits. */
|
|
5
|
+
export const webDocsSearchOutputSchema = Type.Object({
|
|
6
|
+
results: Type.Array(
|
|
7
|
+
Type.Object({
|
|
8
|
+
id: Type.String(),
|
|
9
|
+
name: Type.String(),
|
|
10
|
+
description: Type.String(),
|
|
11
|
+
totalSnippets: Type.Number(),
|
|
12
|
+
trustScore: Type.Number(),
|
|
13
|
+
benchmarkScore: Type.Number(),
|
|
14
|
+
versions: Type.Optional(Type.Array(Type.String())),
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
count: Type.Integer({ minimum: 0 }),
|
|
18
|
+
modelOutput: Type.Object({
|
|
19
|
+
...modelOutputSchema.properties,
|
|
20
|
+
omittedResults: Type.Integer({
|
|
21
|
+
minimum: 0,
|
|
22
|
+
description: "Matches excluded by the compact table's row limit, before text truncation.",
|
|
23
|
+
}),
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** All API matches, with unshortened descriptions and versions. */
|
|
28
|
+
export type WebDocsSearchOutput = Static<typeof webDocsSearchOutputSchema>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentToolResult, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { searchLibrary } from "../../context7-client.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type ModelVisibleOutput, modelOutputMetadata } from "../result.ts";
|
|
4
|
+
import type { WebDocsSearchOutput } from "./result-schema.ts";
|
|
5
|
+
|
|
6
|
+
/** Final library search includes complete matches, not a Markdown table as data. */
|
|
7
|
+
export type SearchResult = AgentToolResult<SearchDetails> & {
|
|
8
|
+
structuredContent: WebDocsSearchOutput;
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
export interface SearchDetails extends Record<string, unknown> {
|
|
6
12
|
count: number;
|
|
@@ -16,7 +22,7 @@ const MAX_DESCRIPTION_CHARS = 120;
|
|
|
16
22
|
const MAX_VERSION_COUNT = 5;
|
|
17
23
|
|
|
18
24
|
/** Assemble the empty-search result. */
|
|
19
|
-
export function buildNoResultsResult(libraryName: string):
|
|
25
|
+
export function buildNoResultsResult(libraryName: string): SearchResult {
|
|
20
26
|
return {
|
|
21
27
|
content: [
|
|
22
28
|
{
|
|
@@ -24,6 +30,11 @@ export function buildNoResultsResult(libraryName: string): AgentToolResult<Searc
|
|
|
24
30
|
text: `No libraries found for "${libraryName}". Try a different search term.`,
|
|
25
31
|
},
|
|
26
32
|
],
|
|
33
|
+
structuredContent: {
|
|
34
|
+
results: [],
|
|
35
|
+
count: 0,
|
|
36
|
+
modelOutput: { truncated: false, omittedResults: 0 },
|
|
37
|
+
},
|
|
27
38
|
details: { count: 0, libraryName },
|
|
28
39
|
};
|
|
29
40
|
}
|
|
@@ -33,9 +44,25 @@ export function buildSearchResult(
|
|
|
33
44
|
libraryName: string,
|
|
34
45
|
results: Awaited<ReturnType<typeof searchLibrary>>,
|
|
35
46
|
output: ModelVisibleOutput,
|
|
36
|
-
):
|
|
47
|
+
): SearchResult {
|
|
37
48
|
return {
|
|
38
49
|
content: [{ type: "text", text: output.text }],
|
|
50
|
+
structuredContent: {
|
|
51
|
+
results: results.map((result) => ({
|
|
52
|
+
id: result.id,
|
|
53
|
+
name: result.name,
|
|
54
|
+
description: result.description,
|
|
55
|
+
totalSnippets: result.totalSnippets,
|
|
56
|
+
trustScore: result.trustScore,
|
|
57
|
+
benchmarkScore: result.benchmarkScore,
|
|
58
|
+
...(result.versions ? { versions: [...result.versions] } : {}),
|
|
59
|
+
})),
|
|
60
|
+
count: results.length,
|
|
61
|
+
modelOutput: {
|
|
62
|
+
...modelOutputMetadata(output),
|
|
63
|
+
omittedResults: Math.max(0, results.length - MAX_SEARCH_RESULTS),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
39
66
|
details: {
|
|
40
67
|
count: results.length,
|
|
41
68
|
libraryName,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Static } from "typebox";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { runSearch } from "./execute.ts";
|
|
4
|
+
import { webDocsSearchOutputSchema } from "./result-schema.ts";
|
|
4
5
|
|
|
5
6
|
export const WEB_DOCS_SEARCH_TOOL_NAME = "web_docs_search";
|
|
6
7
|
export const WEB_DOCS_SEARCH_TOOL_LABEL = "Web Docs Search";
|
|
@@ -24,5 +25,6 @@ export const webDocsSearchSpec = {
|
|
|
24
25
|
name: WEB_DOCS_SEARCH_TOOL_NAME,
|
|
25
26
|
label: WEB_DOCS_SEARCH_TOOL_LABEL,
|
|
26
27
|
parameters: webDocsSearchParameters,
|
|
28
|
+
outputSchema: webDocsSearchOutputSchema,
|
|
27
29
|
execute: runSearch,
|
|
28
30
|
} as const;
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolResult,
|
|
3
|
-
AgentToolUpdateCallback,
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { htmlToMarkdown, wrapAsCodeBlock } from "../../convert.ts";
|
|
7
3
|
import { fetchWithNegotiation, isValidHttpUrl } from "../../fetch.ts";
|
|
8
4
|
import { writeTempFile } from "../../temp-file.ts";
|
|
@@ -12,7 +8,7 @@ import {
|
|
|
12
8
|
type WebFetchMdInput,
|
|
13
9
|
type WebFetchOutputMode,
|
|
14
10
|
} from "./input.ts";
|
|
15
|
-
import { buildFileResult, buildInlineResult, type
|
|
11
|
+
import { buildFileResult, buildInlineResult, type WebFetchResult } from "./result.ts";
|
|
16
12
|
|
|
17
13
|
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
18
14
|
export async function runWebFetch(
|
|
@@ -21,7 +17,7 @@ export async function runWebFetch(
|
|
|
21
17
|
signal: AbortSignal | undefined,
|
|
22
18
|
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
23
19
|
_ctx: ExtensionContext,
|
|
24
|
-
): Promise<
|
|
20
|
+
): Promise<WebFetchResult> {
|
|
25
21
|
const input = (params ?? {}) as WebFetchMdInput;
|
|
26
22
|
const url = String(input.url || "").trim();
|
|
27
23
|
if (!isValidHttpUrl(url)) {
|
|
@@ -52,7 +48,7 @@ export async function runWebFetch(
|
|
|
52
48
|
tempPrefix: "web-fetch-md",
|
|
53
49
|
suffix: ".md",
|
|
54
50
|
});
|
|
55
|
-
return buildInlineResult(base, output);
|
|
51
|
+
return buildInlineResult(base, markdown, output);
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
function shouldReturnFile(outputMode: WebFetchOutputMode, chars: number): boolean {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { modelOutputSchema } from "../result-schema.ts";
|
|
3
|
+
|
|
4
|
+
const metrics = {
|
|
5
|
+
url: Type.String({ description: "Resolved source URL." }),
|
|
6
|
+
chars: Type.Integer({ minimum: 0 }),
|
|
7
|
+
lines: Type.Integer({ minimum: 0 }),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Page payload, or a file reference when auto/file delivery selects a file. */
|
|
11
|
+
export const webFetchMdOutputSchema = Type.Union([
|
|
12
|
+
Type.Object({
|
|
13
|
+
...metrics,
|
|
14
|
+
mode: Type.Literal("inline"),
|
|
15
|
+
markdown: Type.String(),
|
|
16
|
+
modelOutput: modelOutputSchema,
|
|
17
|
+
}),
|
|
18
|
+
Type.Object({
|
|
19
|
+
...metrics,
|
|
20
|
+
mode: Type.Literal("file"),
|
|
21
|
+
filePath: Type.String(),
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** Complete Markdown in inline mode; file mode retains the existing disk delivery. */
|
|
26
|
+
export type WebFetchMdOutput = Static<typeof webFetchMdOutputSchema>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentToolResult, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
3
|
-
import type { WebFetchOutputMode } from "./
|
|
2
|
+
import { type ModelVisibleOutput, modelOutputMetadata } from "../result.ts";
|
|
3
|
+
import type { WebFetchOutputMode } from "./input.ts";
|
|
4
|
+
import type { WebFetchMdOutput } from "./result-schema.ts";
|
|
5
|
+
|
|
6
|
+
/** Final fetch result requires both native representations. */
|
|
7
|
+
export type WebFetchResult = AgentToolResult<WebFetchDetails> & {
|
|
8
|
+
structuredContent: WebFetchMdOutput;
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
export interface WebFetchDetails extends Record<string, unknown> {
|
|
6
12
|
chars: number;
|
|
@@ -21,10 +27,7 @@ export interface WebFetchResultBase {
|
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/** Assemble the file-mode result for one fetch. */
|
|
24
|
-
export function buildFileResult(
|
|
25
|
-
base: WebFetchResultBase,
|
|
26
|
-
filePath: string,
|
|
27
|
-
): AgentToolResult<WebFetchDetails> {
|
|
30
|
+
export function buildFileResult(base: WebFetchResultBase, filePath: string): WebFetchResult {
|
|
28
31
|
return {
|
|
29
32
|
content: [
|
|
30
33
|
{
|
|
@@ -32,6 +35,13 @@ export function buildFileResult(
|
|
|
32
35
|
text: `Content written to ${filePath} (${base.chars.toLocaleString()} chars, ${base.lines.toLocaleString()} lines). Use the read tool to access it.`,
|
|
33
36
|
},
|
|
34
37
|
],
|
|
38
|
+
structuredContent: {
|
|
39
|
+
mode: "file",
|
|
40
|
+
url: base.url,
|
|
41
|
+
chars: base.chars,
|
|
42
|
+
lines: base.lines,
|
|
43
|
+
filePath,
|
|
44
|
+
},
|
|
35
45
|
details: { ...base, filePath },
|
|
36
46
|
};
|
|
37
47
|
}
|
|
@@ -39,10 +49,19 @@ export function buildFileResult(
|
|
|
39
49
|
/** Assemble the inline-mode result for one fetch. */
|
|
40
50
|
export function buildInlineResult(
|
|
41
51
|
base: WebFetchResultBase,
|
|
52
|
+
markdown: string,
|
|
42
53
|
output: ModelVisibleOutput,
|
|
43
|
-
):
|
|
54
|
+
): WebFetchResult {
|
|
44
55
|
return {
|
|
45
56
|
content: [{ type: "text", text: output.text }],
|
|
57
|
+
structuredContent: {
|
|
58
|
+
mode: "inline",
|
|
59
|
+
url: base.url,
|
|
60
|
+
chars: base.chars,
|
|
61
|
+
lines: base.lines,
|
|
62
|
+
markdown,
|
|
63
|
+
modelOutput: modelOutputMetadata(output),
|
|
64
|
+
},
|
|
46
65
|
details: {
|
|
47
66
|
...base,
|
|
48
67
|
truncation: output.truncation,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runWebFetch } from "./execute.ts";
|
|
2
2
|
import { webFetchMdParameters } from "./input.ts";
|
|
3
|
+
import { webFetchMdOutputSchema } from "./result-schema.ts";
|
|
3
4
|
|
|
4
5
|
export {
|
|
5
6
|
WEB_FETCH_INLINE_MAX_CHARS,
|
|
@@ -17,5 +18,6 @@ export const webFetchMdSpec = {
|
|
|
17
18
|
name: WEB_FETCH_MD_TOOL_NAME,
|
|
18
19
|
label: WEB_FETCH_MD_TOOL_LABEL,
|
|
19
20
|
parameters: webFetchMdParameters,
|
|
21
|
+
outputSchema: webFetchMdOutputSchema,
|
|
20
22
|
execute: runWebFetch,
|
|
21
23
|
} as const;
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolResult,
|
|
3
|
-
AgentToolUpdateCallback,
|
|
4
|
-
ExtensionContext,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { limitModelVisibleOutput } from "../result.ts";
|
|
7
3
|
import { runBxContext } from "./bx.ts";
|
|
8
4
|
import { validateWebSearchInput } from "./input.ts";
|
|
9
5
|
import { parseBxResponse } from "./parse.ts";
|
|
10
|
-
import { buildSearchResult, formatSearchResults, type
|
|
6
|
+
import { buildSearchResult, formatSearchResults, type WebSearchResult } from "./result.ts";
|
|
11
7
|
|
|
12
8
|
/** Execute one direct bx context search. */
|
|
13
9
|
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
@@ -17,7 +13,7 @@ export async function runWebSearch(
|
|
|
17
13
|
signal: AbortSignal | undefined,
|
|
18
14
|
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
19
15
|
ctx: ExtensionContext,
|
|
20
|
-
): Promise<
|
|
16
|
+
): Promise<WebSearchResult> {
|
|
21
17
|
const input = validateWebSearchInput(params);
|
|
22
18
|
onUpdate?.({
|
|
23
19
|
content: [{ type: "text", text: "Searching the web..." }],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { modelOutputSchema } from "../result-schema.ts";
|
|
3
|
+
|
|
4
|
+
/** All validated provider sources; no generated answer or private bx envelope. */
|
|
5
|
+
export const webSearchOutputSchema = Type.Object({
|
|
6
|
+
sources: Type.Array(
|
|
7
|
+
Type.Object({
|
|
8
|
+
title: Type.String(),
|
|
9
|
+
url: Type.String(),
|
|
10
|
+
excerpts: Type.Array(Type.String()),
|
|
11
|
+
sourceDate: Type.Optional(
|
|
12
|
+
Type.String({ description: "Provider-reported publication or modification date." }),
|
|
13
|
+
),
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
sourceCount: Type.Integer({ minimum: 0 }),
|
|
17
|
+
excerptCount: Type.Integer({ minimum: 0 }),
|
|
18
|
+
modelOutput: modelOutputSchema,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** Source evidence remains complete even when model-facing Markdown is truncated. */
|
|
22
|
+
export type WebSearchOutput = Static<typeof webSearchOutputSchema>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentToolResult, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
2
|
+
import { type ModelVisibleOutput, modelOutputMetadata } from "../result.ts";
|
|
3
3
|
import type { WebSearchSource } from "./parse.ts";
|
|
4
|
+
import type { WebSearchOutput } from "./result-schema.ts";
|
|
5
|
+
|
|
6
|
+
/** Final search result requires source data as well as direct-call text. */
|
|
7
|
+
export type WebSearchResult = AgentToolResult<WebSearchDetails> & {
|
|
8
|
+
structuredContent: WebSearchOutput;
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
/** Human-facing and model-facing facts for one Web Search result. */
|
|
6
12
|
export interface WebSearchDetails extends Record<string, unknown> {
|
|
@@ -14,12 +20,25 @@ export interface WebSearchDetails extends Record<string, unknown> {
|
|
|
14
20
|
export function buildSearchResult(
|
|
15
21
|
sources: WebSearchSource[],
|
|
16
22
|
output: ModelVisibleOutput,
|
|
17
|
-
):
|
|
23
|
+
): WebSearchResult {
|
|
24
|
+
const sourceCount = sources.length;
|
|
25
|
+
const excerptCount = sources.reduce((count, source) => count + source.excerpts.length, 0);
|
|
18
26
|
return {
|
|
19
27
|
content: [{ type: "text", text: output.text }],
|
|
28
|
+
structuredContent: {
|
|
29
|
+
sources: sources.map(({ title, url, excerpts, sourceDate }) => ({
|
|
30
|
+
title,
|
|
31
|
+
url,
|
|
32
|
+
excerpts: [...excerpts],
|
|
33
|
+
...(sourceDate ? { sourceDate } : {}),
|
|
34
|
+
})),
|
|
35
|
+
sourceCount,
|
|
36
|
+
excerptCount,
|
|
37
|
+
modelOutput: modelOutputMetadata(output),
|
|
38
|
+
},
|
|
20
39
|
details: {
|
|
21
|
-
sourceCount
|
|
22
|
-
excerptCount
|
|
40
|
+
sourceCount,
|
|
41
|
+
excerptCount,
|
|
23
42
|
truncation: output.truncation,
|
|
24
43
|
fullOutputPath: output.fullOutputPath,
|
|
25
44
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runWebSearch } from "./execute.ts";
|
|
2
2
|
import { webSearchParameters } from "./input.ts";
|
|
3
|
+
import { webSearchOutputSchema } from "./result-schema.ts";
|
|
3
4
|
|
|
4
5
|
export {
|
|
5
6
|
type WebSearchInput,
|
|
@@ -14,5 +15,6 @@ export const webSearchSpec = {
|
|
|
14
15
|
name: WEB_SEARCH_TOOL_NAME,
|
|
15
16
|
label: WEB_SEARCH_TOOL_LABEL,
|
|
16
17
|
parameters: webSearchParameters,
|
|
18
|
+
outputSchema: webSearchOutputSchema,
|
|
17
19
|
execute: runWebSearch,
|
|
18
20
|
} as const;
|