@triplef/agent 0.1.17 → 0.1.18

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.
@@ -1,9 +1,4 @@
1
1
  import { z } from 'zod';
2
- import '@triplef/helpers/object-io';
3
- import 'ai';
4
- import '@mozilla/readability';
5
- import 'linkedom';
6
- import 'turndown';
7
2
  import { limitText } from '@triplef/helpers/limit-text';
8
3
 
9
4
  // src/prompts/shared/multimodal-policy.prompt.ts
@@ -162,877 +157,9 @@ function buildLocalizationRule(language) {
162
157
  - Do not translate internal markers, tool arguments, or your reasoning.`;
163
158
  }
164
159
 
165
- // src/schemas/constants/embeddable-video-providers.constant.ts
166
- var EMBEDDABLE_VIDEO_PROVIDER_LABELS = "YouTube, Vimeo, Dailymotion, Loom, Wistia";
167
- var EMBEDDABLE_VIDEO_PROVIDER_CLAUSE = `Only return URLs from supported embeddable providers: ${EMBEDDABLE_VIDEO_PROVIDER_LABELS}, or direct video files. Reject Instagram, Facebook, TikTok, Twitch, X/Twitter, and other unreliable platforms.`;
168
-
169
- // src/schemas/constants/media-counts.constant.ts
170
- var DEFAULT_MEDIA_COUNT = 6;
171
- var MORE_MEDIA_COUNT = 12;
172
-
173
- // src/schemas/constants/url-trust.constants.ts
174
- var BLOCKED_URL_HOSTS = /* @__PURE__ */ new Set([
175
- // Google thumbnail / static asset proxies
176
- "encrypted-tbn0.gstatic.com",
177
- "encrypted-tbn1.gstatic.com",
178
- "encrypted-tbn2.gstatic.com",
179
- "encrypted-tbn3.gstatic.com",
180
- "t0.gstatic.com",
181
- "t1.gstatic.com",
182
- "t2.gstatic.com",
183
- "t3.gstatic.com",
184
- "t4.gstatic.com",
185
- "t5.gstatic.com",
186
- "t6.gstatic.com",
187
- "t7.gstatic.com",
188
- "t8.gstatic.com",
189
- "t9.gstatic.com",
190
- "t10.gstatic.com",
191
- "news.gstatic.com",
192
- "books.gstatic.com",
193
- "maps.gstatic.com",
194
- "lh1.googleusercontent.com",
195
- "lh2.googleusercontent.com",
196
- "lh3.googleusercontent.com",
197
- "lh4.googleusercontent.com",
198
- "lh5.googleusercontent.com",
199
- "lh6.googleusercontent.com"
200
- ]);
201
- var NON_PAGE_EXTENSIONS = /\.(js|css|json|xml|svg|png|jpg|jpeg|gif|webp|ico|mp4|webm|ogg|mov|mkv|avi|flv|m3u8|mpd|pdf|zip|tar|gz|rar|exe|dmg|pkg|deb|rpm|woff|woff2|ttf|otf|eot)(\?.*)?$/i;
202
- var EncyclopediaClassifySchema = z.object({
203
- /**
204
- * One broad lowercase PLURAL family label for the document (e.g. `games`,
205
- * `work`, `health`) — the constellation's cluster tier. Never a specific
206
- * entity, product, company, or title.
207
- */
208
- category: z.string(),
209
- /**
210
- * The narrow topic the document is about (e.g. `wuthering waves`,
211
- * `q3 budget`) — the constellation's cluster tier. A short, specific,
212
- * reusable label, not a sentence and not a URL.
213
- */
214
- topic: z.string()
215
- });
216
-
217
- // src/schemas/helpers/tools/tool-registry.constants.ts
218
- var VARIANT_NAMES = ["grayscale", "denoised", "sharpened", "clahe"];
219
- var TOOL_DESCRIPTIONS = {
220
- brightDataWebSearch: "Search the web using Bright Data SERP API (Google). Returns organic results with titles, snippets, and links. Supports an optional recency window (day/week/month/year) for fresh results.",
221
- brightDataImageSearch: "Search for images using Bright Data SERP API (Google Images). Returns image URLs, source pages, and dimensions. Enforces a minimum of 1280\xD7720 (720p).",
222
- brightDataNewsSearch: "Search latest news using Bright Data SERP API. Returns headlines, sources, dates, and snippets. Supports an optional recency window (day/week/month/year).",
223
- brightDataPlacesSearch: "Search places and businesses using Bright Data SERP API (Google Maps). Returns addresses, phone numbers, ratings, review counts, and coordinates. Query with a business name or business type plus location.",
224
- brightDataShoppingSearch: "Search for products using Bright Data SERP API (Google Shopping). Returns prices, sellers, images, and ratings. Query with the bare product name and model number.",
225
- brightDataVideoSearch: `Search for videos using Bright Data SERP API. Returns titles, links, channel names, duration, and publish dates. Supports an optional recency window (day/week/month/year) for fresh results. ${EMBEDDABLE_VIDEO_PROVIDER_CLAUSE}`,
226
- brightDataWebpageScrape: "Fetch and render a full webpage using Bright Data Web Unlocker API. Returns clean Markdown text. Use for pages behind anti-bot protection that plain fetch cannot reach.",
227
- serperWebSearch: "Search the web using Serper.dev (Google). Returns organic results with titles, snippets, and links. Supports an optional recency window (day/week/month/year) for fresh results.",
228
- serperImageSearch: "Search for images using Serper.dev (Google Images). Prefers 2560\xD71440 (1440p) images and enforces a minimum of 1280\xD7720 (720p) via the Google Images tbs size filter and a client-side dimension filter. Supports an optional recency window (day/week/month/year).",
229
- serperNewsSearch: "Search latest news using Serper.dev. Returns headlines, sources, dates, and snippets. Supports an optional recency window (day/week/month/year).",
230
- serperPlacesSearch: "Search places and businesses using Serper.dev (Google Maps). Returns addresses, phone numbers, ratings, review counts, and coordinates. Query with a business name or business type plus location.",
231
- serperShoppingSearch: "Search for products using Serper.dev (Google Shopping). Returns prices, sellers, delivery info, images, and per-offer ratings. Query with the bare product name and model number.",
232
- serperBusinessReviewsSearch: "Fetch Google Maps reviews for a specific business or place using Serper.dev. Returns reviewer snippets with author names, star ratings, and dates. Use for seller/business reputation, not editorial product reviews.",
233
- serperVideoSearch: `Search for videos using Serper.dev. Returns titles, links, channel names, duration, and publish dates. Supports an optional recency window (day/week/month/year) for fresh results. ${EMBEDDABLE_VIDEO_PROVIDER_CLAUSE}`,
234
- serperWebpageScrape: "Fetch and render a full webpage using Serper.dev scrape API. Returns clean rendered text with its title.",
235
- youtubeVideoSearch: "Search YouTube using the official YouTube Data API. Returns titles, links, channel names, durations, view counts, upload dates, and direct thumbnails. All results are embeddable YouTube videos.",
236
- eodhdSearch: 'Resolve a company, ETF, or index name to an EODHD ticker code (e.g. "Nvidia" \u2192 NVDA.US). Use before fetching quotes, history, technicals, or news for a named entity.',
237
- eodhdQuote: "Fetch the current (delayed) quote for one or more EODHD tickers \u2014 last price, change, change %, open/high/low, volume, previous close.",
238
- eodhdHistory: "Fetch end-of-day OHLCV price history for an EODHD ticker. Returns a compact summary plus the full time series as chartData for the client chart. Use for the time-value chart and buy/sell pressure (volume).",
239
- eodhdTechnical: "Fetch a technical indicator series (RSI, MACD, ADX, SMA, EMA, BBANDS, ATR, \u2026) for an EODHD ticker. Use to gauge buy/sell pressure and momentum.",
240
- eodhdIntraday: "Fetch intraday OHLCV bars for an EODHD ticker and return a per-day, per-price-band volume profile as chartData for the client heatmap. Use to render a volume heatmap across fixed price bands.",
241
- eodhdNews: "Fetch recent financial news for an EODHD ticker \u2014 headlines, links, sources, publish dates. Use to ground a stock-market answer in what is happening around a company.",
242
- eodhdFundamentals: "Fetch company fundamentals for an EODHD ticker \u2014 general info, valuation, and key financial highlights (sector, market cap, P/E, revenue, margins).",
243
- webFetch: "Fetch the full content of a specific URL (e.g. a search result page) as clean Markdown. Prefer fetching the most relevant search results to ground the answer in full source text rather than relying on snippets alone.",
244
- browser_navigate: "Control a real browser: navigate to a URL. Use for interactive browsing \u2014 JS-heavy pages, content behind clicks, tabs, scrolling, or forms \u2014 that static search/fetch cannot reach. Follow up with browser_snapshot to read the page before acting on it.",
245
- browser_navigate_back: "Go back to the previous page in the browser history.",
246
- browser_snapshot: "Read the current browser page as an accessibility snapshot (structured text with element refs). The primary way to see page content and obtain the refs that browser_click/browser_type need.",
247
- browser_click: "Click an element in the browser, referenced by a ref from browser_snapshot.",
248
- browser_type: "Type text into an editable browser element, referenced by a ref from browser_snapshot, optionally submitting with Enter.",
249
- browser_fill_form: "Fill multiple form fields in the browser in one call.",
250
- browser_select_option: "Select an option in a dropdown in the browser.",
251
- browser_press_key: "Press a keyboard key in the browser (Enter, Tab, arrows).",
252
- browser_wait_for: "Wait for text to appear or disappear, or for a time period, in the browser.",
253
- browser_take_screenshot: "Take a screenshot of the current browser page or a specific element.",
254
- browser_tabs: "List, create, close, or switch browser tabs.",
255
- browser_console_messages: "Get console messages from the browser. Use to diagnose JavaScript errors on a page (e.g. when testing a web app).",
256
- browser_network_requests: "List network requests the browser made since page load. Use to diagnose failed or slow requests (e.g. when testing a web app).",
257
- browser_verify_element_visible: "Assert that an element is visible on the current browser page.",
258
- browser_verify_text_visible: "Assert that text is visible on the current browser page.",
259
- requestGrayscale: "Request a grayscale version of the images. Use when color noise or color information is irrelevant, for example when reading text or analyzing shapes.",
260
- requestDenoised: "Request a denoised (blurred) version of the images. Use when the original has noise, grain, or artifacts that hide details.",
261
- requestSharpened: "Request a sharpened version of the images. Use when edges or fine details are blurry.",
262
- requestClahe: "Request a CLAHE (contrast-enhanced) version of the images. Use when details are hidden in shadows or highlights.",
263
- "memory-partition-remember": `Store into the user's fact partition (memory-partition): notable facts about subjects they care about (favorites, interests, projects, followed stocks, people, past topics), preferences and durable details they state, and anything they explicitly ask you to remember. Storing gathered knowledge and noticed preferences is expected \u2014 do not wait for an explicit "remember" instruction.`,
264
- "memory-partition-recall": "Retrieve from the user's fact partition (memory-partition) \u2014 things they told you in past conversations or asked you to remember. These are trusted user statements, not public facts; attribute them to the user and prefer them over web results for anything personal. Check this tool whenever a request touches a subject this user has cared about before.",
265
- "memory-partition-delete": "Delete one exact fact record from the user's fact partition (memory-partition), quoted verbatim from a memory-partition-recall result. Never delete on a guess: recall first, delete the verbatim statement. Needs memory-partition-recall alongside it.",
266
- "memory-cognition-remember": "Store one derived insight into your cognition space (memory-cognition) \u2014 your own understanding of the user (inferred traits, standing interests, working nuances, connections between facts). Use for what you LEARN about the user, not what they stated; stated facts belong in memory-partition-remember.",
267
- "memory-cognition-forget": "Wipe your entire cognition space (memory-cognition) of the user \u2014 the structured profile AND every derived insight \u2014 only when the user asks you to forget your learned understanding of them or to start over. Fact records in the partition are untouched."
268
- };
269
- var BROWSER_TOOL_NAMES = [
270
- "browser_navigate",
271
- "browser_navigate_back",
272
- "browser_snapshot",
273
- "browser_click",
274
- "browser_type",
275
- "browser_fill_form",
276
- "browser_select_option",
277
- "browser_press_key",
278
- "browser_wait_for",
279
- "browser_take_screenshot",
280
- "browser_tabs",
281
- "browser_console_messages",
282
- "browser_network_requests",
283
- "browser_verify_element_visible",
284
- "browser_verify_text_visible"
285
- ];
286
- var TOOL_NAMES = [
287
- "webFetch",
288
- "brightDataWebSearch",
289
- "brightDataImageSearch",
290
- "brightDataNewsSearch",
291
- "brightDataPlacesSearch",
292
- "brightDataShoppingSearch",
293
- "brightDataVideoSearch",
294
- "brightDataWebpageScrape",
295
- "serperWebSearch",
296
- "serperImageSearch",
297
- "serperNewsSearch",
298
- "serperPlacesSearch",
299
- "serperShoppingSearch",
300
- "serperBusinessReviewsSearch",
301
- "serperVideoSearch",
302
- "serperWebpageScrape",
303
- "youtubeVideoSearch",
304
- "eodhdSearch",
305
- "eodhdQuote",
306
- "eodhdHistory",
307
- "eodhdTechnical",
308
- "eodhdIntraday",
309
- "eodhdNews",
310
- "eodhdFundamentals",
311
- "requestGrayscale",
312
- "requestDenoised",
313
- "requestSharpened",
314
- "requestClahe",
315
- "memory-partition-remember",
316
- "memory-partition-recall",
317
- "memory-partition-delete",
318
- "memory-cognition-remember",
319
- "memory-cognition-forget",
320
- ...BROWSER_TOOL_NAMES
321
- ];
322
-
323
- // src/schemas/helpers/url-trust/is-private-or-localhost.helper.ts
324
- function isPrivateOrLocalhost(hostname) {
325
- const lower = hostname.toLowerCase();
326
- if (lower === "localhost" || lower.endsWith(".localhost")) return true;
327
- if (lower === "127.0.0.1" || lower === "0.0.0.0") return true;
328
- if (lower.startsWith("10.")) return true;
329
- if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(lower)) return true;
330
- if (lower.startsWith("192.168.")) return true;
331
- if (lower.startsWith("169.254.")) return true;
332
- return false;
333
- }
334
-
335
- // src/schemas/helpers/url-trust/is-trusted-url.helper.ts
336
- function isYouTubeNonVideoPath(hostname, pathname) {
337
- const lowerHost = hostname.toLowerCase();
338
- if (lowerHost !== "youtube.com" && lowerHost !== "www.youtube.com" && lowerHost !== "m.youtube.com") {
339
- return false;
340
- }
341
- const lowerPath = pathname.toLowerCase();
342
- if (lowerPath.startsWith("/s/") || lowerPath.startsWith("/static/") || lowerPath.startsWith("/js/") || lowerPath.startsWith("/css/") || lowerPath.startsWith("/fonts/") || lowerPath.startsWith("/yts/") || lowerPath.startsWith("/iframe_api") || lowerPath.startsWith("/sw.js") || lowerPath.startsWith("/embed_config") || lowerPath.startsWith("/get_video_info") || lowerPath.startsWith("/api/")) {
343
- return true;
344
- }
345
- return false;
346
- }
347
- function isTrustedUrl(url, options = {}) {
348
- if (!url) return false;
349
- let parsed;
350
- try {
351
- parsed = new URL(url);
352
- } catch {
353
- return false;
354
- }
355
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
356
- return false;
357
- }
358
- const hostname = parsed.hostname.toLowerCase();
359
- const pathname = parsed.pathname;
360
- if (BLOCKED_URL_HOSTS.has(hostname)) {
361
- return false;
362
- }
363
- if (isYouTubeNonVideoPath(hostname, pathname)) {
364
- return false;
365
- }
366
- if (NON_PAGE_EXTENSIONS.test(pathname)) {
367
- return false;
368
- }
369
- if (!options.allowPrivate && isPrivateOrLocalhost(hostname)) {
370
- return false;
371
- }
372
- return true;
373
- }
374
- function safeUrl(message = "must be a safe URL") {
375
- const text = typeof message === "string" ? message : message.message;
376
- return z.string().url({ message: "must be a valid URL" }).refine((value) => isTrustedUrl(value, { allowPrivate: false }), {
377
- message: text
378
- });
379
- }
380
- var LOCAL_STORAGE_URL_PATTERN = /^\/api\/v1\/storage\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9]+$/;
381
- function isAbsoluteHttpUrl(value) {
382
- try {
383
- const protocol = new URL(value).protocol;
384
- return protocol === "http:" || protocol === "https:";
385
- } catch {
386
- return false;
387
- }
388
- }
389
- function safeVideoUrl(message = "must be a safe video URL") {
390
- const text = typeof message === "string" ? message : message.message;
391
- return z.string().refine(isAbsoluteHttpUrl, { message: text });
392
- }
393
- function safeVideoUrlOrEmpty(message = "must be a safe video URL") {
394
- return safeVideoUrl(message).optional().or(z.literal(""));
395
- }
396
- function safeMediaUrl(message = "must be a safe media URL") {
397
- const text = typeof message === "string" ? message : message.message;
398
- return z.string().refine((value) => LOCAL_STORAGE_URL_PATTERN.test(value) || isAbsoluteHttpUrl(value), { message: text });
399
- }
400
- function safeMediaUrlOrEmpty(message = "must be a safe media URL") {
401
- return safeMediaUrl(message).optional().or(z.literal(""));
402
- }
403
- var INDENT = " ";
404
- function formatZodShape(schema, options = {}) {
405
- const jsonSchema = z.toJSONSchema(schema);
406
- return renderNode(jsonSchema, 0, options);
407
- }
408
- function renderNode(node, depth, options) {
409
- if (node.type === "object" && node.properties) return renderObject(node, depth, options);
410
- if (node.type === "object" && node.additionalProperties) {
411
- return `{ [key]: ${renderNode(node.additionalProperties, depth, options)} }`;
412
- }
413
- if (node.type === "array" && node.items) return `[${renderNode(node.items, depth, options)}]`;
414
- const anyOfBranches = node.anyOf?.filter((branch) => branch.type !== "null");
415
- if (anyOfBranches?.length) return anyOfBranches.map((branch) => renderNode(branch, depth, options)).join(" | ");
416
- if (node.enum) return node.enum.map((value) => JSON.stringify(value)).join(" | ");
417
- if (node.type === "integer" || node.type === "number") return "number";
418
- if (node.type === "boolean") return "boolean";
419
- if (node.type === "string") return "string";
420
- if (node.type === "null") return "null";
421
- return "unknown";
422
- }
423
- function renderObject(node, depth, options) {
424
- const properties = node.properties ?? {};
425
- const required = new Set(node.required ?? []);
426
- const entries = Object.entries(properties);
427
- if (entries.length === 0) return "{}";
428
- const pad = INDENT.repeat(depth);
429
- const innerPad = INDENT.repeat(depth + 1);
430
- const lines = entries.map(([key, value]) => {
431
- const optional = required.has(key) ? "" : "?";
432
- const hint = options.overrides?.[key] ?? renderNode(value, depth + 1, options);
433
- return `${innerPad}${JSON.stringify(key)}${optional}: ${hint}`;
434
- });
435
- return `{
436
- ${lines.join(",\n")}
437
- ${pad}}`;
438
- }
439
- var DEFAULT_VARIANT_ID = "default";
440
- var TEMPLATES = [
441
- "article",
442
- "news",
443
- "describe",
444
- "compare",
445
- "ocr",
446
- "summary",
447
- "evaluation",
448
- "product",
449
- "shoplist",
450
- "imagelist",
451
- "videolist",
452
- "stockmarketitem",
453
- "stockmarketlist",
454
- "merge",
455
- "text"
456
- ];
457
- var ImagePlanSchema = z.object({
458
- resize: z.boolean().default(true).describe(
459
- "Whether to resize the original images before sending them to the response model. Default true when images are present."
460
- ),
461
- variants: z.array(z.enum(VARIANT_NAMES)).default([]).describe(
462
- "Optional preprocessing variants to generate for the images (grayscale, denoised, sharpened, clahe). Only use when they would materially improve analysis."
463
- )
464
- });
465
- var IntentSchema = z.object({
466
- template: z.enum(TEMPLATES).describe(
467
- 'Template name: "article" (in-depth research/report \u2014 extensive long-form composed from available snippets), "news" (current events \u2014 a compact brief composed from available snippets), "describe" (single/multi image description), "compare" (compare uploaded images only \u2014 information comparisons use "evaluation"), "ocr" (extract text from images), "summary" (recap prior conversation or topic without new images), "evaluation" (critique/assess something from the conversation), "product" (product details with shop offers and prices), "shoplist" (compact product/shop list for follow-up shopping questions about an already-introduced product), "imagelist" (a pure collection of images about a topic, no article), "videolist" (a pure list/playlist of videos about a topic, no article), "stockmarketitem" (a single stock/ETF/index with price chart, technicals, news, and a recommendation), "stockmarketlist" (a selection of stocks/indices with a market overview), "text" (free chat).'
468
- ),
469
- prompt: z.string().default(DEFAULT_VARIANT_ID).describe(
470
- 'Selected prompt variant for the template. Use "default" unless the user explicitly asks for a specific style.'
471
- ),
472
- tools: z.array(z.enum(TOOL_NAMES)).describe("List of tool names the model decided to invoke."),
473
- getDate: z.boolean().default(true).describe(
474
- 'Whether search queries should be anchored on the current date so results bias toward recent information. Default true. Set to false ONLY for timeless general-knowledge, historical, or conceptual requests where recency does not matter (e.g. "how does photosynthesis work", "history of the Roman Empire", "explain quantum entanglement", creative writing).'
475
- ),
476
- imageCount: z.preprocess((val) => val === null ? 0 : val, z.number().int().min(0).max(50).default(0)).describe(
477
- `Number of images to retrieve when an imageSearch tool is selected. Only set when the user explicitly requests a specific number; otherwise omit or set to 0 and the system will default to ${DEFAULT_MEDIA_COUNT}.`
478
- ),
479
- videoCount: z.preprocess((val) => val === null ? 0 : val, z.number().int().min(0).max(50).default(0)).describe(
480
- `Number of videos to retrieve when a videoSearch tool is selected. Only set when the user explicitly requests a specific number; otherwise omit or set to 0 and the system will default to ${DEFAULT_MEDIA_COUNT}.`
481
- ),
482
- reasoning: z.string().describe("Short explanation of why this template, prompt, and these tools were chosen."),
483
- contextSummary: z.string().default("").describe(
484
- "Query-focused extraction of the prior conversation context that the latest user message references or depends on: established topics/entities, key facts from prior answers the follow-up builds on, user constraints, and \u2014 for imagelist/videolist follow-ups \u2014 the previously shown image/video URLs verbatim. Empty if there is no relevant prior context."
485
- ),
486
- needsClarification: z.boolean().default(false).describe("When true, the request is too ambiguous to classify \u2014 set this instead of picking template/tools."),
487
- clarificationQuestion: z.string().nullable().optional().describe(
488
- "Concise, human-friendly question to ask the user when needsClarification is true. Ask what the user might have meant and offer the most likely interpretations as options. Do not hardcode wording; adapt tone and language to the user."
489
- ),
490
- language: z.string().nullable().optional().describe(
491
- "Two-letter ISO language code of the latest user message (e.g. 'en', 'de', 'ja', 'es'). Detect from the user's text."
492
- ),
493
- plan: z.object({
494
- images: ImagePlanSchema.optional().describe("Image processing plan. Only present when images are attached.")
495
- }).default({}).describe("Execution plan for the response step.")
496
- });
497
- var MemoryClusterSummarySchema = z.object({
498
- title: z.string().describe('A short noun-phrase label for the cluster (2\u20136 words), e.g. "gaming preferences" or "work projects".'),
499
- summary: z.string().describe(
500
- "One or two sentences summarizing what this cluster of memories is about \u2014 the shared theme, the key facts, and how they relate."
501
- )
502
- });
503
- var ConsolidationVerdictSchema = z.object({
504
- verdict: z.enum(["keep", "redundant", "merge"]).describe(
505
- "keep = the new fact adds information not covered by the candidates; redundant = fully covered already (same claim, no new detail, same polarity); merge = it refines/corrects/completes a candidate and mergedText carries the fuller statement."
506
- ),
507
- mergedText: z.string().optional().describe("Required with verdict=merge: one fuller self-contained statement (full restatement, never a diff).")
508
- });
509
- var MemoryEnrichmentSchema = z.object({
510
- /**
511
- * 2–6 stable, reusable lowercase topic labels for the record — the existing
512
- * tags kept plus any missing labels that would help topic-filtered recall.
513
- */
514
- tags: z.array(z.string())
515
- });
516
- var FACT_KINDS = [
517
- "preference",
518
- "decision",
519
- "state",
520
- "contact",
521
- "project",
522
- "possession",
523
- "relationship",
524
- "fact"
525
- ];
526
- var FACT_STABILITIES = ["durable", "volatile"];
527
- var ExtractedFactSchema = z.object({
528
- /**
529
- * The self-contained durable statement — third person, subject up front,
530
- * no "this"/"that" references.
531
- */
532
- text: z.string(),
533
- /**
534
- * The lowercase entity the fact is about (default `user`; a person,
535
- * product, or project name). Maintenance adjudication only ever compares
536
- * facts about the SAME subject.
537
- */
538
- subject: z.string().optional(),
539
- /**
540
- * One broad lowercase PLURAL family label for THIS fact (e.g. `stocks`,
541
- * `pets`, `games`) — inherits the turn-side category when omitted. Never
542
- * a specific entity, product, company, or game title.
543
- */
544
- category: z.string().optional(),
545
- /** What kind of durable thing this is (see FACT_KINDS). */
546
- kind: z.enum(FACT_KINDS),
547
- /** Whether a newer statement is expected to replace this one (see FACT_STABILITIES). */
548
- stability: z.enum(FACT_STABILITIES)
549
- });
550
- var ExtractionSchema = z.object({
551
- /**
552
- * Durable, self-contained facts worth remembering in a later, unrelated
553
- * conversation (preferences, decisions, contact details, project facts),
554
- * each carrying its maintenance metadata (subject, category, kind,
555
- * stability). Empty when nothing in the text is worth remembering.
556
- */
557
- facts: z.array(ExtractedFactSchema),
558
- /**
559
- * 2–6 stable, reusable lowercase topic labels describing the text; the open
560
- * vocabulary that powers topic-filtered recall. Tags are NARROW and
561
- * specific — entity names, product names, game titles (e.g. `amd`,
562
- * `stellar blade`, `stellar blade blood rain`).
563
- */
564
- tags: z.array(z.string()),
565
- /**
566
- * One broad lowercase PLURAL family label for the whole turn-side (e.g.
567
- * `stocks`, `pets`, `games`) — groups the narrow tags into one topic family
568
- * for the constellation's community tier and the relink job's per-category
569
- * passes, and backstops facts that omit their own `category`. Never a
570
- * specific entity, product, company, or game title: `amd` belongs under
571
- * `stocks`; `stellar blade` belongs under `games`. Optional: a turn with
572
- * nothing durable may omit it.
573
- */
574
- category: z.string().optional()
575
- });
576
- var nullishText = z.string().nullish();
577
- var nullishTopics = z.array(z.string()).nullish();
578
- var memoryCognitionProfileSchema = z.object({
579
- /** The user's name or preferred handle, when known. */
580
- name: nullishText,
581
- /** Primary conversation language (BCP-47-ish, e.g. "en", "de"). */
582
- language: nullishText,
583
- /** Coarse location the user works from (IANA name or city), when known. */
584
- timezone: nullishText,
585
- /** Demonstrated skills and domains (e.g. ["TypeScript", "NestJS"]). */
586
- expertise: nullishTopics,
587
- /** Active goals and aspirations — the most durable cognition there is. */
588
- goals: nullishTopics,
589
- /** How the user wants to be answered. */
590
- communication: z.object({
591
- style: nullishText,
592
- detailLevel: nullishText,
593
- formality: nullishText
594
- }).nullish(),
595
- /** Tooling / environment / format preferences (free-form key-value). */
596
- preferences: z.record(z.string(), z.string()).nullish(),
597
- likes: nullishTopics,
598
- dislikes: nullishTopics,
599
- /** Topics the user keeps returning to. */
600
- interests: nullishTopics,
601
- /**
602
- * Durable CONVICTIONS the AI synthesized about the user's world — the
603
- * routing-map topics of its conviction records (depth lives in the
604
- * conviction points at paths `convictions.<slug>`). Convictions are the
605
- * AI's own derived conclusions, never statements the user made.
606
- */
607
- convictions: nullishTopics,
608
- /** The AI's own identity as the user has shaped it (name, role, voice). */
609
- persona: z.object({
610
- /** The name the user gave the AI. */
611
- name: nullishText,
612
- /** The role the user assigned the AI (e.g. "coding assistant"). */
613
- role: nullishText,
614
- /** Short character description the user gave the AI. */
615
- personality: nullishText,
616
- /** How the AI introduces itself, when the user set one. */
617
- greeting: nullishText,
618
- /** How the AI should speak. */
619
- voice: z.object({
620
- tone: nullishText,
621
- formality: nullishText
622
- }).nullish()
623
- }).nullish(),
624
- /**
625
- * Learned corrections — behavioral rules the user taught the AI after it
626
- * got something wrong. Keyed by a short slug (a stable handle for
627
- * update/remove); the value is the imperative directive ("always …" /
628
- * "never …"), optionally with a brief why. Deep-merged like preferences.
629
- */
630
- corrections: z.record(z.string(), z.string()).nullish()
631
- });
632
- var memoryProfileInsightSchema = z.object({
633
- text: z.string(),
634
- path: z.string().optional()
635
- });
636
- var memoryProfileResponseSchema = z.preprocess(
637
- (value) => {
638
- if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
639
- const record = value;
640
- if (record.insights == null) record.insights = [];
641
- else if (Array.isArray(record.insights)) {
642
- record.insights = record.insights.map((item) => typeof item === "string" ? { text: item } : item);
643
- }
644
- return record;
645
- },
646
- z.object({
647
- profile: memoryCognitionProfileSchema.nullish(),
648
- insights: z.array(memoryProfileInsightSchema).max(8),
649
- /**
650
- * One sentence recording what THIS turn was about — the short-term
651
- * conversation memory (episode), recalled later by recency-blended probe.
652
- * Omitted (or null) when the turn had no substance.
653
- */
654
- episode: z.string().nullish()
655
- })
656
- );
657
- var cardSchema = z.object(
658
- {
659
- url: safeUrl({ message: "cards entries must have a valid url" }),
660
- title: z.string().optional(),
661
- description: z.string().optional(),
662
- linkLabel: z.string().optional()
663
- },
664
- { message: "cards entries must be objects with url" }
665
- );
666
- var referenceLineSchema = z.object(
667
- {
668
- value: z.number(),
669
- label: z.string().optional(),
670
- /** A theme token name, e.g. "accent-primary" or "status-error". */
671
- color: z.string().optional()
672
- },
673
- { message: "referenceLines entries must be objects with a numeric value" }
674
- );
675
- var markerSchema = z.object(
676
- {
677
- time: z.string().min(1, { message: "markers entries must have a time" }),
678
- position: z.enum(["aboveBar", "belowBar"]),
679
- /** A theme token name, e.g. "harmony-3" or "status-error". */
680
- color: z.string().optional(),
681
- shape: z.enum(["circle", "arrowUp", "arrowDown", "square"]),
682
- text: z.string().optional()
683
- },
684
- {
685
- message: "markers entries must be objects with time, position, and shape"
686
- }
687
- );
688
- var discardedReferenceSchema = z.discriminatedUnion("type", [
689
- z.object({
690
- type: z.literal("image"),
691
- imageUrl: z.string().min(1),
692
- title: z.string(),
693
- reason: z.string().min(1, { message: "reason must not be empty" })
694
- }),
695
- z.object({
696
- type: z.literal("link"),
697
- url: z.string().min(1),
698
- title: z.string(),
699
- reason: z.string().min(1, { message: "reason must not be empty" })
700
- })
701
- ]);
702
- var internationalCoverageSchema = z.array(
703
- z.object(
704
- {
705
- title: z.string().optional(),
706
- url: safeUrl({
707
- message: "internationalCoverage entries must have a valid url"
708
- }),
709
- sourceName: z.string().optional(),
710
- language: z.string().optional(),
711
- summary: z.string().optional()
712
- },
713
- { message: "internationalCoverage entries must be objects" }
714
- )
715
- );
716
- var referenceGalleryItemSchema = z.object(
717
- {
718
- imageUrl: z.string(),
719
- imageAlt: z.string().optional(),
720
- title: z.string().optional(),
721
- caption: z.string().optional()
722
- },
723
- { message: "galleryItems entries must be objects with imageUrl" }
724
- );
725
- var sourceSchema = z.object(
726
- {
727
- url: safeUrl({ message: "sources entries must have a valid url" }),
728
- title: z.string().optional(),
729
- sourceName: z.string().optional(),
730
- date: z.string().optional(),
731
- snippet: z.string().optional()
732
- },
733
- { message: "sources entries must be objects with url" }
734
- );
735
- function createTextItemSchema(fieldName) {
736
- return z.object(
737
- {
738
- text: z.string().min(1, {
739
- message: `${fieldName} entries must have a non-empty text field`
740
- })
741
- },
742
- { message: `${fieldName} entries must be objects with text` }
743
- );
744
- }
745
- var videoGalleryItemSchema = z.object(
746
- {
747
- videoUrl: safeVideoUrl({
748
- message: "videoGalleryItems.videoUrl must be a valid URL"
749
- }),
750
- title: z.string().min(1, {
751
- message: "videoGalleryItems.title must not be empty"
752
- }),
753
- caption: z.string().min(1, {
754
- message: "videoGalleryItems.caption must not be empty"
755
- }),
756
- duration: z.string().optional(),
757
- channel: z.string().optional(),
758
- date: z.string().optional(),
759
- views: z.number().int().min(0).optional(),
760
- thumbnailUrl: safeMediaUrlOrEmpty({
761
- message: "videoGalleryItems.thumbnailUrl must be a valid URL"
762
- }),
763
- description: z.string().optional()
764
- },
765
- { message: "videoGalleryItems entries must be objects with videoUrl" }
766
- );
767
- function heroVideoHasTitle(data) {
768
- return !data.heroVideoUrl?.trim() || Boolean(data.heroVideoTitle?.trim());
769
- }
770
- var HERO_VIDEO_TITLE_ISSUE = {
771
- message: "heroVideoTitle must not be empty when heroVideoUrl is set",
772
- path: ["heroVideoTitle"]
773
- };
774
-
775
- // src/schemas/response/compare-json.schema.ts
776
- var keyFindingSchema = createTextItemSchema("keyFindings");
777
- z.object({
778
- category: z.string(),
779
- title: z.string().min(1, { message: "title must not be empty" }),
780
- subtitle: z.string(),
781
- sectionContent: z.string(),
782
- galleryTitle: z.string().optional(),
783
- galleryItems: z.array(referenceGalleryItemSchema).optional(),
784
- videoGalleryTitle: z.string().optional(),
785
- videoGalleryItems: z.array(videoGalleryItemSchema).optional(),
786
- keyFindings: z.array(keyFindingSchema).optional(),
787
- sources: z.array(sourceSchema).optional(),
788
- discardedReferences: z.array(discardedReferenceSchema).optional(),
789
- note: z.string().optional(),
790
- internationalCoverage: internationalCoverageSchema.optional()
791
- });
792
- var keyFindingSchema2 = createTextItemSchema("keyFindings");
793
- z.object({
794
- category: z.string(),
795
- title: z.string().min(1, { message: "title must not be empty" }),
796
- subtitle: z.string(),
797
- sectionContent: z.string(),
798
- galleryTitle: z.string().optional(),
799
- galleryItems: z.array(referenceGalleryItemSchema).optional(),
800
- videoGalleryTitle: z.string().optional(),
801
- videoGalleryItems: z.array(videoGalleryItemSchema).optional(),
802
- keyFindings: z.array(keyFindingSchema2).optional(),
803
- sources: z.array(sourceSchema).optional(),
804
- discardedReferences: z.array(discardedReferenceSchema).optional(),
805
- note: z.string().optional(),
806
- internationalCoverage: internationalCoverageSchema.optional()
807
- });
808
- var galleryItemSchema = z.object(
809
- {
810
- imageUrl: safeMediaUrl({
811
- message: "galleryItems.imageUrl must be a valid URL"
812
- }),
813
- imageAlt: z.string().min(1, {
814
- message: "galleryItems.imageAlt must not be empty"
815
- }),
816
- title: z.string().min(1, {
817
- message: "galleryItems.title must not be empty"
818
- }),
819
- caption: z.string().optional()
820
- },
821
- { message: "galleryItems entries must be objects with imageUrl" }
822
- );
823
- var imagelistGalleryItemSchema = galleryItemSchema.extend({
824
- width: z.number().int().positive().optional(),
825
- height: z.number().int().positive().optional(),
826
- source: z.string().optional()
827
- });
828
- z.object({
829
- category: z.string(),
830
- title: z.string().min(1, { message: "title must not be empty" }),
831
- subtitle: z.string(),
832
- galleryItems: z.array(imagelistGalleryItemSchema),
833
- sources: z.array(sourceSchema).optional(),
834
- internationalCoverage: internationalCoverageSchema.optional()
835
- });
836
- var keyFindingSchema3 = createTextItemSchema("keyFindings");
837
- z.object({
838
- category: z.string(),
839
- title: z.string().min(1, { message: "title must not be empty" }),
840
- subtitle: z.string(),
841
- sectionContent: z.string(),
842
- keyFindings: z.array(keyFindingSchema3).optional(),
843
- // Reference material, only when the model researched visible clues online
844
- galleryTitle: z.string().optional(),
845
- galleryItems: z.array(referenceGalleryItemSchema).optional(),
846
- videoGalleryTitle: z.string().optional(),
847
- videoGalleryItems: z.array(videoGalleryItemSchema).optional(),
848
- sources: z.array(sourceSchema).optional(),
849
- discardedReferences: z.array(discardedReferenceSchema).optional(),
850
- internationalCoverage: internationalCoverageSchema.optional()
851
- });
852
- var shopOfferSchema = z.object(
853
- {
854
- title: z.string().optional(),
855
- price: z.string().optional(),
856
- source: z.string().optional(),
857
- link: safeUrl({ message: "shopOffers entries must have a valid link" }),
858
- imageUrl: z.string().url().optional().or(z.literal("")),
859
- delivery: z.string().optional(),
860
- rating: z.number().optional(),
861
- ratingCount: z.number().optional()
862
- },
863
- { message: "shopOffers entries must be objects with a link" }
864
- );
865
- var statHighlightSchema = z.object(
866
- {
867
- label: z.string().min(1, { message: "statHighlights entries must have a label" }),
868
- value: z.string().min(1, { message: "statHighlights entries must have a value" })
869
- },
870
- { message: "statHighlights entries must be objects with label and value" }
871
- );
872
- z.object({
873
- category: z.string(),
874
- title: z.string().min(1, { message: "title must not be empty" }),
875
- subtitle: z.string(),
876
- shortDescription: z.string(),
877
- // Purchase-decision fields
878
- aggregateRating: z.number().min(0).max(5).optional(),
879
- aggregateRatingCount: z.number().int().min(0).optional(),
880
- aggregateRatingLabel: z.string().optional(),
881
- statHighlights: z.array(statHighlightSchema).optional(),
882
- keyPoints: z.array(createTextItemSchema("keyPoints")).optional(),
883
- pros: z.array(createTextItemSchema("pros")).optional(),
884
- cons: z.array(createTextItemSchema("cons")).optional(),
885
- shopOffers: z.array(shopOfferSchema).optional(),
886
- // Media — the product banner is image-only, there is no hero video
887
- heroImageUrl: safeMediaUrlOrEmpty(),
888
- heroImageAlt: z.string().optional(),
889
- heroCaption: z.string().optional(),
890
- galleryTitle: z.string().optional(),
891
- galleryItems: z.array(galleryItemSchema).optional(),
892
- videoGalleryTitle: z.string().optional(),
893
- videoGalleryItems: z.array(videoGalleryItemSchema).optional(),
894
- // Attribution
895
- sources: z.array(sourceSchema).optional(),
896
- internationalCoverage: internationalCoverageSchema.optional()
897
- });
898
- var relatedStorySchema = z.object(
899
- {
900
- title: z.string().min(1, { message: "relatedStories entries must have a title" }),
901
- url: safeUrl({ message: "relatedStories entries must have a valid url" }),
902
- sourceName: z.string().optional(),
903
- imageUrl: safeMediaUrl({
904
- message: "relatedStories entries must have a valid imageUrl"
905
- }),
906
- date: z.string().optional()
907
- },
908
- { message: "relatedStories entries must have title, url, and imageUrl" }
909
- );
910
- var shopOfferSchema2 = z.object(
911
- {
912
- title: z.string().optional(),
913
- price: z.string().optional(),
914
- source: z.string().optional(),
915
- link: safeUrl({ message: "shopOffers entries must have a valid link" }),
916
- imageUrl: safeMediaUrlOrEmpty("shopOffers.imageUrl must be a valid URL"),
917
- delivery: z.string().optional(),
918
- rating: z.number().optional(),
919
- ratingCount: z.number().optional()
920
- },
921
- { message: "shopOffers entries must be objects with a link" }
922
- );
923
- z.object({
924
- category: z.string(),
925
- title: z.string().min(1, { message: "title must not be empty" }),
926
- subtitle: z.string(),
927
- shortDescription: z.string().optional(),
928
- shopOffers: z.array(shopOfferSchema2).optional(),
929
- sources: z.array(sourceSchema).optional(),
930
- internationalCoverage: internationalCoverageSchema.optional()
931
- });
932
- var newsItemSchema = z.object(
933
- {
934
- title: z.string().min(1, {
935
- message: "news entries must have a non-empty title"
936
- }),
937
- url: safeUrl({ message: "news entries must have a valid url" }),
938
- source: z.string().optional(),
939
- date: z.string().optional(),
940
- snippet: z.string().optional()
941
- },
942
- { message: "news entries must be objects with title and url" }
943
- );
944
- var fundamentalsSchema = z.object({
945
- name: z.string().optional(),
946
- sector: z.string().optional(),
947
- industry: z.string().optional(),
948
- marketCap: z.union([z.number(), z.string()]).optional(),
949
- peRatio: z.union([z.number(), z.string()]).optional(),
950
- revenue: z.union([z.number(), z.string()]).optional(),
951
- profitMargin: z.union([z.number(), z.string()]).optional()
952
- });
953
- z.object({
954
- category: z.string(),
955
- title: z.string().min(1, { message: "title must not be empty" }),
956
- subtitle: z.string(),
957
- shortDescription: z.string(),
958
- // Quote
959
- currentPrice: z.number().optional(),
960
- change: z.number().optional(),
961
- changeP: z.number().optional(),
962
- // Recommendation
963
- recommendation: z.string().optional(),
964
- recommendationReasoning: z.string().optional(),
965
- // Stats / context
966
- keyPoints: z.array(createTextItemSchema("keyPoints")).optional(),
967
- fundamentals: fundamentalsSchema.optional(),
968
- // News + attribution
969
- news: z.array(newsItemSchema).optional(),
970
- sources: z.array(sourceSchema).optional(),
971
- internationalCoverage: internationalCoverageSchema.optional(),
972
- // Chart overlays
973
- referenceLines: z.array(referenceLineSchema).optional(),
974
- markers: z.array(markerSchema).optional(),
975
- // Videos
976
- videoGalleryTitle: z.string().optional(),
977
- videoGalleryItems: z.array(videoGalleryItemSchema).optional()
978
- });
979
- var listItemSchema = z.object(
980
- {
981
- name: z.string().min(1, {
982
- message: "items entries must have a non-empty name"
983
- }),
984
- ticker: z.string().min(1, {
985
- message: "items entries must have a non-empty ticker"
986
- }),
987
- price: z.number().optional(),
988
- change: z.number().optional(),
989
- changeP: z.number().optional()
990
- },
991
- { message: "items entries must be objects with name and ticker" }
992
- );
993
- z.object({
994
- category: z.string(),
995
- title: z.string().min(1, { message: "title must not be empty" }),
996
- subtitle: z.string(),
997
- summary: z.string(),
998
- items: z.array(listItemSchema).optional(),
999
- sources: z.array(sourceSchema).optional(),
1000
- internationalCoverage: internationalCoverageSchema.optional(),
1001
- // Chart overlays
1002
- referenceLines: z.array(referenceLineSchema).optional(),
1003
- markers: z.array(markerSchema).optional(),
1004
- // Videos
1005
- videoGalleryTitle: z.string().optional(),
1006
- videoGalleryItems: z.array(videoGalleryItemSchema).optional()
1007
- });
1008
- var keyFindingSchema4 = createTextItemSchema("keyFindings");
1009
- z.object({
1010
- category: z.string(),
1011
- title: z.string().min(1, { message: "title must not be empty" }),
1012
- subtitle: z.string(),
1013
- summary: z.string(),
1014
- keyFindings: z.array(keyFindingSchema4).optional(),
1015
- sources: z.array(sourceSchema).optional(),
1016
- // Media from online research
1017
- heroImageUrl: safeMediaUrlOrEmpty(),
1018
- heroImageAlt: z.string().optional(),
1019
- heroCaption: z.string().optional(),
1020
- heroVideoUrl: safeVideoUrlOrEmpty(),
1021
- heroVideoTitle: z.string().optional(),
1022
- heroVideoCaption: z.string().optional(),
1023
- galleryTitle: z.string().optional(),
1024
- galleryItems: z.array(galleryItemSchema).optional(),
1025
- videoGalleryTitle: z.string().optional(),
1026
- videoGalleryItems: z.array(videoGalleryItemSchema).optional(),
1027
- internationalCoverage: internationalCoverageSchema.optional()
1028
- }).refine(heroVideoHasTitle, HERO_VIDEO_TITLE_ISSUE);
1029
- z.object({
1030
- category: z.string(),
1031
- title: z.string().min(1, { message: "title must not be empty" }),
1032
- subtitle: z.string(),
1033
- videoGalleryItems: z.array(videoGalleryItemSchema),
1034
- internationalCoverage: internationalCoverageSchema.optional()
1035
- });
160
+ // src/schemas/constants/media-counts.constant.ts
161
+ var DEFAULT_MEDIA_COUNT = 6;
162
+ var MORE_MEDIA_COUNT = 12;
1036
163
 
1037
164
  // src/prompts/shared/media-counts.prompt.ts
1038
165
  var MEDIA_COUNTS = `MEDIA COUNTS
@@ -1042,6 +169,10 @@ var MEDIA_COUNTS = `MEDIA COUNTS
1042
169
  - hero media counts toward the total.
1043
170
  - Never exceed available URLs.`;
1044
171
 
172
+ // src/schemas/constants/embeddable-video-providers.constant.ts
173
+ var EMBEDDABLE_VIDEO_PROVIDER_LABELS = "YouTube, Vimeo, Dailymotion, Loom, Wistia";
174
+ var EMBEDDABLE_VIDEO_PROVIDER_CLAUSE = `Only return URLs from supported embeddable providers: ${EMBEDDABLE_VIDEO_PROVIDER_LABELS}, or direct video files. Reject Instagram, Facebook, TikTok, Twitch, X/Twitter, and other unreliable platforms.`;
175
+
1045
176
  // src/prompts/shared/media-rules.prompt.ts
1046
177
  var MEDIA_RULES = `MEDIA RULES
1047
178
  - Media-list templates (imagelist, videolist, shoplist) have NO hero media, and product has an image-only banner with NO hero video \u2014 their mode rules override every hero-related rule below.
@@ -1177,6 +308,150 @@ function composeSnippetKeys(preset) {
1177
308
  const optionalKeys = preset.snippets.flatMap((snippet) => Object.keys(snippet.fields)).filter((key) => !required.has(key));
1178
309
  return { requiredKeys, optionalKeys };
1179
310
  }
311
+
312
+ // src/schemas/constants/url-trust.constants.ts
313
+ var BLOCKED_URL_HOSTS = /* @__PURE__ */ new Set([
314
+ // Google thumbnail / static asset proxies
315
+ "encrypted-tbn0.gstatic.com",
316
+ "encrypted-tbn1.gstatic.com",
317
+ "encrypted-tbn2.gstatic.com",
318
+ "encrypted-tbn3.gstatic.com",
319
+ "t0.gstatic.com",
320
+ "t1.gstatic.com",
321
+ "t2.gstatic.com",
322
+ "t3.gstatic.com",
323
+ "t4.gstatic.com",
324
+ "t5.gstatic.com",
325
+ "t6.gstatic.com",
326
+ "t7.gstatic.com",
327
+ "t8.gstatic.com",
328
+ "t9.gstatic.com",
329
+ "t10.gstatic.com",
330
+ "news.gstatic.com",
331
+ "books.gstatic.com",
332
+ "maps.gstatic.com",
333
+ "lh1.googleusercontent.com",
334
+ "lh2.googleusercontent.com",
335
+ "lh3.googleusercontent.com",
336
+ "lh4.googleusercontent.com",
337
+ "lh5.googleusercontent.com",
338
+ "lh6.googleusercontent.com"
339
+ ]);
340
+ var NON_PAGE_EXTENSIONS = /\.(js|css|json|xml|svg|png|jpg|jpeg|gif|webp|ico|mp4|webm|ogg|mov|mkv|avi|flv|m3u8|mpd|pdf|zip|tar|gz|rar|exe|dmg|pkg|deb|rpm|woff|woff2|ttf|otf|eot)(\?.*)?$/i;
341
+
342
+ // src/schemas/helpers/url-trust/is-private-or-localhost.helper.ts
343
+ function isPrivateOrLocalhost(hostname) {
344
+ const lower = hostname.toLowerCase();
345
+ if (lower === "localhost" || lower.endsWith(".localhost")) return true;
346
+ if (lower === "127.0.0.1" || lower === "0.0.0.0") return true;
347
+ if (lower.startsWith("10.")) return true;
348
+ if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(lower)) return true;
349
+ if (lower.startsWith("192.168.")) return true;
350
+ if (lower.startsWith("169.254.")) return true;
351
+ return false;
352
+ }
353
+
354
+ // src/schemas/helpers/url-trust/is-trusted-url.helper.ts
355
+ function isYouTubeNonVideoPath(hostname, pathname) {
356
+ const lowerHost = hostname.toLowerCase();
357
+ if (lowerHost !== "youtube.com" && lowerHost !== "www.youtube.com" && lowerHost !== "m.youtube.com") {
358
+ return false;
359
+ }
360
+ const lowerPath = pathname.toLowerCase();
361
+ if (lowerPath.startsWith("/s/") || lowerPath.startsWith("/static/") || lowerPath.startsWith("/js/") || lowerPath.startsWith("/css/") || lowerPath.startsWith("/fonts/") || lowerPath.startsWith("/yts/") || lowerPath.startsWith("/iframe_api") || lowerPath.startsWith("/sw.js") || lowerPath.startsWith("/embed_config") || lowerPath.startsWith("/get_video_info") || lowerPath.startsWith("/api/")) {
362
+ return true;
363
+ }
364
+ return false;
365
+ }
366
+ function isTrustedUrl(url, options = {}) {
367
+ if (!url) return false;
368
+ let parsed;
369
+ try {
370
+ parsed = new URL(url);
371
+ } catch {
372
+ return false;
373
+ }
374
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
375
+ return false;
376
+ }
377
+ const hostname = parsed.hostname.toLowerCase();
378
+ const pathname = parsed.pathname;
379
+ if (BLOCKED_URL_HOSTS.has(hostname)) {
380
+ return false;
381
+ }
382
+ if (isYouTubeNonVideoPath(hostname, pathname)) {
383
+ return false;
384
+ }
385
+ if (NON_PAGE_EXTENSIONS.test(pathname)) {
386
+ return false;
387
+ }
388
+ if (!options.allowPrivate && isPrivateOrLocalhost(hostname)) {
389
+ return false;
390
+ }
391
+ return true;
392
+ }
393
+
394
+ // src/schemas/helpers/url-trust/url-schema.helper.ts
395
+ function safeUrl(message = "must be a safe URL") {
396
+ const text = typeof message === "string" ? message : message.message;
397
+ return z.string().url({ message: "must be a valid URL" }).refine((value) => isTrustedUrl(value, { allowPrivate: false }), {
398
+ message: text
399
+ });
400
+ }
401
+ var LOCAL_STORAGE_URL_PATTERN = /^\/api\/v1\/storage\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9]+$/;
402
+ function isAbsoluteHttpUrl(value) {
403
+ try {
404
+ const protocol = new URL(value).protocol;
405
+ return protocol === "http:" || protocol === "https:";
406
+ } catch {
407
+ return false;
408
+ }
409
+ }
410
+ function safeVideoUrl(message = "must be a safe video URL") {
411
+ const text = typeof message === "string" ? message : message.message;
412
+ return z.string().refine(isAbsoluteHttpUrl, { message: text });
413
+ }
414
+ function safeVideoUrlOrEmpty(message = "must be a safe video URL") {
415
+ return safeVideoUrl(message).optional().or(z.literal(""));
416
+ }
417
+ function safeMediaUrl(message = "must be a safe media URL") {
418
+ const text = typeof message === "string" ? message : message.message;
419
+ return z.string().refine((value) => LOCAL_STORAGE_URL_PATTERN.test(value) || isAbsoluteHttpUrl(value), { message: text });
420
+ }
421
+ function safeMediaUrlOrEmpty(message = "must be a safe media URL") {
422
+ return safeMediaUrl(message).optional().or(z.literal(""));
423
+ }
424
+
425
+ // src/schemas/response/video-gallery-item-json.schema.ts
426
+ var videoGalleryItemSchema = z.object(
427
+ {
428
+ videoUrl: safeVideoUrl({
429
+ message: "videoGalleryItems.videoUrl must be a valid URL"
430
+ }),
431
+ title: z.string().min(1, {
432
+ message: "videoGalleryItems.title must not be empty"
433
+ }),
434
+ caption: z.string().min(1, {
435
+ message: "videoGalleryItems.caption must not be empty"
436
+ }),
437
+ duration: z.string().optional(),
438
+ channel: z.string().optional(),
439
+ date: z.string().optional(),
440
+ views: z.number().int().min(0).optional(),
441
+ thumbnailUrl: safeMediaUrlOrEmpty({
442
+ message: "videoGalleryItems.thumbnailUrl must be a valid URL"
443
+ }),
444
+ description: z.string().optional()
445
+ },
446
+ { message: "videoGalleryItems entries must be objects with videoUrl" }
447
+ );
448
+ function heroVideoHasTitle(data) {
449
+ return !data.heroVideoUrl?.trim() || Boolean(data.heroVideoTitle?.trim());
450
+ }
451
+ var HERO_VIDEO_TITLE_ISSUE = {
452
+ message: "heroVideoTitle must not be empty when heroVideoUrl is set",
453
+ path: ["heroVideoTitle"]
454
+ };
1180
455
  var RESPONSE_LAYOUTS = ["classic", "editorial", "split", "mosaic"];
1181
456
  var responseLayoutSchema = z.enum(RESPONSE_LAYOUTS);
1182
457
 
@@ -1210,6 +485,17 @@ var bodyExtensiveSnippet = {
1210
485
  - sectionContent: the main body text as one plain-text string of 4-8 paragraphs: context, quotes, reactions, and background. This is the article's core \u2014 write it in full.
1211
486
  - sectionTitle: a heading for the main body section; omit only when the article needs none.`
1212
487
  };
488
+ var cardSchema = z.object(
489
+ {
490
+ url: safeUrl({ message: "cards entries must have a valid url" }),
491
+ title: z.string().optional(),
492
+ description: z.string().optional(),
493
+ linkLabel: z.string().optional()
494
+ },
495
+ { message: "cards entries must be objects with url" }
496
+ );
497
+
498
+ // src/prompts/snippets/cards.snippet.ts
1213
499
  var cardsSnippet = {
1214
500
  fields: {
1215
501
  cardsTitle: z.string().optional(),
@@ -1228,6 +514,23 @@ var conclusionSnippet = {
1228
514
  instruction: `SNIPPET conclusion (client closing section):
1229
515
  - conclusion: a brief closing summary (1-3 sentences); omit when the article already lands cleanly.`
1230
516
  };
517
+ var galleryItemSchema = z.object(
518
+ {
519
+ imageUrl: safeMediaUrl({
520
+ message: "galleryItems.imageUrl must be a valid URL"
521
+ }),
522
+ imageAlt: z.string().min(1, {
523
+ message: "galleryItems.imageAlt must not be empty"
524
+ }),
525
+ title: z.string().min(1, {
526
+ message: "galleryItems.title must not be empty"
527
+ }),
528
+ caption: z.string().optional()
529
+ },
530
+ { message: "galleryItems entries must be objects with imageUrl" }
531
+ );
532
+
533
+ // src/prompts/snippets/gallery.snippet.ts
1231
534
  var gallerySnippet = {
1232
535
  fields: {
1233
536
  galleryTitle: z.string().optional(),
@@ -1265,6 +568,20 @@ var heroMediaSnippet = {
1265
568
  - heroImageUrl set \u21D2 heroImageAlt MUST be a non-empty descriptive label; heroCaption stays optional.
1266
569
  - Leave empty ONLY when the pools hold no usable media \u2014 never invent URLs.`
1267
570
  };
571
+ var internationalCoverageSchema = z.array(
572
+ z.object(
573
+ {
574
+ title: z.string().optional(),
575
+ url: safeUrl({
576
+ message: "internationalCoverage entries must have a valid url"
577
+ }),
578
+ sourceName: z.string().optional(),
579
+ language: z.string().optional(),
580
+ summary: z.string().optional()
581
+ },
582
+ { message: "internationalCoverage entries must be objects" }
583
+ )
584
+ );
1268
585
 
1269
586
  // src/prompts/instructions/international-coverage.instruction.ts
1270
587
  var INTERNATIONAL_COVERAGE_INSTRUCTIONS = `INTERNATIONAL COVERAGE ASIDE:
@@ -1284,6 +601,18 @@ var internationalCoverageSnippet = {
1284
601
  },
1285
602
  instruction: INTERNATIONAL_COVERAGE_INSTRUCTIONS
1286
603
  };
604
+ function createTextItemSchema(fieldName) {
605
+ return z.object(
606
+ {
607
+ text: z.string().min(1, {
608
+ message: `${fieldName} entries must have a non-empty text field`
609
+ })
610
+ },
611
+ { message: `${fieldName} entries must be objects with text` }
612
+ );
613
+ }
614
+
615
+ // src/prompts/snippets/key-findings.snippet.ts
1287
616
  var keyFindingsSnippet = {
1288
617
  fields: {
1289
618
  keyFindings: z.array(createTextItemSchema("keyFindings")).optional()
@@ -1300,6 +629,18 @@ var quoteSnippet = {
1300
629
  - Needs: a quote that exists verbatim in the retrieved results.
1301
630
  - quote: one notable quote from the sources; omit when none exists.`
1302
631
  };
632
+ var sourceSchema = z.object(
633
+ {
634
+ url: safeUrl({ message: "sources entries must have a valid url" }),
635
+ title: z.string().optional(),
636
+ sourceName: z.string().optional(),
637
+ date: z.string().optional(),
638
+ snippet: z.string().optional()
639
+ },
640
+ { message: "sources entries must be objects with url" }
641
+ );
642
+
643
+ // src/prompts/snippets/sources.snippet.ts
1303
644
  var sourcesSnippet = {
1304
645
  fields: {
1305
646
  sources: z.array(sourceSchema).optional()
@@ -1639,6 +980,20 @@ var leadSnippet = {
1639
980
  - Needs: the core facts from the retrieved results.
1640
981
  - lead: 2-4 sentences answering who, what, when, where, why and how, strongest facts first (inverted pyramid). REQUIRED, non-empty.`
1641
982
  };
983
+ var relatedStorySchema = z.object(
984
+ {
985
+ title: z.string().min(1, { message: "relatedStories entries must have a title" }),
986
+ url: safeUrl({ message: "relatedStories entries must have a valid url" }),
987
+ sourceName: z.string().optional(),
988
+ imageUrl: safeMediaUrl({
989
+ message: "relatedStories entries must have a valid imageUrl"
990
+ }),
991
+ date: z.string().optional()
992
+ },
993
+ { message: "relatedStories entries must have title, url, and imageUrl" }
994
+ );
995
+
996
+ // src/prompts/snippets/related-stories.snippet.ts
1642
997
  var relatedStoriesSnippet = {
1643
998
  fields: {
1644
999
  relatedStories: z.array(relatedStorySchema).optional()
@@ -1757,46 +1112,6 @@ ${params.instructions}`);
1757
1112
  return sections.filter(Boolean).join("\n\n");
1758
1113
  }
1759
1114
 
1760
- // src/tools/constants/recency.constants.ts
1761
- var RECENCY_DESCRIPTION = "Restrict results to the given past period (day=24 hours, week=7 days, month=1 month, year=1 year). Use for fresh content such as news, recent releases, or trending topics; leave unset for evergreen, historical, or general queries.";
1762
-
1763
- // src/tools/constants/standalone-query.constants.ts
1764
- var STANDALONE_QUERY_DESCRIPTION = 'A standalone, self-contained search query that explicitly names the subject (title, entity, brand, person, place, or topic). Never copy the user message verbatim: rewrite short follow-ups (e.g. "what do the reviews say?") into a full query that names the established subject from the conversation (e.g. "Neverness to Everness NTE reviews").';
1765
-
1766
- // src/tools/bright-data/image-search.schema.ts
1767
- z.object({
1768
- query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Add short visual qualifiers describing the subject.`),
1769
- count: z.number().optional().describe("Number of results (max 100)"),
1770
- minWidth: z.number().optional().describe("Minimum image width in pixels (floor 1280 / 720p)."),
1771
- minHeight: z.number().optional().describe("Minimum image height in pixels (floor 720 / 720p)."),
1772
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)"),
1773
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION)
1774
- });
1775
- z.object({
1776
- query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Include the newsworthy angle (announcement, release, event, update).`),
1777
- count: z.number().optional().describe("Number of results (max 100)"),
1778
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1779
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1780
- });
1781
- z.object({
1782
- query: z.string().describe(
1783
- 'A standalone places search query that explicitly names the business or business type plus location (e.g. "MediaMarkt Berlin", "coffee shops in Munich").'
1784
- ),
1785
- count: z.number().optional().describe("Number of results (max 100)"),
1786
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1787
- });
1788
- z.object({
1789
- query: z.string().describe("The exact product name with model number, kept short and standalone."),
1790
- count: z.number().optional().describe("Number of results (max 100)"),
1791
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1792
- });
1793
- z.object({
1794
- query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Add the video type (e.g. review, trailer, tutorial, gameplay).`),
1795
- count: z.number().optional().describe("Number of results (max 100)"),
1796
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1797
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1798
- });
1799
-
1800
1115
  // src/tools/helpers/localized-query-suffix.helper.ts
1801
1116
  function localizedQuerySuffix(lang) {
1802
1117
  if (!lang) return "";
@@ -1814,111 +1129,6 @@ function localizedQuerySuffix(lang) {
1814
1129
  return "";
1815
1130
  }
1816
1131
  }
1817
- z.object({
1818
- query: z.string().describe(STANDALONE_QUERY_DESCRIPTION),
1819
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1820
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1821
- });
1822
- z.object({
1823
- url: z.string().describe("The URL to fetch and render")
1824
- });
1825
- z.object({});
1826
- z.object({});
1827
- z.object({
1828
- text: z.string().min(1).max(2e3).describe(
1829
- 'The derived insight to store, as one self-contained third-person sentence, e.g. "The user prefers single-line if statements".'
1830
- ),
1831
- path: z.string().min(1).max(200).optional().describe(
1832
- 'Optional profile facet this insight deepens, e.g. "likes.cars" \u2014 only when it deepens a stored profile value.'
1833
- )
1834
- });
1835
- z.object({
1836
- text: z.string().min(3).max(2e3).describe(
1837
- "The exact stored statement to delete, quoted verbatim from a memory-partition-recall result. Record texts are the record identity \u2014 no ids needed. Never paraphrase: recall first, then delete the exact text."
1838
- )
1839
- });
1840
- z.object({
1841
- query: z.string().min(1).max(1e3).describe('What to recall as a natural-language question, e.g. "What is Sams phone number?"'),
1842
- tags: z.array(z.string().min(1).max(40)).max(8).optional().describe('Restrict to records tagged with ANY of these topics, e.g. ["work"].'),
1843
- contains: z.string().max(200).optional().describe('Restrict to records whose text contains this exact phrase, e.g. "phone number".'),
1844
- topK: z.number().int().min(1).max(10).optional().describe("Maximum number of results to return (default 5).")
1845
- });
1846
- z.object({
1847
- text: z.string().min(1).max(2e3).describe('The fact to remember, as a self-contained statement, e.g. "Sams phone number is 555-1234".'),
1848
- category: z.string().min(1).max(40).optional().describe(
1849
- 'One broad lowercase PLURAL category the fact belongs to, e.g. "stocks", "games", "pets", "work" \u2014 a family noun, never a specific entity/product/company/game name ("amd" \u2192 "stocks"; "stellar blade" \u2192 "games"). Always include it.'
1850
- ),
1851
- tags: z.array(z.string().min(1).max(40)).max(8).optional().describe(
1852
- 'Optional topic labels (lowercase, reusable, NARROW \u2014 entity/product/game names) so future recall can filter by topic, e.g. ["contacts", "sam"], ["amd"], ["stellar blade"].'
1853
- )
1854
- });
1855
- z.object({
1856
- query: z.string().optional().describe(
1857
- "The exact business or place name, ideally with its location, named explicitly and resolved from the conversation. Used when neither placeId nor cid is known."
1858
- ),
1859
- placeId: z.string().optional().describe("Google Place ID of the business. Most precise identifier \u2014 prefer it when available."),
1860
- cid: z.string().optional().describe("Google CID of the business, e.g. from a places search result."),
1861
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1862
- });
1863
- z.object({
1864
- query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Add short visual qualifiers describing the subject.`),
1865
- count: z.number().optional().describe("Number of results (max 100)"),
1866
- minWidth: z.number().optional().describe(
1867
- "Minimum image width in pixels. Use 1920 when the user wants 1080p-quality images, 2560 for 1440p, 3840 for 4K. The tool always enforces a floor of 1280 (720p)."
1868
- ),
1869
- minHeight: z.number().optional().describe(
1870
- "Minimum image height in pixels. Use 1080 when the user wants 1080p-quality images, 1440 for 1440p, 2160 for 4K. The tool always enforces a floor of 720 (720p)."
1871
- ),
1872
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)"),
1873
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION)
1874
- });
1875
- z.object({
1876
- query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Include the newsworthy angle (announcement, release, event, update).`),
1877
- count: z.number().optional().describe("Number of results (max 100)"),
1878
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1879
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1880
- });
1881
- z.object({
1882
- query: z.string().describe(
1883
- 'A standalone places search query that explicitly names the business or business type plus location (e.g. "MediaMarkt Berlin", "coffee shops in Munich") \u2014 resolve the subject from the conversation; never copy the user message verbatim.'
1884
- ),
1885
- count: z.number().optional().describe("Number of results (max 100)"),
1886
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1887
- });
1888
- z.object({
1889
- query: z.string().describe(
1890
- 'The exact product name with model number, kept short and standalone \u2014 resolve product references from the conversation (e.g. "the headphones we discussed" becomes "Sony WH-1000XM5"). No extra words like "buy", "price", or "review".'
1891
- ),
1892
- count: z.number().optional().describe("Number of results (max 100)"),
1893
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1894
- });
1895
- z.object({
1896
- query: z.string().describe(
1897
- `${STANDALONE_QUERY_DESCRIPTION} Add the video type (e.g. review, trailer, tutorial, gameplay). When the conversation language is not English, phrase the descriptive words in that language and append the language's own name (e.g. "Review Deutsch") to pull localized results.`
1898
- ),
1899
- count: z.number().optional().describe("Number of results (max 100)"),
1900
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1901
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1902
- });
1903
- z.object({
1904
- query: z.string().describe(STANDALONE_QUERY_DESCRIPTION),
1905
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
1906
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1907
- });
1908
- z.object({
1909
- url: z.string().describe("The URL to fetch and render")
1910
- });
1911
- z.object({
1912
- url: z.string().describe("The URL to fetch content from")
1913
- });
1914
- z.object({
1915
- query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Add the video type (e.g. review, trailer, tutorial, gameplay).`),
1916
- count: z.number().optional().describe("Number of results (max 50)"),
1917
- recency: z.enum(["day", "week", "month", "year"]).optional().describe(
1918
- "Restrict results to the given past period (day=24 hours, week=7 days, month=1 month, year=1 year). Use for fresh content such as news, recent releases, or trending topics; leave unset for evergreen, historical, or general queries."
1919
- ),
1920
- lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
1921
- });
1922
1132
 
1923
1133
  // src/prompts/harness/execute/execute-language.helper.ts
1924
1134
  function buildExecuteLanguageInstruction(language, options) {
@@ -2057,6 +1267,112 @@ FINAL REMINDER:
2057
1267
  - Every query names its subject explicitly; never pass the user message verbatim as a query.`;
2058
1268
  }
2059
1269
 
1270
+ // src/schemas/helpers/tools/tool-registry.constants.ts
1271
+ var VARIANT_NAMES = ["grayscale", "denoised", "sharpened", "clahe"];
1272
+ var TOOL_DESCRIPTIONS = {
1273
+ brightDataWebSearch: "Search the web using Bright Data SERP API (Google). Returns organic results with titles, snippets, and links. Supports an optional recency window (day/week/month/year) for fresh results.",
1274
+ brightDataImageSearch: "Search for images using Bright Data SERP API (Google Images). Returns image URLs, source pages, and dimensions. Enforces a minimum of 1280\xD7720 (720p).",
1275
+ brightDataNewsSearch: "Search latest news using Bright Data SERP API. Returns headlines, sources, dates, and snippets. Supports an optional recency window (day/week/month/year).",
1276
+ brightDataPlacesSearch: "Search places and businesses using Bright Data SERP API (Google Maps). Returns addresses, phone numbers, ratings, review counts, and coordinates. Query with a business name or business type plus location.",
1277
+ brightDataShoppingSearch: "Search for products using Bright Data SERP API (Google Shopping). Returns prices, sellers, images, and ratings. Query with the bare product name and model number.",
1278
+ brightDataVideoSearch: `Search for videos using Bright Data SERP API. Returns titles, links, channel names, duration, and publish dates. Supports an optional recency window (day/week/month/year) for fresh results. ${EMBEDDABLE_VIDEO_PROVIDER_CLAUSE}`,
1279
+ brightDataWebpageScrape: "Fetch and render a full webpage using Bright Data Web Unlocker API. Returns clean Markdown text. Use for pages behind anti-bot protection that plain fetch cannot reach.",
1280
+ serperWebSearch: "Search the web using Serper.dev (Google). Returns organic results with titles, snippets, and links. Supports an optional recency window (day/week/month/year) for fresh results.",
1281
+ serperImageSearch: "Search for images using Serper.dev (Google Images). Prefers 2560\xD71440 (1440p) images and enforces a minimum of 1280\xD7720 (720p) via the Google Images tbs size filter and a client-side dimension filter. Supports an optional recency window (day/week/month/year).",
1282
+ serperNewsSearch: "Search latest news using Serper.dev. Returns headlines, sources, dates, and snippets. Supports an optional recency window (day/week/month/year).",
1283
+ serperPlacesSearch: "Search places and businesses using Serper.dev (Google Maps). Returns addresses, phone numbers, ratings, review counts, and coordinates. Query with a business name or business type plus location.",
1284
+ serperShoppingSearch: "Search for products using Serper.dev (Google Shopping). Returns prices, sellers, delivery info, images, and per-offer ratings. Query with the bare product name and model number.",
1285
+ serperBusinessReviewsSearch: "Fetch Google Maps reviews for a specific business or place using Serper.dev. Returns reviewer snippets with author names, star ratings, and dates. Use for seller/business reputation, not editorial product reviews.",
1286
+ serperVideoSearch: `Search for videos using Serper.dev. Returns titles, links, channel names, duration, and publish dates. Supports an optional recency window (day/week/month/year) for fresh results. ${EMBEDDABLE_VIDEO_PROVIDER_CLAUSE}`,
1287
+ serperWebpageScrape: "Fetch and render a full webpage using Serper.dev scrape API. Returns clean rendered text with its title.",
1288
+ youtubeVideoSearch: "Search YouTube using the official YouTube Data API. Returns titles, links, channel names, durations, view counts, upload dates, and direct thumbnails. All results are embeddable YouTube videos.",
1289
+ eodhdSearch: 'Resolve a company, ETF, or index name to an EODHD ticker code (e.g. "Nvidia" \u2192 NVDA.US). Use before fetching quotes, history, technicals, or news for a named entity.',
1290
+ eodhdQuote: "Fetch the current (delayed) quote for one or more EODHD tickers \u2014 last price, change, change %, open/high/low, volume, previous close.",
1291
+ eodhdHistory: "Fetch end-of-day OHLCV price history for an EODHD ticker. Returns a compact summary plus the full time series as chartData for the client chart. Use for the time-value chart and buy/sell pressure (volume).",
1292
+ eodhdTechnical: "Fetch a technical indicator series (RSI, MACD, ADX, SMA, EMA, BBANDS, ATR, \u2026) for an EODHD ticker. Use to gauge buy/sell pressure and momentum.",
1293
+ eodhdIntraday: "Fetch intraday OHLCV bars for an EODHD ticker and return a per-day, per-price-band volume profile as chartData for the client heatmap. Use to render a volume heatmap across fixed price bands.",
1294
+ eodhdNews: "Fetch recent financial news for an EODHD ticker \u2014 headlines, links, sources, publish dates. Use to ground a stock-market answer in what is happening around a company.",
1295
+ eodhdFundamentals: "Fetch company fundamentals for an EODHD ticker \u2014 general info, valuation, and key financial highlights (sector, market cap, P/E, revenue, margins).",
1296
+ webFetch: "Fetch the full content of a specific URL (e.g. a search result page) as clean Markdown. Prefer fetching the most relevant search results to ground the answer in full source text rather than relying on snippets alone.",
1297
+ browser_navigate: "Control a real browser: navigate to a URL. Use for interactive browsing \u2014 JS-heavy pages, content behind clicks, tabs, scrolling, or forms \u2014 that static search/fetch cannot reach. Follow up with browser_snapshot to read the page before acting on it.",
1298
+ browser_navigate_back: "Go back to the previous page in the browser history.",
1299
+ browser_snapshot: "Read the current browser page as an accessibility snapshot (structured text with element refs). The primary way to see page content and obtain the refs that browser_click/browser_type need.",
1300
+ browser_click: "Click an element in the browser, referenced by a ref from browser_snapshot.",
1301
+ browser_type: "Type text into an editable browser element, referenced by a ref from browser_snapshot, optionally submitting with Enter.",
1302
+ browser_fill_form: "Fill multiple form fields in the browser in one call.",
1303
+ browser_select_option: "Select an option in a dropdown in the browser.",
1304
+ browser_press_key: "Press a keyboard key in the browser (Enter, Tab, arrows).",
1305
+ browser_wait_for: "Wait for text to appear or disappear, or for a time period, in the browser.",
1306
+ browser_take_screenshot: "Take a screenshot of the current browser page or a specific element.",
1307
+ browser_tabs: "List, create, close, or switch browser tabs.",
1308
+ browser_console_messages: "Get console messages from the browser. Use to diagnose JavaScript errors on a page (e.g. when testing a web app).",
1309
+ browser_network_requests: "List network requests the browser made since page load. Use to diagnose failed or slow requests (e.g. when testing a web app).",
1310
+ browser_verify_element_visible: "Assert that an element is visible on the current browser page.",
1311
+ browser_verify_text_visible: "Assert that text is visible on the current browser page.",
1312
+ requestGrayscale: "Request a grayscale version of the images. Use when color noise or color information is irrelevant, for example when reading text or analyzing shapes.",
1313
+ requestDenoised: "Request a denoised (blurred) version of the images. Use when the original has noise, grain, or artifacts that hide details.",
1314
+ requestSharpened: "Request a sharpened version of the images. Use when edges or fine details are blurry.",
1315
+ requestClahe: "Request a CLAHE (contrast-enhanced) version of the images. Use when details are hidden in shadows or highlights.",
1316
+ "memory-partition-remember": `Store into the user's fact partition (memory-partition): notable facts about subjects they care about (favorites, interests, projects, followed stocks, people, past topics), preferences and durable details they state, and anything they explicitly ask you to remember. Storing gathered knowledge and noticed preferences is expected \u2014 do not wait for an explicit "remember" instruction.`,
1317
+ "memory-partition-recall": "Retrieve from the user's fact partition (memory-partition) \u2014 things they told you in past conversations or asked you to remember. These are trusted user statements, not public facts; attribute them to the user and prefer them over web results for anything personal. Check this tool whenever a request touches a subject this user has cared about before.",
1318
+ "memory-partition-delete": "Delete one exact fact record from the user's fact partition (memory-partition), quoted verbatim from a memory-partition-recall result. Never delete on a guess: recall first, delete the verbatim statement. Needs memory-partition-recall alongside it.",
1319
+ "memory-cognition-remember": "Store one derived insight into your cognition space (memory-cognition) \u2014 your own understanding of the user (inferred traits, standing interests, working nuances, connections between facts). Use for what you LEARN about the user, not what they stated; stated facts belong in memory-partition-remember.",
1320
+ "memory-cognition-forget": "Wipe your entire cognition space (memory-cognition) of the user \u2014 the structured profile AND every derived insight \u2014 only when the user asks you to forget your learned understanding of them or to start over. Fact records in the partition are untouched."
1321
+ };
1322
+ var BROWSER_TOOL_NAMES = [
1323
+ "browser_navigate",
1324
+ "browser_navigate_back",
1325
+ "browser_snapshot",
1326
+ "browser_click",
1327
+ "browser_type",
1328
+ "browser_fill_form",
1329
+ "browser_select_option",
1330
+ "browser_press_key",
1331
+ "browser_wait_for",
1332
+ "browser_take_screenshot",
1333
+ "browser_tabs",
1334
+ "browser_console_messages",
1335
+ "browser_network_requests",
1336
+ "browser_verify_element_visible",
1337
+ "browser_verify_text_visible"
1338
+ ];
1339
+ var TOOL_NAMES = [
1340
+ "webFetch",
1341
+ "brightDataWebSearch",
1342
+ "brightDataImageSearch",
1343
+ "brightDataNewsSearch",
1344
+ "brightDataPlacesSearch",
1345
+ "brightDataShoppingSearch",
1346
+ "brightDataVideoSearch",
1347
+ "brightDataWebpageScrape",
1348
+ "serperWebSearch",
1349
+ "serperImageSearch",
1350
+ "serperNewsSearch",
1351
+ "serperPlacesSearch",
1352
+ "serperShoppingSearch",
1353
+ "serperBusinessReviewsSearch",
1354
+ "serperVideoSearch",
1355
+ "serperWebpageScrape",
1356
+ "youtubeVideoSearch",
1357
+ "eodhdSearch",
1358
+ "eodhdQuote",
1359
+ "eodhdHistory",
1360
+ "eodhdTechnical",
1361
+ "eodhdIntraday",
1362
+ "eodhdNews",
1363
+ "eodhdFundamentals",
1364
+ "requestGrayscale",
1365
+ "requestDenoised",
1366
+ "requestSharpened",
1367
+ "requestClahe",
1368
+ "memory-partition-remember",
1369
+ "memory-partition-recall",
1370
+ "memory-partition-delete",
1371
+ "memory-cognition-remember",
1372
+ "memory-cognition-forget",
1373
+ ...BROWSER_TOOL_NAMES
1374
+ ];
1375
+
2060
1376
  // src/prompts/harness/helpers/tool-catalog.helper.ts
2061
1377
  function formatToolAvailabilityCatalog(enabledToolNames) {
2062
1378
  const enabledSet = new Set(enabledToolNames);
@@ -2068,6 +1384,64 @@ function formatToolAvailabilityCatalog(enabledToolNames) {
2068
1384
  }
2069
1385
  return lines;
2070
1386
  }
1387
+ var DEFAULT_VARIANT_ID = "default";
1388
+ var TEMPLATES = [
1389
+ "article",
1390
+ "news",
1391
+ "describe",
1392
+ "compare",
1393
+ "ocr",
1394
+ "summary",
1395
+ "evaluation",
1396
+ "product",
1397
+ "shoplist",
1398
+ "imagelist",
1399
+ "videolist",
1400
+ "stockmarketitem",
1401
+ "stockmarketlist",
1402
+ "merge",
1403
+ "text"
1404
+ ];
1405
+ var ImagePlanSchema = z.object({
1406
+ resize: z.boolean().default(true).describe(
1407
+ "Whether to resize the original images before sending them to the response model. Default true when images are present."
1408
+ ),
1409
+ variants: z.array(z.enum(VARIANT_NAMES)).default([]).describe(
1410
+ "Optional preprocessing variants to generate for the images (grayscale, denoised, sharpened, clahe). Only use when they would materially improve analysis."
1411
+ )
1412
+ });
1413
+ var IntentSchema = z.object({
1414
+ template: z.enum(TEMPLATES).describe(
1415
+ 'Template name: "article" (in-depth research/report \u2014 extensive long-form composed from available snippets), "news" (current events \u2014 a compact brief composed from available snippets), "describe" (single/multi image description), "compare" (compare uploaded images only \u2014 information comparisons use "evaluation"), "ocr" (extract text from images), "summary" (recap prior conversation or topic without new images), "evaluation" (critique/assess something from the conversation), "product" (product details with shop offers and prices), "shoplist" (compact product/shop list for follow-up shopping questions about an already-introduced product), "imagelist" (a pure collection of images about a topic, no article), "videolist" (a pure list/playlist of videos about a topic, no article), "stockmarketitem" (a single stock/ETF/index with price chart, technicals, news, and a recommendation), "stockmarketlist" (a selection of stocks/indices with a market overview), "text" (free chat).'
1416
+ ),
1417
+ prompt: z.string().default(DEFAULT_VARIANT_ID).describe(
1418
+ 'Selected prompt variant for the template. Use "default" unless the user explicitly asks for a specific style.'
1419
+ ),
1420
+ tools: z.array(z.enum(TOOL_NAMES)).describe("List of tool names the model decided to invoke."),
1421
+ getDate: z.boolean().default(true).describe(
1422
+ 'Whether search queries should be anchored on the current date so results bias toward recent information. Default true. Set to false ONLY for timeless general-knowledge, historical, or conceptual requests where recency does not matter (e.g. "how does photosynthesis work", "history of the Roman Empire", "explain quantum entanglement", creative writing).'
1423
+ ),
1424
+ imageCount: z.preprocess((val) => val === null ? 0 : val, z.number().int().min(0).max(50).default(0)).describe(
1425
+ `Number of images to retrieve when an imageSearch tool is selected. Only set when the user explicitly requests a specific number; otherwise omit or set to 0 and the system will default to ${DEFAULT_MEDIA_COUNT}.`
1426
+ ),
1427
+ videoCount: z.preprocess((val) => val === null ? 0 : val, z.number().int().min(0).max(50).default(0)).describe(
1428
+ `Number of videos to retrieve when a videoSearch tool is selected. Only set when the user explicitly requests a specific number; otherwise omit or set to 0 and the system will default to ${DEFAULT_MEDIA_COUNT}.`
1429
+ ),
1430
+ reasoning: z.string().describe("Short explanation of why this template, prompt, and these tools were chosen."),
1431
+ contextSummary: z.string().default("").describe(
1432
+ "Query-focused extraction of the prior conversation context that the latest user message references or depends on: established topics/entities, key facts from prior answers the follow-up builds on, user constraints, and \u2014 for imagelist/videolist follow-ups \u2014 the previously shown image/video URLs verbatim. Empty if there is no relevant prior context."
1433
+ ),
1434
+ needsClarification: z.boolean().default(false).describe("When true, the request is too ambiguous to classify \u2014 set this instead of picking template/tools."),
1435
+ clarificationQuestion: z.string().nullable().optional().describe(
1436
+ "Concise, human-friendly question to ask the user when needsClarification is true. Ask what the user might have meant and offer the most likely interpretations as options. Do not hardcode wording; adapt tone and language to the user."
1437
+ ),
1438
+ language: z.string().nullable().optional().describe(
1439
+ "Two-letter ISO language code of the latest user message (e.g. 'en', 'de', 'ja', 'es'). Detect from the user's text."
1440
+ ),
1441
+ plan: z.object({
1442
+ images: ImagePlanSchema.optional().describe("Image processing plan. Only present when images are attached.")
1443
+ }).default({}).describe("Execution plan for the response step.")
1444
+ });
2071
1445
 
2072
1446
  // src/prompts/instructions/compare.instruction.ts
2073
1447
  var COMPARE_VERIFICATION_RULE = `When imageSearch tools ran, availableImages holds cloud reference candidates as search-result DATA (titles, snippets, page sources) \u2014 their pixels are never attached to you. Pick a candidate ONLY when its search result corroborates the subject you identified in the uploaded image(s) (a named entity, visible text, a brand, a character, an authoritative source). galleryItems MUST contain ONLY the picked candidates \u2014 NEVER the uploaded user image(s), which are already visible to the user as attachments; every unpicked candidate belongs in discardedReferences with { "type": "image", imageUrl, title, reason } and a one-line reason (e.g. "result covers a different game"). Retrieved links that failed to corroborate the match MAY be listed with { "type": "link", url, title, reason }.`;
@@ -3447,6 +2821,42 @@ FINAL REMINDER:
3447
2821
  function buildCorrectionPrompt(error, template, options) {
3448
2822
  return correctionTemplate(error, template, options?.finalAttempt);
3449
2823
  }
2824
+ var INDENT = " ";
2825
+ function formatZodShape(schema, options = {}) {
2826
+ const jsonSchema = z.toJSONSchema(schema);
2827
+ return renderNode(jsonSchema, 0, options);
2828
+ }
2829
+ function renderNode(node, depth, options) {
2830
+ if (node.type === "object" && node.properties) return renderObject(node, depth, options);
2831
+ if (node.type === "object" && node.additionalProperties) {
2832
+ return `{ [key]: ${renderNode(node.additionalProperties, depth, options)} }`;
2833
+ }
2834
+ if (node.type === "array" && node.items) return `[${renderNode(node.items, depth, options)}]`;
2835
+ const anyOfBranches = node.anyOf?.filter((branch) => branch.type !== "null");
2836
+ if (anyOfBranches?.length) return anyOfBranches.map((branch) => renderNode(branch, depth, options)).join(" | ");
2837
+ if (node.enum) return node.enum.map((value) => JSON.stringify(value)).join(" | ");
2838
+ if (node.type === "integer" || node.type === "number") return "number";
2839
+ if (node.type === "boolean") return "boolean";
2840
+ if (node.type === "string") return "string";
2841
+ if (node.type === "null") return "null";
2842
+ return "unknown";
2843
+ }
2844
+ function renderObject(node, depth, options) {
2845
+ const properties = node.properties ?? {};
2846
+ const required = new Set(node.required ?? []);
2847
+ const entries = Object.entries(properties);
2848
+ if (entries.length === 0) return "{}";
2849
+ const pad = INDENT.repeat(depth);
2850
+ const innerPad = INDENT.repeat(depth + 1);
2851
+ const lines = entries.map(([key, value]) => {
2852
+ const optional = required.has(key) ? "" : "?";
2853
+ const hint = options.overrides?.[key] ?? renderNode(value, depth + 1, options);
2854
+ return `${innerPad}${JSON.stringify(key)}${optional}: ${hint}`;
2855
+ });
2856
+ return `{
2857
+ ${lines.join(",\n")}
2858
+ ${pad}}`;
2859
+ }
3450
2860
 
3451
2861
  // src/prompts/helpers/build-structured-prompt.helper.ts
3452
2862
  function buildStructuredPrompt(schema, template, options) {
@@ -3565,6 +2975,20 @@ function buildConvictionSynthesisPrompt(evidence) {
3565
2975
  }
3566
2976
  return lines.join("\n");
3567
2977
  }
2978
+ var EncyclopediaClassifySchema = z.object({
2979
+ /**
2980
+ * One broad lowercase PLURAL family label for the document (e.g. `games`,
2981
+ * `work`, `health`) — the constellation's cluster tier. Never a specific
2982
+ * entity, product, company, or title.
2983
+ */
2984
+ category: z.string(),
2985
+ /**
2986
+ * The narrow topic the document is about (e.g. `wuthering waves`,
2987
+ * `q3 budget`) — the constellation's cluster tier. A short, specific,
2988
+ * reusable label, not a sentence and not a URL.
2989
+ */
2990
+ topic: z.string()
2991
+ });
3568
2992
 
3569
2993
  // src/prompts/memory/vocabulary-section.helper.ts
3570
2994
  function buildVocabularySection(knownCategories = [], knownTags = []) {
@@ -3656,6 +3080,14 @@ function buildFrictionPrompt(input) {
3656
3080
  }
3657
3081
  return lines.join("\n");
3658
3082
  }
3083
+ var MemoryClusterSummarySchema = z.object({
3084
+ title: z.string().describe('A short noun-phrase label for the cluster (2\u20136 words), e.g. "gaming preferences" or "work projects".'),
3085
+ summary: z.string().describe(
3086
+ "One or two sentences summarizing what this cluster of memories is about \u2014 the shared theme, the key facts, and how they relate."
3087
+ )
3088
+ });
3089
+
3090
+ // src/prompts/memory/memory-cluster-prompt.constant.ts
3659
3091
  var MEMORY_CLUSTER_INSTRUCTIONS = buildStructuredPrompt(MemoryClusterSummarySchema, {
3660
3092
  before: `MEMORY CLUSTER SUMMARY \u2014 one purpose: write a short title + summary for ONE cluster of related memory records, so the cluster can be recalled as a whole without reading every member.
3661
3093
 
@@ -3681,6 +3113,12 @@ ${limitText(members, params.maxPayloadChars)}`,
3681
3113
  "Write the title + summary JSON object now."
3682
3114
  ].join("\n\n");
3683
3115
  }
3116
+ var ConsolidationVerdictSchema = z.object({
3117
+ verdict: z.enum(["keep", "redundant", "merge"]).describe(
3118
+ "keep = the new fact adds information not covered by the candidates; redundant = fully covered already (same claim, no new detail, same polarity); merge = it refines/corrects/completes a candidate and mergedText carries the fuller statement."
3119
+ ),
3120
+ mergedText: z.string().optional().describe("Required with verdict=merge: one fuller self-contained statement (full restatement, never a diff).")
3121
+ });
3684
3122
 
3685
3123
  // src/prompts/memory/memory-consolidate-prompt.constant.ts
3686
3124
  var MEMORY_CONSOLIDATE_INSTRUCTIONS = buildStructuredPrompt(ConsolidationVerdictSchema, {
@@ -3716,6 +3154,13 @@ ${candidates}`,
3716
3154
  "Decide exactly one verdict (keep / redundant / merge) and output ONLY the JSON object."
3717
3155
  ].join("\n\n");
3718
3156
  }
3157
+ var MemoryEnrichmentSchema = z.object({
3158
+ /**
3159
+ * 2–6 stable, reusable lowercase topic labels for the record — the existing
3160
+ * tags kept plus any missing labels that would help topic-filtered recall.
3161
+ */
3162
+ tags: z.array(z.string())
3163
+ });
3719
3164
 
3720
3165
  // src/prompts/memory/memory-enrich-prompt.constant.ts
3721
3166
  function buildEnrichPrompt() {
@@ -3737,6 +3182,89 @@ FINAL REMINDER:
3737
3182
  - Output ONLY valid JSON matching the exact schema above. No markdown code fences, no explanations, preamble, or postscript.`
3738
3183
  });
3739
3184
  }
3185
+ var nullishText = z.string().nullish();
3186
+ var nullishTopics = z.array(z.string()).nullish();
3187
+ var memoryCognitionProfileSchema = z.object({
3188
+ /** The user's name or preferred handle, when known. */
3189
+ name: nullishText,
3190
+ /** Primary conversation language (BCP-47-ish, e.g. "en", "de"). */
3191
+ language: nullishText,
3192
+ /** Coarse location the user works from (IANA name or city), when known. */
3193
+ timezone: nullishText,
3194
+ /** Demonstrated skills and domains (e.g. ["TypeScript", "NestJS"]). */
3195
+ expertise: nullishTopics,
3196
+ /** Active goals and aspirations — the most durable cognition there is. */
3197
+ goals: nullishTopics,
3198
+ /** How the user wants to be answered. */
3199
+ communication: z.object({
3200
+ style: nullishText,
3201
+ detailLevel: nullishText,
3202
+ formality: nullishText
3203
+ }).nullish(),
3204
+ /** Tooling / environment / format preferences (free-form key-value). */
3205
+ preferences: z.record(z.string(), z.string()).nullish(),
3206
+ likes: nullishTopics,
3207
+ dislikes: nullishTopics,
3208
+ /** Topics the user keeps returning to. */
3209
+ interests: nullishTopics,
3210
+ /**
3211
+ * Durable CONVICTIONS the AI synthesized about the user's world — the
3212
+ * routing-map topics of its conviction records (depth lives in the
3213
+ * conviction points at paths `convictions.<slug>`). Convictions are the
3214
+ * AI's own derived conclusions, never statements the user made.
3215
+ */
3216
+ convictions: nullishTopics,
3217
+ /** The AI's own identity as the user has shaped it (name, role, voice). */
3218
+ persona: z.object({
3219
+ /** The name the user gave the AI. */
3220
+ name: nullishText,
3221
+ /** The role the user assigned the AI (e.g. "coding assistant"). */
3222
+ role: nullishText,
3223
+ /** Short character description the user gave the AI. */
3224
+ personality: nullishText,
3225
+ /** How the AI introduces itself, when the user set one. */
3226
+ greeting: nullishText,
3227
+ /** How the AI should speak. */
3228
+ voice: z.object({
3229
+ tone: nullishText,
3230
+ formality: nullishText
3231
+ }).nullish()
3232
+ }).nullish(),
3233
+ /**
3234
+ * Learned corrections — behavioral rules the user taught the AI after it
3235
+ * got something wrong. Keyed by a short slug (a stable handle for
3236
+ * update/remove); the value is the imperative directive ("always …" /
3237
+ * "never …"), optionally with a brief why. Deep-merged like preferences.
3238
+ */
3239
+ corrections: z.record(z.string(), z.string()).nullish()
3240
+ });
3241
+ var memoryProfileInsightSchema = z.object({
3242
+ text: z.string(),
3243
+ path: z.string().optional()
3244
+ });
3245
+ var memoryProfileResponseSchema = z.preprocess(
3246
+ (value) => {
3247
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
3248
+ const record = value;
3249
+ if (record.insights == null) record.insights = [];
3250
+ else if (Array.isArray(record.insights)) {
3251
+ record.insights = record.insights.map((item) => typeof item === "string" ? { text: item } : item);
3252
+ }
3253
+ return record;
3254
+ },
3255
+ z.object({
3256
+ profile: memoryCognitionProfileSchema.nullish(),
3257
+ insights: z.array(memoryProfileInsightSchema).max(8),
3258
+ /**
3259
+ * One sentence recording what THIS turn was about — the short-term
3260
+ * conversation memory (episode), recalled later by recency-blended probe.
3261
+ * Omitted (or null) when the turn had no substance.
3262
+ */
3263
+ episode: z.string().nullish()
3264
+ })
3265
+ );
3266
+
3267
+ // src/prompts/memory/memory-profile-prompt.constant.ts
3740
3268
  var MEMORY_PROFILE_INSTRUCTIONS = buildStructuredPrompt(memoryProfileResponseSchema, {
3741
3269
  before: `COGNITION JOB \u2014 maintain YOUR evolving understanding of THIS user.
3742
3270
 
@@ -3860,6 +3388,66 @@ function buildMemoryWritePrompt(params) {
3860
3388
  ].filter(Boolean).join("\n\n");
3861
3389
  }
3862
3390
  var MEMORY_WRITE_VERDICT = 'Decide: store each durable user-specific fact with one memory-partition-remember call, each derived understanding with one memory-cognition-remember call, or answer "none" if the turn surfaced nothing durable about this user.';
3391
+ var FACT_KINDS = [
3392
+ "preference",
3393
+ "decision",
3394
+ "state",
3395
+ "contact",
3396
+ "project",
3397
+ "possession",
3398
+ "relationship",
3399
+ "fact"
3400
+ ];
3401
+ var FACT_STABILITIES = ["durable", "volatile"];
3402
+ var ExtractedFactSchema = z.object({
3403
+ /**
3404
+ * The self-contained durable statement — third person, subject up front,
3405
+ * no "this"/"that" references.
3406
+ */
3407
+ text: z.string(),
3408
+ /**
3409
+ * The lowercase entity the fact is about (default `user`; a person,
3410
+ * product, or project name). Maintenance adjudication only ever compares
3411
+ * facts about the SAME subject.
3412
+ */
3413
+ subject: z.string().optional(),
3414
+ /**
3415
+ * One broad lowercase PLURAL family label for THIS fact (e.g. `stocks`,
3416
+ * `pets`, `games`) — inherits the turn-side category when omitted. Never
3417
+ * a specific entity, product, company, or game title.
3418
+ */
3419
+ category: z.string().optional(),
3420
+ /** What kind of durable thing this is (see FACT_KINDS). */
3421
+ kind: z.enum(FACT_KINDS),
3422
+ /** Whether a newer statement is expected to replace this one (see FACT_STABILITIES). */
3423
+ stability: z.enum(FACT_STABILITIES)
3424
+ });
3425
+ var ExtractionSchema = z.object({
3426
+ /**
3427
+ * Durable, self-contained facts worth remembering in a later, unrelated
3428
+ * conversation (preferences, decisions, contact details, project facts),
3429
+ * each carrying its maintenance metadata (subject, category, kind,
3430
+ * stability). Empty when nothing in the text is worth remembering.
3431
+ */
3432
+ facts: z.array(ExtractedFactSchema),
3433
+ /**
3434
+ * 2–6 stable, reusable lowercase topic labels describing the text; the open
3435
+ * vocabulary that powers topic-filtered recall. Tags are NARROW and
3436
+ * specific — entity names, product names, game titles (e.g. `amd`,
3437
+ * `stellar blade`, `stellar blade blood rain`).
3438
+ */
3439
+ tags: z.array(z.string()),
3440
+ /**
3441
+ * One broad lowercase PLURAL family label for the whole turn-side (e.g.
3442
+ * `stocks`, `pets`, `games`) — groups the narrow tags into one topic family
3443
+ * for the constellation's community tier and the relink job's per-category
3444
+ * passes, and backstops facts that omit their own `category`. Never a
3445
+ * specific entity, product, company, or game title: `amd` belongs under
3446
+ * `stocks`; `stellar blade` belongs under `games`. Optional: a turn with
3447
+ * nothing durable may omit it.
3448
+ */
3449
+ category: z.string().optional()
3450
+ });
3863
3451
 
3864
3452
  // src/prompts/memory/vectorize-prompt.constant.ts
3865
3453
  function buildExtractionPrompt(knownCategories = [], knownTags = []) {