@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.
@@ -15,6 +15,7 @@ const logger_js_1 = __importDefault(require("./logger.js"));
15
15
  const zod_1 = require("zod");
16
16
  const transform_json_schema_1 = require("@anthropic-ai/sdk/lib/transform-json-schema");
17
17
  const runtimeContext_js_1 = require("./runtimeContext.js");
18
+ const tavily_js_1 = require("./tavily.js");
18
19
  const JSON_ESCAPE_INSTRUCTION = `
19
20
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
20
21
  - Newlines → \\n
@@ -608,114 +609,12 @@ const resolveTemperatureSetting = (modelName, config) => {
608
609
  }
609
610
  return { temperature: clamped };
610
611
  };
611
- /**
612
- * Whether a Claude model can run web search through dynamic filtering, where
613
- * Claude writes and runs code that filters the search results before they reach
614
- * the context window instead of loading every result into it. Requires Claude
615
- * 4.6 or later (the models with programmatic tool calling); on anything earlier
616
- * the filtering tool versions return a 400 unless search is pinned to
617
- * `allowed_callers: ["direct"]`.
618
- * @param modelName - The Claude model identifier being routed
619
- * @returns `true` when the model supports dynamic filtering
620
- */
621
- const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
622
- /^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
623
- /**
624
- * Normalizes `config.webSearch` into an options object, returning `null` when
625
- * web search is off so callers can use it as the single enablement gate.
626
- */
627
- const getWebSearchOptions = (config) => {
628
- const webSearch = config?.webSearch;
629
- if (!webSearch)
630
- return null;
631
- return webSearch === true ? {} : webSearch;
632
- };
633
- /**
634
- * Builds the provider-native web search tool definition for a model:
635
- * - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
636
- * API within a single request and answered with citations. Defaults to the
637
- * dynamic-filtering tool version on models that support it, so search results
638
- * are filtered by code before they reach the context window.
639
- * - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
640
- * is governed by `search_context_size` (OpenAI defaults to `medium`).
641
- * - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
642
- * result-filtering or context-size control; leaving `searchTypes` unset keeps
643
- * grounding on text-only web results rather than image bytes.
644
- * Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
645
- * warning instead of throwing so one config can be pointed at any model.
646
- * @param modelName - The model identifier being routed
647
- * @param options - Normalized options from `getWebSearchOptions`
648
- * @returns The provider's tool definition, or `null` when unsupported
649
- */
650
- const buildWebSearchTool = (modelName, options) => {
651
- const { allowedDomains, blockedDomains, userLocation } = options;
652
- if (modelName.startsWith("claude-")) {
653
- // The API returns a 400 when both filters are present, so fail locally
654
- // rather than paying for the round trip.
655
- if (allowedDomains && blockedDomains) {
656
- throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
657
- }
658
- // Prefer the newest tool version the model can actually run. Basic search
659
- // loads every result into the context window; from `web_search_20260209`
660
- // Claude filters them with code first, and `web_search_20260318` can also
661
- // keep the consumed results out of the response.
662
- const canFilter = supportsAnthropicDynamicFiltering(modelName);
663
- const type = options.toolVersion ||
664
- (canFilter ? "web_search_20260318" : "web_search_20250305");
665
- const version = Number(type.slice(-8));
666
- // Filtering versions default to running search from inside code execution.
667
- // Say so explicitly when it is not wanted (or not possible), which is what
668
- // the API requires from models without programmatic tool calling.
669
- const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
670
- return {
671
- type,
672
- name: "web_search",
673
- ...(options.maxUses ? { max_uses: options.maxUses } : {}),
674
- ...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
675
- ...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
676
- ...(userLocation
677
- ? { user_location: { type: "approximate", ...userLocation } }
678
- : {}),
679
- ...(directOnly ? { allowed_callers: ["direct"] } : {}),
680
- ...(version >= 20260318
681
- ? { response_inclusion: options.responseInclusion || "excluded" }
682
- : {}),
683
- };
684
- }
685
- if (modelName.startsWith("gpt-")) {
686
- return {
687
- type: "web_search",
688
- ...(allowedDomains
689
- ? { filters: { allowed_domains: allowedDomains } }
690
- : {}),
691
- ...(userLocation
692
- ? { user_location: { type: "approximate", ...userLocation } }
693
- : {}),
694
- ...(options.searchContextSize
695
- ? { search_context_size: options.searchContextSize }
696
- : {}),
697
- };
698
- }
699
- if (modelName.startsWith("gemini-")) {
700
- // `timeRangeFilter` is the only filter the Gemini API exposes;
701
- // `excludeDomains` is a Vertex AI field and is rejected here, so domain
702
- // filters are deliberately not mapped for Google.
703
- return {
704
- googleSearch: options.timeRange
705
- ? { timeRangeFilter: options.timeRange }
706
- : {},
707
- };
708
- }
709
- logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
710
- return null;
711
- };
712
612
  /**
713
613
  * Flattens LangChain message content into plain text. Content is a string for
714
- * ordinary completions, but every provider switches to an array of blocks once
715
- * a server-side tool runsAnthropic interleaves `server_tool_use` and
716
- * `web_search_tool_result` blocks with the answer text, and OpenAI's Responses
717
- * API returns annotated text blocks — so without this the JSON parse pipeline
718
- * would receive a non-string and every web-search call would fail.
614
+ * ordinary completions, but providers switch to an array of blocks whenever the
615
+ * turn carries more than prose tool-use blocks in agentic runs, or Anthropic's
616
+ * thinking blocks so without this the JSON parse pipeline would receive a
617
+ * non-string and the call would fail.
719
618
  * @param content - A message's `content` field, or a raw string
720
619
  * @returns The concatenated text of all text blocks
721
620
  */
@@ -734,108 +633,6 @@ const extractTextContent = (content) => {
734
633
  }
735
634
  return text;
736
635
  };
737
- const createWebSearchUsage = () => ({
738
- reportedRequests: 0,
739
- callIds: new Set(),
740
- grounded: false,
741
- sources: new Map(),
742
- });
743
- /**
744
- * Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
745
- * call on every chunk of a stream and on messages that involved no search.
746
- */
747
- const collectWebSearchUsage = (payload, usage) => {
748
- if (!payload)
749
- return;
750
- const addSource = (url, title) => {
751
- if (typeof url === "string" && url && !usage.sources.has(url)) {
752
- usage.sources.set(url, {
753
- url,
754
- ...(typeof title === "string" ? { title } : {}),
755
- });
756
- }
757
- };
758
- if (Array.isArray(payload.content)) {
759
- for (const block of payload.content) {
760
- if (!block || typeof block !== "object")
761
- continue;
762
- // Anthropic: results of a search the API executed server-side.
763
- if (block.type === "web_search_tool_result" &&
764
- Array.isArray(block.content)) {
765
- for (const result of block.content) {
766
- addSource(result?.url, result?.title);
767
- }
768
- }
769
- // Anthropic: citations attached to the answer's text blocks.
770
- if (Array.isArray(block.citations)) {
771
- for (const citation of block.citations) {
772
- addSource(citation?.url, citation?.title);
773
- }
774
- }
775
- // OpenAI Responses API: one block per executed search, plus url citations.
776
- if (block.type === "web_search_call" && block.id) {
777
- usage.callIds.add(block.id);
778
- }
779
- if (Array.isArray(block.annotations)) {
780
- for (const annotation of block.annotations) {
781
- if (annotation?.type === "url_citation") {
782
- addSource(annotation.url, annotation.title);
783
- }
784
- }
785
- }
786
- }
787
- }
788
- const metadata = payload.response_metadata;
789
- if (!metadata)
790
- return;
791
- const requests = metadata.usage?.server_tool_use?.web_search_requests;
792
- if (typeof requests === "number" && requests > usage.reportedRequests) {
793
- usage.reportedRequests = requests;
794
- }
795
- const grounding = metadata.groundingMetadata;
796
- if (grounding) {
797
- usage.grounded = true;
798
- for (const chunk of grounding.groundingChunks || []) {
799
- addSource(chunk?.web?.uri, chunk?.web?.title);
800
- }
801
- }
802
- };
803
- /**
804
- * Adds a call's web-search usage to the caller-supplied tracker. Searches are
805
- * billed per request rather than per token (Anthropic charges $10 per 1,000
806
- * searches; Google charges per grounded request), so the rate is read from the
807
- * `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
808
- * accumulated under `<model>_webSearches`. Sources are appended to
809
- * `tracker.webSearchSources`, deduplicated by URL across the whole run.
810
- */
811
- const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
812
- if (!tracker || !modelName)
813
- return;
814
- // The three signals describe the same searches from different providers, so
815
- // the largest one is the count rather than their sum.
816
- const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
817
- if (!searches && usage.sources.size === 0)
818
- return;
819
- if (typeof tracker.cost !== "number")
820
- tracker.cost = 0;
821
- if (!tracker.tokens || typeof tracker.tokens !== "object")
822
- tracker.tokens = {};
823
- if (searches > 0) {
824
- const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
825
- if (Number.isFinite(addedCost) && addedCost > 0)
826
- tracker.cost += addedCost;
827
- const key = `${modelName}_webSearches`;
828
- tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
829
- }
830
- if (usage.sources.size > 0) {
831
- const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
832
- for (const source of usage.sources.values()) {
833
- if (!sources.some((existing) => existing.url === source.url)) {
834
- sources.push(source);
835
- }
836
- }
837
- }
838
- };
839
636
  /**
840
637
  * Instantiates and returns the appropriate LangChain chat model based on the model
841
638
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
@@ -862,13 +659,6 @@ const getLLMModel = (modelName, config, schema = null) => {
862
659
  // Resolve `temperature` with presence/support/range handling (see
863
660
  // resolveTemperatureSetting). Applied uniformly to every provider below.
864
661
  const modelSettings = resolveTemperatureSetting(modelName, config);
865
- // Native web search (see buildWebSearchTool). The tool is bound to the model
866
- // so both `.invoke()` and `.stream()` pick it up; the provider runs the search
867
- // server-side within the same request, so no client-side agent loop is needed.
868
- const webSearchOptions = getWebSearchOptions(config);
869
- const webSearchTool = webSearchOptions
870
- ? buildWebSearchTool(modelName, webSearchOptions)
871
- : null;
872
662
  // Claude models (Anthropic)
873
663
  if (modelName.startsWith("claude-")) {
874
664
  // Anthropic's SDK rejects non-streamed requests when max_tokens is large
@@ -902,7 +692,7 @@ const getLLMModel = (modelName, config, schema = null) => {
902
692
  ...outputConfig,
903
693
  ...modelSettings,
904
694
  });
905
- return webSearchTool ? model.bindTools([webSearchTool]) : model;
695
+ return model;
906
696
  }
907
697
  // Gemini models (Google)
908
698
  else if (modelName.startsWith("gemini-")) {
@@ -915,11 +705,6 @@ const getLLMModel = (modelName, config, schema = null) => {
915
705
  ...(schema ? { json: true } : {}),
916
706
  ...modelSettings,
917
707
  });
918
- // Combining grounding with structured output requires Gemini 3 or later;
919
- // Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
920
- const bound = webSearchTool
921
- ? model.bindTools([webSearchTool])
922
- : model;
923
708
  // `responseSchema` additionally constrains the output shape. It is a
924
709
  // call-time option (not a constructor field), so it is bound onto the model
925
710
  // via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
@@ -927,9 +712,9 @@ const getLLMModel = (modelName, config, schema = null) => {
927
712
  // parse/validate pipeline is unchanged.
928
713
  if (schema) {
929
714
  const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
930
- return bound.withConfig({ responseSchema: jsonSchema });
715
+ return model.withConfig({ responseSchema: jsonSchema });
931
716
  }
932
- return bound;
717
+ return model;
933
718
  }
934
719
  // GPT models (OpenAI)
935
720
  else if (modelName.startsWith("gpt-")) {
@@ -937,41 +722,24 @@ const getLLMModel = (modelName, config, schema = null) => {
937
722
  apiKey: config.openAIAPIKey,
938
723
  max_tokens: config.maxTokens || 200000,
939
724
  modelName: modelName,
940
- // `web_search` is a hosted Responses API tool, so the request has to go to
941
- // `/v1/responses` rather than `/v1/chat/completions`.
942
- ...(webSearchTool ? { useResponsesApi: true } : {}),
943
725
  ...modelSettings,
944
726
  };
945
- // Use native structured output with a JSON schema. The two endpoints spell
946
- // the same thing differently — Chat Completions takes `response_format`,
947
- // the Responses API takes `text.format` with the schema flattened one level
948
- // — and `modelKwargs` is spread verbatim into whichever request is built.
727
+ // Use native structured output with a JSON schema. `modelKwargs` is spread
728
+ // verbatim into the Chat Completions request.
949
729
  if (schema) {
950
730
  const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
951
- openAISettings.modelKwargs = webSearchTool
952
- ? {
953
- text: {
954
- format: {
955
- type: "json_schema",
956
- name: "response_schema",
957
- strict: true,
958
- schema: jsonSchema,
959
- },
731
+ openAISettings.modelKwargs = {
732
+ response_format: {
733
+ type: "json_schema",
734
+ json_schema: {
735
+ name: "response_schema",
736
+ strict: true,
737
+ schema: jsonSchema,
960
738
  },
961
- }
962
- : {
963
- response_format: {
964
- type: "json_schema",
965
- json_schema: {
966
- name: "response_schema",
967
- strict: true,
968
- schema: jsonSchema,
969
- },
970
- },
971
- };
739
+ },
740
+ };
972
741
  }
973
- const model = new openai_1.ChatOpenAI(openAISettings);
974
- return webSearchTool ? model.bindTools([webSearchTool]) : model;
742
+ return new openai_1.ChatOpenAI(openAISettings);
975
743
  }
976
744
  // OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
977
745
  const openAICompatible = getOpenAICompatibleProvider(modelName, config);
@@ -996,22 +764,28 @@ const getLLMModel = (modelName, config, schema = null) => {
996
764
  * an empty array
997
765
  * @param responseFormat - Optional structured response format descriptor passed to the
998
766
  * LangChain agent constructor
999
- * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
767
+ * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
768
+ * etc.), plus optional `tavily` (`true` or a `TavilyConfig`) to append the web tools
769
+ * @param usageTracker - Optional accumulator the Tavily tools bill their credits and
770
+ * record their sources into; the LLM's own token usage is tracked by `runAgent`
1000
771
  * @returns A configured LangChain agent instance ready to be run with `runAgent`
1001
772
  */
1002
- const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
1003
- // Native web search joins the agent's tool list instead of being bound inside
1004
- // `getLLMModel`: the agent binds its own tools to the model, which would drop
1005
- // anything already bound there.
1006
- const webSearchOptions = getWebSearchOptions(config);
1007
- const webSearchTool = webSearchOptions
1008
- ? buildWebSearchTool(modelName, webSearchOptions)
1009
- : null;
773
+ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, usageTracker = null) => {
774
+ // The Tavily tools join the agent's tool list rather than being bound inside
775
+ // `getLLMModel`: they run client-side, so only the agent loop can execute
776
+ // them, and the agent binds its own tools to the model anyway — anything
777
+ // bound there would be dropped.
778
+ const tavilyOptions = (0, tavily_js_1.getTavilyOptions)(config);
779
+ let tavilyTools = [];
780
+ if (tavilyOptions) {
781
+ (0, tavily_js_1.assertTavilyUsable)(config, tavilyOptions);
782
+ tavilyTools = (0, tavily_js_1.buildTavilyTools)(config, tavilyOptions, usageTracker);
783
+ }
1010
784
  const agent = (0, langchain_1.createAgent)({
1011
785
  name: name,
1012
- model: getLLMModel(modelName, { ...config, webSearch: null }),
786
+ model: getLLMModel(modelName, config),
1013
787
  systemPrompt: systemPrompt.trim(),
1014
- tools: webSearchTool ? [...tools, webSearchTool] : tools,
788
+ tools: tavilyTools.length > 0 ? [...tools, ...tavilyTools] : tools,
1015
789
  ...(responseFormat ? { responseFormat: responseFormat } : {}),
1016
790
  });
1017
791
  return agent;
@@ -1098,13 +872,6 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
1098
872
  }
1099
873
  }
1100
874
  updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
1101
- // Web-search activity is spread across the agent's messages — one search may
1102
- // be reported by the message that ran it and cited by a later one.
1103
- const webSearchUsage = createWebSearchUsage();
1104
- for (const message of response?.messages || []) {
1105
- collectWebSearchUsage(message, webSearchUsage);
1106
- }
1107
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1108
875
  const endTime = Date.now();
1109
876
  const duration = endTime - startTime;
1110
877
  logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -1342,15 +1109,15 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
1342
1109
  * When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
1343
1110
  * system prompt and the parsed result is optionally validated against `schema`.
1344
1111
  *
1345
- * Setting `config.webSearch` enables the provider's native web search tool in every
1346
- * mode (see `buildWebSearchTool`). The provider runs the search server-side inside the
1347
- * same request, so the return contract is unchanged; the searches performed and the
1348
- * sources cited are recorded on `usageTracker`.
1112
+ * Setting `config.tavily` adds the Tavily web tools (`web_search`, `web_extract`, and
1113
+ * optionally `web_map` / `web_crawl`). They execute in this process, so they require
1114
+ * `config.agentic` to be `true`; the credits spent and the URLs returned are recorded
1115
+ * on `usageTracker`.
1349
1116
  * @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
1350
1117
  * `"gemini-1.5-pro"`
1351
1118
  * @param config - Configuration object with API keys, `temperature`, optional `agentic`
1352
- * flag, optional `recursionLimit`, and optional `webSearch` (`true` or a
1353
- * `WebSearchConfig`)
1119
+ * flag, optional `recursionLimit`, and optional `tavily` (`true` or a
1120
+ * `TavilyConfig`)
1354
1121
  * @param prompt - The prompt to send; either a plain string (user message only) or an
1355
1122
  * array of `{ role, content }` message objects
1356
1123
  * @param onProgressReport - Optional async callback invoked with `{ message, progress }`
@@ -1401,9 +1168,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1401
1168
  ? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
1402
1169
  : JSON_ESCAPE_INSTRUCTION;
1403
1170
  }
1404
- // Create the agent with tools
1171
+ // Create the agent with tools. The tracker is handed over at construction
1172
+ // because the Tavily tools bill their own credits as they run, rather than
1173
+ // being reconciled from the response afterwards.
1405
1174
  const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
1406
- config);
1175
+ config, usageTracker);
1407
1176
  // Run the agent with progress callback
1408
1177
  const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
1409
1178
  // Extract content from agent response
@@ -1536,7 +1305,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1536
1305
  output_tokens: 0,
1537
1306
  total_tokens: 0,
1538
1307
  };
1539
- let webSearchUsage = createWebSearchUsage();
1540
1308
  // Inner loop: wait + retry on 429 around stream setup and consumption.
1541
1309
  // Usage is only recorded on a successful stream — partial streams that
1542
1310
  // error out with a rate limit are not counted. A 429 fired mid-stream
@@ -1546,7 +1314,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1546
1314
  rawContent = "";
1547
1315
  chunkCount = 0;
1548
1316
  streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
1549
- webSearchUsage = createWebSearchUsage();
1550
1317
  try {
1551
1318
  // Honour caller cancellation: passing the signal tears down the
1552
1319
  // upstream HTTP request so a cancelled call stops billing tokens.
@@ -1559,9 +1326,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1559
1326
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
1560
1327
  }
1561
1328
  accumulateChunkUsage(streamUsage, chunk);
1562
- collectWebSearchUsage(chunk, webSearchUsage);
1563
1329
  // Counting every chunk (not just the ones carrying text) keeps
1564
- // progress ticking through the pause while a search runs.
1330
+ // progress ticking through pauses in the stream.
1565
1331
  chunkCount++;
1566
1332
  rawContent += extractTextContent(chunk?.content ?? chunk);
1567
1333
  if (chunkCount % progressReportInterval === 0) {
@@ -1591,7 +1357,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1591
1357
  }
1592
1358
  }
1593
1359
  updateUsageTracker(usageTracker, modelName, streamUsage, config);
1594
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1595
1360
  if (!rawContent) {
1596
1361
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
1597
1362
  }
@@ -1696,10 +1461,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1696
1461
  }
1697
1462
  }
1698
1463
  updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
1699
- const webSearchUsage = createWebSearchUsage();
1700
- collectWebSearchUsage(response, webSearchUsage);
1701
- updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1702
- // Flattened because a server-tool turn returns content blocks, not a string.
1464
+ // Flattened because providers return content blocks, not a string, once
1465
+ // the turn carries anything besides prose.
1703
1466
  const rawContent = extractTextContent(response?.content ?? response);
1704
1467
  // If not expecting JSON, return raw content directly
1705
1468
  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"}