@alasano/pi-exa 0.0.1

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.
@@ -0,0 +1,328 @@
1
+ import { defineTool } from '@earendil-works/pi-coding-agent';
2
+ import type { AgentToolUpdateCallback } from '@earendil-works/pi-coding-agent';
3
+ import type { Static } from '@sinclair/typebox';
4
+ import {
5
+ createAgentRun,
6
+ getAgentRun,
7
+ getRunIdFromAgentEvent,
8
+ isTerminalAgentStatus,
9
+ pollAgentRunUntilFinished,
10
+ streamAgentRunEvents,
11
+ type ExaAgentCreateRequest,
12
+ } from '../agent';
13
+ import type { AgentRunTracker } from '../agent-tracker';
14
+ import { countAgentSources, formatAgentRunResponse } from '../format';
15
+ import { truncateToolOutput } from '../output';
16
+ import {
17
+ buildAgentRunPreview,
18
+ metadataFromArgs,
19
+ renderExaCall,
20
+ renderExaResult,
21
+ sendProgress,
22
+ } from '../render';
23
+ import { WebAgentParamsSchema } from '../schemas';
24
+ import type { ExaAgentEvent, ExaAgentRun, ExaToolDetails, PreviewDetails } from '../types';
25
+ import { compactObject } from '../util';
26
+ import { withExaApiKey, errorResult } from './helpers';
27
+
28
+ type WebAgentParams = Static<typeof WebAgentParamsSchema>;
29
+
30
+ const DEFAULT_AGENT_POLL_INTERVAL_MS = 4000;
31
+ const DEFAULT_AGENT_TIMEOUT_MS = 10 * 60 * 1000;
32
+
33
+ export interface WebAgentRequest extends ExaAgentCreateRequest {}
34
+
35
+ type WebAgentDetails =
36
+ | (Partial<ExaToolDetails<ExaAgentRun, WebAgentRequest>> & {
37
+ mode: 'wait' | 'background';
38
+ monitor?: 'stream' | 'poll';
39
+ timedOut?: boolean;
40
+ events?: string[];
41
+ })
42
+ | undefined;
43
+
44
+ function createTimeoutSignal(parentSignal: AbortSignal | undefined, timeoutMs: number) {
45
+ const controller = new AbortController();
46
+ let timedOut = false;
47
+
48
+ const timeout = setTimeout(() => {
49
+ timedOut = true;
50
+ controller.abort(new Error(`Timed out after ${timeoutMs}ms.`));
51
+ }, timeoutMs);
52
+
53
+ const abortFromParent = () => {
54
+ controller.abort(parentSignal?.reason ?? new Error('Operation aborted.'));
55
+ };
56
+ parentSignal?.addEventListener('abort', abortFromParent, { once: true });
57
+
58
+ return {
59
+ signal: controller.signal,
60
+ isTimedOut: () => timedOut,
61
+ dispose: () => {
62
+ clearTimeout(timeout);
63
+ parentSignal?.removeEventListener('abort', abortFromParent);
64
+ },
65
+ };
66
+ }
67
+
68
+ function agentEventProgress(event: ExaAgentEvent): string {
69
+ const runId = getRunIdFromAgentEvent(event);
70
+ const status = typeof event.data.status === 'string' ? event.data.status : undefined;
71
+ return [
72
+ `Exa Agent event: ${event.event}`,
73
+ runId ? `run=${runId}` : undefined,
74
+ status ? `status=${status}` : undefined,
75
+ ]
76
+ .filter(Boolean)
77
+ .join(' | ');
78
+ }
79
+
80
+ function sendAgentTimelineUpdate(
81
+ onUpdate: AgentToolUpdateCallback<WebAgentDetails> | undefined,
82
+ events: string[],
83
+ summary = 'running',
84
+ ) {
85
+ const preview: PreviewDetails = {
86
+ kind: 'agent',
87
+ summary,
88
+ lines: events.slice(-6),
89
+ expandedLines: events,
90
+ };
91
+
92
+ onUpdate?.({
93
+ content: [{ type: 'text', text: events[events.length - 1] || 'Running Exa Agent...' }],
94
+ details: {
95
+ endpoint: '/agent/runs',
96
+ mode: 'wait',
97
+ monitor: 'stream',
98
+ events,
99
+ preview,
100
+ },
101
+ });
102
+ }
103
+
104
+ function formatElapsedTime(ms: number): string {
105
+ const seconds = Math.max(0, Math.floor(ms / 1000));
106
+ if (seconds < 60) return `${seconds}s`;
107
+ const minutes = Math.floor(seconds / 60);
108
+ const remainingSeconds = seconds % 60;
109
+ return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`;
110
+ }
111
+
112
+ function sendAgentPollingUpdate(
113
+ onUpdate: AgentToolUpdateCallback<WebAgentDetails> | undefined,
114
+ run: ExaAgentRun,
115
+ startedAt: number,
116
+ ) {
117
+ const elapsed = formatElapsedTime(Date.now() - startedAt);
118
+ const lines = [`Run ID: ${run.id}`, `Status: ${run.status}`];
119
+ const preview: PreviewDetails = {
120
+ kind: 'agent',
121
+ summary: `${run.status} | elapsed ${elapsed}`,
122
+ lines,
123
+ expandedLines: lines,
124
+ };
125
+
126
+ onUpdate?.({
127
+ content: [{ type: 'text', text: `Exa Agent run ${run.id} is ${run.status}; polling...` }],
128
+ details: {
129
+ endpoint: '/agent/runs',
130
+ response: run,
131
+ requestId: run.id,
132
+ mode: 'wait',
133
+ monitor: 'poll',
134
+ preview,
135
+ },
136
+ });
137
+ }
138
+
139
+ function remainingTimeout(startedAt: number, timeoutMs: number): number {
140
+ return Math.max(1000, timeoutMs - (Date.now() - startedAt));
141
+ }
142
+
143
+ export function buildWebAgentRequest(params: WebAgentParams): WebAgentRequest {
144
+ return compactObject({
145
+ query: params.query,
146
+ systemPrompt: params.systemPrompt,
147
+ input: params.input,
148
+ outputSchema: params.outputSchema,
149
+ effort: params.effort,
150
+ previousRunId: params.previousRunId,
151
+ metadata: params.metadata,
152
+ }) as WebAgentRequest;
153
+ }
154
+
155
+ async function waitWithPolling(
156
+ apiKey: string,
157
+ request: WebAgentRequest,
158
+ params: WebAgentParams,
159
+ signal: AbortSignal | undefined,
160
+ onUpdate: AgentToolUpdateCallback<WebAgentDetails> | undefined,
161
+ ): Promise<{ run: ExaAgentRun; timedOut: boolean }> {
162
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_AGENT_POLL_INTERVAL_MS;
163
+ const timeoutMs = params.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
164
+ const startedAt = Date.now();
165
+
166
+ sendProgress(onUpdate, 'Starting Exa Agent run...');
167
+ const initialRun = await createAgentRun(apiKey, request, signal);
168
+ sendAgentPollingUpdate(onUpdate, initialRun, startedAt);
169
+
170
+ if (isTerminalAgentStatus(initialRun.status)) return { run: initialRun, timedOut: false };
171
+
172
+ return pollAgentRunUntilFinished(apiKey, initialRun.id, {
173
+ pollIntervalMs,
174
+ timeoutMs,
175
+ signal,
176
+ onPoll: (run) => {
177
+ sendAgentPollingUpdate(onUpdate, run, startedAt);
178
+ },
179
+ });
180
+ }
181
+
182
+ async function waitWithStreaming(
183
+ apiKey: string,
184
+ request: WebAgentRequest,
185
+ params: WebAgentParams,
186
+ signal: AbortSignal | undefined,
187
+ onUpdate: AgentToolUpdateCallback<WebAgentDetails> | undefined,
188
+ ): Promise<{ run: ExaAgentRun; timedOut: boolean }> {
189
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_AGENT_POLL_INTERVAL_MS;
190
+ const timeoutMs = params.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
191
+ const startedAt = Date.now();
192
+ const timeoutSignal = createTimeoutSignal(signal, timeoutMs);
193
+ let runId: string | undefined;
194
+ let terminalFromStream = false;
195
+ const events: string[] = [];
196
+
197
+ try {
198
+ sendProgress(onUpdate, 'Starting Exa Agent run with streaming events...');
199
+ for await (const event of streamAgentRunEvents(apiKey, request, timeoutSignal.signal)) {
200
+ runId = getRunIdFromAgentEvent(event) ?? runId;
201
+ const status = typeof event.data.status === 'string' ? event.data.status : undefined;
202
+ terminalFromStream = terminalFromStream || isTerminalAgentStatus(status);
203
+ const line = agentEventProgress(event);
204
+ events.push(line);
205
+ sendAgentTimelineUpdate(onUpdate, events, status || 'running');
206
+ }
207
+ } catch (error) {
208
+ if (!runId || (!timeoutSignal.isTimedOut() && signal?.aborted)) throw error;
209
+ if (timeoutSignal.isTimedOut()) {
210
+ const run = await getAgentRun(apiKey, runId, signal);
211
+ return { run, timedOut: true };
212
+ }
213
+
214
+ sendProgress(onUpdate, `Exa Agent stream ended; polling run ${runId}...`);
215
+ return pollAgentRunUntilFinished(apiKey, runId, {
216
+ pollIntervalMs,
217
+ timeoutMs: remainingTimeout(startedAt, timeoutMs),
218
+ signal,
219
+ });
220
+ } finally {
221
+ timeoutSignal.dispose();
222
+ }
223
+
224
+ if (!runId) throw new Error('Exa Agent stream ended before a run ID was returned.');
225
+
226
+ if (terminalFromStream) {
227
+ return { run: await getAgentRun(apiKey, runId, signal), timedOut: false };
228
+ }
229
+
230
+ return pollAgentRunUntilFinished(apiKey, runId, {
231
+ pollIntervalMs,
232
+ timeoutMs: remainingTimeout(startedAt, timeoutMs),
233
+ signal,
234
+ });
235
+ }
236
+
237
+ export function createWebAgentTool(tracker: AgentRunTracker) {
238
+ return defineTool<typeof WebAgentParamsSchema, WebAgentDetails>({
239
+ name: 'web_agent_exa',
240
+ label: 'Exa Agent',
241
+ description:
242
+ 'Create an Exa Agent run for deep web research, list-building, enrichment, or structured multi-hop workflows. Supports foreground wait and background tracking.',
243
+ promptSnippet: 'Run an Exa Agent research/list-building/enrichment workflow',
244
+ promptGuidelines: [
245
+ 'Use web_agent_exa when the task needs multi-hop research, list building, row enrichment, or structured fields across many sources.',
246
+ 'Prefer normal search/answer/fetch tools for simple lookup. Use Agent only when it is clearly the better tool or recommend it to the user when the task fits.',
247
+ 'For structured output, provide outputSchema and bound arrays with maxItems so scope and contact-enrichment cost stay predictable.',
248
+ 'Use mode=background for long-running or expensive Agent work when the user does not need the result in the current turn.',
249
+ 'Completed foreground wait runs return the full Agent result payload, including structured output and grounding.',
250
+ 'After starting a background run, do not poll with web_agent_get_exa or web_agent_events_exa unless the user explicitly asks; pi-exa tracks the run and sends a compact follow-up when it completes.',
251
+ 'Treat background follow-ups as completion notices. Call web_agent_get_exa before answering with detailed findings from a background run.',
252
+ ],
253
+ parameters: WebAgentParamsSchema,
254
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
255
+ const request = buildWebAgentRequest(params);
256
+ const mode = params.mode ?? 'wait';
257
+ const monitor = params.monitor ?? 'stream';
258
+
259
+ try {
260
+ return await withExaApiKey(ctx, async (apiKey) => {
261
+ let run: ExaAgentRun;
262
+ let timedOut = false;
263
+
264
+ if (mode === 'background') {
265
+ sendProgress(onUpdate, 'Starting Exa Agent run in background...');
266
+ run = await createAgentRun(apiKey, request, signal);
267
+ await tracker.track(run, { pollIntervalMs: params.pollIntervalMs }, ctx);
268
+ } else if (monitor === 'poll') {
269
+ const result = await waitWithPolling(apiKey, request, params, signal, onUpdate);
270
+ run = result.run;
271
+ timedOut = result.timedOut;
272
+ } else {
273
+ const result = await waitWithStreaming(apiKey, request, params, signal, onUpdate);
274
+ run = result.run;
275
+ timedOut = result.timedOut;
276
+ }
277
+
278
+ if (timedOut) await tracker.track(run, { pollIntervalMs: params.pollIntervalMs }, ctx);
279
+
280
+ const output = await truncateToolOutput(
281
+ formatAgentRunResponse(run, { timedOut, background: mode === 'background' }),
282
+ 'web-agent-exa',
283
+ 'Use web_agent_get_exa for the run ID, simplify outputSchema, reduce maxItems, or use mode=background. ',
284
+ { maxLines: Number.MAX_SAFE_INTEGER, maxBytes: Number.MAX_SAFE_INTEGER },
285
+ );
286
+
287
+ return {
288
+ content: [{ type: 'text', text: output.text }],
289
+ details: {
290
+ endpoint: '/agent/runs',
291
+ request,
292
+ response: run,
293
+ requestId: run.id,
294
+ count: countAgentSources(run.output?.grounding),
295
+ costDollars: run.costDollars,
296
+ preview: buildAgentRunPreview(run, output, {
297
+ timedOut,
298
+ background: mode === 'background',
299
+ }),
300
+ truncated: output.truncation.truncated,
301
+ truncation: output.truncation,
302
+ fullOutputPath: output.fullOutputPath,
303
+ mode,
304
+ monitor: mode === 'wait' ? monitor : undefined,
305
+ timedOut,
306
+ },
307
+ isError: run.status === 'failed',
308
+ };
309
+ });
310
+ } catch (error) {
311
+ return errorResult(error);
312
+ }
313
+ },
314
+ renderCall(args, theme) {
315
+ const metadataArgs = args.mode === 'background' ? { ...args, monitor: undefined } : args;
316
+
317
+ return renderExaCall(
318
+ 'web_agent_exa',
319
+ `"${args.query}"`,
320
+ theme,
321
+ metadataFromArgs(metadataArgs, ['mode', 'monitor', 'effort', 'previousRunId']),
322
+ );
323
+ },
324
+ renderResult(result, options, theme) {
325
+ return renderExaResult(result, options, theme, 'Running Exa Agent...');
326
+ },
327
+ });
328
+ }
@@ -0,0 +1,92 @@
1
+ import { defineTool } from '@earendil-works/pi-coding-agent';
2
+ import type { Static } from '@sinclair/typebox';
3
+ import { exaPost } from '../client';
4
+ import { formatAnswerResponse } from '../format';
5
+ import { truncateToolOutput } from '../output';
6
+ import {
7
+ buildAnswerPreview,
8
+ metadataFromArgs,
9
+ renderExaCall,
10
+ renderExaResult,
11
+ sendProgress,
12
+ } from '../render';
13
+ import { WebAnswerParamsSchema } from '../schemas';
14
+ import type { ExaAnswerResponse, ExaToolDetails, JsonObject } from '../types';
15
+ import { compactObject } from '../util';
16
+ import { withExaApiKey, errorResult } from './helpers';
17
+
18
+ type WebAnswerParams = Static<typeof WebAnswerParamsSchema>;
19
+
20
+ export interface WebAnswerRequest {
21
+ query: string;
22
+ text?: boolean;
23
+ outputSchema?: JsonObject;
24
+ }
25
+
26
+ export function buildWebAnswerRequest(params: WebAnswerParams): WebAnswerRequest {
27
+ return compactObject({
28
+ query: params.query,
29
+ text: params.text,
30
+ outputSchema: params.outputSchema,
31
+ }) as WebAnswerRequest;
32
+ }
33
+
34
+ export function createWebAnswerTool() {
35
+ return defineTool<
36
+ typeof WebAnswerParamsSchema,
37
+ ExaToolDetails<ExaAnswerResponse, WebAnswerRequest> | undefined
38
+ >({
39
+ name: 'web_answer_exa',
40
+ label: 'Exa Web Answer',
41
+ description:
42
+ 'Ask Exa Answer for a direct sourced answer. Use when a generated answer with citations is better than a search result list.',
43
+ promptSnippet: 'Ask Exa for a sourced answer',
44
+ promptGuidelines: [
45
+ 'Use web_answer_exa when the user wants a direct sourced answer; use web_search_exa when the agent needs to inspect or compare sources itself.',
46
+ 'Do not set web_answer_exa text=true unless citation page text is necessary.',
47
+ ],
48
+ parameters: WebAnswerParamsSchema,
49
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
50
+ const request = buildWebAnswerRequest(params);
51
+ try {
52
+ return await withExaApiKey(ctx, async (apiKey) => {
53
+ sendProgress(onUpdate, 'Getting grounded answer from Exa...');
54
+ const response = await exaPost<ExaAnswerResponse>(apiKey, '/answer', request, signal);
55
+ const output = await truncateToolOutput(
56
+ formatAnswerResponse(response),
57
+ 'web-answer-exa',
58
+ 'Set text=false or ask a narrower question. ',
59
+ );
60
+ return {
61
+ content: [{ type: 'text', text: output.text }],
62
+ details: {
63
+ endpoint: '/answer',
64
+ request,
65
+ response,
66
+ requestId: response.requestId,
67
+ count: response.citations?.length ?? 0,
68
+ costDollars: response.costDollars,
69
+ preview: buildAnswerPreview(response, output),
70
+ truncated: output.truncation.truncated,
71
+ truncation: output.truncation,
72
+ fullOutputPath: output.fullOutputPath,
73
+ },
74
+ };
75
+ });
76
+ } catch (error) {
77
+ return errorResult(error);
78
+ }
79
+ },
80
+ renderCall(args, theme) {
81
+ return renderExaCall(
82
+ 'web_answer_exa',
83
+ `"${args.query}"`,
84
+ theme,
85
+ metadataFromArgs(args, ['text']),
86
+ );
87
+ },
88
+ renderResult(result, options, theme) {
89
+ return renderExaResult(result, options, theme, 'Getting grounded answer from Exa...');
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,94 @@
1
+ import { defineTool } from '@earendil-works/pi-coding-agent';
2
+ import type { Static } from '@sinclair/typebox';
3
+ import { exaPost } from '../client';
4
+ import { formatContentsResponse, hasContentsErrors } from '../format';
5
+ import { truncateToolOutput } from '../output';
6
+ import {
7
+ buildContentsPreview,
8
+ metadataFromArgs,
9
+ renderExaCall,
10
+ renderExaResult,
11
+ sendProgress,
12
+ } from '../render';
13
+ import { WebFetchParamsSchema } from '../schemas';
14
+ import type { ExaContentsResponse, ExaToolDetails } from '../types';
15
+ import { withExaApiKey, errorResult } from './helpers';
16
+
17
+ type WebFetchParams = Static<typeof WebFetchParamsSchema>;
18
+
19
+ export interface WebFetchRequest {
20
+ urls: string[];
21
+ text: {
22
+ maxCharacters: number;
23
+ };
24
+ }
25
+
26
+ export function buildWebFetchRequest(params: WebFetchParams): WebFetchRequest {
27
+ return {
28
+ urls: params.urls,
29
+ text: {
30
+ maxCharacters: params.maxCharacters ?? 3000,
31
+ },
32
+ };
33
+ }
34
+
35
+ export function createWebFetchTool() {
36
+ return defineTool<
37
+ typeof WebFetchParamsSchema,
38
+ ExaToolDetails<ExaContentsResponse, WebFetchRequest> | undefined
39
+ >({
40
+ name: 'web_fetch_exa',
41
+ label: 'Exa Web Fetch',
42
+ description:
43
+ 'Read clean markdown content from known URLs with Exa. Use after search when selected URLs need fuller content.',
44
+ promptSnippet: 'Fetch clean markdown from known URLs with Exa',
45
+ promptGuidelines: [
46
+ 'Use web_fetch_exa only after selecting promising URLs; avoid fetching every search result.',
47
+ 'Keep web_fetch_exa maxCharacters as small as the task allows.',
48
+ ],
49
+ parameters: WebFetchParamsSchema,
50
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
51
+ const request = buildWebFetchRequest(params);
52
+ try {
53
+ return await withExaApiKey(ctx, async (apiKey) => {
54
+ sendProgress(onUpdate, 'Fetching contents from Exa...');
55
+ const response = await exaPost<ExaContentsResponse>(apiKey, '/contents', request, signal);
56
+ const output = await truncateToolOutput(
57
+ formatContentsResponse(response),
58
+ 'web-fetch-exa',
59
+ 'Fetch fewer URLs or reduce maxCharacters. ',
60
+ );
61
+ return {
62
+ content: [{ type: 'text', text: output.text }],
63
+ details: {
64
+ endpoint: '/contents',
65
+ request,
66
+ response,
67
+ requestId: response.requestId,
68
+ count: response.results?.length ?? 0,
69
+ costDollars: response.costDollars,
70
+ preview: buildContentsPreview(response, params.urls.length, output),
71
+ truncated: output.truncation.truncated,
72
+ truncation: output.truncation,
73
+ fullOutputPath: output.fullOutputPath,
74
+ },
75
+ isError: hasContentsErrors(response) && !response.results?.length,
76
+ };
77
+ });
78
+ } catch (error) {
79
+ return errorResult(error);
80
+ }
81
+ },
82
+ renderCall(args, theme) {
83
+ return renderExaCall(
84
+ 'web_fetch_exa',
85
+ `${args.urls.length} URL(s)`,
86
+ theme,
87
+ metadataFromArgs(args, ['maxCharacters']),
88
+ );
89
+ },
90
+ renderResult(result, options, theme) {
91
+ return renderExaResult(result, options, theme, 'Fetching contents from Exa...');
92
+ },
93
+ });
94
+ }
@@ -0,0 +1,164 @@
1
+ import { defineTool } from '@earendil-works/pi-coding-agent';
2
+ import type { Static } from '@sinclair/typebox';
3
+ import { exaPost } from '../client';
4
+ import { formatSearchResponse } from '../format';
5
+ import { truncateToolOutput } from '../output';
6
+ import {
7
+ buildSearchPreview,
8
+ metadataFromArgs,
9
+ renderExaCall,
10
+ renderExaResult,
11
+ sendProgress,
12
+ } from '../render';
13
+ import { WebSearchAdvancedParamsSchema } from '../schemas';
14
+ import type { ExaSearchResponse, ExaToolDetails, JsonObject, SearchType } from '../types';
15
+ import { compactObject } from '../util';
16
+ import { withExaApiKey, errorResult } from './helpers';
17
+
18
+ type WebSearchAdvancedParams = Static<typeof WebSearchAdvancedParamsSchema>;
19
+
20
+ type HighlightsRequest = true | { maxCharacters?: number; query?: string };
21
+
22
+ export interface WebSearchAdvancedRequest {
23
+ query: string;
24
+ type: SearchType;
25
+ numResults: number;
26
+ contents: JsonObject;
27
+ category?: string;
28
+ includeDomains?: string[];
29
+ excludeDomains?: string[];
30
+ startPublishedDate?: string;
31
+ endPublishedDate?: string;
32
+ startCrawlDate?: string;
33
+ endCrawlDate?: string;
34
+ includeText?: string[];
35
+ excludeText?: string[];
36
+ userLocation?: string;
37
+ moderation?: boolean;
38
+ additionalQueries?: string[];
39
+ }
40
+
41
+ function buildHighlights(params: WebSearchAdvancedParams): HighlightsRequest | undefined {
42
+ if (!params.enableHighlights) return undefined;
43
+
44
+ if (params.highlightsMaxCharacters !== undefined || params.highlightsQuery !== undefined) {
45
+ return compactObject({
46
+ maxCharacters: params.highlightsMaxCharacters,
47
+ query: params.highlightsQuery,
48
+ });
49
+ }
50
+
51
+ return true;
52
+ }
53
+
54
+ function buildContents(params: WebSearchAdvancedParams): JsonObject {
55
+ return compactObject({
56
+ text:
57
+ params.textMaxCharacters !== undefined ? { maxCharacters: params.textMaxCharacters } : true,
58
+ context:
59
+ params.contextMaxCharacters !== undefined
60
+ ? { maxCharacters: params.contextMaxCharacters }
61
+ : undefined,
62
+ summary: params.enableSummary
63
+ ? params.summaryQuery
64
+ ? { query: params.summaryQuery }
65
+ : true
66
+ : undefined,
67
+ highlights: buildHighlights(params),
68
+ maxAgeHours: params.maxAgeHours,
69
+ livecrawlTimeout: params.livecrawlTimeout,
70
+ subpages: params.subpages,
71
+ subpageTarget: params.subpageTarget,
72
+ });
73
+ }
74
+
75
+ export function buildWebSearchAdvancedRequest(
76
+ params: WebSearchAdvancedParams,
77
+ ): WebSearchAdvancedRequest {
78
+ return compactObject({
79
+ query: params.query,
80
+ type: params.type ?? 'auto',
81
+ numResults: params.numResults ?? 10,
82
+ contents: buildContents(params),
83
+ category: params.category,
84
+ includeDomains: params.includeDomains,
85
+ excludeDomains: params.excludeDomains,
86
+ startPublishedDate: params.startPublishedDate,
87
+ endPublishedDate: params.endPublishedDate,
88
+ startCrawlDate: params.startCrawlDate,
89
+ endCrawlDate: params.endCrawlDate,
90
+ includeText: params.includeText,
91
+ excludeText: params.excludeText,
92
+ userLocation: params.userLocation,
93
+ moderation: params.moderation,
94
+ additionalQueries: params.additionalQueries,
95
+ }) as WebSearchAdvancedRequest;
96
+ }
97
+
98
+ export function createWebSearchAdvancedTool() {
99
+ return defineTool<
100
+ typeof WebSearchAdvancedParamsSchema,
101
+ ExaToolDetails<ExaSearchResponse, WebSearchAdvancedRequest> | undefined
102
+ >({
103
+ name: 'web_search_advanced_exa',
104
+ label: 'Exa Advanced Search',
105
+ description:
106
+ 'Advanced Exa web search with filters, domain restrictions, date ranges, highlights, summaries, freshness, and subpage crawling.',
107
+ promptSnippet: 'Search Exa with filters, dates, domains, and content controls',
108
+ promptGuidelines: [
109
+ 'Use web_search_advanced_exa when the search requires filters, dates, domains, categories, summaries, freshness, or subpages.',
110
+ 'Use textMaxCharacters when the task needs text but must keep each result bounded.',
111
+ ],
112
+ parameters: WebSearchAdvancedParamsSchema,
113
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
114
+ const request = buildWebSearchAdvancedRequest(params);
115
+ try {
116
+ return await withExaApiKey(ctx, async (apiKey) => {
117
+ sendProgress(onUpdate, 'Searching Exa with advanced filters...');
118
+ const response = await exaPost<ExaSearchResponse>(apiKey, '/search', request, signal);
119
+ const output = await truncateToolOutput(
120
+ formatSearchResponse(response),
121
+ 'web-search-advanced-exa',
122
+ 'Reduce numResults, set textMaxCharacters, disable optional summaries/highlights, or narrow filters. ',
123
+ );
124
+ return {
125
+ content: [{ type: 'text', text: output.text }],
126
+ details: {
127
+ endpoint: '/search',
128
+ request,
129
+ response,
130
+ requestId: response.requestId,
131
+ count: response.results?.length ?? 0,
132
+ costDollars: response.costDollars,
133
+ preview: buildSearchPreview(response, output),
134
+ truncated: output.truncation.truncated,
135
+ truncation: output.truncation,
136
+ fullOutputPath: output.fullOutputPath,
137
+ },
138
+ };
139
+ });
140
+ } catch (error) {
141
+ return errorResult(error);
142
+ }
143
+ },
144
+ renderCall(args, theme) {
145
+ return renderExaCall(
146
+ 'web_search_advanced_exa',
147
+ `"${args.query}"`,
148
+ theme,
149
+ metadataFromArgs(args, [
150
+ 'type',
151
+ 'numResults',
152
+ 'category',
153
+ 'textMaxCharacters',
154
+ 'contextMaxCharacters',
155
+ 'enableHighlights',
156
+ 'enableSummary',
157
+ ]),
158
+ );
159
+ },
160
+ renderResult(result, options, theme) {
161
+ return renderExaResult(result, options, theme, 'Searching Exa with advanced filters...');
162
+ },
163
+ });
164
+ }