@aliou/pi-synthetic 0.18.1 → 0.18.2
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 +3 -3
- package/package.json +10 -5
- package/src/extensions/provider/index.ts +1 -1
- package/src/extensions/provider/models.ts +29 -56
- package/src/extensions/web-search/tool.ts +213 -231
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ pi install npm:@aliou/pi-synthetic
|
|
|
41
41
|
pi install git:github.com/aliou/pi-synthetic
|
|
42
42
|
|
|
43
43
|
# Local development
|
|
44
|
-
pi -e
|
|
44
|
+
pi -e .
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
## Usage
|
|
@@ -165,7 +165,7 @@ pnpm run test
|
|
|
165
165
|
### Test Locally
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
|
-
pi -e
|
|
168
|
+
pi -e .
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
## Release
|
|
@@ -180,7 +180,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
|
|
|
180
180
|
|
|
181
181
|
## Requirements
|
|
182
182
|
|
|
183
|
-
- Pi coding agent v0.
|
|
183
|
+
- Pi coding agent v0.77.0+
|
|
184
184
|
- Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`)
|
|
185
185
|
|
|
186
186
|
## Links
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
"!src/**/*.test.ts"
|
|
34
34
|
],
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": "
|
|
37
|
-
"@earendil-works/pi-tui": "
|
|
36
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
37
|
+
"@earendil-works/pi-tui": "*",
|
|
38
|
+
"typebox": "*"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@aliou/pi-utils-settings": "^0.15.0",
|
|
@@ -44,12 +45,13 @@
|
|
|
44
45
|
"@aliou/biome-plugins": "^0.8.1",
|
|
45
46
|
"@biomejs/biome": "^2.4.15",
|
|
46
47
|
"@changesets/cli": "^2.27.11",
|
|
47
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "0.77.0",
|
|
48
49
|
"typebox": "^1.1.37",
|
|
49
50
|
"@types/node": "^25.0.10",
|
|
50
51
|
"husky": "^9.1.7",
|
|
51
52
|
"typescript": "^5.9.3",
|
|
52
|
-
"vitest": "^4.0.18"
|
|
53
|
+
"vitest": "^4.0.18",
|
|
54
|
+
"@earendil-works/pi-tui": "0.77.0"
|
|
53
55
|
},
|
|
54
56
|
"peerDependenciesMeta": {
|
|
55
57
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -57,6 +59,9 @@
|
|
|
57
59
|
},
|
|
58
60
|
"@earendil-works/pi-tui": {
|
|
59
61
|
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"typebox": {
|
|
64
|
+
"optional": true
|
|
60
65
|
}
|
|
61
66
|
},
|
|
62
67
|
"scripts": {
|
|
@@ -80,7 +80,7 @@ export function registerSyntheticProvider(
|
|
|
80
80
|
): void {
|
|
81
81
|
pi.registerProvider("synthetic", {
|
|
82
82
|
baseUrl: "https://api.synthetic.new/openai/v1",
|
|
83
|
-
apiKey: "SYNTHETIC_API_KEY",
|
|
83
|
+
apiKey: "$SYNTHETIC_API_KEY",
|
|
84
84
|
api: "openai-completions",
|
|
85
85
|
headers: {
|
|
86
86
|
Referer: "https://pi.dev",
|
|
@@ -51,11 +51,11 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
|
|
|
51
51
|
name: "syn:large:vision",
|
|
52
52
|
aliasFor: "hf:moonshotai/Kimi-K2.6",
|
|
53
53
|
},
|
|
54
|
-
// API: syn:small:vision → alias for hf:
|
|
54
|
+
// API: syn:small:vision → alias for hf:Qwen/Qwen3.6-27B
|
|
55
55
|
{
|
|
56
56
|
id: "syn:small:vision",
|
|
57
57
|
name: "syn:small:vision",
|
|
58
|
-
aliasFor: "hf:
|
|
58
|
+
aliasFor: "hf:Qwen/Qwen3.6-27B",
|
|
59
59
|
},
|
|
60
60
|
// API: hf:zai-org/GLM-4.7 → ctx=202752
|
|
61
61
|
{
|
|
@@ -84,33 +84,6 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
|
|
|
84
84
|
contextWindow: 202752,
|
|
85
85
|
maxTokens: 65536,
|
|
86
86
|
},
|
|
87
|
-
// API: hf:zai-org/GLM-5 → ctx=196608, out=65536
|
|
88
|
-
{
|
|
89
|
-
id: "hf:zai-org/GLM-5",
|
|
90
|
-
name: "zai-org/GLM-5",
|
|
91
|
-
provider: "synthetic",
|
|
92
|
-
reasoning: true,
|
|
93
|
-
thinkingLevelMap: {
|
|
94
|
-
off: "none",
|
|
95
|
-
minimal: null,
|
|
96
|
-
low: null,
|
|
97
|
-
medium: "medium",
|
|
98
|
-
high: null,
|
|
99
|
-
xhigh: null,
|
|
100
|
-
},
|
|
101
|
-
compat: {
|
|
102
|
-
supportsReasoningEffort: true,
|
|
103
|
-
},
|
|
104
|
-
input: ["text"],
|
|
105
|
-
cost: {
|
|
106
|
-
input: 1,
|
|
107
|
-
output: 3,
|
|
108
|
-
cacheRead: 1,
|
|
109
|
-
cacheWrite: 0,
|
|
110
|
-
},
|
|
111
|
-
contextWindow: 196608,
|
|
112
|
-
maxTokens: 65536,
|
|
113
|
-
},
|
|
114
87
|
// API: hf:zai-org/GLM-5.1 → ctx=196608, out=65536
|
|
115
88
|
{
|
|
116
89
|
id: "hf:zai-org/GLM-5.1",
|
|
@@ -166,33 +139,6 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
|
|
|
166
139
|
contextWindow: 196608,
|
|
167
140
|
maxTokens: 65536,
|
|
168
141
|
},
|
|
169
|
-
// models.dev: synthetic/hf:deepseek-ai/DeepSeek-V3.2 → ctx=162816, out=8000
|
|
170
|
-
{
|
|
171
|
-
id: "hf:deepseek-ai/DeepSeek-V3.2",
|
|
172
|
-
name: "deepseek-ai/DeepSeek-V3.2",
|
|
173
|
-
provider: "fireworks",
|
|
174
|
-
reasoning: true,
|
|
175
|
-
thinkingLevelMap: {
|
|
176
|
-
off: "none",
|
|
177
|
-
minimal: null,
|
|
178
|
-
low: null,
|
|
179
|
-
medium: "medium",
|
|
180
|
-
high: null,
|
|
181
|
-
xhigh: null,
|
|
182
|
-
},
|
|
183
|
-
compat: {
|
|
184
|
-
supportsReasoningEffort: true,
|
|
185
|
-
},
|
|
186
|
-
input: ["text"],
|
|
187
|
-
cost: {
|
|
188
|
-
input: 0.56,
|
|
189
|
-
output: 1.68,
|
|
190
|
-
cacheRead: 0.56,
|
|
191
|
-
cacheWrite: 0,
|
|
192
|
-
},
|
|
193
|
-
contextWindow: 162816,
|
|
194
|
-
maxTokens: 8000,
|
|
195
|
-
},
|
|
196
142
|
// models.dev: synthetic/hf:openai/gpt-oss-120b → ctx=128000, out=32768
|
|
197
143
|
{
|
|
198
144
|
id: "hf:openai/gpt-oss-120b",
|
|
@@ -279,6 +225,33 @@ export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
|
|
|
279
225
|
contextWindow: 262144,
|
|
280
226
|
maxTokens: 65536,
|
|
281
227
|
},
|
|
228
|
+
// API: hf:Qwen/Qwen3.6-27B → ctx=262144, out=65536
|
|
229
|
+
{
|
|
230
|
+
id: "hf:Qwen/Qwen3.6-27B",
|
|
231
|
+
name: "Qwen/Qwen3.6-27B",
|
|
232
|
+
provider: "synthetic",
|
|
233
|
+
reasoning: true,
|
|
234
|
+
thinkingLevelMap: {
|
|
235
|
+
off: "none",
|
|
236
|
+
minimal: null,
|
|
237
|
+
low: null,
|
|
238
|
+
medium: "medium",
|
|
239
|
+
high: null,
|
|
240
|
+
xhigh: null,
|
|
241
|
+
},
|
|
242
|
+
compat: {
|
|
243
|
+
supportsReasoningEffort: true,
|
|
244
|
+
},
|
|
245
|
+
input: ["text", "image"],
|
|
246
|
+
cost: {
|
|
247
|
+
input: 0.45,
|
|
248
|
+
output: 3.6,
|
|
249
|
+
cacheRead: 0.45,
|
|
250
|
+
cacheWrite: 0,
|
|
251
|
+
},
|
|
252
|
+
contextWindow: 262144,
|
|
253
|
+
maxTokens: 65536,
|
|
254
|
+
},
|
|
282
255
|
// API: hf:MiniMaxAI/MiniMax-M2.5 → ctx=191488, out=65536
|
|
283
256
|
{
|
|
284
257
|
id: "hf:MiniMaxAI/MiniMax-M2.5",
|
|
@@ -3,16 +3,11 @@ import { writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
|
|
6
|
-
import type {
|
|
7
|
-
AgentToolResult,
|
|
8
|
-
ExtensionAPI,
|
|
9
|
-
ExtensionContext,
|
|
10
|
-
Theme,
|
|
11
|
-
ToolRenderResultOptions,
|
|
12
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
7
|
import {
|
|
14
8
|
DEFAULT_MAX_BYTES,
|
|
15
9
|
DEFAULT_MAX_LINES,
|
|
10
|
+
defineTool,
|
|
16
11
|
formatSize,
|
|
17
12
|
keyHint,
|
|
18
13
|
truncateHead,
|
|
@@ -60,261 +55,248 @@ const SearchParams = Type.Object({
|
|
|
60
55
|
|
|
61
56
|
type SearchParamsType = Static<typeof SearchParams>;
|
|
62
57
|
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
58
|
+
export const syntheticWebSearchTool = defineTool({
|
|
59
|
+
name: SYNTHETIC_WEB_SEARCH_TOOL,
|
|
60
|
+
label: "Synthetic: Web Search",
|
|
61
|
+
description: `Search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates. Use for finding documentation, articles, recent information, or any web content. Results are fresh and not cached by Synthetic. Results are truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first). If truncated, full output is saved to a temp file.`,
|
|
62
|
+
promptSnippet: "Search the web using Synthetic's zero-data-retention API",
|
|
63
|
+
promptGuidelines: [
|
|
64
|
+
"Use synthetic_web_search for finding documentation, articles, recent information, or any web content.",
|
|
65
|
+
"Write specific queries with names, dates, versions, or locations for synthetic_web_search.",
|
|
66
|
+
"synthetic_web_search results are fresh and not cached by Synthetic.",
|
|
67
|
+
],
|
|
68
|
+
parameters: SearchParams,
|
|
69
|
+
|
|
70
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
71
|
+
onUpdate?.({
|
|
72
|
+
content: [{ type: "text", text: "Searching..." }],
|
|
73
|
+
details: { query: params.query },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!configLoader.getConfig().webSearch) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await fetch("https://api.synthetic.new/v2/search", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${apiKey}`,
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({ query: params.query }),
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text();
|
|
101
|
+
throw new Error(`Search API error: ${response.status} ${errorText}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let data: SyntheticSearchResponse;
|
|
105
|
+
try {
|
|
106
|
+
data = await response.json();
|
|
107
|
+
} catch (parseError) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
parseError instanceof Error
|
|
110
|
+
? `Failed to parse search results: ${parseError.message}`
|
|
111
|
+
: "Failed to parse search results",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let content = `Found ${data.results.length} result(s):\n\n`;
|
|
116
|
+
const resultDetails: WebSearchResultDetails[] = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
119
|
+
const result = data.results[i];
|
|
120
|
+
const slug = result.title
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
123
|
+
.replace(/(^-|-$)/g, "")
|
|
124
|
+
.slice(0, 40);
|
|
125
|
+
const truncation = truncateHead(result.text, {
|
|
126
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
127
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
88
128
|
});
|
|
89
129
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
130
|
+
let preview = truncation.content;
|
|
131
|
+
let tempFilePath: string | undefined;
|
|
95
132
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
133
|
+
if (truncation.truncated) {
|
|
134
|
+
tempFilePath = join(
|
|
135
|
+
tmpdir(),
|
|
136
|
+
`pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
|
|
100
137
|
);
|
|
138
|
+
await writeFile(tempFilePath, result.text, "utf8");
|
|
139
|
+
preview += `\n\n[Result truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full result: ${tempFilePath}]`;
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
content += `## ${result.title}\n`;
|
|
143
|
+
content += `URL: ${result.url}\n`;
|
|
144
|
+
content += `Published: ${result.published}\n`;
|
|
145
|
+
content += `\n${preview}\n`;
|
|
146
|
+
content += "\n---\n\n";
|
|
147
|
+
|
|
148
|
+
resultDetails.push({
|
|
149
|
+
title: result.title,
|
|
150
|
+
url: result.url,
|
|
151
|
+
published: result.published,
|
|
152
|
+
truncated: truncation.truncated,
|
|
153
|
+
tempFilePath,
|
|
154
|
+
totalLines: truncation.totalLines,
|
|
155
|
+
totalBytes: truncation.totalBytes,
|
|
156
|
+
outputLines: truncation.outputLines,
|
|
157
|
+
outputBytes: truncation.outputBytes,
|
|
111
158
|
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: content }],
|
|
163
|
+
details: {
|
|
164
|
+
results: resultDetails,
|
|
165
|
+
query: params.query,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
renderCall(args: SearchParamsType, theme: Theme) {
|
|
171
|
+
return new ToolCallHeader(
|
|
172
|
+
{
|
|
173
|
+
toolName: "Synthetic: WebSearch",
|
|
174
|
+
mainArg: `"${args.query}"`,
|
|
175
|
+
showColon: true,
|
|
176
|
+
},
|
|
177
|
+
theme,
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
renderResult(result, options, theme: Theme) {
|
|
182
|
+
const { expanded, isPartial } = options;
|
|
183
|
+
|
|
184
|
+
if (isPartial) {
|
|
185
|
+
return new Text(
|
|
186
|
+
theme.fg("muted", "Synthetic: WebSearch: fetching..."),
|
|
187
|
+
0,
|
|
188
|
+
0,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const details = result.details as WebSearchDetails | undefined;
|
|
193
|
+
const results = details?.results || [];
|
|
194
|
+
const container = new Container();
|
|
195
|
+
|
|
196
|
+
// When the tool throws, the framework calls renderResult with
|
|
197
|
+
// details={} (empty object) and the error message in content.
|
|
198
|
+
// Detect this by checking for missing results in details.
|
|
199
|
+
if (!details?.results) {
|
|
200
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
201
|
+
const errorMsg =
|
|
202
|
+
(textBlock?.type === "text" && textBlock.text) || "Search failed";
|
|
203
|
+
container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
|
|
204
|
+
return container;
|
|
205
|
+
}
|
|
112
206
|
|
|
113
|
-
|
|
114
|
-
const errorText = await response.text();
|
|
115
|
-
throw new Error(`Search API error: ${response.status} ${errorText}`);
|
|
116
|
-
}
|
|
207
|
+
const hasTruncation = results.some((r) => r.truncated);
|
|
117
208
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
209
|
+
if (results.length === 0) {
|
|
210
|
+
container.addChild(
|
|
211
|
+
new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
|
|
212
|
+
);
|
|
213
|
+
} else if (!expanded) {
|
|
214
|
+
// Collapsed: show result count + first result title
|
|
215
|
+
let text = theme.fg("success", `Found ${results.length} result(s)`);
|
|
216
|
+
if (hasTruncation) {
|
|
217
|
+
text += theme.fg("warning", " (truncated)");
|
|
127
218
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const result = data.results[i];
|
|
134
|
-
const slug = result.title
|
|
135
|
-
.toLowerCase()
|
|
136
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
137
|
-
.replace(/(^-|-$)/g, "")
|
|
138
|
-
.slice(0, 40);
|
|
139
|
-
const truncation = truncateHead(result.text, {
|
|
140
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
141
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
let preview = truncation.content;
|
|
145
|
-
let tempFilePath: string | undefined;
|
|
146
|
-
|
|
147
|
-
if (truncation.truncated) {
|
|
148
|
-
tempFilePath = join(
|
|
149
|
-
tmpdir(),
|
|
150
|
-
`pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
|
|
151
|
-
);
|
|
152
|
-
await writeFile(tempFilePath, result.text, "utf8");
|
|
153
|
-
preview += `\n\n[Result truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full result: ${tempFilePath}]`;
|
|
219
|
+
const first = results[0];
|
|
220
|
+
if (first) {
|
|
221
|
+
text += `\n ${theme.fg("dim", first.title)}`;
|
|
222
|
+
if (results.length > 1) {
|
|
223
|
+
text += theme.fg("dim", ` (+${results.length - 1} more)`);
|
|
154
224
|
}
|
|
155
|
-
|
|
156
|
-
content += `## ${result.title}\n`;
|
|
157
|
-
content += `URL: ${result.url}\n`;
|
|
158
|
-
content += `Published: ${result.published}\n`;
|
|
159
|
-
content += `\n${preview}\n`;
|
|
160
|
-
content += "\n---\n\n";
|
|
161
|
-
|
|
162
|
-
resultDetails.push({
|
|
163
|
-
title: result.title,
|
|
164
|
-
url: result.url,
|
|
165
|
-
published: result.published,
|
|
166
|
-
truncated: truncation.truncated,
|
|
167
|
-
tempFilePath,
|
|
168
|
-
totalLines: truncation.totalLines,
|
|
169
|
-
totalBytes: truncation.totalBytes,
|
|
170
|
-
outputLines: truncation.outputLines,
|
|
171
|
-
outputBytes: truncation.outputBytes,
|
|
172
|
-
});
|
|
173
225
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
renderCall(args: SearchParamsType, theme: Theme) {
|
|
185
|
-
return new ToolCallHeader(
|
|
186
|
-
{
|
|
187
|
-
toolName: "Synthetic: WebSearch",
|
|
188
|
-
mainArg: `"${args.query}"`,
|
|
189
|
-
showColon: true,
|
|
190
|
-
},
|
|
191
|
-
theme,
|
|
192
|
-
);
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
renderResult(
|
|
196
|
-
result: AgentToolResult<WebSearchDetails>,
|
|
197
|
-
options: ToolRenderResultOptions,
|
|
198
|
-
theme: Theme,
|
|
199
|
-
) {
|
|
200
|
-
const { expanded, isPartial } = options;
|
|
201
|
-
|
|
202
|
-
if (isPartial) {
|
|
203
|
-
return new Text(
|
|
204
|
-
theme.fg("muted", "Synthetic: WebSearch: fetching..."),
|
|
226
|
+
text += theme.fg("muted", ` ${keyHint("app.tools.expand", "to expand")}`);
|
|
227
|
+
container.addChild(new Text(text, 0, 0));
|
|
228
|
+
} else {
|
|
229
|
+
// Expanded: show each result with title, URL, date, and snippet
|
|
230
|
+
container.addChild(
|
|
231
|
+
new Text(
|
|
232
|
+
theme.fg("success", `Found ${results.length} result(s)`),
|
|
205
233
|
0,
|
|
206
234
|
0,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const details = result.details;
|
|
211
|
-
const results = details?.results || [];
|
|
212
|
-
const container = new Container();
|
|
213
|
-
|
|
214
|
-
// When the tool throws, the framework calls renderResult with
|
|
215
|
-
// details={} (empty object) and the error message in content.
|
|
216
|
-
// Detect this by checking for missing results in details.
|
|
217
|
-
if (!details?.results) {
|
|
218
|
-
const textBlock = result.content.find((c) => c.type === "text");
|
|
219
|
-
const errorMsg =
|
|
220
|
-
(textBlock?.type === "text" && textBlock.text) || "Search failed";
|
|
221
|
-
container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
|
|
222
|
-
return container;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const hasTruncation = results.some((r) => r.truncated);
|
|
235
|
+
),
|
|
236
|
+
);
|
|
226
237
|
|
|
227
|
-
|
|
228
|
-
container.addChild(
|
|
229
|
-
new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
|
|
230
|
-
);
|
|
231
|
-
} else if (!expanded) {
|
|
232
|
-
// Collapsed: show result count + first result title
|
|
233
|
-
let text = theme.fg("success", `Found ${results.length} result(s)`);
|
|
234
|
-
if (hasTruncation) {
|
|
235
|
-
text += theme.fg("warning", " (truncated)");
|
|
236
|
-
}
|
|
237
|
-
const first = results[0];
|
|
238
|
-
if (first) {
|
|
239
|
-
text += `\n ${theme.fg("dim", first.title)}`;
|
|
240
|
-
if (results.length > 1) {
|
|
241
|
-
text += theme.fg("dim", ` (+${results.length - 1} more)`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
text += theme.fg(
|
|
245
|
-
"muted",
|
|
246
|
-
` ${keyHint("app.tools.expand", "to expand")}`,
|
|
247
|
-
);
|
|
248
|
-
container.addChild(new Text(text, 0, 0));
|
|
249
|
-
} else {
|
|
250
|
-
// Expanded: show each result with title, URL, date, and snippet
|
|
238
|
+
for (const r of results) {
|
|
239
|
+
container.addChild(new Text("", 0, 0));
|
|
251
240
|
container.addChild(
|
|
252
241
|
new Text(
|
|
253
|
-
theme.fg("
|
|
242
|
+
`${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
|
|
254
243
|
0,
|
|
255
244
|
0,
|
|
256
245
|
),
|
|
257
246
|
);
|
|
247
|
+
container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
|
|
248
|
+
if (r.published) {
|
|
249
|
+
container.addChild(
|
|
250
|
+
new Text(
|
|
251
|
+
` ${theme.fg("muted", `Published: ${r.published}`)}`,
|
|
252
|
+
0,
|
|
253
|
+
0,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
container.addChild(new Text("", 0, 0));
|
|
258
|
+
if (r.truncated) {
|
|
261
259
|
container.addChild(
|
|
262
260
|
new Text(
|
|
263
|
-
|
|
261
|
+
` ${theme.fg("warning", `Truncated: ${r.outputLines} of ${r.totalLines} lines (${formatSize(r.outputBytes)} of ${formatSize(r.totalBytes)}). Full content: ${r.tempFilePath}`)}`,
|
|
264
262
|
0,
|
|
265
263
|
0,
|
|
266
264
|
),
|
|
267
265
|
);
|
|
268
|
-
container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
|
|
269
|
-
if (r.published) {
|
|
270
|
-
container.addChild(
|
|
271
|
-
new Text(
|
|
272
|
-
` ${theme.fg("muted", `Published: ${r.published}`)}`,
|
|
273
|
-
0,
|
|
274
|
-
0,
|
|
275
|
-
),
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (r.truncated) {
|
|
280
|
-
container.addChild(
|
|
281
|
-
new Text(
|
|
282
|
-
` ${theme.fg("warning", `Truncated: ${r.outputLines} of ${r.totalLines} lines (${formatSize(r.outputBytes)} of ${formatSize(r.totalBytes)}). Full content: ${r.tempFilePath}`)}`,
|
|
283
|
-
0,
|
|
284
|
-
0,
|
|
285
|
-
),
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
266
|
}
|
|
289
267
|
}
|
|
290
|
-
|
|
291
|
-
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const footerItems: { label: string; value: string }[] = [];
|
|
271
|
+
footerItems.push({
|
|
272
|
+
label: "results",
|
|
273
|
+
value: `${results.length} result(s)`,
|
|
274
|
+
});
|
|
275
|
+
if (hasTruncation) {
|
|
276
|
+
const truncatedCount = results.filter((r) => r.truncated).length;
|
|
292
277
|
footerItems.push({
|
|
293
|
-
label: "
|
|
294
|
-
value: `${
|
|
278
|
+
label: "truncated",
|
|
279
|
+
value: `${truncatedCount}`,
|
|
295
280
|
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}),
|
|
315
|
-
);
|
|
281
|
+
}
|
|
282
|
+
if (!expanded) {
|
|
283
|
+
footerItems.push({
|
|
284
|
+
label: "",
|
|
285
|
+
value: keyHint("app.tools.expand", "to expand"),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
container.addChild(new Text("", 0, 0));
|
|
289
|
+
container.addChild(
|
|
290
|
+
new ToolFooter(theme, {
|
|
291
|
+
items: footerItems,
|
|
292
|
+
separator: " | ",
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return container;
|
|
297
|
+
},
|
|
298
|
+
});
|
|
316
299
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
300
|
+
export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
301
|
+
pi.registerTool(syntheticWebSearchTool);
|
|
320
302
|
}
|