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