@triplef/agent 0.1.11 → 0.1.12

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 CHANGED
@@ -16,7 +16,7 @@ The tripleF (3F) agent domain — structured-output schemas, prompt builders, an
16
16
 
17
17
  ## Subpath exports
18
18
 
19
- - `@triplef/agent/schemas` — Zod schemas and `z.infer` types for intent classification, response templates, and memory extraction/consolidation/profile, plus URL-trust and Zod-shape helpers (`formatZodShape`, `deriveSchemaKeys`) and the `LexiconSelectInput`/`LexiconSelectResult` retrieval-selection contract.
19
+ - `@triplef/agent/schemas` — Zod schemas and `z.infer` types for intent classification, response templates, and memory extraction/consolidation/profile, plus URL-trust and Zod-shape helpers (`formatZodShape`, `deriveSchemaKeys`) and the `EncyclopediaSelectInput`/`EncyclopediaSelectResult` retrieval-selection contract.
20
20
  - `@triplef/agent/prompts` — harness and memory prompt builders, the snippet system, and `buildStructuredPrompt` (renders a schema's JSON shape into a prompt template).
21
21
  - `@triplef/agent/tools` — search/tool factories (Serper, Bright Data, YouTube, web-fetch, image-variants, memory) with a decoupled `ToolDependencies` contract.
22
22
 
@@ -1,5 +1,5 @@
1
- import { T as TemplateName, F as FormatZodShapeOptions } from '../intent.schema-CUQoRJ5x.js';
2
- export { D as DEFAULT_VARIANT_ID } from '../intent.schema-CUQoRJ5x.js';
1
+ import { T as TemplateName, F as FormatZodShapeOptions } from '../intent.schema-DnxaUhmz.js';
2
+ export { D as DEFAULT_VARIANT_ID } from '../intent.schema-DnxaUhmz.js';
3
3
  import * as zod from 'zod';
4
4
  import { z, ZodTypeAny, ZodType } from 'zod';
5
5
  import * as zod_v4_core from 'zod/v4/core';
@@ -1,11 +1,47 @@
1
1
  import { z } from 'zod';
2
- export { D as DEFAULT_VARIANT_ID, F as FormatZodShapeOptions, I as IntentResult, a as IntentSchema, T as TemplateName, f as formatZodShape } from '../intent.schema-CUQoRJ5x.js';
2
+ export { D as DEFAULT_VARIANT_ID, F as FormatZodShapeOptions, I as IntentResult, a as IntentSchema, T as TemplateName, f as formatZodShape } from '../intent.schema-DnxaUhmz.js';
3
3
 
4
4
  declare const BLOCKED_IMAGE_HOSTS: Set<string>;
5
5
 
6
6
  declare const BLOCKED_URL_HOSTS: Set<string>;
7
7
  declare const NON_PAGE_EXTENSIONS: RegExp;
8
8
 
9
+ interface EncyclopediaSourceDocument {
10
+ url?: string;
11
+ title?: string;
12
+ content: string;
13
+ }
14
+ interface EncyclopediaSelectedChunk {
15
+ url?: string;
16
+ title?: string;
17
+ content: string;
18
+ score: number;
19
+ sourceType?: 'content' | 'result';
20
+ }
21
+ interface EncyclopediaSearchResult {
22
+ url: string;
23
+ title?: string;
24
+ snippet: string;
25
+ }
26
+ interface EncyclopediaSelectInput {
27
+ query: string;
28
+ documents: EncyclopediaSourceDocument[];
29
+ searchResults?: EncyclopediaSearchResult[];
30
+ budgetChars?: number;
31
+ partitionScope?: string;
32
+ model?: string;
33
+ }
34
+ interface EncyclopediaSelectResult {
35
+ chunks: EncyclopediaSelectedChunk[];
36
+ consideredChunks: number;
37
+ selectedChunks: number;
38
+ droppedByThreshold: number;
39
+ inputChunksDropped: number;
40
+ pastChunks?: EncyclopediaSelectedChunk[];
41
+ reusedDocs?: number;
42
+ storedDocs?: number;
43
+ }
44
+
9
45
  declare function categorizeTools(toolNames: readonly string[]): Record<string, string[]>;
10
46
 
11
47
  declare const VARIANT_NAMES: readonly ["grayscale", "denoised", "sharpened", "clahe"];
@@ -139,41 +175,6 @@ declare function deriveSchemaKeys(schema: z.ZodType): {
139
175
 
140
176
  declare function formatZodIssues(issues: z.ZodIssue[]): string;
141
177
 
142
- interface LexiconSourceDocument {
143
- url?: string;
144
- title?: string;
145
- content: string;
146
- }
147
- interface LexiconSelectedChunk {
148
- url?: string;
149
- title?: string;
150
- content: string;
151
- score: number;
152
- sourceType?: 'content' | 'result';
153
- }
154
- interface LexiconSearchResult {
155
- url: string;
156
- title?: string;
157
- snippet: string;
158
- }
159
- interface LexiconSelectInput {
160
- query: string;
161
- documents: LexiconSourceDocument[];
162
- searchResults?: LexiconSearchResult[];
163
- budgetChars?: number;
164
- partitionScope?: string;
165
- }
166
- interface LexiconSelectResult {
167
- chunks: LexiconSelectedChunk[];
168
- consideredChunks: number;
169
- selectedChunks: number;
170
- droppedByThreshold: number;
171
- inputChunksDropped: number;
172
- pastChunks?: LexiconSelectedChunk[];
173
- reusedDocs?: number;
174
- storedDocs?: number;
175
- }
176
-
177
178
  declare const ConsolidationVerdictSchema: z.ZodObject<{
178
179
  verdict: z.ZodEnum<{
179
180
  merge: "merge";
@@ -908,4 +909,4 @@ declare const videolistSchema: z.ZodObject<{
908
909
  }, z.core.$strip>>>;
909
910
  }, z.core.$strip>;
910
911
 
911
- export { BLOCKED_IMAGE_HOSTS, BLOCKED_URL_HOSTS, BROWSER_TOOL_NAMES, COGNITION_LIMIT_DEFAULT, COGNITION_LIMIT_MAX, COGNITION_LIMIT_MIN, COGNITION_PURGE_BATCH, type ConsolidationVerdict, ConsolidationVerdictSchema, EPISODE_PROBE_LIMIT_DEFAULT, EPISODE_PROBE_LIMIT_MIN, EPISODE_RECENCY_MIDPOINT_DEFAULT, EPISODE_RECENCY_MIDPOINT_MAX, EPISODE_RECENCY_MIDPOINT_MIN, EPISODE_RECENCY_SCALE_SECONDS_DEFAULT, EPISODE_RECENCY_SCALE_SECONDS_MAX, EPISODE_RECENCY_SCALE_SECONDS_MIN, EPISODE_RECENCY_WEIGHT_DEFAULT, EPISODE_RECENCY_WEIGHT_MAX, EPISODE_RECENCY_WEIGHT_MIN, EPISODE_SCORE_THRESHOLD_DEFAULT, EPISODE_SCORE_THRESHOLD_MAX, EPISODE_SCORE_THRESHOLD_MIN, EPISODE_TAGS, EPISODE_TEXT_LIMIT, ExtractionSchema, HERO_VIDEO_TITLE_ISSUE, INSIGHTS_MAX_PER_TURN, INSIGHT_TAGS, INSIGHT_TEXT_LIMIT, type LexiconSearchResult, type LexiconSelectInput, type LexiconSelectResult, type LexiconSelectedChunk, type LexiconSourceDocument, MEMORY_TOOL_NAMES, type MemoryCognitionProfile, type MemoryEnrichment, MemoryEnrichmentSchema, type MemoryExtraction, type MemoryProfileInsight, type MemoryProfileResponse, NON_PAGE_EXTENSIONS, type ProviderConfig, TOOL_DESCRIPTIONS, TOOL_NAMES, type ToolName, VARIANT_NAMES, type VariantName, cardSchema, categorizeTools, clampCognitionLimit, clampEpisodeProbeLimit, clampEpisodeRecencyMidpoint, clampEpisodeRecencyScaleSeconds, clampEpisodeRecencyWeight, clampEpisodeScoreThreshold, compareSchema, createTextItemSchema, deriveSchemaKeys, describeSchema, discardedReferenceSchema, formatZodIssues, galleryItemSchema, hasBlockedImageHost, heroVideoHasTitle, imagelistSchema, internationalCoverageSchema, isAllFieldsNullWipe, isPrivateOrLocalhost, isTrustedImageUrl, isTrustedUrl, markerSchema, memoryProfileResponseSchema, mergeCognitionProfiles, normalizeInsightPath, ocrSchema, parseStoredProfile, productSchema, referenceGalleryItemSchema, referenceLineSchema, relatedStorySchema, safeMediaUrl, safeMediaUrlOrEmpty, safeUrl, safeVideoUrl, safeVideoUrlOrEmpty, shoplistSchema, sourceSchema, stockmarketItemSchema, stockmarketListSchema, summarySchema, videoGalleryItemSchema, videolistSchema };
912
+ export { BLOCKED_IMAGE_HOSTS, BLOCKED_URL_HOSTS, BROWSER_TOOL_NAMES, COGNITION_LIMIT_DEFAULT, COGNITION_LIMIT_MAX, COGNITION_LIMIT_MIN, COGNITION_PURGE_BATCH, type ConsolidationVerdict, ConsolidationVerdictSchema, EPISODE_PROBE_LIMIT_DEFAULT, EPISODE_PROBE_LIMIT_MIN, EPISODE_RECENCY_MIDPOINT_DEFAULT, EPISODE_RECENCY_MIDPOINT_MAX, EPISODE_RECENCY_MIDPOINT_MIN, EPISODE_RECENCY_SCALE_SECONDS_DEFAULT, EPISODE_RECENCY_SCALE_SECONDS_MAX, EPISODE_RECENCY_SCALE_SECONDS_MIN, EPISODE_RECENCY_WEIGHT_DEFAULT, EPISODE_RECENCY_WEIGHT_MAX, EPISODE_RECENCY_WEIGHT_MIN, EPISODE_SCORE_THRESHOLD_DEFAULT, EPISODE_SCORE_THRESHOLD_MAX, EPISODE_SCORE_THRESHOLD_MIN, EPISODE_TAGS, EPISODE_TEXT_LIMIT, type EncyclopediaSearchResult, type EncyclopediaSelectInput, type EncyclopediaSelectResult, type EncyclopediaSelectedChunk, type EncyclopediaSourceDocument, ExtractionSchema, HERO_VIDEO_TITLE_ISSUE, INSIGHTS_MAX_PER_TURN, INSIGHT_TAGS, INSIGHT_TEXT_LIMIT, MEMORY_TOOL_NAMES, type MemoryCognitionProfile, type MemoryEnrichment, MemoryEnrichmentSchema, type MemoryExtraction, type MemoryProfileInsight, type MemoryProfileResponse, NON_PAGE_EXTENSIONS, type ProviderConfig, TOOL_DESCRIPTIONS, TOOL_NAMES, type ToolName, VARIANT_NAMES, type VariantName, cardSchema, categorizeTools, clampCognitionLimit, clampEpisodeProbeLimit, clampEpisodeRecencyMidpoint, clampEpisodeRecencyScaleSeconds, clampEpisodeRecencyWeight, clampEpisodeScoreThreshold, compareSchema, createTextItemSchema, deriveSchemaKeys, describeSchema, discardedReferenceSchema, formatZodIssues, galleryItemSchema, hasBlockedImageHost, heroVideoHasTitle, imagelistSchema, internationalCoverageSchema, isAllFieldsNullWipe, isPrivateOrLocalhost, isTrustedImageUrl, isTrustedUrl, markerSchema, memoryProfileResponseSchema, mergeCognitionProfiles, normalizeInsightPath, ocrSchema, parseStoredProfile, productSchema, referenceGalleryItemSchema, referenceLineSchema, relatedStorySchema, safeMediaUrl, safeMediaUrlOrEmpty, safeUrl, safeVideoUrl, safeVideoUrlOrEmpty, shoplistSchema, sourceSchema, stockmarketItemSchema, stockmarketListSchema, summarySchema, videoGalleryItemSchema, videolistSchema };
@@ -401,6 +401,11 @@ interface MemoryPoint {
401
401
  path?: string;
402
402
  createdAt: string;
403
403
  score?: number;
404
+ isConsolidated?: boolean;
405
+ isReflected?: boolean;
406
+ isFriction?: boolean;
407
+ superseded?: boolean;
408
+ supersededBy?: string;
404
409
  }
405
410
 
406
411
  interface MemoryPartitionRecallDeps {
@@ -261,6 +261,21 @@ function tbsSizeLabelForPixels(pixels) {
261
261
  return selected.label;
262
262
  }
263
263
 
264
+ // src/tools/bright-data/helpers/map-bright-data-image-result.helper.ts
265
+ function mapBrightDataImageResult(r) {
266
+ return {
267
+ title: r.title || "",
268
+ // Prefer the real image URL; `image` is a base64 thumbnail data
269
+ // URI that our trust rules reject.
270
+ imageUrl: r.original_image || r.image_url || r.imageUrl || r.link || "",
271
+ sourcePageUrl: r.source_link || r.link || "",
272
+ width: r.width,
273
+ height: r.height,
274
+ source: r.source || "",
275
+ domain: ""
276
+ };
277
+ }
278
+
264
279
  // src/tools/bright-data/image-search.tool.ts
265
280
  function createBrightDataImageSearch(deps) {
266
281
  return tool({
@@ -304,17 +319,7 @@ function createBrightDataImageSearch(deps) {
304
319
  deps.logger.warn(`Bright Data image search returned 0 results for "${query}"`);
305
320
  return { results: [] };
306
321
  }
307
- const results = images.map((r) => ({
308
- title: r.title || "",
309
- // Prefer the real image URL; `image` is a base64 thumbnail data
310
- // URI that our trust rules reject.
311
- imageUrl: r.original_image || r.image_url || r.imageUrl || r.link || "",
312
- sourcePageUrl: r.source_link || r.link || "",
313
- width: r.width,
314
- height: r.height,
315
- source: r.source || "",
316
- domain: ""
317
- })).filter((r) => {
322
+ const results = images.map(mapBrightDataImageResult).filter((r) => {
318
323
  if (!isTrustedImageUrl(r.imageUrl)) return false;
319
324
  const w = r.width ?? 0;
320
325
  const h = r.height ?? 0;
@@ -336,6 +341,20 @@ var brightDataNewsSearchSchema = z.object({
336
341
  recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
337
342
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
338
343
  });
344
+
345
+ // src/tools/bright-data/helpers/map-bright-data-news-result.helper.ts
346
+ function mapBrightDataNewsResult(r) {
347
+ return {
348
+ title: r.title,
349
+ snippet: r.description || "",
350
+ url: r.link,
351
+ source: r.source || SOURCE,
352
+ date: r.date || "",
353
+ imageUrl: r.image_url || ""
354
+ };
355
+ }
356
+
357
+ // src/tools/bright-data/news-search.tool.ts
339
358
  function createBrightDataNewsSearch(deps) {
340
359
  return tool({
341
360
  description: 'Search the latest news using Bright Data SERP API. Returns headlines, sources, dates, and snippets. Pass recency ("day"|"week"|"month"|"year") to restrict to a recent period. ' + STANDALONE_QUERY_TOOL_CLAUSE,
@@ -361,14 +380,7 @@ function createBrightDataNewsSearch(deps) {
361
380
  });
362
381
  const news = data.news ?? [];
363
382
  if (!news.length) return { results: [] };
364
- const results = news.map((r) => ({
365
- title: r.title,
366
- snippet: r.description || "",
367
- url: r.link,
368
- source: r.source || SOURCE,
369
- date: r.date || "",
370
- imageUrl: r.image_url || ""
371
- }));
383
+ const results = news.map(mapBrightDataNewsResult);
372
384
  deps.logger.log(`Bright Data news returned ${results.length} results for "${query}"`);
373
385
  return { results };
374
386
  } catch (err) {
@@ -385,6 +397,23 @@ var brightDataPlacesSearchSchema = z.object({
385
397
  count: z.number().optional().describe("Number of results (max 100)"),
386
398
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
387
399
  });
400
+
401
+ // src/tools/bright-data/helpers/map-bright-data-place-result.helper.ts
402
+ function mapBrightDataPlaceResult(r) {
403
+ return {
404
+ title: r.title || "",
405
+ address: r.address || "",
406
+ phoneNumber: r.phone || "",
407
+ latitude: r.latitude,
408
+ longitude: r.longitude,
409
+ rating: r.rating,
410
+ ratingCount: r.reviews_cnt,
411
+ type: r.type || "",
412
+ website: r.website || ""
413
+ };
414
+ }
415
+
416
+ // src/tools/bright-data/places-search.tool.ts
388
417
  function createBrightDataPlacesSearch(deps) {
389
418
  return tool({
390
419
  description: 'Search for places and businesses using Bright Data SERP API (Google Maps local results). Returns names, addresses, ratings, review counts, and coordinates. Phrase the query like a Google Maps search: a business name, a business type, or a business type plus location (e.g. "MediaMarkt Berlin", "coffee shops in Munich").',
@@ -415,17 +444,7 @@ function createBrightDataPlacesSearch(deps) {
415
444
  deps.logger.warn(`Bright Data places returned 0 results for "${query}"`);
416
445
  return { results: [] };
417
446
  }
418
- const results = places.map((r) => ({
419
- title: r.title || "",
420
- address: r.address || "",
421
- phoneNumber: r.phone || "",
422
- latitude: r.latitude,
423
- longitude: r.longitude,
424
- rating: r.rating,
425
- ratingCount: r.reviews_cnt,
426
- type: r.type || "",
427
- website: r.website || ""
428
- }));
447
+ const results = places.map(mapBrightDataPlaceResult);
429
448
  return { results };
430
449
  } catch (err) {
431
450
  deps.logger.warn(`Bright Data places search failed for "${query}": ${String(err)}`);
@@ -439,6 +458,22 @@ var brightDataShoppingSearchSchema = z.object({
439
458
  count: z.number().optional().describe("Number of results (max 100)"),
440
459
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
441
460
  });
461
+
462
+ // src/tools/bright-data/helpers/map-bright-data-shopping-result.helper.ts
463
+ function mapBrightDataShoppingResult(r) {
464
+ return {
465
+ title: r.title || "",
466
+ price: r.price || "",
467
+ link: r.link || "",
468
+ source: r.source || "",
469
+ imageUrl: r.image_url || r.image || "",
470
+ delivery: r.delivery || "",
471
+ rating: r.rating,
472
+ ratingCount: r.rating_count
473
+ };
474
+ }
475
+
476
+ // src/tools/bright-data/shopping-search.tool.ts
442
477
  function createBrightDataShoppingSearch(deps) {
443
478
  return tool({
444
479
  description: 'Search for products using Bright Data SERP API (Google Shopping). Returns prices, sellers, images, and ratings. Phrase the query as the bare product name with model number (e.g. "Sony WH-1000XM5") \u2014 do NOT add words like "review", "test", or long descriptive sentences.',
@@ -469,16 +504,7 @@ function createBrightDataShoppingSearch(deps) {
469
504
  deps.logger.warn(`Bright Data shopping returned 0 results for "${query}"`);
470
505
  return { results: [] };
471
506
  }
472
- const results = shopping.map((r) => ({
473
- title: r.title || "",
474
- price: r.price || "",
475
- link: r.link || "",
476
- source: r.source || "",
477
- imageUrl: r.image_url || r.image || "",
478
- delivery: r.delivery || "",
479
- rating: r.rating,
480
- ratingCount: r.rating_count
481
- }));
507
+ const results = shopping.map(mapBrightDataShoppingResult);
482
508
  return { results };
483
509
  } catch (err) {
484
510
  deps.logger.warn(`Bright Data shopping search failed for "${query}": ${String(err)}`);
@@ -494,6 +520,24 @@ var brightDataVideoSearchSchema = z.object({
494
520
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
495
521
  });
496
522
 
523
+ // src/tools/helpers/localized-query-suffix.helper.ts
524
+ function localizedQuerySuffix(lang) {
525
+ if (!lang) return "";
526
+ let code;
527
+ try {
528
+ code = new Intl.Locale(lang).language;
529
+ } catch {
530
+ return "";
531
+ }
532
+ if (!code || code === "en") return "";
533
+ try {
534
+ const name = new Intl.DisplayNames([code], { type: "language" }).of(code);
535
+ return !name || name === code ? "" : name;
536
+ } catch {
537
+ return "";
538
+ }
539
+ }
540
+
497
541
  // src/tools/helpers/build-youtube-thumbnail-url.helper.ts
498
542
  var YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
499
543
  function extractYoutubeVideoId(url) {
@@ -521,22 +565,21 @@ function buildYoutubeThumbnailUrl(url) {
521
565
  return id ? `https://i.ytimg.com/vi/${id}/maxresdefault.jpg` : void 0;
522
566
  }
523
567
 
524
- // src/tools/helpers/localized-query-suffix.helper.ts
525
- function localizedQuerySuffix(lang) {
526
- if (!lang) return "";
527
- let code;
528
- try {
529
- code = new Intl.Locale(lang).language;
530
- } catch {
531
- return "";
532
- }
533
- if (!code || code === "en") return "";
534
- try {
535
- const name = new Intl.DisplayNames([code], { type: "language" }).of(code);
536
- return !name || name === code ? "" : name;
537
- } catch {
538
- return "";
539
- }
568
+ // src/tools/bright-data/helpers/map-bright-data-video-result.helper.ts
569
+ function mapBrightDataVideoResult(r) {
570
+ return {
571
+ title: r.title || "",
572
+ link: r.link || "",
573
+ snippet: r.description || "",
574
+ channel: "",
575
+ duration: r.duration || "",
576
+ date: "",
577
+ // `image` is an embedded base64 thumbnail — derive a direct YouTube
578
+ // thumbnail from the link instead.
579
+ thumbnailUrl: buildYoutubeThumbnailUrl(r.link || "") ?? "",
580
+ source: SOURCE,
581
+ views: 0
582
+ };
540
583
  }
541
584
 
542
585
  // src/tools/bright-data/video-search.tool.ts
@@ -571,19 +614,7 @@ function createBrightDataVideoSearch(deps) {
571
614
  });
572
615
  const videos = data.organic ?? [];
573
616
  if (!videos.length) return { results: [] };
574
- const results = videos.map((r) => ({
575
- title: r.title || "",
576
- link: r.link || "",
577
- snippet: r.description || "",
578
- channel: "",
579
- duration: r.duration || "",
580
- date: "",
581
- // `image` is an embedded base64 thumbnail — derive a direct YouTube
582
- // thumbnail from the link instead.
583
- thumbnailUrl: buildYoutubeThumbnailUrl(r.link || "") ?? "",
584
- source: SOURCE,
585
- views: 0
586
- }));
617
+ const results = videos.map(mapBrightDataVideoResult);
587
618
  deps.logger.log(`Bright Data video search returned ${results.length} results for "${searchQuery}"`);
588
619
  return { results };
589
620
  } catch (err) {
@@ -598,6 +629,18 @@ var brightDataWebSearchSchema = z.object({
598
629
  recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
599
630
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
600
631
  });
632
+
633
+ // src/tools/bright-data/helpers/map-bright-data-web-result.helper.ts
634
+ function mapBrightDataWebResult(r) {
635
+ return {
636
+ title: r.title,
637
+ snippet: r.description || "",
638
+ url: r.link,
639
+ source: SOURCE
640
+ };
641
+ }
642
+
643
+ // src/tools/bright-data/web-search.tool.ts
601
644
  function createBrightDataWebSearch(deps) {
602
645
  return tool({
603
646
  description: 'Search the web using Bright Data SERP API (Google). Returns organic results with titles, snippets, and links. Pass recency ("day"|"week"|"month"|"year") to restrict to fresh results. ' + STANDALONE_QUERY_TOOL_CLAUSE,
@@ -625,12 +668,7 @@ function createBrightDataWebSearch(deps) {
625
668
  deps.logger.warn(`Bright Data returned 0 results for "${query}"`);
626
669
  return { results: [] };
627
670
  }
628
- const results = organic.map((r) => ({
629
- title: r.title,
630
- snippet: r.description || "",
631
- url: r.link,
632
- source: SOURCE
633
- }));
671
+ const results = organic.map(mapBrightDataWebResult);
634
672
  deps.logger.log(`Bright Data returned ${results.length} results for "${query}"`);
635
673
  return { results };
636
674
  } catch (err) {
@@ -705,6 +743,16 @@ function pickMerchantResult(results, storeToken) {
705
743
  return void 0;
706
744
  }
707
745
 
746
+ // src/tools/helpers/apply-resolved-merchant-url.helper.ts
747
+ function applyResolvedMerchantUrl(offer, merchantUrlByLink) {
748
+ return merchantUrlByLink.has(offer.link) ? { ...offer, link: merchantUrlByLink.get(offer.link) } : offer;
749
+ }
750
+
751
+ // src/tools/helpers/map-organic-entry-to-url.helper.ts
752
+ function mapOrganicEntryToUrl(entry) {
753
+ return { url: entry.link };
754
+ }
755
+
708
756
  // src/tools/helpers/store-host-token.helper.ts
709
757
  var NON_IDENTITY_TOKENS = /* @__PURE__ */ new Set([
710
758
  // company/legal forms
@@ -772,30 +820,9 @@ function storeHostToken(source) {
772
820
  return source.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 0 && !NON_IDENTITY_TOKENS.has(token)).join("");
773
821
  }
774
822
 
775
- // src/tools/helpers/serper-shop-links.ts
823
+ // src/tools/helpers/resolve-offer-merchant-url.helper.ts
776
824
  var SERPER_SEARCH_URL = "https://google.serper.dev/search";
777
- var MAX_LINK_RESOLUTIONS = 12;
778
825
  var RESOLUTION_RESULT_COUNT = 3;
779
- async function resolveSerperShopOfferLinks(offers, deps) {
780
- const googleLinked = offers.filter((offer) => offer.link && isGoogleHostUrl(offer.link));
781
- if (googleLinked.length === 0) return offers;
782
- const resolvable = googleLinked.slice(0, MAX_LINK_RESOLUTIONS);
783
- const resolutions = await Promise.all(
784
- resolvable.map(async (offer) => ({
785
- offer,
786
- merchantUrl: await fetchMerchantUrl(offer, deps)
787
- }))
788
- );
789
- const merchantUrlByLink = /* @__PURE__ */ new Map();
790
- for (const { offer, merchantUrl } of resolutions) {
791
- if (merchantUrl) merchantUrlByLink.set(offer.link, merchantUrl);
792
- }
793
- if (merchantUrlByLink.size === 0) return offers;
794
- deps.logger.log(`Resolved ${merchantUrlByLink.size}/${googleLinked.length} Google shop link(s) to merchant URLs`);
795
- return offers.map(
796
- (offer) => merchantUrlByLink.has(offer.link) ? { ...offer, link: merchantUrlByLink.get(offer.link) } : offer
797
- );
798
- }
799
826
  async function fetchMerchantUrl(offer, deps) {
800
827
  const token = storeHostToken(offer.source ?? "");
801
828
  if (!token || !offer.title) return void 0;
@@ -819,14 +846,33 @@ async function fetchMerchantUrl(offer, deps) {
819
846
  );
820
847
  if (!res.ok) return void 0;
821
848
  const data = await res.json();
822
- return pickMerchantResult(
823
- (data.organic ?? []).map((entry) => ({ url: entry.link })),
824
- token
825
- );
849
+ return pickMerchantResult((data.organic ?? []).map(mapOrganicEntryToUrl), token);
826
850
  } catch {
827
851
  return void 0;
828
852
  }
829
853
  }
854
+ async function resolveOfferMerchantUrl(offer, deps) {
855
+ return {
856
+ offer,
857
+ merchantUrl: await fetchMerchantUrl(offer, deps)
858
+ };
859
+ }
860
+
861
+ // src/tools/helpers/serper-shop-links.ts
862
+ var MAX_LINK_RESOLUTIONS = 12;
863
+ async function resolveSerperShopOfferLinks(offers, deps) {
864
+ const googleLinked = offers.filter((offer) => offer.link && isGoogleHostUrl(offer.link));
865
+ if (googleLinked.length === 0) return offers;
866
+ const resolvable = googleLinked.slice(0, MAX_LINK_RESOLUTIONS);
867
+ const resolutions = await Promise.all(resolvable.map((offer) => resolveOfferMerchantUrl(offer, deps)));
868
+ const merchantUrlByLink = /* @__PURE__ */ new Map();
869
+ for (const { offer, merchantUrl } of resolutions) {
870
+ if (merchantUrl) merchantUrlByLink.set(offer.link, merchantUrl);
871
+ }
872
+ if (merchantUrlByLink.size === 0) return offers;
873
+ deps.logger.log(`Resolved ${merchantUrlByLink.size}/${googleLinked.length} Google shop link(s) to merchant URLs`);
874
+ return offers.map((offer) => applyResolvedMerchantUrl(offer, merchantUrlByLink));
875
+ }
830
876
  var variantRequestSchema = z.object({});
831
877
  function createVariantRequestTool(variant) {
832
878
  const descriptions = {
@@ -950,7 +996,8 @@ function createMemoryPartitionRecallTool(deps) {
950
996
  const lines = hits.map((hit) => {
951
997
  const who = hit.role === "user" ? "the user" : "you (assistant)";
952
998
  const when = hit.createdAt ? ` on ${new Date(hit.createdAt).toISOString().slice(0, 10)}` : "";
953
- return `- "${hit.text}" \u2014 stated by ${who}${when}`;
999
+ const contested = hit.isFriction ? " \u2014 \u26A0 CONTESTED (an open conflict exists; treat with caution)" : "";
1000
+ return `- "${hit.text}" \u2014 stated by ${who}${when}${contested}`;
954
1001
  });
955
1002
  return `YOUR MEMORY OF THIS USER (trusted statements they said or asked you to remember \u2014 answer from them and attribute them to the user; never present them as public web knowledge):
956
1003
  ${lines.join("\n")}`;
@@ -1000,6 +1047,18 @@ var serperBusinessReviewsSearchSchema = z.object({
1000
1047
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1001
1048
  });
1002
1049
 
1050
+ // src/tools/serper/helpers/map-serper-review-result.helper.ts
1051
+ function mapSerperReviewResult(r, placeName) {
1052
+ return {
1053
+ author: r.user?.name || "",
1054
+ snippet: r.snippet || "",
1055
+ rating: r.rating,
1056
+ date: r.isoDate || r.date || "",
1057
+ likes: r.likes ?? 0,
1058
+ place: placeName
1059
+ };
1060
+ }
1061
+
1003
1062
  // src/tools/serper/serper.constants.ts
1004
1063
  var HEADERS = (apiKey) => ({
1005
1064
  "X-API-KEY": apiKey,
@@ -1053,14 +1112,7 @@ function createSerperBusinessReviewsSearch(deps) {
1053
1112
  return { results: [] };
1054
1113
  }
1055
1114
  const placeName = data.placeInfo?.title || "";
1056
- const results = data.reviews.map((r) => ({
1057
- author: r.user?.name || "",
1058
- snippet: r.snippet || "",
1059
- rating: r.rating,
1060
- date: r.isoDate || r.date || "",
1061
- likes: r.likes ?? 0,
1062
- place: placeName
1063
- }));
1115
+ const results = data.reviews.map((r) => mapSerperReviewResult(r, placeName));
1064
1116
  return {
1065
1117
  results,
1066
1118
  place: data.placeInfo ? {
@@ -1085,6 +1137,21 @@ var serperImageSearchSchema = z.object({
1085
1137
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)"),
1086
1138
  recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION)
1087
1139
  });
1140
+
1141
+ // src/tools/serper/helpers/map-serper-image-result.helper.ts
1142
+ function mapSerperImageResult(r) {
1143
+ return {
1144
+ title: r.title || "",
1145
+ imageUrl: r.imageUrl || r.image || "",
1146
+ sourcePageUrl: r.link || "",
1147
+ width: r.imageWidth ?? r.width,
1148
+ height: r.imageHeight ?? r.height,
1149
+ source: r.source || "",
1150
+ domain: r.domain || ""
1151
+ };
1152
+ }
1153
+
1154
+ // src/tools/serper/image-search.tool.ts
1088
1155
  function createSerperImageSearch(deps) {
1089
1156
  return tool({
1090
1157
  description: 'Search for images using Serper.dev (Google Images). Returns image URLs, thumbnails, source pages, and dimensions. The tool prefers 2560\xD71440 (1440p) images and always enforces a minimum of 1280\xD7720 (720p). It passes the appropriate Google Images `tbs=isz:lt,islt:<bucket>` size filter server-side, drops any returned images whose dimensions are below 1280\xD7720, and rejects untrusted domains such as Google thumbnail proxies (encrypted-tbn*.gstatic.com, t*.gstatic.com), data URIs, localhost, and private IPs. You do not need to pass minWidth/minHeight for the default 720p floor. If the user asks for a higher resolution, pass minWidth/minHeight and the tool will pick the smallest Google bucket that can satisfy the requested area. Common reference: 1280\xD7720 (720p) ~0.9 MP, 1920\xD71080 (1080p) ~2 MP, 2560\xD71440 (1440p) ~3.7 MP, 3840\xD72160 (4K) ~8.3 MP. Pass recency ("day"|"week"|"month"|"year") to restrict to recently published images. ' + STANDALONE_QUERY_TOOL_CLAUSE,
@@ -1131,15 +1198,7 @@ function createSerperImageSearch(deps) {
1131
1198
  deps.logger.warn(`Serper.dev Image Search returned 0 results for "${query}"`);
1132
1199
  return { results: [] };
1133
1200
  }
1134
- const results = data.images.map((r) => ({
1135
- title: r.title || "",
1136
- imageUrl: r.imageUrl || r.image || "",
1137
- sourcePageUrl: r.link || "",
1138
- width: r.imageWidth ?? r.width,
1139
- height: r.imageHeight ?? r.height,
1140
- source: r.source || "",
1141
- domain: r.domain || ""
1142
- })).filter((r) => {
1201
+ const results = data.images.map(mapSerperImageResult).filter((r) => {
1143
1202
  if (!isTrustedImageUrl(r.imageUrl)) return false;
1144
1203
  const w = r.width ?? 0;
1145
1204
  const h = r.height ?? 0;
@@ -1157,6 +1216,20 @@ var serperNewsSearchSchema = z.object({
1157
1216
  recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1158
1217
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1159
1218
  });
1219
+
1220
+ // src/tools/serper/helpers/map-serper-news-result.helper.ts
1221
+ function mapSerperNewsResult(r) {
1222
+ return {
1223
+ title: r.title,
1224
+ snippet: r.snippet || "",
1225
+ url: r.link,
1226
+ source: r.source || "",
1227
+ date: r.date || "",
1228
+ imageUrl: r.imageUrl || ""
1229
+ };
1230
+ }
1231
+
1232
+ // src/tools/serper/news-search.tool.ts
1160
1233
  function createSerperNewsSearch(deps) {
1161
1234
  return tool({
1162
1235
  description: 'Search the latest news using Serper.dev. Returns headlines, sources, dates, and snippets. Pass recency ("day"|"week"|"month"|"year") to restrict to a recent period. ' + STANDALONE_QUERY_TOOL_CLAUSE,
@@ -1186,14 +1259,7 @@ function createSerperNewsSearch(deps) {
1186
1259
  if (!res.ok) return { results: [] };
1187
1260
  const data = await res.json();
1188
1261
  if (!data.news?.length) return { results: [] };
1189
- const results = data.news.map((r) => ({
1190
- title: r.title,
1191
- snippet: r.snippet || "",
1192
- url: r.link,
1193
- source: r.source || "",
1194
- date: r.date || "",
1195
- imageUrl: r.imageUrl || ""
1196
- }));
1262
+ const results = data.news.map(mapSerperNewsResult);
1197
1263
  return { results };
1198
1264
  }
1199
1265
  });
@@ -1205,6 +1271,24 @@ var serperPlacesSearchSchema = z.object({
1205
1271
  count: z.number().optional().describe("Number of results (max 100)"),
1206
1272
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1207
1273
  });
1274
+
1275
+ // src/tools/serper/helpers/map-serper-place-result.helper.ts
1276
+ function mapSerperPlaceResult(r) {
1277
+ return {
1278
+ title: r.title,
1279
+ address: r.address || "",
1280
+ phoneNumber: r.phoneNumber || "",
1281
+ latitude: r.latitude,
1282
+ longitude: r.longitude,
1283
+ rating: r.rating,
1284
+ ratingCount: r.ratingCount,
1285
+ type: r.type || "",
1286
+ website: r.website || "",
1287
+ cid: r.cid || ""
1288
+ };
1289
+ }
1290
+
1291
+ // src/tools/serper/places-search.tool.ts
1208
1292
  function createSerperPlacesSearch(deps) {
1209
1293
  return tool({
1210
1294
  description: 'Search for places and businesses using Serper.dev (Google Maps). Returns addresses, phone numbers, ratings, review counts, and coordinates. Phrase the query like a Google Maps search: a business name, a business type, or a business type plus location (e.g. "MediaMarkt Berlin", "coffee shops in Munich").',
@@ -1235,18 +1319,7 @@ function createSerperPlacesSearch(deps) {
1235
1319
  deps.logger.warn(`Serper.dev Places returned 0 results for "${query}"`);
1236
1320
  return { results: [] };
1237
1321
  }
1238
- const results = data.places.map((r) => ({
1239
- title: r.title,
1240
- address: r.address || "",
1241
- phoneNumber: r.phoneNumber || "",
1242
- latitude: r.latitude,
1243
- longitude: r.longitude,
1244
- rating: r.rating,
1245
- ratingCount: r.ratingCount,
1246
- type: r.type || "",
1247
- website: r.website || "",
1248
- cid: r.cid || ""
1249
- }));
1322
+ const results = data.places.map(mapSerperPlaceResult);
1250
1323
  return { results };
1251
1324
  }
1252
1325
  });
@@ -1258,6 +1331,22 @@ var serperShoppingSearchSchema = z.object({
1258
1331
  count: z.number().optional().describe("Number of results (max 100)"),
1259
1332
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1260
1333
  });
1334
+
1335
+ // src/tools/serper/helpers/map-serper-shopping-result.helper.ts
1336
+ function mapSerperShoppingResult(r) {
1337
+ return {
1338
+ title: r.title,
1339
+ price: r.price || "",
1340
+ link: r.link || "",
1341
+ source: r.source || "",
1342
+ imageUrl: r.imageUrl || "",
1343
+ delivery: r.delivery || "",
1344
+ rating: r.rating,
1345
+ ratingCount: r.ratingCount
1346
+ };
1347
+ }
1348
+
1349
+ // src/tools/serper/shopping-search.tool.ts
1261
1350
  function createSerperShoppingSearch(deps) {
1262
1351
  return tool({
1263
1352
  description: 'Search for products using Serper.dev (Google Shopping). Returns prices, sellers, delivery info, images, and per-offer ratings. Phrase the query as the bare product name with model number (e.g. "Sony WH-1000XM5") \u2014 do NOT add words like "review", "test", or long descriptive sentences, they hurt shopping result quality.',
@@ -1290,16 +1379,7 @@ function createSerperShoppingSearch(deps) {
1290
1379
  deps.logger.warn(`Serper.dev Shopping returned 0 results for "${query}"`);
1291
1380
  return { results: [] };
1292
1381
  }
1293
- const results = data.shopping.map((r) => ({
1294
- title: r.title,
1295
- price: r.price || "",
1296
- link: r.link || "",
1297
- source: r.source || "",
1298
- imageUrl: r.imageUrl || "",
1299
- delivery: r.delivery || "",
1300
- rating: r.rating,
1301
- ratingCount: r.ratingCount
1302
- }));
1382
+ const results = data.shopping.map(mapSerperShoppingResult);
1303
1383
  return {
1304
1384
  results: await resolveSerperShopOfferLinks(results, {
1305
1385
  apiKey: cfg.apiKey,
@@ -1333,6 +1413,29 @@ function repairVideoLink(link) {
1333
1413
  return CONTAMINATION_PATTERN.test(link) ? void 0 : link;
1334
1414
  }
1335
1415
 
1416
+ // src/tools/serper/helpers/map-serper-video-result.helper.ts
1417
+ function mapSerperVideoResult(input) {
1418
+ const { r, link } = input;
1419
+ return {
1420
+ title: r.title,
1421
+ link,
1422
+ // Keep the provider's original link when it had to be repaired —
1423
+ // auditability for provider-side payload corruption.
1424
+ ...link !== r.link ? { originalLink: r.link } : {},
1425
+ snippet: r.snippet || "",
1426
+ channel: r.channel || "",
1427
+ duration: r.duration || "",
1428
+ date: r.date || "",
1429
+ // Serper thumbnails are Google proxy images (blocked by our image
1430
+ // trust rules) — derive a direct thumbnail for YouTube instead.
1431
+ // maxresdefault is not guaranteed to exist; consumers degrade to
1432
+ // hqdefault/mqdefault on failure.
1433
+ thumbnailUrl: buildYoutubeThumbnailUrl(link) ?? "",
1434
+ source: r.source || "",
1435
+ views: r.views ?? 0
1436
+ };
1437
+ }
1438
+
1336
1439
  // src/tools/serper/video-search.tool.ts
1337
1440
  function createSerperVideoSearch(deps) {
1338
1441
  return tool({
@@ -1368,24 +1471,7 @@ function createSerperVideoSearch(deps) {
1368
1471
  const results = data.videos.flatMap((r) => {
1369
1472
  const link = repairVideoLink(r.link);
1370
1473
  return link ? [{ r, link }] : [];
1371
- }).slice(0, num).map(({ r, link }) => ({
1372
- title: r.title,
1373
- link,
1374
- // Keep the provider's original link when it had to be repaired —
1375
- // auditability for provider-side payload corruption.
1376
- ...link !== r.link ? { originalLink: r.link } : {},
1377
- snippet: r.snippet || "",
1378
- channel: r.channel || "",
1379
- duration: r.duration || "",
1380
- date: r.date || "",
1381
- // Serper thumbnails are Google proxy images (blocked by our image
1382
- // trust rules) — derive a direct thumbnail for YouTube instead.
1383
- // maxresdefault is not guaranteed to exist; consumers degrade to
1384
- // hqdefault/mqdefault on failure.
1385
- thumbnailUrl: buildYoutubeThumbnailUrl(link) ?? "",
1386
- source: r.source || "",
1387
- views: r.views ?? 0
1388
- }));
1474
+ }).slice(0, num).map(mapSerperVideoResult);
1389
1475
  deps.logger.log(`Serper.dev Video search returned ${results.length} results for "${searchQuery}"`);
1390
1476
  return { results };
1391
1477
  }
@@ -1396,6 +1482,18 @@ var serperWebSearchSchema = z.object({
1396
1482
  recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1397
1483
  lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1398
1484
  });
1485
+
1486
+ // src/tools/serper/helpers/map-serper-web-result.helper.ts
1487
+ function mapSerperWebResult(r) {
1488
+ return {
1489
+ title: r.title,
1490
+ snippet: r.snippet || "",
1491
+ url: r.link,
1492
+ source: "serper"
1493
+ };
1494
+ }
1495
+
1496
+ // src/tools/serper/web-search.tool.ts
1399
1497
  function createSerperWebSearch(deps) {
1400
1498
  return tool({
1401
1499
  description: 'Search the web using Serper.dev (Google results). Returns organic results with titles, snippets, and links. Pass recency ("day"|"week"|"month"|"year") to restrict to fresh results. ' + STANDALONE_QUERY_TOOL_CLAUSE,
@@ -1430,12 +1528,7 @@ function createSerperWebSearch(deps) {
1430
1528
  deps.logger.warn(`Serper.dev returned 0 results for "${query}"`);
1431
1529
  return { results: [] };
1432
1530
  }
1433
- const results = data.organic.map((r) => ({
1434
- title: r.title,
1435
- snippet: r.snippet || "",
1436
- url: r.link,
1437
- source: "serper"
1438
- }));
1531
+ const results = data.organic.map(mapSerperWebResult);
1439
1532
  deps.logger.log(`Serper.dev returned ${results.length} results for "${query}"`);
1440
1533
  return { results };
1441
1534
  }
@@ -1550,6 +1643,35 @@ function createWebFetchTool() {
1550
1643
  }
1551
1644
  });
1552
1645
  }
1646
+
1647
+ // src/tools/youtube/helpers/map-youtube-video-result.helper.ts
1648
+ function formatIsoDuration(iso) {
1649
+ const match = iso?.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/);
1650
+ if (!match) return "";
1651
+ const hours = Number(match[1] ?? 0);
1652
+ const minutes = Number(match[2] ?? 0);
1653
+ const seconds = Number(match[3] ?? 0);
1654
+ const pad = (n) => String(n).padStart(2, "0");
1655
+ return hours ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
1656
+ }
1657
+ function mapYoutubeVideoResult(item, stats) {
1658
+ const snippet = item.snippet ?? {};
1659
+ const id = item.id.videoId;
1660
+ const detail = stats.get(id);
1661
+ const thumbs = snippet.thumbnails ?? {};
1662
+ return {
1663
+ title: snippet.title ?? "",
1664
+ link: `https://www.youtube.com/watch?v=${id}`,
1665
+ snippet: snippet.description ?? "",
1666
+ channel: snippet.channelTitle ?? "",
1667
+ duration: formatIsoDuration(detail?.duration),
1668
+ date: snippet.publishedAt ?? "",
1669
+ thumbnailUrl: thumbs.maxres?.url ?? thumbs.high?.url ?? thumbs.medium?.url ?? "",
1670
+ source: "youtube",
1671
+ views: detail?.viewCount ?? 0,
1672
+ lang: detail?.lang
1673
+ };
1674
+ }
1553
1675
  var youtubeVideoSearchSchema = z.object({
1554
1676
  query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Add the video type (e.g. review, trailer, tutorial, gameplay).`),
1555
1677
  count: z.number().optional().describe("Number of results (max 50)"),
@@ -1580,15 +1702,6 @@ async function readErrorReason(res) {
1580
1702
  return void 0;
1581
1703
  }
1582
1704
  }
1583
- function formatIsoDuration(iso) {
1584
- const match = iso?.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/);
1585
- if (!match) return "";
1586
- const hours = Number(match[1] ?? 0);
1587
- const minutes = Number(match[2] ?? 0);
1588
- const seconds = Number(match[3] ?? 0);
1589
- const pad = (n) => String(n).padStart(2, "0");
1590
- return hours ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
1591
- }
1592
1705
  async function fetchYoutubeVideoStats(videoIds, apiKey, logger) {
1593
1706
  const params = new URLSearchParams({
1594
1707
  part: "statistics,contentDetails,snippet",
@@ -1668,24 +1781,7 @@ function createYoutubeVideoSearch(deps) {
1668
1781
  cfg.apiKey,
1669
1782
  deps.logger
1670
1783
  );
1671
- const results = items.map((item) => {
1672
- const snippet = item.snippet ?? {};
1673
- const id = item.id.videoId;
1674
- const detail = stats.get(id);
1675
- const thumbs = snippet.thumbnails ?? {};
1676
- return {
1677
- title: snippet.title ?? "",
1678
- link: `https://www.youtube.com/watch?v=${id}`,
1679
- snippet: snippet.description ?? "",
1680
- channel: snippet.channelTitle ?? "",
1681
- duration: formatIsoDuration(detail?.duration),
1682
- date: snippet.publishedAt ?? "",
1683
- thumbnailUrl: thumbs.maxres?.url ?? thumbs.high?.url ?? thumbs.medium?.url ?? "",
1684
- source: "youtube",
1685
- views: detail?.viewCount ?? 0,
1686
- lang: detail?.lang
1687
- };
1688
- });
1784
+ const results = items.map((item) => mapYoutubeVideoResult(item, stats));
1689
1785
  deps.logger.log(`YouTube video search returned ${results.length} results for "${query}"`);
1690
1786
  return { results };
1691
1787
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@triplef/agent",
3
3
  "description": "tripleF (3F) agent domain — structured-output schemas, prompt builders, and model tools shared across the apps.",
4
- "version": "0.1.11",
4
+ "version": "0.1.12",
5
5
  "packageManager": "pnpm@11.25.0",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "turndown": "^7.2.4"
61
61
  },
62
62
  "peerDependencies": {
63
- "ai": "^7.0.85",
63
+ "ai": "^7.0.87",
64
64
  "zod": "^4.5.4"
65
65
  },
66
66
  "peerDependenciesMeta": {
@@ -92,7 +92,7 @@
92
92
  "ts-unused-exports": "^11.0.1",
93
93
  "tsup": "^8.5.1",
94
94
  "typescript": "^5.9.3",
95
- "typescript-eslint": "^8.68.0",
95
+ "typescript-eslint": "^8.69.0",
96
96
  "vitest": "^4.1.11"
97
97
  }
98
98
  }
@@ -10,6 +10,7 @@ declare const TEMPLATES: readonly ["article", "news", "describe", "compare", "oc
10
10
  type TemplateName = (typeof TEMPLATES)[number];
11
11
  declare const IntentSchema: z.ZodObject<{
12
12
  template: z.ZodEnum<{
13
+ text: "text";
13
14
  article: "article";
14
15
  news: "news";
15
16
  describe: "describe";
@@ -24,7 +25,6 @@ declare const IntentSchema: z.ZodObject<{
24
25
  stockmarketitem: "stockmarketitem";
25
26
  stockmarketlist: "stockmarketlist";
26
27
  merge: "merge";
27
- text: "text";
28
28
  }>;
29
29
  prompt: z.ZodDefault<z.ZodString>;
30
30
  tools: z.ZodArray<z.ZodEnum<{