@ct-agents/worker 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ct-agents/worker",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -12,10 +12,10 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "zod": "4.4.3",
15
- "@ct-agents/memory": "0.0.1",
16
- "@ct-agents/prompts": "0.0.1",
17
- "@ct-agents/tools": "0.0.1",
18
- "@ct-agents/protocol": "0.0.1"
15
+ "@ct-agents/memory": "0.1.0",
16
+ "@ct-agents/prompts": "0.1.0",
17
+ "@ct-agents/protocol": "0.1.0",
18
+ "@ct-agents/tools": "0.1.0"
19
19
  },
20
20
  "optionalDependencies": {
21
21
  "dockerode": "4.0.9"
package/src/index.ts CHANGED
@@ -6,7 +6,6 @@ import type {
6
6
  EnvironmentId,
7
7
  EnvironmentExecutionMode,
8
8
  EnvironmentHostedPolicy,
9
- EnvironmentResourceSlotRegistrationDto,
10
9
  ExecutionScope,
11
10
  HarnessId,
12
11
  ResourceSlots,
@@ -27,7 +26,7 @@ import type { MemoryResource, SkillResource } from '@ct-agents/protocol';
27
26
  import { createLoadMemoryToolHandler, createUpdateMemoryToolHandler } from '@ct-agents/memory';
28
27
  import { createLoadSkillToolHandler } from '@ct-agents/prompts';
29
28
  import { createSandboxTools } from '@ct-agents/tools';
30
- import { createAskUserToolHandler } from '@ct-agents/tools/builtin-tools';
29
+ import { createAskUserToolHandler, createWebSearchToolHandler } from '@ct-agents/tools/builtin-tools';
31
30
  import { z } from 'zod';
32
31
  import {
33
32
  createPlatformDatabaseProxyResource,
@@ -44,6 +43,7 @@ export type ResourceOptionsParser = {
44
43
 
45
44
  export type EnvironmentWorkerResourceFactoryContext = {
46
45
  item: ToolWorkItem;
46
+ abortSignal?: AbortSignal;
47
47
  };
48
48
 
49
49
  export type EnvironmentWorkerResourceImplementation<
@@ -88,6 +88,7 @@ export type EnvironmentWorkerInput = {
88
88
  resolveResources?: ResolveResources;
89
89
  resourceImpls?: EnvironmentWorkerResourceImplementations;
90
90
  platformResourceProxy?: EnvironmentWorkerPlatformResourceProxy;
91
+ abortSignal?: AbortSignal;
91
92
  maxBatchSize?: number;
92
93
  harness?: {
93
94
  id: string;
@@ -144,6 +145,7 @@ export function createPlatformBuiltinToolHandlers(): ToolHandler[] {
144
145
  createLoadSkillToolHandler(),
145
146
  createLoadMemoryToolHandler(),
146
147
  createUpdateMemoryToolHandler(),
148
+ createWebSearchToolHandler(),
147
149
  ...createSandboxTools(),
148
150
  ];
149
151
  }
@@ -304,6 +306,7 @@ export class EnvironmentWorker {
304
306
  hostedPolicy: this.input.hostedPolicy,
305
307
  resources,
306
308
  toolCallEvent: buildToolCallEvent(item),
309
+ abortSignal: this.input.abortSignal,
307
310
  };
308
311
  }
309
312
 
@@ -333,7 +336,10 @@ export class EnvironmentWorker {
333
336
  const parsedOptions = implementation.options
334
337
  ? implementation.options.parse(binding.options)
335
338
  : binding.options;
336
- const instance = await implementation.factory(parsedOptions, { item });
339
+ const instance = await implementation.factory(parsedOptions, {
340
+ item,
341
+ ...(this.input.abortSignal ? { abortSignal: this.input.abortSignal } : {}),
342
+ });
337
343
  resources[slotName] = instance;
338
344
  if (hasDispose(instance)) {
339
345
  disposers.push(async () => {
@@ -534,41 +540,6 @@ export type HttpWorkQueueClientInput = {
534
540
  signal?: AbortSignal;
535
541
  };
536
542
 
537
- export type RegisterEnvironmentCatalogInput = {
538
- baseUrl: string;
539
- environmentKey: string;
540
- tools: ToolDescriptor[];
541
- resourceSlots?: EnvironmentResourceSlotRegistrationDto[];
542
- fetchImpl?: typeof fetch;
543
- signal?: AbortSignal;
544
- };
545
-
546
- export async function registerEnvironmentCatalog(input: RegisterEnvironmentCatalogInput): Promise<{
547
- registered: number;
548
- resourceSlots: number;
549
- }> {
550
- const fetchImpl = input.fetchImpl ?? fetch;
551
- const headers = new Headers();
552
- headers.set('x-api-key', input.environmentKey);
553
- headers.set('content-type', 'application/json');
554
- const body = {
555
- tools: input.tools,
556
- ...(input.resourceSlots ? { resourceSlots: input.resourceSlots } : {}),
557
- };
558
- const response = await fetchImpl(`${trimTrailingSlash(input.baseUrl)}/v1/tools/registrations`, {
559
- method: 'POST',
560
- headers,
561
- body: JSON.stringify(body),
562
- signal: input.signal,
563
- });
564
- const data = await parseApiEnvelope(response, '平台工具目录上报失败');
565
- const record = expectRecord(data, '工具目录上报响应缺少 data');
566
- return {
567
- registered: expectNumber(record.registered, 'registered'),
568
- resourceSlots: expectNumber(record.resourceSlots, 'resourceSlots'),
569
- };
570
- }
571
-
572
543
  export function zodObjectToJsonSchema(schema: z.ZodType<unknown>): Record<string, unknown> {
573
544
  const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
574
545
  const normalized = normalizeCatalogJsonSchema(jsonSchema, true);
@@ -1,2 +1,3 @@
1
1
  export * from './database/index.js';
2
2
  export * from './platform-proxy.js';
3
+ export * from './web-search/index.js';
@@ -0,0 +1 @@
1
+ export * from './tavily.js';
@@ -0,0 +1,408 @@
1
+ import { z } from 'zod';
2
+ import type {
3
+ ResourceDefinition,
4
+ WebSearchErrorCode,
5
+ WebSearchInput,
6
+ WebSearchResource,
7
+ WebSearchResult,
8
+ WebSearchWarning,
9
+ } from '@ct-agents/protocol';
10
+ import type {
11
+ EnvironmentWorkerResourceImplementation,
12
+ } from '../../index.js';
13
+
14
+ const TAVILY_SEARCH_ENDPOINT = 'https://api.tavily.com/search';
15
+ const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
16
+ const MAX_SNIPPET_BYTES = 8 * 1024;
17
+ const MAX_CONTENT_BYTES = 64 * 1024;
18
+ const MAX_TOTAL_TEXT_BYTES = 512 * 1024;
19
+ const MAX_TITLE_BYTES = 8 * 1024;
20
+ const MAX_URL_BYTES = 16 * 1024;
21
+ const MAX_PUBLISHED_AT_BYTES = 1024;
22
+ const MAX_REQUEST_ID_BYTES = 1024;
23
+ const RESULT_BUDGET_RESERVE_BYTES = 4 * 1024;
24
+
25
+ export type TavilyWebSearchOptions = {
26
+ apiKey: string;
27
+ timeoutMs: number;
28
+ };
29
+
30
+ export const tavilyWebSearchOptionsSchema = z.object({
31
+ apiKey: z.string().trim().min(1).meta({ secret: true }),
32
+ timeoutMs: z.number().int().min(1_000).max(120_000).default(30_000),
33
+ }).strict();
34
+
35
+ const tavilyResultSchema = z.object({
36
+ title: z.string(),
37
+ url: z.url(),
38
+ content: z.string().nullable().optional(),
39
+ raw_content: z.string().nullable().optional(),
40
+ score: z.number().optional(),
41
+ published_date: z.string().nullable().optional(),
42
+ favicon: z.string().nullable().optional(),
43
+ }).strict();
44
+
45
+ const tavilyResponseSchema = z.object({
46
+ query: z.string().optional(),
47
+ answer: z.string().nullable().optional(),
48
+ images: z.array(z.unknown()).optional(),
49
+ follow_up_questions: z.array(z.string()).nullable().optional(),
50
+ request_id: z.string().max(MAX_REQUEST_ID_BYTES).optional(),
51
+ response_time: z.number().optional(),
52
+ results: z.array(tavilyResultSchema),
53
+ }).strict();
54
+
55
+ export type TavilyWebSearchDependencies = {
56
+ fetchImpl?: typeof fetch;
57
+ now?: () => number;
58
+ dateNow?: () => Date;
59
+ };
60
+
61
+ function durationSince(startedAt: number, now: () => number): number {
62
+ return Math.max(0, Math.round(now() - startedAt));
63
+ }
64
+
65
+ function failure(
66
+ input: WebSearchInput,
67
+ startedAt: number,
68
+ now: () => number,
69
+ dateNow: () => Date,
70
+ code: WebSearchErrorCode,
71
+ message: string,
72
+ retryable: boolean,
73
+ providerRequestId?: string,
74
+ ): WebSearchResult {
75
+ return {
76
+ success: false,
77
+ query: input.query,
78
+ searchedAt: dateNow().toISOString(),
79
+ durationMs: durationSince(startedAt, now),
80
+ ...(providerRequestId ? { providerRequestId } : {}),
81
+ error: { code, message, retryable },
82
+ };
83
+ }
84
+
85
+ function httpFailure(
86
+ status: number,
87
+ ): { code: WebSearchErrorCode; message: string; retryable: boolean } {
88
+ if (status === 400) return { code: 'WEB_SEARCH_INVALID_REQUEST', message: '搜索请求被供应商拒绝,请检查输入', retryable: false };
89
+ if (status === 401) return { code: 'WEB_SEARCH_AUTH_FAILED', message: '搜索服务认证失败,请检查 Environment 密钥', retryable: false };
90
+ if (status === 429) return { code: 'WEB_SEARCH_RATE_LIMITED', message: '搜索服务请求过于频繁,请稍后重试', retryable: true };
91
+ if (status === 432 || status === 433) return { code: 'WEB_SEARCH_QUOTA_EXCEEDED', message: '搜索服务额度不足,请检查订阅', retryable: false };
92
+ if (status >= 500) return { code: 'WEB_SEARCH_PROVIDER_UNAVAILABLE', message: '搜索服务暂时不可用,请稍后重试', retryable: true };
93
+ return { code: 'WEB_SEARCH_PROVIDER_ERROR', message: '搜索服务返回了非预期响应', retryable: false };
94
+ }
95
+
96
+ function buildRequest(input: WebSearchInput): { body: Record<string, unknown>; warnings: WebSearchWarning[] } {
97
+ const topic = input.topic ?? 'general';
98
+ const warnings: WebSearchWarning[] = [];
99
+ const body: Record<string, unknown> = {
100
+ query: input.query,
101
+ topic,
102
+ search_depth: (input.depth ?? 'standard') === 'deep' ? 'advanced' : 'basic',
103
+ max_results: input.maxResults ?? 5,
104
+ include_raw_content: input.contentFormat && input.contentFormat !== 'none' ? input.contentFormat : false,
105
+ include_answer: false,
106
+ include_images: false,
107
+ include_image_descriptions: false,
108
+ include_favicon: false,
109
+ include_usage: false,
110
+ auto_parameters: false,
111
+ };
112
+ if (input.includeDomains) body.include_domains = input.includeDomains;
113
+ if (input.excludeDomains) body.exclude_domains = input.excludeDomains;
114
+ if (input.timeFilter?.type === 'relative') {
115
+ if (topic === 'finance') {
116
+ warnings.push({ code: 'TIME_FILTER_NOT_APPLIED', message: 'finance 主题不支持相对时间过滤,已忽略该条件' });
117
+ } else {
118
+ body.time_range = input.timeFilter.range;
119
+ }
120
+ } else if (input.timeFilter?.type === 'absolute') {
121
+ if (input.timeFilter.from) body.start_date = input.timeFilter.from;
122
+ if (input.timeFilter.to) body.end_date = input.timeFilter.to;
123
+ }
124
+ if (input.region) {
125
+ if (topic === 'general') {
126
+ body.country = new Intl.DisplayNames(['en'], { type: 'region' }).of(input.region) ?? input.region;
127
+ } else {
128
+ warnings.push({ code: 'REGION_NOT_APPLIED', message: `${topic} 主题不支持地区过滤,已忽略该条件` });
129
+ }
130
+ }
131
+ return { body, warnings };
132
+ }
133
+
134
+ async function readBoundedBody(response: Response): Promise<string> {
135
+ const declaredLength = Number(response.headers.get('content-length'));
136
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
137
+ await cancelResponseBody(response);
138
+ throw new ResponseTooLargeError();
139
+ }
140
+ if (!response.body) return '';
141
+ const reader = response.body.getReader();
142
+ const chunks: Uint8Array[] = [];
143
+ let size = 0;
144
+ try {
145
+ while (true) {
146
+ const { done, value } = await reader.read();
147
+ if (done) break;
148
+ size += value.byteLength;
149
+ if (size > MAX_RESPONSE_BYTES) {
150
+ await reader.cancel().catch(() => undefined);
151
+ throw new ResponseTooLargeError();
152
+ }
153
+ chunks.push(value);
154
+ }
155
+ } finally {
156
+ reader.releaseLock();
157
+ }
158
+ const bytes = new Uint8Array(size);
159
+ let offset = 0;
160
+ for (const chunk of chunks) {
161
+ bytes.set(chunk, offset);
162
+ offset += chunk.byteLength;
163
+ }
164
+ try {
165
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
166
+ } catch {
167
+ throw new InvalidResponseEncodingError();
168
+ }
169
+ }
170
+
171
+ class ResponseTooLargeError extends Error {}
172
+ class InvalidResponseEncodingError extends Error {}
173
+ class ProviderSecurityError extends Error {}
174
+
175
+ async function cancelResponseBody(response: Response): Promise<void> {
176
+ try {
177
+ await response.body?.cancel();
178
+ } catch {
179
+ // 释放连接是 best-effort,不能覆盖原始 HTTP 或响应预算错误。
180
+ }
181
+ }
182
+
183
+ function containsSecret(value: string | null | undefined, apiKey: string): boolean {
184
+ return typeof value === 'string' && value.includes(apiKey);
185
+ }
186
+
187
+ function assertNoSecret(value: string | null | undefined, apiKey: string): void {
188
+ if (containsSecret(value, apiKey)) {
189
+ throw new ProviderSecurityError('供应商响应包含搜索服务密钥');
190
+ }
191
+ }
192
+
193
+ function normalizeProviderRequestId(value: string | null | undefined, apiKey: string): string | undefined {
194
+ if (!value) return undefined;
195
+ assertNoSecret(value, apiKey);
196
+ if (new TextEncoder().encode(value).byteLength > MAX_REQUEST_ID_BYTES) {
197
+ throw new ProviderSecurityError('供应商 request id 超过限制');
198
+ }
199
+ return value;
200
+ }
201
+
202
+ function truncateUtf8(value: string, maxBytes: number): { value: string; truncated: boolean; bytes: number } {
203
+ const encoded = new TextEncoder().encode(value);
204
+ if (encoded.byteLength <= maxBytes) return { value, truncated: false, bytes: encoded.byteLength };
205
+ let end = Math.max(0, maxBytes);
206
+ const decoder = new TextDecoder('utf-8', { fatal: true });
207
+ while (end > 0) {
208
+ try {
209
+ const result = decoder.decode(encoded.subarray(0, end));
210
+ return { value: result, truncated: true, bytes: end };
211
+ } catch {
212
+ end -= 1;
213
+ }
214
+ }
215
+ return { value: '', truncated: true, bytes: 0 };
216
+ }
217
+
218
+ function normalizeSuccess(
219
+ input: WebSearchInput,
220
+ response: z.infer<typeof tavilyResponseSchema>,
221
+ warnings: WebSearchWarning[],
222
+ startedAt: number,
223
+ now: () => number,
224
+ dateNow: () => Date,
225
+ apiKey: string,
226
+ ): WebSearchResult {
227
+ const searchedAt = dateNow().toISOString();
228
+ const encoder = new TextEncoder();
229
+ const fixedBytes = encoder.encode(input.query).byteLength
230
+ + encoder.encode(searchedAt).byteLength
231
+ + encoder.encode('tavily').byteLength
232
+ + RESULT_BUDGET_RESERVE_BYTES;
233
+ const budget = {
234
+ remaining: Math.max(0, MAX_TOTAL_TEXT_BYTES - fixedBytes),
235
+ truncated: false,
236
+ };
237
+ const takeText = (value: string, maxBytes: number) => {
238
+ const result = truncateUtf8(value, Math.min(maxBytes, budget.remaining));
239
+ budget.remaining -= result.bytes;
240
+ budget.truncated ||= result.truncated;
241
+ return result.value;
242
+ };
243
+
244
+ const rawRequestId = normalizeProviderRequestId(response.request_id, apiKey);
245
+ const providerRequestId = rawRequestId ? takeText(rawRequestId, MAX_REQUEST_ID_BYTES) : undefined;
246
+ const maxResults = input.maxResults ?? 5;
247
+ if (response.results.length > maxResults) budget.truncated = true;
248
+ const results = response.results.slice(0, maxResults).flatMap((item) => {
249
+ assertNoSecret(item.title, apiKey);
250
+ assertNoSecret(item.url, apiKey);
251
+ assertNoSecret(item.content, apiKey);
252
+ assertNoSecret(item.raw_content, apiKey);
253
+ assertNoSecret(item.published_date, apiKey);
254
+ const url = new URL(item.url);
255
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('搜索结果 URL 协议无效');
256
+ const source = url.hostname.toLowerCase();
257
+ const urlBytes = encoder.encode(item.url).byteLength;
258
+ const sourceBytes = encoder.encode(source).byteLength;
259
+ if (urlBytes > MAX_URL_BYTES || urlBytes + sourceBytes > budget.remaining) {
260
+ budget.truncated = true;
261
+ return [];
262
+ }
263
+ budget.remaining -= urlBytes + sourceBytes;
264
+ const title = takeText(item.title, MAX_TITLE_BYTES);
265
+ const publishedAt = item.published_date ? takeText(item.published_date, MAX_PUBLISHED_AT_BYTES) : undefined;
266
+ const snippet = takeText(item.content ?? '', MAX_SNIPPET_BYTES);
267
+ const content = input.contentFormat && input.contentFormat !== 'none' && item.raw_content !== null && item.raw_content !== undefined
268
+ ? takeText(item.raw_content, MAX_CONTENT_BYTES)
269
+ : undefined;
270
+ return [{
271
+ title,
272
+ url: item.url,
273
+ source,
274
+ ...(publishedAt ? { publishedAt } : {}),
275
+ snippet,
276
+ ...(content !== undefined ? { content } : {}),
277
+ ...(item.score !== undefined ? { relevanceScore: item.score } : {}),
278
+ }];
279
+ });
280
+ const finalWarnings = [...warnings];
281
+ if (budget.truncated) finalWarnings.push({ code: 'CONTENT_TRUNCATED', message: '搜索结果内容已按平台响应预算裁剪' });
282
+ return {
283
+ success: true,
284
+ query: input.query,
285
+ searchedAt,
286
+ provider: 'tavily',
287
+ ...(providerRequestId ? { providerRequestId } : {}),
288
+ durationMs: durationSince(startedAt, now),
289
+ results,
290
+ ...(finalWarnings.length > 0 ? { warnings: finalWarnings } : {}),
291
+ };
292
+ }
293
+
294
+ export function createTavilyWebSearchResource(
295
+ rawOptions: { apiKey: string; timeoutMs?: number },
296
+ dependencies: TavilyWebSearchDependencies = {},
297
+ ): WebSearchResource {
298
+ const options = tavilyWebSearchOptionsSchema.parse(rawOptions);
299
+ const fetchImpl = dependencies.fetchImpl ?? fetch;
300
+ const now = dependencies.now ?? (() => performance.now());
301
+ const dateNow = dependencies.dateNow ?? (() => new Date());
302
+ return {
303
+ async search(input, context) {
304
+ const startedAt = now();
305
+ const { body, warnings } = buildRequest(input);
306
+ const controller = new AbortController();
307
+ let abortSource: 'timeout' | 'upstream' | undefined;
308
+ const abortRequest = (source: 'timeout' | 'upstream') => {
309
+ if (controller.signal.aborted) return;
310
+ abortSource = source;
311
+ controller.abort();
312
+ };
313
+ const onUpstreamAbort = () => abortRequest('upstream');
314
+ if (context?.abortSignal?.aborted) {
315
+ throw new DOMException('搜索请求已中断', 'AbortError');
316
+ }
317
+ context?.abortSignal?.addEventListener('abort', onUpstreamAbort, { once: true });
318
+ const timer = setTimeout(() => abortRequest('timeout'), options.timeoutMs);
319
+ try {
320
+ const response = await fetchImpl(TAVILY_SEARCH_ENDPOINT, {
321
+ method: 'POST',
322
+ redirect: 'error',
323
+ headers: {
324
+ authorization: `Bearer ${options.apiKey}`,
325
+ 'content-type': 'application/json',
326
+ },
327
+ body: JSON.stringify(body),
328
+ signal: controller.signal,
329
+ });
330
+ let headerRequestId: string | undefined;
331
+ try {
332
+ headerRequestId = normalizeProviderRequestId(response.headers.get('x-request-id'), options.apiKey);
333
+ } catch {
334
+ await cancelResponseBody(response);
335
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_PROVIDER_ERROR', '搜索服务响应包含不安全的 request id', false);
336
+ }
337
+ if (!response.ok) {
338
+ await cancelResponseBody(response);
339
+ const mapped = httpFailure(response.status);
340
+ return failure(input, startedAt, now, dateNow, mapped.code, mapped.message, mapped.retryable, headerRequestId);
341
+ }
342
+ const text = await readBoundedBody(response);
343
+ let json: unknown;
344
+ try {
345
+ json = JSON.parse(text) as unknown;
346
+ } catch {
347
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_PROVIDER_ERROR', '搜索服务返回了无法解析的响应', false, headerRequestId);
348
+ }
349
+ const parsed = tavilyResponseSchema.safeParse(json);
350
+ if (!parsed.success) {
351
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_PROVIDER_ERROR', '搜索服务响应格式不符合预期', false, headerRequestId);
352
+ }
353
+ try {
354
+ return normalizeSuccess(input, parsed.data, warnings, startedAt, now, dateNow, options.apiKey);
355
+ } catch {
356
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_PROVIDER_ERROR', '搜索服务响应包含无效结果', false, headerRequestId);
357
+ }
358
+ } catch (error) {
359
+ if (error instanceof ResponseTooLargeError) {
360
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_RESPONSE_TOO_LARGE', '搜索服务响应超过 2 MiB 限制', false);
361
+ }
362
+ if (error instanceof InvalidResponseEncodingError) {
363
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_PROVIDER_ERROR', '搜索服务响应不是有效的 UTF-8', false);
364
+ }
365
+ if (abortSource === 'upstream') {
366
+ throw new DOMException('搜索请求已中断', 'AbortError');
367
+ }
368
+ if (abortSource === 'timeout') {
369
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_TIMEOUT', '搜索服务请求超时,请稍后重试', true);
370
+ }
371
+ return failure(input, startedAt, now, dateNow, 'WEB_SEARCH_PROVIDER_UNAVAILABLE', '无法连接搜索服务,请稍后重试', true);
372
+ } finally {
373
+ clearTimeout(timer);
374
+ context?.abortSignal?.removeEventListener('abort', onUpstreamAbort);
375
+ }
376
+ },
377
+ };
378
+ }
379
+
380
+ export function createTavilyWebSearchResourceImplementation(
381
+ dependencies: TavilyWebSearchDependencies = {},
382
+ ): EnvironmentWorkerResourceImplementation<WebSearchResource, TavilyWebSearchOptions> {
383
+ return {
384
+ title: 'Tavily',
385
+ description: '通过 Tavily 官方 Search API 搜索公开互联网内容。',
386
+ options: tavilyWebSearchOptionsSchema,
387
+ factory: (options) => createTavilyWebSearchResource(options, dependencies),
388
+ };
389
+ }
390
+
391
+ export function createTavilyWebSearchResourceDefinition(
392
+ dependencies: TavilyWebSearchDependencies = {},
393
+ ): ResourceDefinition<WebSearchResource, TavilyWebSearchOptions> {
394
+ return {
395
+ id: 'webSearch',
396
+ title: '互联网搜索',
397
+ description: '供应商无关的互联网搜索资源。',
398
+ implementations: [{
399
+ id: 'tavily',
400
+ title: 'Tavily',
401
+ description: '通过 Tavily 官方 Search API 搜索公开互联网内容。',
402
+ supportedExecutionModes: ['hosted', 'self_hosted'],
403
+ optionsSchema: tavilyWebSearchOptionsSchema,
404
+ optionsJsonSchema: z.toJSONSchema(tavilyWebSearchOptionsSchema),
405
+ factory: (options) => createTavilyWebSearchResource(tavilyWebSearchOptionsSchema.parse(options), dependencies),
406
+ }],
407
+ };
408
+ }