@stackfactor/agent-utils 1.2.19 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import logger from "./logger.js";
10
10
  import { z } from "zod";
11
11
  import { transformJSONSchema } from "@anthropic-ai/sdk/lib/transform-json-schema";
12
12
  import { getAbortSignal } from "./runtimeContext.js";
13
+ import { assertTavilyUsable, buildTavilyTools, getTavilyOptions, } from "./tavily.js";
13
14
  const JSON_ESCAPE_INSTRUCTION = `
14
15
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
15
16
  - Newlines → \\n
@@ -603,114 +604,12 @@ const resolveTemperatureSetting = (modelName, config) => {
603
604
  }
604
605
  return { temperature: clamped };
605
606
  };
606
- /**
607
- * Whether a Claude model can run web search through dynamic filtering, where
608
- * Claude writes and runs code that filters the search results before they reach
609
- * the context window instead of loading every result into it. Requires Claude
610
- * 4.6 or later (the models with programmatic tool calling); on anything earlier
611
- * the filtering tool versions return a 400 unless search is pinned to
612
- * `allowed_callers: ["direct"]`.
613
- * @param modelName - The Claude model identifier being routed
614
- * @returns `true` when the model supports dynamic filtering
615
- */
616
- const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
617
- /^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
618
- /**
619
- * Normalizes `config.webSearch` into an options object, returning `null` when
620
- * web search is off so callers can use it as the single enablement gate.
621
- */
622
- const getWebSearchOptions = (config) => {
623
- const webSearch = config?.webSearch;
624
- if (!webSearch)
625
- return null;
626
- return webSearch === true ? {} : webSearch;
627
- };
628
- /**
629
- * Builds the provider-native web search tool definition for a model:
630
- * - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
631
- * API within a single request and answered with citations. Defaults to the
632
- * dynamic-filtering tool version on models that support it, so search results
633
- * are filtered by code before they reach the context window.
634
- * - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
635
- * is governed by `search_context_size` (OpenAI defaults to `medium`).
636
- * - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
637
- * result-filtering or context-size control; leaving `searchTypes` unset keeps
638
- * grounding on text-only web results rather than image bytes.
639
- * Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
640
- * warning instead of throwing so one config can be pointed at any model.
641
- * @param modelName - The model identifier being routed
642
- * @param options - Normalized options from `getWebSearchOptions`
643
- * @returns The provider's tool definition, or `null` when unsupported
644
- */
645
- const buildWebSearchTool = (modelName, options) => {
646
- const { allowedDomains, blockedDomains, userLocation } = options;
647
- if (modelName.startsWith("claude-")) {
648
- // The API returns a 400 when both filters are present, so fail locally
649
- // rather than paying for the round trip.
650
- if (allowedDomains && blockedDomains) {
651
- throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
652
- }
653
- // Prefer the newest tool version the model can actually run. Basic search
654
- // loads every result into the context window; from `web_search_20260209`
655
- // Claude filters them with code first, and `web_search_20260318` can also
656
- // keep the consumed results out of the response.
657
- const canFilter = supportsAnthropicDynamicFiltering(modelName);
658
- const type = options.toolVersion ||
659
- (canFilter ? "web_search_20260318" : "web_search_20250305");
660
- const version = Number(type.slice(-8));
661
- // Filtering versions default to running search from inside code execution.
662
- // Say so explicitly when it is not wanted (or not possible), which is what
663
- // the API requires from models without programmatic tool calling.
664
- const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
665
- return {
666
- type,
667
- name: "web_search",
668
- ...(options.maxUses ? { max_uses: options.maxUses } : {}),
669
- ...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
670
- ...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
671
- ...(userLocation
672
- ? { user_location: { type: "approximate", ...userLocation } }
673
- : {}),
674
- ...(directOnly ? { allowed_callers: ["direct"] } : {}),
675
- ...(version >= 20260318
676
- ? { response_inclusion: options.responseInclusion || "excluded" }
677
- : {}),
678
- };
679
- }
680
- if (modelName.startsWith("gpt-")) {
681
- return {
682
- type: "web_search",
683
- ...(allowedDomains
684
- ? { filters: { allowed_domains: allowedDomains } }
685
- : {}),
686
- ...(userLocation
687
- ? { user_location: { type: "approximate", ...userLocation } }
688
- : {}),
689
- ...(options.searchContextSize
690
- ? { search_context_size: options.searchContextSize }
691
- : {}),
692
- };
693
- }
694
- if (modelName.startsWith("gemini-")) {
695
- // `timeRangeFilter` is the only filter the Gemini API exposes;
696
- // `excludeDomains` is a Vertex AI field and is rejected here, so domain
697
- // filters are deliberately not mapped for Google.
698
- return {
699
- googleSearch: options.timeRange
700
- ? { timeRangeFilter: options.timeRange }
701
- : {},
702
- };
703
- }
704
- logger.log(null, logger.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
705
- return null;
706
- };
707
607
  /**
708
608
  * Flattens LangChain message content into plain text. Content is a string for
709
- * ordinary completions, but every provider switches to an array of blocks once
710
- * a server-side tool runsAnthropic interleaves `server_tool_use` and
711
- * `web_search_tool_result` blocks with the answer text, and OpenAI's Responses
712
- * API returns annotated text blocks — so without this the JSON parse pipeline
713
- * would receive a non-string and every web-search call would fail.
609
+ * ordinary completions, but providers switch to an array of blocks whenever the
610
+ * turn carries more than prose tool-use blocks in agentic runs, or Anthropic's
611
+ * thinking blocks so without this the JSON parse pipeline would receive a
612
+ * non-string and the call would fail.
714
613
  * @param content - A message's `content` field, or a raw string
715
614
  * @returns The concatenated text of all text blocks
716
615
  */
@@ -729,108 +628,6 @@ const extractTextContent = (content) => {
729
628
  }
730
629
  return text;
731
630
  };
732
- const createWebSearchUsage = () => ({
733
- reportedRequests: 0,
734
- callIds: new Set(),
735
- grounded: false,
736
- sources: new Map(),
737
- });
738
- /**
739
- * Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
740
- * call on every chunk of a stream and on messages that involved no search.
741
- */
742
- const collectWebSearchUsage = (payload, usage) => {
743
- if (!payload)
744
- return;
745
- const addSource = (url, title) => {
746
- if (typeof url === "string" && url && !usage.sources.has(url)) {
747
- usage.sources.set(url, {
748
- url,
749
- ...(typeof title === "string" ? { title } : {}),
750
- });
751
- }
752
- };
753
- if (Array.isArray(payload.content)) {
754
- for (const block of payload.content) {
755
- if (!block || typeof block !== "object")
756
- continue;
757
- // Anthropic: results of a search the API executed server-side.
758
- if (block.type === "web_search_tool_result" &&
759
- Array.isArray(block.content)) {
760
- for (const result of block.content) {
761
- addSource(result?.url, result?.title);
762
- }
763
- }
764
- // Anthropic: citations attached to the answer's text blocks.
765
- if (Array.isArray(block.citations)) {
766
- for (const citation of block.citations) {
767
- addSource(citation?.url, citation?.title);
768
- }
769
- }
770
- // OpenAI Responses API: one block per executed search, plus url citations.
771
- if (block.type === "web_search_call" && block.id) {
772
- usage.callIds.add(block.id);
773
- }
774
- if (Array.isArray(block.annotations)) {
775
- for (const annotation of block.annotations) {
776
- if (annotation?.type === "url_citation") {
777
- addSource(annotation.url, annotation.title);
778
- }
779
- }
780
- }
781
- }
782
- }
783
- const metadata = payload.response_metadata;
784
- if (!metadata)
785
- return;
786
- const requests = metadata.usage?.server_tool_use?.web_search_requests;
787
- if (typeof requests === "number" && requests > usage.reportedRequests) {
788
- usage.reportedRequests = requests;
789
- }
790
- const grounding = metadata.groundingMetadata;
791
- if (grounding) {
792
- usage.grounded = true;
793
- for (const chunk of grounding.groundingChunks || []) {
794
- addSource(chunk?.web?.uri, chunk?.web?.title);
795
- }
796
- }
797
- };
798
- /**
799
- * Adds a call's web-search usage to the caller-supplied tracker. Searches are
800
- * billed per request rather than per token (Anthropic charges $10 per 1,000
801
- * searches; Google charges per grounded request), so the rate is read from the
802
- * `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
803
- * accumulated under `<model>_webSearches`. Sources are appended to
804
- * `tracker.webSearchSources`, deduplicated by URL across the whole run.
805
- */
806
- const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
807
- if (!tracker || !modelName)
808
- return;
809
- // The three signals describe the same searches from different providers, so
810
- // the largest one is the count rather than their sum.
811
- const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
812
- if (!searches && usage.sources.size === 0)
813
- return;
814
- if (typeof tracker.cost !== "number")
815
- tracker.cost = 0;
816
- if (!tracker.tokens || typeof tracker.tokens !== "object")
817
- tracker.tokens = {};
818
- if (searches > 0) {
819
- const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
820
- if (Number.isFinite(addedCost) && addedCost > 0)
821
- tracker.cost += addedCost;
822
- const key = `${modelName}_webSearches`;
823
- tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
824
- }
825
- if (usage.sources.size > 0) {
826
- const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
827
- for (const source of usage.sources.values()) {
828
- if (!sources.some((existing) => existing.url === source.url)) {
829
- sources.push(source);
830
- }
831
- }
832
- }
833
- };
834
631
  /**
835
632
  * Instantiates and returns the appropriate LangChain chat model based on the model
836
633
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
@@ -857,13 +654,6 @@ const getLLMModel = (modelName, config, schema = null) => {
857
654
  // Resolve `temperature` with presence/support/range handling (see
858
655
  // resolveTemperatureSetting). Applied uniformly to every provider below.
859
656
  const modelSettings = resolveTemperatureSetting(modelName, config);
860
- // Native web search (see buildWebSearchTool). The tool is bound to the model
861
- // so both `.invoke()` and `.stream()` pick it up; the provider runs the search
862
- // server-side within the same request, so no client-side agent loop is needed.
863
- const webSearchOptions = getWebSearchOptions(config);
864
- const webSearchTool = webSearchOptions
865
- ? buildWebSearchTool(modelName, webSearchOptions)
866
- : null;
867
657
  // Claude models (Anthropic)
868
658
  if (modelName.startsWith("claude-")) {
869
659
  // Anthropic's SDK rejects non-streamed requests when max_tokens is large
@@ -897,7 +687,7 @@ const getLLMModel = (modelName, config, schema = null) => {
897
687
  ...outputConfig,
898
688
  ...modelSettings,
899
689
  });
900
- return webSearchTool ? model.bindTools([webSearchTool]) : model;
690
+ return model;
901
691
  }
902
692
  // Gemini models (Google)
903
693
  else if (modelName.startsWith("gemini-")) {
@@ -910,11 +700,6 @@ const getLLMModel = (modelName, config, schema = null) => {
910
700
  ...(schema ? { json: true } : {}),
911
701
  ...modelSettings,
912
702
  });
913
- // Combining grounding with structured output requires Gemini 3 or later;
914
- // Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
915
- const bound = webSearchTool
916
- ? model.bindTools([webSearchTool])
917
- : model;
918
703
  // `responseSchema` additionally constrains the output shape. It is a
919
704
  // call-time option (not a constructor field), so it is bound onto the model
920
705
  // via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
@@ -922,9 +707,9 @@ const getLLMModel = (modelName, config, schema = null) => {
922
707
  // parse/validate pipeline is unchanged.
923
708
  if (schema) {
924
709
  const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
925
- return bound.withConfig({ responseSchema: jsonSchema });
710
+ return model.withConfig({ responseSchema: jsonSchema });
926
711
  }
927
- return bound;
712
+ return model;
928
713
  }
929
714
  // GPT models (OpenAI)
930
715
  else if (modelName.startsWith("gpt-")) {
@@ -932,41 +717,24 @@ const getLLMModel = (modelName, config, schema = null) => {
932
717
  apiKey: config.openAIAPIKey,
933
718
  max_tokens: config.maxTokens || 200000,
934
719
  modelName: modelName,
935
- // `web_search` is a hosted Responses API tool, so the request has to go to
936
- // `/v1/responses` rather than `/v1/chat/completions`.
937
- ...(webSearchTool ? { useResponsesApi: true } : {}),
938
720
  ...modelSettings,
939
721
  };
940
- // Use native structured output with a JSON schema. The two endpoints spell
941
- // the same thing differently — Chat Completions takes `response_format`,
942
- // the Responses API takes `text.format` with the schema flattened one level
943
- // — and `modelKwargs` is spread verbatim into whichever request is built.
722
+ // Use native structured output with a JSON schema. `modelKwargs` is spread
723
+ // verbatim into the Chat Completions request.
944
724
  if (schema) {
945
725
  const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
946
- openAISettings.modelKwargs = webSearchTool
947
- ? {
948
- text: {
949
- format: {
950
- type: "json_schema",
951
- name: "response_schema",
952
- strict: true,
953
- schema: jsonSchema,
954
- },
726
+ openAISettings.modelKwargs = {
727
+ response_format: {
728
+ type: "json_schema",
729
+ json_schema: {
730
+ name: "response_schema",
731
+ strict: true,
732
+ schema: jsonSchema,
955
733
  },
956
- }
957
- : {
958
- response_format: {
959
- type: "json_schema",
960
- json_schema: {
961
- name: "response_schema",
962
- strict: true,
963
- schema: jsonSchema,
964
- },
965
- },
966
- };
734
+ },
735
+ };
967
736
  }
968
- const model = new ChatOpenAI(openAISettings);
969
- return webSearchTool ? model.bindTools([webSearchTool]) : model;
737
+ return new ChatOpenAI(openAISettings);
970
738
  }
971
739
  // OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
972
740
  const openAICompatible = getOpenAICompatibleProvider(modelName, config);
@@ -991,22 +759,28 @@ const getLLMModel = (modelName, config, schema = null) => {
991
759
  * an empty array
992
760
  * @param responseFormat - Optional structured response format descriptor passed to the
993
761
  * LangChain agent constructor
994
- * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
762
+ * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
763
+ * etc.), plus optional `tavily` (`true` or a `TavilyConfig`) to append the web tools
764
+ * @param usageTracker - Optional accumulator the Tavily tools bill their credits and
765
+ * record their sources into; the LLM's own token usage is tracked by `runAgent`
995
766
  * @returns A configured LangChain agent instance ready to be run with `runAgent`
996
767
  */
997
- const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
998
- // Native web search joins the agent's tool list instead of being bound inside
999
- // `getLLMModel`: the agent binds its own tools to the model, which would drop
1000
- // anything already bound there.
1001
- const webSearchOptions = getWebSearchOptions(config);
1002
- const webSearchTool = webSearchOptions
1003
- ? buildWebSearchTool(modelName, webSearchOptions)
1004
- : null;
768
+ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, usageTracker = null) => {
769
+ // The Tavily tools join the agent's tool list rather than being bound inside
770
+ // `getLLMModel`: they run client-side, so only the agent loop can execute
771
+ // them, and the agent binds its own tools to the model anyway — anything
772
+ // bound there would be dropped.
773
+ const tavilyOptions = getTavilyOptions(config);
774
+ let tavilyTools = [];
775
+ if (tavilyOptions) {
776
+ assertTavilyUsable(config, tavilyOptions);
777
+ tavilyTools = buildTavilyTools(config, tavilyOptions, usageTracker);
778
+ }
1005
779
  const agent = createLangChainAgent({
1006
780
  name: name,
1007
- model: getLLMModel(modelName, { ...config, webSearch: null }),
781
+ model: getLLMModel(modelName, config),
1008
782
  systemPrompt: systemPrompt.trim(),
1009
- tools: webSearchTool ? [...tools, webSearchTool] : tools,
783
+ tools: tavilyTools.length > 0 ? [...tools, ...tavilyTools] : tools,
1010
784
  ...(responseFormat ? { responseFormat: responseFormat } : {}),
1011
785
  });
1012
786
  return agent;
@@ -1093,13 +867,6 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
1093
867
  }
1094
868
  }
1095
869
  updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
1096
- // Web-search activity is spread across the agent's messages — one search may
1097
- // be reported by the message that ran it and cited by a later one.
1098
- const webSearchUsage = createWebSearchUsage();
1099
- for (const message of response?.messages || []) {
1100
- collectWebSearchUsage(message, webSearchUsage);
1101
- }
1102
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1103
870
  const endTime = Date.now();
1104
871
  const duration = endTime - startTime;
1105
872
  logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -1337,15 +1104,15 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
1337
1104
  * When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
1338
1105
  * system prompt and the parsed result is optionally validated against `schema`.
1339
1106
  *
1340
- * Setting `config.webSearch` enables the provider's native web search tool in every
1341
- * mode (see `buildWebSearchTool`). The provider runs the search server-side inside the
1342
- * same request, so the return contract is unchanged; the searches performed and the
1343
- * sources cited are recorded on `usageTracker`.
1107
+ * Setting `config.tavily` adds the Tavily web tools (`web_search`, `web_extract`, and
1108
+ * optionally `web_map` / `web_crawl`). They execute in this process, so they require
1109
+ * `config.agentic` to be `true`; the credits spent and the URLs returned are recorded
1110
+ * on `usageTracker`.
1344
1111
  * @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
1345
1112
  * `"gemini-1.5-pro"`
1346
1113
  * @param config - Configuration object with API keys, `temperature`, optional `agentic`
1347
- * flag, optional `recursionLimit`, and optional `webSearch` (`true` or a
1348
- * `WebSearchConfig`)
1114
+ * flag, optional `recursionLimit`, and optional `tavily` (`true` or a
1115
+ * `TavilyConfig`)
1349
1116
  * @param prompt - The prompt to send; either a plain string (user message only) or an
1350
1117
  * array of `{ role, content }` message objects
1351
1118
  * @param onProgressReport - Optional async callback invoked with `{ message, progress }`
@@ -1396,9 +1163,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1396
1163
  ? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
1397
1164
  : JSON_ESCAPE_INSTRUCTION;
1398
1165
  }
1399
- // Create the agent with tools
1166
+ // Create the agent with tools. The tracker is handed over at construction
1167
+ // because the Tavily tools bill their own credits as they run, rather than
1168
+ // being reconciled from the response afterwards.
1400
1169
  const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
1401
- config);
1170
+ config, usageTracker);
1402
1171
  // Run the agent with progress callback
1403
1172
  const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
1404
1173
  // Extract content from agent response
@@ -1531,7 +1300,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1531
1300
  output_tokens: 0,
1532
1301
  total_tokens: 0,
1533
1302
  };
1534
- let webSearchUsage = createWebSearchUsage();
1535
1303
  // Inner loop: wait + retry on 429 around stream setup and consumption.
1536
1304
  // Usage is only recorded on a successful stream — partial streams that
1537
1305
  // error out with a rate limit are not counted. A 429 fired mid-stream
@@ -1541,7 +1309,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1541
1309
  rawContent = "";
1542
1310
  chunkCount = 0;
1543
1311
  streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
1544
- webSearchUsage = createWebSearchUsage();
1545
1312
  try {
1546
1313
  // Honour caller cancellation: passing the signal tears down the
1547
1314
  // upstream HTTP request so a cancelled call stops billing tokens.
@@ -1554,9 +1321,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1554
1321
  throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
1555
1322
  }
1556
1323
  accumulateChunkUsage(streamUsage, chunk);
1557
- collectWebSearchUsage(chunk, webSearchUsage);
1558
1324
  // Counting every chunk (not just the ones carrying text) keeps
1559
- // progress ticking through the pause while a search runs.
1325
+ // progress ticking through pauses in the stream.
1560
1326
  chunkCount++;
1561
1327
  rawContent += extractTextContent(chunk?.content ?? chunk);
1562
1328
  if (chunkCount % progressReportInterval === 0) {
@@ -1586,7 +1352,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1586
1352
  }
1587
1353
  }
1588
1354
  updateUsageTracker(usageTracker, modelName, streamUsage, config);
1589
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1590
1355
  if (!rawContent) {
1591
1356
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
1592
1357
  }
@@ -1691,10 +1456,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1691
1456
  }
1692
1457
  }
1693
1458
  updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
1694
- const webSearchUsage = createWebSearchUsage();
1695
- collectWebSearchUsage(response, webSearchUsage);
1696
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1697
- // Flattened because a server-tool turn returns content blocks, not a string.
1459
+ // Flattened because providers return content blocks, not a string, once
1460
+ // the turn carries anything besides prose.
1698
1461
  const rawContent = extractTextContent(response?.content ?? response);
1699
1462
  // If not expecting JSON, return raw content directly
1700
1463
  if (!expectsJsonResponse) {
@@ -0,0 +1,91 @@
1
+ import type { UsageTracker } from "./langChain.js";
2
+ export type TavilyToolName = "search" | "extract" | "crawl" | "map";
3
+ /**
4
+ * Caller-facing options for the Tavily tools. Enable them by setting
5
+ * `config.tavily` to `true` (defaults for every field) or to one of these
6
+ * objects, and supply `config.tavilyAPIKey`.
7
+ *
8
+ * Tavily runs client-side: the model emits a tool call, this process performs
9
+ * the HTTP request, and the result is appended to the conversation. That only
10
+ * happens inside an agent loop, so `config.agentic` must be `true` — see
11
+ * `assertTavilyUsable`.
12
+ */
13
+ export type TavilyConfig = {
14
+ /**
15
+ * Which tools to expose. Every enabled tool spends its description tokens on
16
+ * every request, so this defaults to `["search", "extract"]`. Add `"map"` to
17
+ * let the model enumerate a site's URLs and `"crawl"` to let it read a whole
18
+ * section — `crawl` returns full content for every page it visits, which is
19
+ * the easiest way to exhaust a context window here.
20
+ */
21
+ tools?: TavilyToolName[];
22
+ /** Passed through to `/search`. Tavily defaults to `"basic"`. */
23
+ searchDepth?: "basic" | "advanced" | "fast" | "ultra-fast";
24
+ /** `"advanced"` also pulls tables and embedded content, at double the credits. */
25
+ extractDepth?: "basic" | "advanced";
26
+ /** Results per search. Defaults to `5`. */
27
+ maxResults?: number;
28
+ /**
29
+ * Relevant chunks returned per URL when the model supplies a `query` to
30
+ * `web_extract`. Defaults to `3`; ignored for whole-page extraction.
31
+ */
32
+ chunksPerSource?: number;
33
+ /** Per-URL character cap before truncation. Defaults to `60000`. */
34
+ maxCharsPerUrl?: number;
35
+ /** Character cap across a single tool call. Defaults to `150000`. */
36
+ maxCharsTotal?: number;
37
+ /** Restricts `/search` to these domains. */
38
+ includeDomains?: string[];
39
+ /** Excludes these domains from `/search`. */
40
+ excludeDomains?: string[];
41
+ /** Maximum pages a single `web_crawl` or `web_map` call may return. */
42
+ crawlLimit?: number;
43
+ /**
44
+ * Returns markdown (the default) or flattened text. Markdown preserves table
45
+ * and heading structure, which costs fewer tokens than the prose equivalent
46
+ * for the tabular documents this is usually pointed at.
47
+ */
48
+ format?: "markdown" | "text";
49
+ };
50
+ /**
51
+ * Normalizes `config.tavily` into an options object, returning `null` when the
52
+ * Tavily tools are off so callers can use it as the single enablement gate.
53
+ * @param config - The integration config object
54
+ * @returns Normalized options, or `null` when Tavily is not enabled
55
+ */
56
+ export declare const getTavilyOptions: (config: any) => TavilyConfig | null;
57
+ /**
58
+ * Fails fast on the two ways a Tavily config cannot work. Tavily tools are
59
+ * executed by the agent loop in this process, so outside agentic mode the model
60
+ * would be handed tools whose calls nothing answers: the turn ends silently and
61
+ * looks like the model ignored them. Called once at config time rather than
62
+ * discovered per request.
63
+ * @param config - The integration config object
64
+ * @param options - Normalized options from `getTavilyOptions`
65
+ */
66
+ export declare const assertTavilyUsable: (config: any, options: TavilyConfig) => void;
67
+ /**
68
+ * Builds the LangChain tools for the Tavily endpoints named in
69
+ * `options.tools`, closed over the run's config and usage tracker. Tools are
70
+ * created per run rather than shared, because each one bills into that run's
71
+ * tracker.
72
+ *
73
+ * `web_extract` covers both of the reading modes Tavily supports through one
74
+ * tool: with a `query` the API returns only the matching chunks of each page,
75
+ * and without one it returns the whole document. Splitting that into two tools
76
+ * would duplicate the description tokens on every request to express a
77
+ * distinction the API already makes with one optional field.
78
+ *
79
+ * @param config - Config object carrying `tavilyAPIKey` and cost constants
80
+ * @param options - Normalized options from `getTavilyOptions`
81
+ * @param usageTracker - Optional accumulator billed for every Tavily call
82
+ * @returns LangChain tool instances to append to the agent's tool list
83
+ */
84
+ export declare const buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
85
+ declare const _default: {
86
+ assertTavilyUsable: (config: any, options: TavilyConfig) => void;
87
+ buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
88
+ getTavilyOptions: (config: any) => TavilyConfig | null;
89
+ };
90
+ export default _default;
91
+ //# sourceMappingURL=tavily.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA6BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAqLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;;iCA3dyC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA6NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BA/OiC,GAAG,KAAG,YAAY,GAAG,IAAI;;AA4elE,wBAIE"}