@librechat/agents 3.2.65 → 3.2.67

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.
Files changed (78) hide show
  1. package/dist/cjs/graphs/Graph.cjs +15 -2
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/createToolPolicyHook.cjs +5 -5
  4. package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
  5. package/dist/cjs/instrumentation.cjs +15 -3
  6. package/dist/cjs/instrumentation.cjs.map +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +1 -2
  8. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  9. package/dist/cjs/langfuseTraceShaping.cjs +51 -24
  10. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +8 -0
  12. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +8 -0
  14. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  15. package/dist/cjs/tools/BashExecutor.cjs +9 -8
  16. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  17. package/dist/cjs/tools/CodeExecutor.cjs +9 -7
  18. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolSearch.cjs +59 -14
  20. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  21. package/dist/cjs/tools/search/tool.cjs +5 -3
  22. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  23. package/dist/esm/graphs/Graph.mjs +15 -2
  24. package/dist/esm/graphs/Graph.mjs.map +1 -1
  25. package/dist/esm/hooks/createToolPolicyHook.mjs +5 -5
  26. package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
  27. package/dist/esm/instrumentation.mjs +15 -3
  28. package/dist/esm/instrumentation.mjs.map +1 -1
  29. package/dist/esm/langfuseToolOutputTracing.mjs +1 -2
  30. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  31. package/dist/esm/langfuseTraceShaping.mjs +51 -24
  32. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  33. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +8 -0
  34. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  35. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +8 -0
  36. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  37. package/dist/esm/tools/BashExecutor.mjs +9 -8
  38. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  39. package/dist/esm/tools/CodeExecutor.mjs +9 -7
  40. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  41. package/dist/esm/tools/ToolSearch.mjs +59 -14
  42. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  43. package/dist/esm/tools/search/tool.mjs +5 -3
  44. package/dist/esm/tools/search/tool.mjs.map +1 -1
  45. package/dist/types/graphs/Graph.d.ts +2 -0
  46. package/dist/types/hooks/createToolPolicyHook.d.ts +12 -15
  47. package/dist/types/langfuseTraceShaping.d.ts +12 -6
  48. package/dist/types/tools/BashExecutor.d.ts +7 -6
  49. package/dist/types/tools/CodeExecutor.d.ts +7 -5
  50. package/dist/types/tools/search/types.d.ts +2 -0
  51. package/dist/types/types/graph.d.ts +10 -3
  52. package/package.json +1 -1
  53. package/src/graphs/Graph.ts +21 -3
  54. package/src/hooks/__tests__/createToolPolicyHook.test.ts +13 -4
  55. package/src/hooks/createToolPolicyHook.ts +16 -19
  56. package/src/instrumentation.ts +20 -0
  57. package/src/langfuseToolOutputTracing.ts +2 -4
  58. package/src/langfuseTraceShaping.ts +73 -20
  59. package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +110 -0
  60. package/src/llm/anthropic/utils/message_inputs.ts +15 -0
  61. package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +122 -0
  62. package/src/llm/bedrock/utils/message_inputs.ts +13 -0
  63. package/src/specs/langfuse-instrumentation.test.ts +64 -0
  64. package/src/specs/langfuse-routing.integration.test.ts +49 -4
  65. package/src/specs/langfuse-tool-output-tracing.test.ts +5 -4
  66. package/src/specs/langfuse-trace-shaping.test.ts +80 -9
  67. package/src/specs/subagent.test.ts +180 -0
  68. package/src/tools/BashExecutor.ts +9 -8
  69. package/src/tools/CodeExecutor.ts +9 -7
  70. package/src/tools/ToolSearch.ts +81 -23
  71. package/src/tools/__tests__/BashExecutor.test.ts +16 -5
  72. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +17 -6
  73. package/src/tools/__tests__/ToolSearch.test.ts +151 -0
  74. package/src/tools/__tests__/hitl.test.ts +114 -1
  75. package/src/tools/search/source-processing.test.ts +57 -0
  76. package/src/tools/search/tool.ts +13 -1
  77. package/src/tools/search/types.ts +2 -0
  78. package/src/types/graph.ts +10 -3
@@ -1 +1 @@
1
- {"version":3,"file":"tool.mjs","names":["params"],"sources":["../../../../src/tools/search/tool.ts"],"sourcesContent":["import { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type { RunnableConfig } from '@langchain/core/runnables';\nimport type * as t from './types';\nimport {\n WebSearchToolDescription,\n WebSearchToolName,\n countrySchema,\n imagesSchema,\n videosSchema,\n querySchema,\n dateSchema,\n newsSchema,\n DATE_RANGE,\n} from './schema';\nimport { createSearchAPI, createSourceProcessor } from './search';\nimport { createSerperScraper } from './serper-scraper';\nimport { createTavilyScraper } from './tavily-scraper';\nimport { createFirecrawlScraper } from './firecrawl';\nimport { createCrwScraper } from './crw-scraper';\nimport { expandHighlights } from './highlights';\nimport { formatResultsForLLM } from './format';\nimport { createDefaultLogger } from './utils';\nimport { createReranker } from './rerankers';\nimport { Constants } from '@/common';\n\n/**\n * Executes parallel searches and merges the results,\n * deduplicating top stories by link\n */\nexport async function executeParallelSearches({\n searchAPI,\n query,\n date,\n country,\n safeSearch,\n images,\n videos,\n news,\n logger,\n}: {\n searchAPI: ReturnType<typeof createSearchAPI>;\n query: string;\n date?: DATE_RANGE;\n country?: string;\n safeSearch: t.SearchToolConfig['safeSearch'];\n images: boolean;\n videos: boolean;\n news: boolean;\n logger: t.Logger;\n}): Promise<t.SearchResult> {\n // Prepare all search tasks to run in parallel\n const searchTasks: Promise<t.SearchResult>[] = [\n // Main search\n searchAPI.getSources({\n query,\n date,\n country,\n safeSearch,\n }),\n ];\n\n if (images) {\n searchTasks.push(\n searchAPI\n .getSources({\n query,\n date,\n country,\n safeSearch,\n type: 'images',\n })\n .catch((error) => {\n logger.error('Error fetching images:', error);\n return {\n success: false,\n error: `Images search failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n })\n );\n }\n if (videos) {\n searchTasks.push(\n searchAPI\n .getSources({\n query,\n date,\n country,\n safeSearch,\n type: 'videos',\n })\n .catch((error) => {\n logger.error('Error fetching videos:', error);\n return {\n success: false,\n error: `Videos search failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n })\n );\n }\n if (news) {\n searchTasks.push(\n searchAPI\n .getSources({\n query,\n date,\n country,\n safeSearch,\n type: 'news',\n })\n .catch((error) => {\n logger.error('Error fetching news:', error);\n return {\n success: false,\n error: `News search failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n })\n );\n }\n\n // Run all searches in parallel\n const results = await Promise.all(searchTasks);\n\n // Get the main search result (first result)\n const mainResult = results[0];\n if (!mainResult.success) {\n throw new Error(mainResult.error ?? 'Search failed');\n }\n\n // Merge additional results with the main results\n const mergedResults = { ...mainResult.data };\n\n // Convert existing news to topStories if present\n if (mergedResults.news !== undefined && mergedResults.news.length > 0) {\n const existingNewsAsTopStories = mergedResults.news\n .filter((newsItem) => newsItem.link !== undefined && newsItem.link !== '')\n .map((newsItem) => ({\n title: newsItem.title ?? '',\n link: newsItem.link ?? '',\n source: newsItem.source ?? '',\n date: newsItem.date ?? '',\n imageUrl: newsItem.imageUrl ?? '',\n processed: false,\n }));\n mergedResults.topStories = [\n ...(mergedResults.topStories ?? []),\n ...existingNewsAsTopStories,\n ];\n delete mergedResults.news;\n }\n\n results.slice(1).forEach((result) => {\n if (result.success && result.data !== undefined) {\n if (result.data.images !== undefined && result.data.images.length > 0) {\n mergedResults.images = [\n ...(mergedResults.images ?? []),\n ...result.data.images,\n ];\n }\n if (result.data.videos !== undefined && result.data.videos.length > 0) {\n mergedResults.videos = [\n ...(mergedResults.videos ?? []),\n ...result.data.videos,\n ];\n }\n if (result.data.news !== undefined && result.data.news.length > 0) {\n const newsAsTopStories = result.data.news.map((newsItem) => ({\n ...newsItem,\n link: newsItem.link ?? '',\n }));\n mergedResults.topStories = [\n ...(mergedResults.topStories ?? []),\n ...newsAsTopStories,\n ];\n }\n }\n });\n\n if (\n mergedResults.topStories !== undefined &&\n mergedResults.topStories.length > 1\n ) {\n /** The main search's own news results and the parallel news sub-search\n * frequently return the same stories — keep the first occurrence of each\n * link so duplicates aren't scraped, reranked, and formatted repeatedly */\n const seenLinks = new Set<string>();\n mergedResults.topStories = mergedResults.topStories.filter((story) => {\n if (!story.link || seenLinks.has(story.link)) {\n return false;\n }\n seenLinks.add(story.link);\n return true;\n });\n }\n\n return { success: true, data: mergedResults };\n}\n\nfunction createSearchProcessor({\n searchAPI,\n safeSearch,\n supportsImages,\n supportsVideos,\n supportsNews,\n sourceProcessor,\n onGetHighlights,\n logger,\n}: {\n safeSearch: t.SearchToolConfig['safeSearch'];\n supportsImages: boolean;\n supportsVideos: boolean;\n supportsNews: boolean;\n searchAPI: ReturnType<typeof createSearchAPI>;\n sourceProcessor: ReturnType<typeof createSourceProcessor>;\n onGetHighlights: t.SearchToolConfig['onGetHighlights'];\n logger: t.Logger;\n}) {\n return async function ({\n query,\n date,\n country,\n proMode = true,\n maxSources = 5,\n onSearchResults,\n images = false,\n videos = false,\n news = false,\n }: {\n query: string;\n country?: string;\n date?: DATE_RANGE;\n proMode?: boolean;\n maxSources?: number;\n onSearchResults: t.SearchToolConfig['onSearchResults'];\n images?: boolean;\n videos?: boolean;\n news?: boolean;\n }): Promise<t.SearchResultData> {\n try {\n // Execute parallel searches and merge results\n const searchResult = await executeParallelSearches({\n searchAPI,\n query,\n date,\n country,\n safeSearch,\n images: supportsImages && images,\n videos: supportsVideos && videos,\n news: supportsNews && news,\n logger,\n });\n\n onSearchResults?.(searchResult);\n\n const processedSources = await sourceProcessor.processSources({\n query,\n news,\n result: searchResult,\n proMode,\n onGetHighlights,\n numElements: maxSources,\n });\n\n return expandHighlights(processedSources);\n } catch (error) {\n logger.error('Error in search:', error);\n return {\n organic: [],\n topStories: [],\n images: [],\n videos: [],\n news: [],\n relatedSearches: [],\n error: error instanceof Error ? error.message : String(error),\n };\n }\n };\n}\n\nfunction createOnSearchResults({\n runnableConfig,\n onSearchResults,\n}: {\n runnableConfig: RunnableConfig;\n onSearchResults: t.SearchToolConfig['onSearchResults'];\n}) {\n return function (results: t.SearchResult): void {\n if (!onSearchResults) {\n return;\n }\n onSearchResults(results, runnableConfig);\n };\n}\n\nfunction createTool({\n schema,\n search,\n maxOutputChars,\n onSearchResults: _onSearchResults,\n}: {\n schema: Record<string, unknown>;\n search: ReturnType<typeof createSearchProcessor>;\n maxOutputChars?: number;\n onSearchResults: t.SearchToolConfig['onSearchResults'];\n}): DynamicStructuredTool {\n return tool(\n async (rawParams, runnableConfig) => {\n const params = rawParams as SearchToolParams;\n const { query, date, country: _c, images, videos, news } = params;\n const country = typeof _c === 'string' && _c ? _c : undefined;\n const searchResult = await search({\n query,\n date,\n country,\n images,\n videos,\n news,\n onSearchResults: createOnSearchResults({\n runnableConfig,\n onSearchResults: _onSearchResults,\n }),\n });\n const turn = runnableConfig.toolCall?.turn ?? 0;\n const { output, references } = formatResultsForLLM(\n turn,\n searchResult,\n maxOutputChars\n );\n const data: t.SearchResultData = { turn, ...searchResult, references };\n return [output, { [Constants.WEB_SEARCH]: data }];\n },\n {\n name: WebSearchToolName,\n description: WebSearchToolDescription,\n schema: schema,\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\n/**\n * Creates a search tool with configurable search and scraper providers.\n *\n * Search providers: Serper (Google results), SearXNG (self-hosted meta-search), Tavily (AI-optimized), fastCRW (Firecrawl-compatible, self-host or cloud).\n * Scraper providers: Firecrawl (default, full-featured), Serper (lightweight), Tavily (batch extraction), fastCRW (Firecrawl-compatible, self-host or cloud).\n *\n * The country schema field is exposed to the LLM for providers that support localized results.\n */\n/** Input params type for search tool */\ninterface SearchToolParams {\n query: string;\n date?: DATE_RANGE;\n country?: string;\n images?: boolean;\n videos?: boolean;\n news?: boolean;\n}\n\nexport const createSearchTool = (\n config: t.SearchToolConfig = {}\n): DynamicStructuredTool => {\n const {\n searchProvider = 'serper',\n serperApiKey,\n searxngInstanceUrl,\n searxngApiKey,\n tavilyApiKey,\n tavilySearchUrl,\n tavilyExtractUrl,\n tavilySearchOptions,\n keenableApiKey,\n keenableApiUrl,\n keenableSearchOptions,\n rerankerType = 'cohere',\n rerankerTimeout,\n topResults = 5,\n maxContentLength,\n chunkSize,\n chunkOverlap,\n maxOutputChars,\n strategies = ['no_extraction'],\n filterContent = true,\n safeSearch = 1,\n scraperProvider = 'firecrawl',\n firecrawlApiKey,\n firecrawlApiUrl,\n firecrawlVersion,\n firecrawlOptions,\n serperScraperOptions,\n tavilyScraperOptions,\n crwApiKey,\n crwApiUrl,\n crwSearchOptions,\n crwScraperOptions,\n scraperTimeout,\n jinaApiKey,\n jinaApiUrl,\n cohereApiKey,\n onSearchResults: _onSearchResults,\n onGetHighlights,\n } = config;\n\n const logger = config.logger || createDefaultLogger();\n const effectiveTavilySearchOptions =\n searchProvider === 'tavily' && config.safeSearch != null\n ? {\n ...tavilySearchOptions,\n safeSearch: config.safeSearch !== 0,\n }\n : tavilySearchOptions;\n\n const schemaProperties: Record<string, unknown> = {\n query: querySchema,\n date: dateSchema,\n images: imagesSchema,\n videos: videosSchema,\n news: newsSchema,\n };\n\n if (searchProvider === 'serper' || searchProvider === 'tavily') {\n schemaProperties.country = countrySchema;\n }\n\n const toolSchema = {\n type: 'object',\n properties: schemaProperties,\n required: ['query'],\n };\n\n const searchAPI = createSearchAPI({\n searchProvider,\n serperApiKey,\n searxngInstanceUrl,\n searxngApiKey,\n tavilyApiKey,\n tavilySearchUrl,\n tavilySearchOptions: effectiveTavilySearchOptions,\n keenableApiKey,\n keenableApiUrl,\n keenableSearchOptions,\n crwApiKey,\n crwApiUrl,\n crwSearchOptions,\n });\n\n /** Create scraper based on scraperProvider */\n let scraperInstance: t.BaseScraper;\n\n if (scraperProvider === 'serper') {\n scraperInstance = createSerperScraper({\n ...serperScraperOptions,\n apiKey: serperApiKey,\n timeout: scraperTimeout ?? serperScraperOptions?.timeout,\n logger,\n });\n } else if (scraperProvider === 'tavily') {\n scraperInstance = createTavilyScraper({\n ...tavilyScraperOptions,\n apiKey:\n tavilyScraperOptions?.apiKey ??\n tavilyApiKey ??\n process.env.TAVILY_API_KEY,\n apiUrl: tavilyScraperOptions?.apiUrl ?? tavilyExtractUrl,\n timeout: scraperTimeout ?? tavilyScraperOptions?.timeout,\n logger,\n });\n } else if (scraperProvider === 'crw') {\n scraperInstance = createCrwScraper({\n ...crwScraperOptions,\n apiKey:\n crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,\n apiUrl: crwScraperOptions?.apiUrl ?? crwApiUrl,\n timeout: scraperTimeout ?? crwScraperOptions?.timeout,\n formats: crwScraperOptions?.formats ?? ['markdown', 'rawHtml'],\n logger,\n });\n } else {\n scraperInstance = createFirecrawlScraper({\n ...firecrawlOptions,\n apiKey: firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY,\n apiUrl: firecrawlApiUrl,\n version: firecrawlVersion,\n timeout: scraperTimeout ?? firecrawlOptions?.timeout,\n formats: firecrawlOptions?.formats ?? ['markdown', 'rawHtml'],\n logger,\n });\n }\n\n const selectedReranker = createReranker({\n rerankerType,\n jinaApiKey,\n jinaApiUrl,\n cohereApiKey,\n rerankerTimeout,\n logger,\n });\n\n if (!selectedReranker) {\n logger.warn('No reranker selected. Using default ranking.');\n }\n\n const sourceProcessor = createSourceProcessor(\n {\n reranker: selectedReranker,\n topResults,\n maxContentLength,\n chunkSize,\n chunkOverlap,\n strategies,\n filterContent,\n logger,\n },\n scraperInstance\n );\n\n const search = createSearchProcessor({\n searchAPI,\n safeSearch,\n // Keenable is organic-only: its API ignores `type`, so image/news\n // sub-searches would spend rate limit and merge nothing.\n supportsImages: searchProvider !== 'keenable',\n supportsVideos:\n searchProvider !== 'tavily' &&\n searchProvider !== 'keenable' &&\n searchProvider !== 'crw',\n supportsNews: searchProvider !== 'keenable',\n sourceProcessor,\n onGetHighlights,\n logger,\n });\n\n return createTool({\n search,\n schema: toolSchema,\n maxOutputChars,\n onSearchResults: _onSearchResults,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,eAAsB,wBAAwB,EAC5C,WACA,OACA,MACA,SACA,YACA,QACA,QACA,MACA,UAW0B;CAE1B,MAAM,cAAyC,CAE7C,UAAU,WAAW;EACnB;EACA;EACA;EACA;CACF,CAAC,CACH;CAEA,IAAI,QACF,YAAY,KACV,UACG,WAAW;EACV;EACA;EACA;EACA;EACA,MAAM;CACR,CAAC,CAAC,CACD,OAAO,UAAU;EAChB,OAAO,MAAM,0BAA0B,KAAK;EAC5C,OAAO;GACL,SAAS;GACT,OAAO,yBAAyB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF;CACF,CAAC,CACL;CAEF,IAAI,QACF,YAAY,KACV,UACG,WAAW;EACV;EACA;EACA;EACA;EACA,MAAM;CACR,CAAC,CAAC,CACD,OAAO,UAAU;EAChB,OAAO,MAAM,0BAA0B,KAAK;EAC5C,OAAO;GACL,SAAS;GACT,OAAO,yBAAyB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF;CACF,CAAC,CACL;CAEF,IAAI,MACF,YAAY,KACV,UACG,WAAW;EACV;EACA;EACA;EACA;EACA,MAAM;CACR,CAAC,CAAC,CACD,OAAO,UAAU;EAChB,OAAO,MAAM,wBAAwB,KAAK;EAC1C,OAAO;GACL,SAAS;GACT,OAAO,uBAAuB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrF;CACF,CAAC,CACL;CAIF,MAAM,UAAU,MAAM,QAAQ,IAAI,WAAW;CAG7C,MAAM,aAAa,QAAQ;CAC3B,IAAI,CAAC,WAAW,SACd,MAAM,IAAI,MAAM,WAAW,SAAS,eAAe;CAIrD,MAAM,gBAAgB,EAAE,GAAG,WAAW,KAAK;CAG3C,IAAI,cAAc,SAAS,KAAA,KAAa,cAAc,KAAK,SAAS,GAAG;EACrE,MAAM,2BAA2B,cAAc,KAC5C,QAAQ,aAAa,SAAS,SAAS,KAAA,KAAa,SAAS,SAAS,EAAE,CAAC,CACzE,KAAK,cAAc;GAClB,OAAO,SAAS,SAAS;GACzB,MAAM,SAAS,QAAQ;GACvB,QAAQ,SAAS,UAAU;GAC3B,MAAM,SAAS,QAAQ;GACvB,UAAU,SAAS,YAAY;GAC/B,WAAW;EACb,EAAE;EACJ,cAAc,aAAa,CACzB,GAAI,cAAc,cAAc,CAAC,GACjC,GAAG,wBACL;EACA,OAAO,cAAc;CACvB;CAEA,QAAQ,MAAM,CAAC,CAAC,CAAC,SAAS,WAAW;EACnC,IAAI,OAAO,WAAW,OAAO,SAAS,KAAA,GAAW;GAC/C,IAAI,OAAO,KAAK,WAAW,KAAA,KAAa,OAAO,KAAK,OAAO,SAAS,GAClE,cAAc,SAAS,CACrB,GAAI,cAAc,UAAU,CAAC,GAC7B,GAAG,OAAO,KAAK,MACjB;GAEF,IAAI,OAAO,KAAK,WAAW,KAAA,KAAa,OAAO,KAAK,OAAO,SAAS,GAClE,cAAc,SAAS,CACrB,GAAI,cAAc,UAAU,CAAC,GAC7B,GAAG,OAAO,KAAK,MACjB;GAEF,IAAI,OAAO,KAAK,SAAS,KAAA,KAAa,OAAO,KAAK,KAAK,SAAS,GAAG;IACjE,MAAM,mBAAmB,OAAO,KAAK,KAAK,KAAK,cAAc;KAC3D,GAAG;KACH,MAAM,SAAS,QAAQ;IACzB,EAAE;IACF,cAAc,aAAa,CACzB,GAAI,cAAc,cAAc,CAAC,GACjC,GAAG,gBACL;GACF;EACF;CACF,CAAC;CAED,IACE,cAAc,eAAe,KAAA,KAC7B,cAAc,WAAW,SAAS,GAClC;;;;EAIA,MAAM,4BAAY,IAAI,IAAY;EAClC,cAAc,aAAa,cAAc,WAAW,QAAQ,UAAU;GACpE,IAAI,CAAC,MAAM,QAAQ,UAAU,IAAI,MAAM,IAAI,GACzC,OAAO;GAET,UAAU,IAAI,MAAM,IAAI;GACxB,OAAO;EACT,CAAC;CACH;CAEA,OAAO;EAAE,SAAS;EAAM,MAAM;CAAc;AAC9C;AAEA,SAAS,sBAAsB,EAC7B,WACA,YACA,gBACA,gBACA,cACA,iBACA,iBACA,UAUC;CACD,OAAO,eAAgB,EACrB,OACA,MACA,SACA,UAAU,MACV,aAAa,GACb,iBACA,SAAS,OACT,SAAS,OACT,OAAO,SAWuB;EAC9B,IAAI;GAEF,MAAM,eAAe,MAAM,wBAAwB;IACjD;IACA;IACA;IACA;IACA;IACA,QAAQ,kBAAkB;IAC1B,QAAQ,kBAAkB;IAC1B,MAAM,gBAAgB;IACtB;GACF,CAAC;GAED,kBAAkB,YAAY;GAW9B,OAAO,iBAAiB,MATO,gBAAgB,eAAe;IAC5D;IACA;IACA,QAAQ;IACR;IACA;IACA,aAAa;GACf,CAAC,CAEuC;EAC1C,SAAS,OAAO;GACd,OAAO,MAAM,oBAAoB,KAAK;GACtC,OAAO;IACL,SAAS,CAAC;IACV,YAAY,CAAC;IACb,QAAQ,CAAC;IACT,QAAQ,CAAC;IACT,MAAM,CAAC;IACP,iBAAiB,CAAC;IAClB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D;EACF;CACF;AACF;AAEA,SAAS,sBAAsB,EAC7B,gBACA,mBAIC;CACD,OAAO,SAAU,SAA+B;EAC9C,IAAI,CAAC,iBACH;EAEF,gBAAgB,SAAS,cAAc;CACzC;AACF;AAEA,SAAS,WAAW,EAClB,QACA,QACA,gBACA,iBAAiB,oBAMO;CACxB,OAAO,KACL,OAAO,WAAW,mBAAmB;EAEnC,MAAM,EAAE,OAAO,MAAM,SAAS,IAAI,QAAQ,QAAQ,SAASA;EAE3D,MAAM,eAAe,MAAM,OAAO;GAChC;GACA;GACA,SAJc,OAAO,OAAO,YAAY,KAAK,KAAK,KAAA;GAKlD;GACA;GACA;GACA,iBAAiB,sBAAsB;IACrC;IACA,iBAAiB;GACnB,CAAC;EACH,CAAC;EACD,MAAM,OAAO,eAAe,UAAU,QAAQ;EAC9C,MAAM,EAAE,QAAQ,eAAe,oBAC7B,MACA,cACA,cACF;EACA,MAAM,OAA2B;GAAE;GAAM,GAAG;GAAc;EAAW;EACrE,OAAO,CAAC,QAAQ,GAAA,eAA0B,KAAK,CAAC;CAClD,GACA;EACE,MAAM;EACN,aAAa;EACL;EACR,gBAAA;CACF,CACF;AACF;AAoBA,MAAa,oBACX,SAA6B,CAAC,MACJ;CAC1B,MAAM,EACJ,iBAAiB,UACjB,cACA,oBACA,eACA,cACA,iBACA,kBACA,qBACA,gBACA,gBACA,uBACA,eAAe,UACf,iBACA,aAAa,GACb,kBACA,WACA,cACA,gBACA,aAAa,CAAC,eAAe,GAC7B,gBAAgB,MAChB,aAAa,GACb,kBAAkB,aAClB,iBACA,iBACA,kBACA,kBACA,sBACA,sBACA,WACA,WACA,kBACA,mBACA,gBACA,YACA,YACA,cACA,iBAAiB,kBACjB,oBACE;CAEJ,MAAM,SAAS,OAAO,UAAU,oBAAoB;CACpD,MAAM,+BACJ,mBAAmB,YAAY,OAAO,cAAc,OAChD;EACA,GAAG;EACH,YAAY,OAAO,eAAe;CACpC,IACE;CAEN,MAAM,mBAA4C;EAChD,OAAO;EACP,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,MAAM;CACR;CAEA,IAAI,mBAAmB,YAAY,mBAAmB,UACpD,iBAAiB,UAAU;CAG7B,MAAM,aAAa;EACjB,MAAM;EACN,YAAY;EACZ,UAAU,CAAC,OAAO;CACpB;CAEA,MAAM,YAAY,gBAAgB;EAChC;EACA;EACA;EACA;EACA;EACA;EACA,qBAAqB;EACrB;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;;CAGD,IAAI;CAEJ,IAAI,oBAAoB,UACtB,kBAAkB,oBAAoB;EACpC,GAAG;EACH,QAAQ;EACR,SAAS,kBAAkB,sBAAsB;EACjD;CACF,CAAC;MACI,IAAI,oBAAoB,UAC7B,kBAAkB,oBAAoB;EACpC,GAAG;EACH,QACE,sBAAsB,UACtB,gBACA,QAAQ,IAAI;EACd,QAAQ,sBAAsB,UAAU;EACxC,SAAS,kBAAkB,sBAAsB;EACjD;CACF,CAAC;MACI,IAAI,oBAAoB,OAC7B,kBAAkB,iBAAiB;EACjC,GAAG;EACH,QACE,mBAAmB,UAAU,aAAa,QAAQ,IAAI;EACxD,QAAQ,mBAAmB,UAAU;EACrC,SAAS,kBAAkB,mBAAmB;EAC9C,SAAS,mBAAmB,WAAW,CAAC,YAAY,SAAS;EAC7D;CACF,CAAC;MAED,kBAAkB,uBAAuB;EACvC,GAAG;EACH,QAAQ,mBAAmB,QAAQ,IAAI;EACvC,QAAQ;EACR,SAAS;EACT,SAAS,kBAAkB,kBAAkB;EAC7C,SAAS,kBAAkB,WAAW,CAAC,YAAY,SAAS;EAC5D;CACF,CAAC;CAGH,MAAM,mBAAmB,eAAe;EACtC;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,IAAI,CAAC,kBACH,OAAO,KAAK,8CAA8C;CAG5D,MAAM,kBAAkB,sBACtB;EACE,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;CACF,GACA,eACF;CAkBA,OAAO,WAAW;EAChB,QAjBa,sBAAsB;GACnC;GACA;GAGA,gBAAgB,mBAAmB;GACnC,gBACE,mBAAmB,YACnB,mBAAmB,cACnB,mBAAmB;GACrB,cAAc,mBAAmB;GACjC;GACA;GACA;EACF,CAGO;EACL,QAAQ;EACR;EACA,iBAAiB;CACnB,CAAC;AACH"}
1
+ {"version":3,"file":"tool.mjs","names":["params"],"sources":["../../../../src/tools/search/tool.ts"],"sourcesContent":["import { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type { RunnableConfig } from '@langchain/core/runnables';\nimport type * as t from './types';\nimport {\n WebSearchToolDescription,\n WebSearchToolName,\n countrySchema,\n imagesSchema,\n videosSchema,\n querySchema,\n dateSchema,\n newsSchema,\n DATE_RANGE,\n} from './schema';\nimport { createSearchAPI, createSourceProcessor } from './search';\nimport { createSerperScraper } from './serper-scraper';\nimport { createTavilyScraper } from './tavily-scraper';\nimport { createFirecrawlScraper } from './firecrawl';\nimport { createCrwScraper } from './crw-scraper';\nimport { expandHighlights } from './highlights';\nimport { formatResultsForLLM } from './format';\nimport { createDefaultLogger } from './utils';\nimport { createReranker } from './rerankers';\nimport { Constants } from '@/common';\n\n/**\n * Executes parallel searches and merges the results,\n * deduplicating top stories by link\n */\nexport async function executeParallelSearches({\n searchAPI,\n query,\n date,\n country,\n safeSearch,\n images,\n videos,\n news,\n logger,\n}: {\n searchAPI: ReturnType<typeof createSearchAPI>;\n query: string;\n date?: DATE_RANGE;\n country?: string;\n safeSearch: t.SearchToolConfig['safeSearch'];\n images: boolean;\n videos: boolean;\n news: boolean;\n logger: t.Logger;\n}): Promise<t.SearchResult> {\n // Prepare all search tasks to run in parallel\n const searchTasks: Promise<t.SearchResult>[] = [\n // Main search\n searchAPI.getSources({\n query,\n date,\n country,\n safeSearch,\n }),\n ];\n\n if (images) {\n searchTasks.push(\n searchAPI\n .getSources({\n query,\n date,\n country,\n safeSearch,\n type: 'images',\n })\n .catch((error) => {\n logger.error('Error fetching images:', error);\n return {\n success: false,\n error: `Images search failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n })\n );\n }\n if (videos) {\n searchTasks.push(\n searchAPI\n .getSources({\n query,\n date,\n country,\n safeSearch,\n type: 'videos',\n })\n .catch((error) => {\n logger.error('Error fetching videos:', error);\n return {\n success: false,\n error: `Videos search failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n })\n );\n }\n if (news) {\n searchTasks.push(\n searchAPI\n .getSources({\n query,\n date,\n country,\n safeSearch,\n type: 'news',\n })\n .catch((error) => {\n logger.error('Error fetching news:', error);\n return {\n success: false,\n error: `News search failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n })\n );\n }\n\n // Run all searches in parallel\n const results = await Promise.all(searchTasks);\n\n // Get the main search result (first result)\n const mainResult = results[0];\n if (!mainResult.success) {\n throw new Error(mainResult.error ?? 'Search failed');\n }\n\n // Merge additional results with the main results\n const mergedResults = { ...mainResult.data };\n\n // Convert existing news to topStories if present\n if (mergedResults.news !== undefined && mergedResults.news.length > 0) {\n const existingNewsAsTopStories = mergedResults.news\n .filter((newsItem) => newsItem.link !== undefined && newsItem.link !== '')\n .map((newsItem) => ({\n title: newsItem.title ?? '',\n link: newsItem.link ?? '',\n source: newsItem.source ?? '',\n date: newsItem.date ?? '',\n imageUrl: newsItem.imageUrl ?? '',\n processed: false,\n }));\n mergedResults.topStories = [\n ...(mergedResults.topStories ?? []),\n ...existingNewsAsTopStories,\n ];\n delete mergedResults.news;\n }\n\n results.slice(1).forEach((result) => {\n if (result.success && result.data !== undefined) {\n if (result.data.images !== undefined && result.data.images.length > 0) {\n mergedResults.images = [\n ...(mergedResults.images ?? []),\n ...result.data.images,\n ];\n }\n if (result.data.videos !== undefined && result.data.videos.length > 0) {\n mergedResults.videos = [\n ...(mergedResults.videos ?? []),\n ...result.data.videos,\n ];\n }\n if (result.data.news !== undefined && result.data.news.length > 0) {\n const newsAsTopStories = result.data.news.map((newsItem) => ({\n ...newsItem,\n link: newsItem.link ?? '',\n }));\n mergedResults.topStories = [\n ...(mergedResults.topStories ?? []),\n ...newsAsTopStories,\n ];\n }\n }\n });\n\n if (\n mergedResults.topStories !== undefined &&\n mergedResults.topStories.length > 1\n ) {\n /** The main search's own news results and the parallel news sub-search\n * frequently return the same stories — keep the first occurrence of each\n * link so duplicates aren't scraped, reranked, and formatted repeatedly */\n const seenLinks = new Set<string>();\n mergedResults.topStories = mergedResults.topStories.filter((story) => {\n if (!story.link || seenLinks.has(story.link)) {\n return false;\n }\n seenLinks.add(story.link);\n return true;\n });\n }\n\n return { success: true, data: mergedResults };\n}\n\nfunction createSearchProcessor({\n searchAPI,\n safeSearch,\n supportsImages,\n supportsVideos,\n supportsNews,\n sourceProcessor,\n onGetHighlights,\n mainExpandBy,\n separatorExpandBy,\n logger,\n}: {\n safeSearch: t.SearchToolConfig['safeSearch'];\n supportsImages: boolean;\n supportsVideos: boolean;\n supportsNews: boolean;\n searchAPI: ReturnType<typeof createSearchAPI>;\n sourceProcessor: ReturnType<typeof createSourceProcessor>;\n onGetHighlights: t.SearchToolConfig['onGetHighlights'];\n mainExpandBy: t.SearchToolConfig['mainExpandBy'];\n separatorExpandBy: t.SearchToolConfig['separatorExpandBy'];\n logger: t.Logger;\n}) {\n return async function ({\n query,\n date,\n country,\n proMode = true,\n maxSources = 5,\n onSearchResults,\n images = false,\n videos = false,\n news = false,\n }: {\n query: string;\n country?: string;\n date?: DATE_RANGE;\n proMode?: boolean;\n maxSources?: number;\n onSearchResults: t.SearchToolConfig['onSearchResults'];\n images?: boolean;\n videos?: boolean;\n news?: boolean;\n }): Promise<t.SearchResultData> {\n try {\n // Execute parallel searches and merge results\n const searchResult = await executeParallelSearches({\n searchAPI,\n query,\n date,\n country,\n safeSearch,\n images: supportsImages && images,\n videos: supportsVideos && videos,\n news: supportsNews && news,\n logger,\n });\n\n onSearchResults?.(searchResult);\n\n const processedSources = await sourceProcessor.processSources({\n query,\n news,\n result: searchResult,\n proMode,\n onGetHighlights,\n numElements: maxSources,\n });\n\n return expandHighlights(\n processedSources,\n mainExpandBy,\n separatorExpandBy\n );\n } catch (error) {\n logger.error('Error in search:', error);\n return {\n organic: [],\n topStories: [],\n images: [],\n videos: [],\n news: [],\n relatedSearches: [],\n error: error instanceof Error ? error.message : String(error),\n };\n }\n };\n}\n\nfunction createOnSearchResults({\n runnableConfig,\n onSearchResults,\n}: {\n runnableConfig: RunnableConfig;\n onSearchResults: t.SearchToolConfig['onSearchResults'];\n}) {\n return function (results: t.SearchResult): void {\n if (!onSearchResults) {\n return;\n }\n onSearchResults(results, runnableConfig);\n };\n}\n\nfunction createTool({\n schema,\n search,\n maxOutputChars,\n onSearchResults: _onSearchResults,\n}: {\n schema: Record<string, unknown>;\n search: ReturnType<typeof createSearchProcessor>;\n maxOutputChars?: number;\n onSearchResults: t.SearchToolConfig['onSearchResults'];\n}): DynamicStructuredTool {\n return tool(\n async (rawParams, runnableConfig) => {\n const params = rawParams as SearchToolParams;\n const { query, date, country: _c, images, videos, news } = params;\n const country = typeof _c === 'string' && _c ? _c : undefined;\n const searchResult = await search({\n query,\n date,\n country,\n images,\n videos,\n news,\n onSearchResults: createOnSearchResults({\n runnableConfig,\n onSearchResults: _onSearchResults,\n }),\n });\n const turn = runnableConfig.toolCall?.turn ?? 0;\n const { output, references } = formatResultsForLLM(\n turn,\n searchResult,\n maxOutputChars\n );\n const data: t.SearchResultData = { turn, ...searchResult, references };\n return [output, { [Constants.WEB_SEARCH]: data }];\n },\n {\n name: WebSearchToolName,\n description: WebSearchToolDescription,\n schema: schema,\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\n/**\n * Creates a search tool with configurable search and scraper providers.\n *\n * Search providers: Serper (Google results), SearXNG (self-hosted meta-search), Tavily (AI-optimized), fastCRW (Firecrawl-compatible, self-host or cloud).\n * Scraper providers: Firecrawl (default, full-featured), Serper (lightweight), Tavily (batch extraction), fastCRW (Firecrawl-compatible, self-host or cloud).\n *\n * The country schema field is exposed to the LLM for providers that support localized results.\n */\n/** Input params type for search tool */\ninterface SearchToolParams {\n query: string;\n date?: DATE_RANGE;\n country?: string;\n images?: boolean;\n videos?: boolean;\n news?: boolean;\n}\n\nexport const createSearchTool = (\n config: t.SearchToolConfig = {}\n): DynamicStructuredTool => {\n const {\n searchProvider = 'serper',\n serperApiKey,\n searxngInstanceUrl,\n searxngApiKey,\n tavilyApiKey,\n tavilySearchUrl,\n tavilyExtractUrl,\n tavilySearchOptions,\n keenableApiKey,\n keenableApiUrl,\n keenableSearchOptions,\n rerankerType = 'cohere',\n rerankerTimeout,\n topResults = 5,\n maxContentLength,\n chunkSize,\n chunkOverlap,\n mainExpandBy,\n separatorExpandBy,\n maxOutputChars,\n strategies = ['no_extraction'],\n filterContent = true,\n safeSearch = 1,\n scraperProvider = 'firecrawl',\n firecrawlApiKey,\n firecrawlApiUrl,\n firecrawlVersion,\n firecrawlOptions,\n serperScraperOptions,\n tavilyScraperOptions,\n crwApiKey,\n crwApiUrl,\n crwSearchOptions,\n crwScraperOptions,\n scraperTimeout,\n jinaApiKey,\n jinaApiUrl,\n cohereApiKey,\n onSearchResults: _onSearchResults,\n onGetHighlights,\n } = config;\n\n const logger = config.logger || createDefaultLogger();\n const effectiveTavilySearchOptions =\n searchProvider === 'tavily' && config.safeSearch != null\n ? {\n ...tavilySearchOptions,\n safeSearch: config.safeSearch !== 0,\n }\n : tavilySearchOptions;\n\n const schemaProperties: Record<string, unknown> = {\n query: querySchema,\n date: dateSchema,\n images: imagesSchema,\n videos: videosSchema,\n news: newsSchema,\n };\n\n if (searchProvider === 'serper' || searchProvider === 'tavily') {\n schemaProperties.country = countrySchema;\n }\n\n const toolSchema = {\n type: 'object',\n properties: schemaProperties,\n required: ['query'],\n };\n\n const searchAPI = createSearchAPI({\n searchProvider,\n serperApiKey,\n searxngInstanceUrl,\n searxngApiKey,\n tavilyApiKey,\n tavilySearchUrl,\n tavilySearchOptions: effectiveTavilySearchOptions,\n keenableApiKey,\n keenableApiUrl,\n keenableSearchOptions,\n crwApiKey,\n crwApiUrl,\n crwSearchOptions,\n });\n\n /** Create scraper based on scraperProvider */\n let scraperInstance: t.BaseScraper;\n\n if (scraperProvider === 'serper') {\n scraperInstance = createSerperScraper({\n ...serperScraperOptions,\n apiKey: serperApiKey,\n timeout: scraperTimeout ?? serperScraperOptions?.timeout,\n logger,\n });\n } else if (scraperProvider === 'tavily') {\n scraperInstance = createTavilyScraper({\n ...tavilyScraperOptions,\n apiKey:\n tavilyScraperOptions?.apiKey ??\n tavilyApiKey ??\n process.env.TAVILY_API_KEY,\n apiUrl: tavilyScraperOptions?.apiUrl ?? tavilyExtractUrl,\n timeout: scraperTimeout ?? tavilyScraperOptions?.timeout,\n logger,\n });\n } else if (scraperProvider === 'crw') {\n scraperInstance = createCrwScraper({\n ...crwScraperOptions,\n apiKey:\n crwScraperOptions?.apiKey ?? crwApiKey ?? process.env.CRW_API_KEY,\n apiUrl: crwScraperOptions?.apiUrl ?? crwApiUrl,\n timeout: scraperTimeout ?? crwScraperOptions?.timeout,\n formats: crwScraperOptions?.formats ?? ['markdown', 'rawHtml'],\n logger,\n });\n } else {\n scraperInstance = createFirecrawlScraper({\n ...firecrawlOptions,\n apiKey: firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY,\n apiUrl: firecrawlApiUrl,\n version: firecrawlVersion,\n timeout: scraperTimeout ?? firecrawlOptions?.timeout,\n formats: firecrawlOptions?.formats ?? ['markdown', 'rawHtml'],\n logger,\n });\n }\n\n const selectedReranker = createReranker({\n rerankerType,\n jinaApiKey,\n jinaApiUrl,\n cohereApiKey,\n rerankerTimeout,\n logger,\n });\n\n if (!selectedReranker) {\n logger.warn('No reranker selected. Using default ranking.');\n }\n\n const sourceProcessor = createSourceProcessor(\n {\n reranker: selectedReranker,\n topResults,\n maxContentLength,\n chunkSize,\n chunkOverlap,\n strategies,\n filterContent,\n logger,\n },\n scraperInstance\n );\n\n const search = createSearchProcessor({\n searchAPI,\n safeSearch,\n // Keenable is organic-only: its API ignores `type`, so image/news\n // sub-searches would spend rate limit and merge nothing.\n supportsImages: searchProvider !== 'keenable',\n supportsVideos:\n searchProvider !== 'tavily' &&\n searchProvider !== 'keenable' &&\n searchProvider !== 'crw',\n supportsNews: searchProvider !== 'keenable',\n sourceProcessor,\n onGetHighlights,\n mainExpandBy,\n separatorExpandBy,\n logger,\n });\n\n return createTool({\n search,\n schema: toolSchema,\n maxOutputChars,\n onSearchResults: _onSearchResults,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,eAAsB,wBAAwB,EAC5C,WACA,OACA,MACA,SACA,YACA,QACA,QACA,MACA,UAW0B;CAE1B,MAAM,cAAyC,CAE7C,UAAU,WAAW;EACnB;EACA;EACA;EACA;CACF,CAAC,CACH;CAEA,IAAI,QACF,YAAY,KACV,UACG,WAAW;EACV;EACA;EACA;EACA;EACA,MAAM;CACR,CAAC,CAAC,CACD,OAAO,UAAU;EAChB,OAAO,MAAM,0BAA0B,KAAK;EAC5C,OAAO;GACL,SAAS;GACT,OAAO,yBAAyB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF;CACF,CAAC,CACL;CAEF,IAAI,QACF,YAAY,KACV,UACG,WAAW;EACV;EACA;EACA;EACA;EACA,MAAM;CACR,CAAC,CAAC,CACD,OAAO,UAAU;EAChB,OAAO,MAAM,0BAA0B,KAAK;EAC5C,OAAO;GACL,SAAS;GACT,OAAO,yBAAyB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF;CACF,CAAC,CACL;CAEF,IAAI,MACF,YAAY,KACV,UACG,WAAW;EACV;EACA;EACA;EACA;EACA,MAAM;CACR,CAAC,CAAC,CACD,OAAO,UAAU;EAChB,OAAO,MAAM,wBAAwB,KAAK;EAC1C,OAAO;GACL,SAAS;GACT,OAAO,uBAAuB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrF;CACF,CAAC,CACL;CAIF,MAAM,UAAU,MAAM,QAAQ,IAAI,WAAW;CAG7C,MAAM,aAAa,QAAQ;CAC3B,IAAI,CAAC,WAAW,SACd,MAAM,IAAI,MAAM,WAAW,SAAS,eAAe;CAIrD,MAAM,gBAAgB,EAAE,GAAG,WAAW,KAAK;CAG3C,IAAI,cAAc,SAAS,KAAA,KAAa,cAAc,KAAK,SAAS,GAAG;EACrE,MAAM,2BAA2B,cAAc,KAC5C,QAAQ,aAAa,SAAS,SAAS,KAAA,KAAa,SAAS,SAAS,EAAE,CAAC,CACzE,KAAK,cAAc;GAClB,OAAO,SAAS,SAAS;GACzB,MAAM,SAAS,QAAQ;GACvB,QAAQ,SAAS,UAAU;GAC3B,MAAM,SAAS,QAAQ;GACvB,UAAU,SAAS,YAAY;GAC/B,WAAW;EACb,EAAE;EACJ,cAAc,aAAa,CACzB,GAAI,cAAc,cAAc,CAAC,GACjC,GAAG,wBACL;EACA,OAAO,cAAc;CACvB;CAEA,QAAQ,MAAM,CAAC,CAAC,CAAC,SAAS,WAAW;EACnC,IAAI,OAAO,WAAW,OAAO,SAAS,KAAA,GAAW;GAC/C,IAAI,OAAO,KAAK,WAAW,KAAA,KAAa,OAAO,KAAK,OAAO,SAAS,GAClE,cAAc,SAAS,CACrB,GAAI,cAAc,UAAU,CAAC,GAC7B,GAAG,OAAO,KAAK,MACjB;GAEF,IAAI,OAAO,KAAK,WAAW,KAAA,KAAa,OAAO,KAAK,OAAO,SAAS,GAClE,cAAc,SAAS,CACrB,GAAI,cAAc,UAAU,CAAC,GAC7B,GAAG,OAAO,KAAK,MACjB;GAEF,IAAI,OAAO,KAAK,SAAS,KAAA,KAAa,OAAO,KAAK,KAAK,SAAS,GAAG;IACjE,MAAM,mBAAmB,OAAO,KAAK,KAAK,KAAK,cAAc;KAC3D,GAAG;KACH,MAAM,SAAS,QAAQ;IACzB,EAAE;IACF,cAAc,aAAa,CACzB,GAAI,cAAc,cAAc,CAAC,GACjC,GAAG,gBACL;GACF;EACF;CACF,CAAC;CAED,IACE,cAAc,eAAe,KAAA,KAC7B,cAAc,WAAW,SAAS,GAClC;;;;EAIA,MAAM,4BAAY,IAAI,IAAY;EAClC,cAAc,aAAa,cAAc,WAAW,QAAQ,UAAU;GACpE,IAAI,CAAC,MAAM,QAAQ,UAAU,IAAI,MAAM,IAAI,GACzC,OAAO;GAET,UAAU,IAAI,MAAM,IAAI;GACxB,OAAO;EACT,CAAC;CACH;CAEA,OAAO;EAAE,SAAS;EAAM,MAAM;CAAc;AAC9C;AAEA,SAAS,sBAAsB,EAC7B,WACA,YACA,gBACA,gBACA,cACA,iBACA,iBACA,cACA,mBACA,UAYC;CACD,OAAO,eAAgB,EACrB,OACA,MACA,SACA,UAAU,MACV,aAAa,GACb,iBACA,SAAS,OACT,SAAS,OACT,OAAO,SAWuB;EAC9B,IAAI;GAEF,MAAM,eAAe,MAAM,wBAAwB;IACjD;IACA;IACA;IACA;IACA;IACA,QAAQ,kBAAkB;IAC1B,QAAQ,kBAAkB;IAC1B,MAAM,gBAAgB;IACtB;GACF,CAAC;GAED,kBAAkB,YAAY;GAW9B,OAAO,iBACL,MAV6B,gBAAgB,eAAe;IAC5D;IACA;IACA,QAAQ;IACR;IACA;IACA,aAAa;GACf,CAAC,GAIC,cACA,iBACF;EACF,SAAS,OAAO;GACd,OAAO,MAAM,oBAAoB,KAAK;GACtC,OAAO;IACL,SAAS,CAAC;IACV,YAAY,CAAC;IACb,QAAQ,CAAC;IACT,QAAQ,CAAC;IACT,MAAM,CAAC;IACP,iBAAiB,CAAC;IAClB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D;EACF;CACF;AACF;AAEA,SAAS,sBAAsB,EAC7B,gBACA,mBAIC;CACD,OAAO,SAAU,SAA+B;EAC9C,IAAI,CAAC,iBACH;EAEF,gBAAgB,SAAS,cAAc;CACzC;AACF;AAEA,SAAS,WAAW,EAClB,QACA,QACA,gBACA,iBAAiB,oBAMO;CACxB,OAAO,KACL,OAAO,WAAW,mBAAmB;EAEnC,MAAM,EAAE,OAAO,MAAM,SAAS,IAAI,QAAQ,QAAQ,SAASA;EAE3D,MAAM,eAAe,MAAM,OAAO;GAChC;GACA;GACA,SAJc,OAAO,OAAO,YAAY,KAAK,KAAK,KAAA;GAKlD;GACA;GACA;GACA,iBAAiB,sBAAsB;IACrC;IACA,iBAAiB;GACnB,CAAC;EACH,CAAC;EACD,MAAM,OAAO,eAAe,UAAU,QAAQ;EAC9C,MAAM,EAAE,QAAQ,eAAe,oBAC7B,MACA,cACA,cACF;EACA,MAAM,OAA2B;GAAE;GAAM,GAAG;GAAc;EAAW;EACrE,OAAO,CAAC,QAAQ,GAAA,eAA0B,KAAK,CAAC;CAClD,GACA;EACE,MAAM;EACN,aAAa;EACL;EACR,gBAAA;CACF,CACF;AACF;AAoBA,MAAa,oBACX,SAA6B,CAAC,MACJ;CAC1B,MAAM,EACJ,iBAAiB,UACjB,cACA,oBACA,eACA,cACA,iBACA,kBACA,qBACA,gBACA,gBACA,uBACA,eAAe,UACf,iBACA,aAAa,GACb,kBACA,WACA,cACA,cACA,mBACA,gBACA,aAAa,CAAC,eAAe,GAC7B,gBAAgB,MAChB,aAAa,GACb,kBAAkB,aAClB,iBACA,iBACA,kBACA,kBACA,sBACA,sBACA,WACA,WACA,kBACA,mBACA,gBACA,YACA,YACA,cACA,iBAAiB,kBACjB,oBACE;CAEJ,MAAM,SAAS,OAAO,UAAU,oBAAoB;CACpD,MAAM,+BACJ,mBAAmB,YAAY,OAAO,cAAc,OAChD;EACA,GAAG;EACH,YAAY,OAAO,eAAe;CACpC,IACE;CAEN,MAAM,mBAA4C;EAChD,OAAO;EACP,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,MAAM;CACR;CAEA,IAAI,mBAAmB,YAAY,mBAAmB,UACpD,iBAAiB,UAAU;CAG7B,MAAM,aAAa;EACjB,MAAM;EACN,YAAY;EACZ,UAAU,CAAC,OAAO;CACpB;CAEA,MAAM,YAAY,gBAAgB;EAChC;EACA;EACA;EACA;EACA;EACA;EACA,qBAAqB;EACrB;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;;CAGD,IAAI;CAEJ,IAAI,oBAAoB,UACtB,kBAAkB,oBAAoB;EACpC,GAAG;EACH,QAAQ;EACR,SAAS,kBAAkB,sBAAsB;EACjD;CACF,CAAC;MACI,IAAI,oBAAoB,UAC7B,kBAAkB,oBAAoB;EACpC,GAAG;EACH,QACE,sBAAsB,UACtB,gBACA,QAAQ,IAAI;EACd,QAAQ,sBAAsB,UAAU;EACxC,SAAS,kBAAkB,sBAAsB;EACjD;CACF,CAAC;MACI,IAAI,oBAAoB,OAC7B,kBAAkB,iBAAiB;EACjC,GAAG;EACH,QACE,mBAAmB,UAAU,aAAa,QAAQ,IAAI;EACxD,QAAQ,mBAAmB,UAAU;EACrC,SAAS,kBAAkB,mBAAmB;EAC9C,SAAS,mBAAmB,WAAW,CAAC,YAAY,SAAS;EAC7D;CACF,CAAC;MAED,kBAAkB,uBAAuB;EACvC,GAAG;EACH,QAAQ,mBAAmB,QAAQ,IAAI;EACvC,QAAQ;EACR,SAAS;EACT,SAAS,kBAAkB,kBAAkB;EAC7C,SAAS,kBAAkB,WAAW,CAAC,YAAY,SAAS;EAC5D;CACF,CAAC;CAGH,MAAM,mBAAmB,eAAe;EACtC;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,IAAI,CAAC,kBACH,OAAO,KAAK,8CAA8C;CAG5D,MAAM,kBAAkB,sBACtB;EACE,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;CACF,GACA,eACF;CAoBA,OAAO,WAAW;EAChB,QAnBa,sBAAsB;GACnC;GACA;GAGA,gBAAgB,mBAAmB;GACnC,gBACE,mBAAmB,YACnB,mBAAmB,cACnB,mBAAmB;GACrB,cAAc,mBAAmB;GACjC;GACA;GACA;GACA;GACA;EACF,CAGO;EACL,QAAQ;EACR;EACA,iBAAiB;CACnB,CAAC;AACH"}
@@ -49,6 +49,8 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
49
49
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
50
50
  invokedToolIds?: Set<string>;
51
51
  handlerRegistry: HandlerRegistry | undefined;
52
+ /** Host registry retained only for forwarding tools from nested child graphs. */
53
+ protected parentToolHandlerRegistry: HandlerRegistry | undefined;
52
54
  /**
53
55
  * True when event-driven tool execution can be routed through callbacks even
54
56
  * though this graph intentionally does not own the full handler registry.
@@ -3,11 +3,11 @@
3
3
  * permission policies (allow / deny / ask lists + a global mode) without
4
4
  * hand-rolling matching, precedence, and decision logic per-host.
5
5
  *
6
- * Maps directly to the Claude Code Agent SDK permission vocabulary
7
- * (`allowed_tools` / `disallowed_tools` / `permissionMode`) so users of
8
- * either SDK can think in the same terms. See the README's HITL section
9
- * for the cross-walk and `docs/hooks-design-report.md` for the broader
10
- * hook system context.
6
+ * Uses the Claude Code Agent SDK permission vocabulary (`allowed_tools` /
7
+ * `disallowed_tools` / `permissionMode`) while treating modes as fallbacks
8
+ * for calls that match no explicit rule. See the README's HITL section for
9
+ * the cross-walk and `docs/hooks-design-report.md` for the broader hook
10
+ * system context.
11
11
  */
12
12
  import type { HookCallback } from './types';
13
13
  /**
@@ -18,10 +18,8 @@ import type { HookCallback } from './types';
18
18
  * - `dontAsk` — unmatched tools are denied; the human is never
19
19
  * prompted. Useful for headless / API agents where a
20
20
  * silent denial is preferable to a hung interrupt.
21
- * - `bypass` — every tool is approved, except those matching `deny`
22
- * patterns. The kill switch you flip when you trust
23
- * the agent and want to stop being asked. Equivalent to
24
- * Claude Code's `bypassPermissions`.
21
+ * - `bypass` — unmatched tools are approved. Explicit `deny` and `ask`
22
+ * rules still apply.
25
23
  */
26
24
  export type ToolPolicyMode = 'default' | 'dontAsk' | 'bypass';
27
25
  export interface ToolPolicyConfig {
@@ -43,9 +41,8 @@ export interface ToolPolicyConfig {
43
41
  */
44
42
  deny?: readonly string[];
45
43
  /**
46
- * Tool name patterns that always trigger human approval, regardless
47
- * of `mode: 'default'` vs `'dontAsk'`. In `mode: 'bypass'` these are
48
- * still bypassed (because that's what bypass means).
44
+ * Tool name patterns that always trigger human approval. Wins over
45
+ * `allow` and every mode, but not `deny`.
49
46
  */
50
47
  ask?: readonly string[];
51
48
  /**
@@ -70,12 +67,12 @@ export interface ToolPolicyConfig {
70
67
  * registry.register('PreToolUse', { hooks: [policyHook] });
71
68
  * ```
72
69
  *
73
- * Evaluation order matches Claude Code's permission flow:
70
+ * Explicit rules take precedence over fallback modes:
74
71
  *
75
72
  * 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).
76
- * 2. `mode === 'bypass'` → `'allow'`.
73
+ * 2. `ask` rule match → `'ask'`.
77
74
  * 3. `allow` rule match → `'allow'`.
78
- * 4. `ask` rule match → `'ask'`.
75
+ * 4. `mode === 'bypass'` → `'allow'`.
79
76
  * 5. `mode === 'dontAsk'` → `'deny'`.
80
77
  * 6. fallthrough → `'ask'`.
81
78
  *
@@ -3,6 +3,8 @@ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
3
  * LangGraph plumbing observations that add noise without information:
4
4
  * the duplicated `__start__` channel-seed nodes and anonymous
5
5
  * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).
6
+ * Internal ToolNode batch spans are disabled at their source so traced child
7
+ * tools retain an exported parent. Explicitly traced ToolNodes are preserved.
6
8
  */
7
9
  export declare function shouldDropLangfuseSpan(spanName: string): boolean;
8
10
  /**
@@ -10,11 +12,15 @@ export declare function shouldDropLangfuseSpan(spanName: string): boolean;
10
12
  * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
11
13
  * (`provider__model`) — strip it so switching models doesn't break
12
14
  * name-based logic (item 1).
13
- * - Tool node spans are renamed to the actual tool name(s) and their
14
- * input scoped to the pending tool-call args instead of the whole
15
- * chat history (items 3 & 4).
16
- * - The root span (and trace) input/output become the user question and
17
- * assistant response so the session view reads as a conversation
18
- * (item 2).
15
+ * - LLM generation spans keep the provider client class name (`ChatOpenAI`,
16
+ * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
17
+ * name reflects the operation, not the model (the model stays on the
18
+ * generation's model attribute).
19
+ * - Agent nodes become `agent` observations, while tool-dispatch nodes become
20
+ * stable `chain` observations whose input is scoped to the pending calls.
21
+ * Individual child calls remain `tool` observations (items 3 & 4).
22
+ * - Agent trace roots become `agent` observations and title trace roots become
23
+ * `chain` observations. Root and trace input/output are reduced to the user
24
+ * question and assistant response when chat messages are available (item 2).
19
25
  */
20
26
  export declare function shapeLangfuseSpan(span: ReadableSpan): void;
@@ -20,13 +20,14 @@ export declare const BashExecutionToolSchema: {
20
20
  };
21
21
  export declare const BashExecutionToolDescription: string;
22
22
  /**
23
- * Bash statefulness is filesystem-tier: on a warm session the machine (files
24
- * including /tmp, installed packages, background processes) persists between
25
- * calls, but each call may start a fresh shell so shell variables and cwd
26
- * are NOT reliable, and the machine can be reset at any time. Only /mnt/data
27
- * is durable.
23
+ * Bash statefulness is filesystem-tier and scoped to `/mnt/data`. The machine
24
+ * is warm across calls, but each call runs in a fresh sandbox (new process
25
+ * tree + private /tmp), so background processes are reaped when the call ends
26
+ * and anything written outside /mnt/data is discarded. The note must not
27
+ * promise otherwise: a model told background processes survive will start a
28
+ * server in one call and assume it is listening in the next.
28
29
  */
29
- export declare const STATEFUL_BASH_NOTE = "Session state (best-effort): commands in this conversation usually run on the same machine, so files (including /tmp), installed packages, and running background processes from earlier calls typically persist. Each call may still start a fresh shell \u2014 do not rely on shell variables or the working directory carrying over \u2014 and the machine may be reset at any time. Only /mnt/data is durable.";
30
+ export declare const STATEFUL_BASH_NOTE = "Session state: commands in this conversation run on the same warm machine, so files written to /mnt/data persist between calls. Each call runs in a fresh, isolated sandbox: shell variables, the working directory, /tmp, and background processes do NOT survive after the call returns \u2014 a process started in one call is terminated when that call ends. Only /mnt/data is durable (the machine itself may also be reset at any time).";
30
31
  export declare const StatefulBashExecutionToolDescription: string;
31
32
  /**
32
33
  * Supplemental prompt documenting the tool-output reference feature.
@@ -39,12 +39,14 @@ export declare function buildCodeApiHttpErrorMessage(method: string, endpoint: s
39
39
  }): Promise<string>;
40
40
  export declare const CodeExecutionToolDescription: string;
41
41
  /**
42
- * Best-effort statefulness note. Deliberately hedged: warm reuse is an
43
- * optimization, not a guarantee (the runtime may be reset on idle timeout,
44
- * eviction, or the 8h VM lifetime), so the model must never depend on carried
45
- * state for correctness and must persist anything durable to /mnt/data.
42
+ * Statefulness here is FILESYSTEM-tier, not runtime-tier. Executions in a
43
+ * session reuse one warm machine, so `/mnt/data` carries across calls but
44
+ * every execution is a brand-new interpreter process in a fresh sandbox, so
45
+ * variables and imports never survive. The note must not imply otherwise: a
46
+ * model told its in-memory state persists writes `df = ...` in one call and
47
+ * `df.head()` in the next, then hits a NameError it was told to treat as rare.
46
48
  */
47
- export declare const STATEFUL_ENV_NOTE = "Session state (best-effort): consecutive executions in this conversation usually share one runtime, so variables, imports, and in-memory data from earlier successful calls are typically still available. The runtime may be reset at any time, so treat carried-over state as an optimization, never a guarantee. Anything that must survive MUST be written to /mnt/data. If a NameError/ImportError signals lost state, re-run the needed setup and continue.";
49
+ export declare const STATEFUL_ENV_NOTE = "Session state: executions in this conversation run on the same warm machine, so files persist between calls \u2014 but each execution is a NEW process. Variables, imports, and in-memory data NEVER carry over: every call must re-import and rebuild the state it needs. Only /mnt/data is durable (the machine itself may also be reset at any time), so write anything that must survive there and read it back next call.";
48
50
  export declare const StatefulCodeExecutionToolDescription: string;
49
51
  export declare function buildCodeExecutionToolDescription(opts?: {
50
52
  statefulSessions?: boolean;
@@ -200,6 +200,8 @@ export interface ProcessSourcesConfig {
200
200
  * configurable via the `SEARCH_CHUNK_OVERLAP` env var. Clamped below
201
201
  * `chunkSize`. */
202
202
  chunkOverlap?: number;
203
+ mainExpandBy?: number;
204
+ separatorExpandBy?: number;
203
205
  strategies?: string[];
204
206
  filterContent?: boolean;
205
207
  reranker?: BaseReranker;
@@ -406,9 +406,9 @@ export type LangfuseToolOutputTracingConfig = {
406
406
  };
407
407
  export type LangfuseToolNodeTracingConfig = {
408
408
  /**
409
- * Overrides ToolNode callback tracing. ToolNode spans are exported by the
410
- * env-backed Langfuse callback, so this only enables tracing when that
411
- * callback is configured.
409
+ * Opts into the internal ToolNode batch observation. Graph tool-dispatch
410
+ * and individual tool observations are exported without this wrapper, so
411
+ * the default is false to avoid a redundant hierarchy level.
412
412
  */
413
413
  enabled?: boolean;
414
414
  };
@@ -417,6 +417,13 @@ export interface LangfuseConfig {
417
417
  publicKey?: string;
418
418
  secretKey?: string;
419
419
  baseUrl?: string;
420
+ /**
421
+ * Environment identifier attached to exported traces (Langfuse
422
+ * `environment`). When unset, falls back to `LANGFUSE_TRACING_ENVIRONMENT`
423
+ * then `NODE_ENV`, so production traces are not collapsed under the
424
+ * `default` environment.
425
+ */
426
+ environment?: string;
420
427
  metadata?: Record<string, string | number | boolean | null | undefined>;
421
428
  /**
422
429
  * Internal OTLP span attributes to attach to Langfuse observations before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.65",
3
+ "version": "3.2.67",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -96,6 +96,18 @@ const { AGENT, TOOLS, SUMMARIZE } = GraphNodeKeys;
96
96
  /** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
97
97
  const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
98
98
 
99
+ function createToolHandlerRegistry(
100
+ source: HandlerRegistry | undefined
101
+ ): HandlerRegistry | undefined {
102
+ const toolHandler = source?.getHandler(GraphEvents.ON_TOOL_EXECUTE);
103
+ if (toolHandler == null) {
104
+ return undefined;
105
+ }
106
+ const registry = new HandlerRegistry();
107
+ registry.register(GraphEvents.ON_TOOL_EXECUTE, toolHandler);
108
+ return registry;
109
+ }
110
+
99
111
  /**
100
112
  * Start index of the span post-prune formatters can mutate in place: the
101
113
  * trailing tool batch plus its owning AI message (artifact formatting touches
@@ -576,6 +588,8 @@ export abstract class Graph<
576
588
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
577
589
  invokedToolIds?: Set<string>;
578
590
  handlerRegistry: HandlerRegistry | undefined;
591
+ /** Host registry retained only for forwarding tools from nested child graphs. */
592
+ protected parentToolHandlerRegistry: HandlerRegistry | undefined;
579
593
  /**
580
594
  * True when event-driven tool execution can be routed through callbacks even
581
595
  * though this graph intentionally does not own the full handler registry.
@@ -661,6 +675,7 @@ export abstract class Graph<
661
675
  this.prelimMessageIdsByStepKey = new Map();
662
676
  this.invokedToolIds = undefined;
663
677
  this.handlerRegistry = undefined;
678
+ this.parentToolHandlerRegistry = undefined;
664
679
  this.hookRegistry = undefined;
665
680
  this.humanInTheLoop = undefined;
666
681
  this.toolOutputReferences = undefined;
@@ -2376,7 +2391,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2376
2391
  );
2377
2392
  if (resolvedConfigs.length > 0) {
2378
2393
  const getParentHandlerRegistry = (): HandlerRegistry | undefined =>
2379
- this.handlerRegistry;
2394
+ this.handlerRegistry ?? this.parentToolHandlerRegistry;
2380
2395
  const executor = new SubagentExecutor({
2381
2396
  configs: new Map(resolvedConfigs.map((c) => [c.type, c])),
2382
2397
  parentSignal: this.signal,
@@ -2393,6 +2408,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2393
2408
  maxDepth: effectiveSubagentDepth,
2394
2409
  createChildGraph: (input): StandardGraph => {
2395
2410
  const childGraph = new StandardGraph(input);
2411
+ const toolHandlerRegistry = createToolHandlerRegistry(
2412
+ getParentHandlerRegistry()
2413
+ );
2396
2414
  childGraph.hookRegistry = this.hookRegistry;
2397
2415
  /**
2398
2416
  * Do not propagate `humanInTheLoop` into the child graph yet:
@@ -2415,9 +2433,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2415
2433
  // have the executable graphTool, the guard correctly applies.
2416
2434
  childGraph.interruptingToolNames = this.interruptingToolNames;
2417
2435
  childGraph.toolExecution = this.toolExecution;
2436
+ childGraph.parentToolHandlerRegistry = toolHandlerRegistry;
2418
2437
  childGraph.eventToolExecutionAvailable =
2419
- this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
2420
- null;
2438
+ toolHandlerRegistry != null;
2421
2439
  return childGraph;
2422
2440
  },
2423
2441
  });
@@ -98,12 +98,12 @@ describe('createToolPolicyHook — bypass mode', () => {
98
98
  expect((await callHook(hook, 'read_file')).decision).toBe('allow');
99
99
  });
100
100
 
101
- it('overrides explicit ask patterns (bypass means stop asking)', async () => {
101
+ it('still asks tools that match an explicit ask pattern', async () => {
102
102
  const hook = createToolPolicyHook({
103
103
  mode: 'bypass',
104
104
  ask: ['execute_*'],
105
105
  });
106
- expect((await callHook(hook, 'execute_code')).decision).toBe('allow');
106
+ expect((await callHook(hook, 'execute_code')).decision).toBe('ask');
107
107
  });
108
108
  });
109
109
 
@@ -154,6 +154,15 @@ describe('createToolPolicyHook — pattern matching', () => {
154
154
  });
155
155
 
156
156
  describe('createToolPolicyHook — precedence', () => {
157
+ it('deny wins over ask', async () => {
158
+ const hook = createToolPolicyHook({
159
+ mode: 'default',
160
+ deny: ['execute_delete'],
161
+ ask: ['execute_*'],
162
+ });
163
+ expect((await callHook(hook, 'execute_delete')).decision).toBe('deny');
164
+ });
165
+
157
166
  it('deny wins over allow', async () => {
158
167
  const hook = createToolPolicyHook({
159
168
  mode: 'default',
@@ -173,13 +182,13 @@ describe('createToolPolicyHook — precedence', () => {
173
182
  expect((await callHook(hook, 'anything_else')).decision).toBe('allow');
174
183
  });
175
184
 
176
- it('allow wins over ask in default mode', async () => {
185
+ it('ask wins over allow in default mode', async () => {
177
186
  const hook = createToolPolicyHook({
178
187
  mode: 'default',
179
188
  allow: ['execute_safe'],
180
189
  ask: ['execute_*'],
181
190
  });
182
- expect((await callHook(hook, 'execute_safe')).decision).toBe('allow');
191
+ expect((await callHook(hook, 'execute_safe')).decision).toBe('ask');
183
192
  expect((await callHook(hook, 'execute_dangerous')).decision).toBe('ask');
184
193
  });
185
194
  });
@@ -3,11 +3,11 @@
3
3
  * permission policies (allow / deny / ask lists + a global mode) without
4
4
  * hand-rolling matching, precedence, and decision logic per-host.
5
5
  *
6
- * Maps directly to the Claude Code Agent SDK permission vocabulary
7
- * (`allowed_tools` / `disallowed_tools` / `permissionMode`) so users of
8
- * either SDK can think in the same terms. See the README's HITL section
9
- * for the cross-walk and `docs/hooks-design-report.md` for the broader
10
- * hook system context.
6
+ * Uses the Claude Code Agent SDK permission vocabulary (`allowed_tools` /
7
+ * `disallowed_tools` / `permissionMode`) while treating modes as fallbacks
8
+ * for calls that match no explicit rule. See the README's HITL section for
9
+ * the cross-walk and `docs/hooks-design-report.md` for the broader hook
10
+ * system context.
11
11
  */
12
12
 
13
13
  import type { HookCallback, PreToolUseHookOutput, ToolDecision } from './types';
@@ -20,10 +20,8 @@ import type { HookCallback, PreToolUseHookOutput, ToolDecision } from './types';
20
20
  * - `dontAsk` — unmatched tools are denied; the human is never
21
21
  * prompted. Useful for headless / API agents where a
22
22
  * silent denial is preferable to a hung interrupt.
23
- * - `bypass` — every tool is approved, except those matching `deny`
24
- * patterns. The kill switch you flip when you trust
25
- * the agent and want to stop being asked. Equivalent to
26
- * Claude Code's `bypassPermissions`.
23
+ * - `bypass` — unmatched tools are approved. Explicit `deny` and `ask`
24
+ * rules still apply.
27
25
  */
28
26
  export type ToolPolicyMode = 'default' | 'dontAsk' | 'bypass';
29
27
 
@@ -46,9 +44,8 @@ export interface ToolPolicyConfig {
46
44
  */
47
45
  deny?: readonly string[];
48
46
  /**
49
- * Tool name patterns that always trigger human approval, regardless
50
- * of `mode: 'default'` vs `'dontAsk'`. In `mode: 'bypass'` these are
51
- * still bypassed (because that's what bypass means).
47
+ * Tool name patterns that always trigger human approval. Wins over
48
+ * `allow` and every mode, but not `deny`.
52
49
  */
53
50
  ask?: readonly string[];
54
51
  /**
@@ -115,12 +112,12 @@ function formatReason(
115
112
  * registry.register('PreToolUse', { hooks: [policyHook] });
116
113
  * ```
117
114
  *
118
- * Evaluation order matches Claude Code's permission flow:
115
+ * Explicit rules take precedence over fallback modes:
119
116
  *
120
117
  * 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).
121
- * 2. `mode === 'bypass'` → `'allow'`.
118
+ * 2. `ask` rule match → `'ask'`.
122
119
  * 3. `allow` rule match → `'allow'`.
123
- * 4. `ask` rule match → `'ask'`.
120
+ * 4. `mode === 'bypass'` → `'allow'`.
124
121
  * 5. `mode === 'dontAsk'` → `'deny'`.
125
122
  * 6. fallthrough → `'ask'`.
126
123
  *
@@ -168,14 +165,14 @@ function decide(
168
165
  if (denyMatch(toolName)) {
169
166
  return 'deny';
170
167
  }
171
- if (mode === 'bypass') {
172
- return 'allow';
168
+ if (askMatch(toolName)) {
169
+ return 'ask';
173
170
  }
174
171
  if (allowMatch(toolName)) {
175
172
  return 'allow';
176
173
  }
177
- if (askMatch(toolName)) {
178
- return 'ask';
174
+ if (mode === 'bypass') {
175
+ return 'allow';
179
176
  }
180
177
  if (mode === 'dontAsk') {
181
178
  return 'deny';
@@ -75,17 +75,35 @@ export function ensureOpenTelemetryContextManager(): void {
75
75
  }
76
76
  }
77
77
 
78
+ function resolveLangfuseEnvironment(
79
+ langfuse?: t.LangfuseConfig
80
+ ): string | undefined {
81
+ const candidates = [
82
+ langfuse?.environment,
83
+ process.env.LANGFUSE_TRACING_ENVIRONMENT,
84
+ process.env.NODE_ENV,
85
+ ];
86
+ for (const candidate of candidates) {
87
+ if (candidate != null && candidate.trim() !== '') {
88
+ return candidate.trim();
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+
78
94
  function getLangfuseSpanProcessorParams(
79
95
  langfuse?: t.LangfuseConfig
80
96
  ): LangfuseSpanProcessorParams | undefined {
81
97
  if (langfuse?.enabled === false) {
82
98
  return undefined;
83
99
  }
100
+ const environment = resolveLangfuseEnvironment(langfuse);
84
101
  if (hasLangfuseConfigCredentials(langfuse)) {
85
102
  return {
86
103
  publicKey: langfuse.publicKey,
87
104
  secretKey: langfuse.secretKey,
88
105
  ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
106
+ ...(isPresent(environment) ? { environment } : {}),
89
107
  };
90
108
  }
91
109
  if (hasLangfuseEnvConfig()) {
@@ -97,6 +115,7 @@ function getLangfuseSpanProcessorParams(
97
115
  publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
98
116
  secretKey: process.env.LANGFUSE_SECRET_KEY as string,
99
117
  ...(isPresent(baseUrl) ? { baseUrl } : {}),
118
+ ...(isPresent(environment) ? { environment } : {}),
100
119
  };
101
120
  }
102
121
  if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
@@ -104,6 +123,7 @@ function getLangfuseSpanProcessorParams(
104
123
  publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
105
124
  secretKey: process.env.LANGFUSE_SECRET_KEY as string,
106
125
  baseUrl: langfuse.baseUrl,
126
+ ...(isPresent(environment) ? { environment } : {}),
107
127
  };
108
128
  }
109
129
  return undefined;
@@ -483,10 +483,8 @@ export function shouldTraceToolNodeForLangfuse({
483
483
  }
484
484
 
485
485
  const explicit = langfuse?.toolNodeTracing?.enabled;
486
- if (explicit != null) {
487
- return (
488
- explicit && (hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys())
489
- );
486
+ if (explicit !== true) {
487
+ return false;
490
488
  }
491
489
 
492
490
  return hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys();