pi-web-search 1.2.0 โ 1.3.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 +35 -71
- package/package.json +8 -2
- package/src/index.ts +0 -1
- package/src/url_context.ts +1 -1
- package/src/utils.ts +150 -40
- package/src/web_search.ts +4 -8
package/README.md
CHANGED
|
@@ -1,95 +1,59 @@
|
|
|
1
1
|
# pi-web-search
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Provider-native web search for [pi](https://github.com/earendil-works/pi-coding-agent), across Google Gemini, OpenAI, and Anthropic, plus Gemini-only URL Context analysis.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Tools
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **๐ URL Context** - Gemini-native analysis of up to 20 public URLs, including web pages, documents, images, and YouTube videos
|
|
9
|
-
- **๐งพ Search telemetry** - Tool results include provider, search queries, result URLs, citations, and whether native search metadata was actually observed
|
|
10
|
-
- **๐ Canonicalized Google grounding links** - Gemini grounding redirect URLs are resolved to their underlying destination URLs when possible
|
|
7
|
+
### `web_search`
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
Search the web using your currently selected model. Automatically picks the right provider API:
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- `web_search` now follows the current model across Google Gemini, OpenAI Responses, and Anthropic Messages
|
|
21
|
-
- `url_context` is explicitly Gemini-only and is removed from the active tool set for non-Gemini models
|
|
22
|
-
- Provider-native search telemetry now exposes queries, URLs, citations, and verification metadata in tool `details`
|
|
23
|
-
- Gemini grounding redirect URLs are resolved to canonical destination URLs when possible
|
|
24
|
-
- Real test scripts now read configuration from `.env` / `.env.example`
|
|
25
|
-
- The package now ships source-only TypeScript entrypoints for pi; no `dist/` build output is required in the project or published package
|
|
26
|
-
|
|
27
|
-
## Behavior notes
|
|
28
|
-
|
|
29
|
-
- `web_search` follows the **currently selected model** when that model exposes a supported native search API.
|
|
30
|
-
- `web_search` supports Google Gemini, OpenAI Responses, Anthropic Messages, and compatible proxy providers exposing those APIs.
|
|
31
|
-
- `url_context` is intentionally **Gemini-only**. When the current model is not Gemini-compatible, the extension removes `url_context` from the active tool set and the executor also fast-fails with a clear unsupported-provider message as a second safety net.
|
|
32
|
-
- If `web_search` returns only plain text and does **not** return native search metadata, the tool output includes a warning and marks the result as ungrounded in `details`.
|
|
33
|
-
- If `url_context` does not return verified URL-context metadata, the tool output includes an explicit warning so the caller can distinguish a best-effort summary from a verified retrieval.
|
|
34
|
-
- For Gemini grounding results, the extension attempts to resolve Google redirect URLs to the final destination URL before returning `sources` / `searchResults`.
|
|
35
|
-
|
|
36
|
-
## Tool matrix
|
|
11
|
+
| Provider | API |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Google Gemini | Grounding with Google Search |
|
|
14
|
+
| OpenAI | Responses API web search |
|
|
15
|
+
| Anthropic | Messages API web search |
|
|
37
16
|
|
|
38
|
-
|
|
39
|
-
|---|---|---|---|---|
|
|
40
|
-
| `web_search` | โ
| โ
| โ
| โ
|
|
|
41
|
-
| `url_context` | โ
| โ | โ | โ
, but only when the selected model is Gemini-compatible |
|
|
17
|
+
Supports passing up to 20 additional URLs to analyze alongside the query.
|
|
42
18
|
|
|
43
|
-
|
|
19
|
+
### `url_context`
|
|
44
20
|
|
|
45
|
-
|
|
21
|
+
Gemini-only. Analyze up to 20 public URLs โ web pages, documents, images, and YouTube videos. Uses Gemini's native URL Context retrieval with verified metadata.
|
|
46
22
|
|
|
47
|
-
|
|
23
|
+
When using `google-generative-ai`, YouTube URLs are passed as `file_data` for native video understanding.
|
|
48
24
|
|
|
49
|
-
|
|
50
|
-
- google-generative-ai
|
|
51
|
-
- openai
|
|
52
|
-
- anthropic
|
|
53
|
-
- compatible proxy providers whose selected model API is OpenAI Responses, Anthropic Messages, or Google Generative AI
|
|
25
|
+
## Install
|
|
54
26
|
|
|
55
|
-
|
|
27
|
+
```bash
|
|
28
|
+
pi install npm:pi-web-search
|
|
29
|
+
```
|
|
56
30
|
|
|
57
|
-
|
|
58
|
-
- google-generative-ai
|
|
59
|
-
- compatible proxy providers whose selected model API is Google Generative AI
|
|
31
|
+
## Usage
|
|
60
32
|
|
|
61
|
-
|
|
33
|
+
No extra config needed. Select a supported current model in pi and the tools auto-detect the matching provider API.
|
|
62
34
|
|
|
63
|
-
|
|
35
|
+
`web_search` will not scan configured models and pick one automatically when the current model does not support native search. To use a dedicated search model, opt in explicitly with `~/.pi/agent/web-search.json`:
|
|
64
36
|
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"provider": "openai",
|
|
40
|
+
"model": "gpt-5.1"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
68
43
|
|
|
69
|
-
|
|
70
|
-
node tests/real-web-search.mjs
|
|
44
|
+
When this file exists, `web_search` uses the configured provider/model first. If it is missing, `web_search` uses the current conversation model. If the selected model does not support native search, the tool returns an error instead of falling back.
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
# Passes when url_context either returns verified metadata/URLs
|
|
74
|
-
# or emits the explicit unverified-warning path.
|
|
75
|
-
node tests/real-url-context.mjs
|
|
76
|
-
```
|
|
46
|
+
`url_context` is automatically removed from active tools when using a non-Gemini model.
|
|
77
47
|
|
|
78
|
-
|
|
48
|
+
## Test
|
|
79
49
|
|
|
80
50
|
```bash
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
PI_WEB_SEARCH_MODEL_ANTHROPIC=provider/model-anthropic
|
|
86
|
-
PI_WEB_SEARCH_MODEL_GOOGLE=provider/model-google
|
|
87
|
-
PI_URL_CONTEXT_MODEL=provider/model-google
|
|
51
|
+
cp .env.example .env # edit with your models
|
|
52
|
+
npm test # unit tests
|
|
53
|
+
npm run test:real:web-search
|
|
54
|
+
npm run test:real:url-context
|
|
88
55
|
```
|
|
89
56
|
|
|
90
|
-
|
|
57
|
+
## License
|
|
91
58
|
|
|
92
|
-
|
|
93
|
-
cp .env.example .env
|
|
94
|
-
# then edit .env with models/extensions available in your local pi setup
|
|
95
|
-
```
|
|
59
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Provider-native web search for pi across Google Gemini, OpenAI, and Anthropic, plus Gemini URL Context",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"pi",
|
|
9
9
|
"pi-extension",
|
|
10
|
+
"pi-package",
|
|
10
11
|
"web-search",
|
|
11
12
|
"gemini",
|
|
12
13
|
"google",
|
|
@@ -38,13 +39,18 @@
|
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@earendil-works/pi-ai": "^0.75.4",
|
|
40
41
|
"@earendil-works/pi-coding-agent": "^0.75.4",
|
|
41
|
-
"@sinclair/typebox": "^0.34.49",
|
|
42
42
|
"@types/node": "^25.9.1",
|
|
43
|
+
"typebox": "^1.1.38",
|
|
43
44
|
"typescript": "^6.0.3"
|
|
44
45
|
},
|
|
45
46
|
"pi": {
|
|
46
47
|
"extensions": [
|
|
47
48
|
"./src/index.ts"
|
|
48
49
|
]
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@earendil-works/pi-ai": "*",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
+
"typebox": "*"
|
|
49
55
|
}
|
|
50
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -77,7 +77,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
77
77
|
|
|
78
78
|
const toolManager = createModelScopedToolManager(pi);
|
|
79
79
|
pi.on("session_start", (_event, ctx) => toolManager.sync(ctx.model));
|
|
80
|
-
pi.on("session_switch", (_event, ctx) => toolManager.sync(ctx.model));
|
|
81
80
|
pi.on("session_tree", (_event, ctx) => toolManager.sync(ctx.model));
|
|
82
81
|
pi.on("model_select", (event) => toolManager.sync(event.model));
|
|
83
82
|
}
|
package/src/url_context.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type, type Static } from "
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
3
|
import { callApiStream, getConfig, applyCitations } from "./api.ts";
|
|
4
4
|
import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.ts";
|
|
5
5
|
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ExtensionContext, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Model } from "@earendil-works/pi-ai";
|
|
3
3
|
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
4
7
|
import { getProviderKind } from "./api.ts";
|
|
5
8
|
|
|
6
9
|
// --- Formatting ---
|
|
@@ -15,63 +18,170 @@ export function formatResult(text: string, details: any): AgentToolResult<any> {
|
|
|
15
18
|
|
|
16
19
|
// --- Model Selection ---
|
|
17
20
|
|
|
21
|
+
const SUPPORTED_PROVIDERS = ["google-generative-ai", "openai-responses", "anthropic-messages"];
|
|
22
|
+
const WEB_SEARCH_CONFIG_PATH = join(homedir(), ".pi", "agent", "web-search.json");
|
|
23
|
+
|
|
24
|
+
type WebSearchModelConfig =
|
|
25
|
+
| { status: "missing"; path: string; }
|
|
26
|
+
| { status: "configured"; path: string; provider: string; modelId: string; }
|
|
27
|
+
| { status: "invalid"; path: string; error: string; };
|
|
28
|
+
|
|
18
29
|
function isSupportedSearchModel(model: Model<any> | undefined): model is Model<any> {
|
|
19
30
|
if (!model) return false;
|
|
20
31
|
return getProviderKind(model) !== "unsupported";
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
"google",
|
|
26
|
-
"google-generative-ai",
|
|
27
|
-
"openai",
|
|
28
|
-
"anthropic",
|
|
29
|
-
];
|
|
30
|
-
const providerIndex = priorities.indexOf(model.provider);
|
|
31
|
-
if (providerIndex >= 0) return providerIndex;
|
|
32
|
-
return priorities.length;
|
|
34
|
+
function describeModel(model: Model<any>): string {
|
|
35
|
+
return `${model.id} (${model.provider}/${model.api})`;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
const patterns = [
|
|
38
|
-
/gemini-3.*flash/i,
|
|
39
|
-
/gemini-2\.5.*flash/i,
|
|
40
|
-
/gemini-2\.0.*flash/i,
|
|
41
|
-
/gemini.*flash/i,
|
|
42
|
-
/gpt-5\..*mini/i,
|
|
43
|
-
/gpt-4\.1.*mini/i,
|
|
44
|
-
/gpt-4o-mini/i,
|
|
45
|
-
/claude.*haiku/i,
|
|
46
|
-
/claude.*sonnet/i,
|
|
47
|
-
];
|
|
48
|
-
const patternIndex = patterns.findIndex((pattern) => pattern.test(id));
|
|
49
|
-
return patternIndex >= 0 ? patternIndex : patterns.length;
|
|
38
|
+
function getWebSearchConfigPath(): string {
|
|
39
|
+
return process.env.PI_WEB_SEARCH_CONFIG || WEB_SEARCH_CONFIG_PATH;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
function readWebSearchModelConfig(): WebSearchModelConfig {
|
|
43
|
+
const path = getWebSearchConfigPath();
|
|
44
|
+
let raw: string;
|
|
45
|
+
try {
|
|
46
|
+
raw = readFileSync(path, "utf8");
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
if (error?.code === "ENOENT") return { status: "missing", path };
|
|
49
|
+
return { status: "invalid", path, error: error?.message || String(error) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let parsed: any;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw);
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
return { status: "invalid", path, error: `Invalid JSON: ${error?.message || String(error)}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
60
|
+
return { status: "invalid", path, error: "Expected a JSON object with provider and model fields" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const provider = parsed.provider;
|
|
64
|
+
const modelId = parsed.model ?? parsed.modelId;
|
|
65
|
+
if (typeof provider !== "string" || provider.trim() === "") {
|
|
66
|
+
return { status: "invalid", path, error: "Missing required string field: provider" };
|
|
67
|
+
}
|
|
68
|
+
if (typeof modelId !== "string" || modelId.trim() === "") {
|
|
69
|
+
return { status: "invalid", path, error: "Missing required string field: model" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { status: "configured", path, provider: provider.trim(), modelId: modelId.trim() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getAvailableSupportedModels(ctx: ExtensionContext): string[] {
|
|
76
|
+
try {
|
|
77
|
+
return ctx.modelRegistry.getAvailable()
|
|
78
|
+
.filter(isSupportedSearchModel)
|
|
79
|
+
.map(describeModel);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
57
82
|
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function getModel(ctx: ExtensionContext): Promise<Model<any> | undefined> {
|
|
86
|
+
// Only use the currently selected model. Do not silently fall back to another
|
|
87
|
+
// configured model, because that can surprise users with unexpected API costs.
|
|
88
|
+
return isSupportedSearchModel(ctx.model) ? ctx.model : undefined;
|
|
89
|
+
}
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
export async function getWebSearchModel(ctx: ExtensionContext): Promise<Model<any> | undefined> {
|
|
92
|
+
const config = readWebSearchModelConfig();
|
|
93
|
+
if (config.status === "invalid") return undefined;
|
|
94
|
+
|
|
95
|
+
if (config.status === "configured") {
|
|
96
|
+
const model = ctx.modelRegistry.find(config.provider, config.modelId);
|
|
97
|
+
return isSupportedSearchModel(model) ? model : undefined;
|
|
98
|
+
}
|
|
61
99
|
|
|
62
|
-
return
|
|
63
|
-
const byProvider = providerPriority(a) - providerPriority(b);
|
|
64
|
-
if (byProvider !== 0) return byProvider;
|
|
65
|
-
return modelPriority(a) - modelPriority(b);
|
|
66
|
-
})[0];
|
|
100
|
+
return getModel(ctx);
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
// --- Error Results ---
|
|
70
104
|
|
|
71
105
|
export function missingConfigResult(ctx: ExtensionContext): AgentToolResult<any> {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
106
|
+
const availableSupportedModels = getAvailableSupportedModels(ctx);
|
|
107
|
+
const supportedList = SUPPORTED_PROVIDERS.join(", ");
|
|
108
|
+
|
|
109
|
+
if (ctx.model) {
|
|
110
|
+
const availableHint = availableSupportedModels.length > 0
|
|
111
|
+
? ` Select one of these configured supported models manually: ${availableSupportedModels.join(", ")}.`
|
|
112
|
+
: ` Configure and select a supported provider: ${supportedList}.`;
|
|
113
|
+
const msg = `The current model ${describeModel(ctx.model)} does not support native web search. pi-web-search will not switch to another configured model automatically to avoid unexpected API costs.${availableHint}`;
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: `Failed: ${msg}` }],
|
|
116
|
+
details: {
|
|
117
|
+
error: "unsupported_model",
|
|
118
|
+
currentModel: describeModel(ctx.model),
|
|
119
|
+
availableSupportedModels,
|
|
120
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const availableHint = availableSupportedModels.length > 0
|
|
126
|
+
? ` Select one of these configured supported models manually: ${availableSupportedModels.join(", ")}.`
|
|
127
|
+
: ` Configure and select a supported provider: ${supportedList}.`;
|
|
128
|
+
const msg = `No current model selected for web search.${availableHint}`;
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: `Failed: ${msg}` }],
|
|
131
|
+
details: {
|
|
132
|
+
error: "missing_config",
|
|
133
|
+
availableSupportedModels,
|
|
134
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function missingWebSearchConfigResult(ctx: ExtensionContext): AgentToolResult<any> {
|
|
140
|
+
const config = readWebSearchModelConfig();
|
|
141
|
+
const availableSupportedModels = getAvailableSupportedModels(ctx);
|
|
142
|
+
const supportedList = SUPPORTED_PROVIDERS.join(", ");
|
|
143
|
+
|
|
144
|
+
if (config.status === "invalid") {
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text", text: `Failed: Invalid web search model config at ${config.path}: ${config.error}. Fix the file or remove it to use the current conversation model.` }],
|
|
147
|
+
details: {
|
|
148
|
+
error: "invalid_config",
|
|
149
|
+
configPath: config.path,
|
|
150
|
+
configError: config.error,
|
|
151
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (config.status === "configured") {
|
|
157
|
+
const model = ctx.modelRegistry.find(config.provider, config.modelId);
|
|
158
|
+
if (!model) {
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: `Failed: Configured web search model ${config.provider}/${config.modelId} from ${config.path} was not found. Fix the file or remove it to use the current conversation model.` }],
|
|
161
|
+
details: {
|
|
162
|
+
error: "configured_model_not_found",
|
|
163
|
+
configPath: config.path,
|
|
164
|
+
configuredProvider: config.provider,
|
|
165
|
+
configuredModel: config.modelId,
|
|
166
|
+
availableSupportedModels,
|
|
167
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: `Failed: Configured web search model ${describeModel(model)} from ${config.path} does not support native web search. Configure a model backed by ${supportedList}, or remove the file to use the current conversation model.` }],
|
|
173
|
+
details: {
|
|
174
|
+
error: "unsupported_model",
|
|
175
|
+
configPath: config.path,
|
|
176
|
+
configuredModel: describeModel(model),
|
|
177
|
+
configuredProvider: config.provider,
|
|
178
|
+
availableSupportedModels,
|
|
179
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return missingConfigResult(ctx);
|
|
75
185
|
}
|
|
76
186
|
|
|
77
187
|
export function errorResult(e: Error): AgentToolResult<any> {
|
package/src/web_search.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type, type Static } from "
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
3
|
import { callApiStream, getConfig, applyCitations } from "./api.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { getWebSearchModel, missingWebSearchConfigResult, errorResult, formatResult } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
export const WebSearchSchema = Type.Object({
|
|
7
7
|
query: Type.String({ description: "The search query or question to answer" }),
|
|
@@ -19,8 +19,8 @@ export async function webSearch(
|
|
|
19
19
|
onUpdate: AgentToolUpdateCallback | undefined,
|
|
20
20
|
ctx: ExtensionContext
|
|
21
21
|
) {
|
|
22
|
-
const model = await
|
|
23
|
-
if (!model) return
|
|
22
|
+
const model = await getWebSearchModel(ctx);
|
|
23
|
+
if (!model) return missingWebSearchConfigResult(ctx);
|
|
24
24
|
|
|
25
25
|
const hasUrls = params.urls && params.urls.length > 0;
|
|
26
26
|
const urlCount = hasUrls ? params.urls!.length : 0;
|
|
@@ -84,10 +84,6 @@ export async function webSearch(
|
|
|
84
84
|
failed.forEach((f: any) => { summary += `\n- ${f.url}: ${f.status}`; });
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
if (result.nativeSearchUsed === false) {
|
|
88
|
-
summary += `\n\n## Search Verification\nโ ๏ธ No verified native search metadata was returned by provider ${result.providerKind || "unknown"}. Treat the answer as ungrounded unless sources/searchResults are present in tool details.`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
87
|
// Add sources
|
|
92
88
|
if (sources.length > 0) {
|
|
93
89
|
summary += `\n\n## Sources\n${sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`;
|