@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.
- package/dist/cjs/graphs/Graph.cjs +15 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createToolPolicyHook.cjs +5 -5
- package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +15 -3
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +1 -2
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +51 -24
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +8 -0
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +8 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +9 -8
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +9 -7
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +59 -14
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +5 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +15 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createToolPolicyHook.mjs +5 -5
- package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +15 -3
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +1 -2
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +51 -24
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +8 -0
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +8 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +9 -8
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +9 -7
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +59 -14
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +5 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/hooks/createToolPolicyHook.d.ts +12 -15
- package/dist/types/langfuseTraceShaping.d.ts +12 -6
- package/dist/types/tools/BashExecutor.d.ts +7 -6
- package/dist/types/tools/CodeExecutor.d.ts +7 -5
- package/dist/types/tools/search/types.d.ts +2 -0
- package/dist/types/types/graph.d.ts +10 -3
- package/package.json +1 -1
- package/src/graphs/Graph.ts +21 -3
- package/src/hooks/__tests__/createToolPolicyHook.test.ts +13 -4
- package/src/hooks/createToolPolicyHook.ts +16 -19
- package/src/instrumentation.ts +20 -0
- package/src/langfuseToolOutputTracing.ts +2 -4
- package/src/langfuseTraceShaping.ts +73 -20
- package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +110 -0
- package/src/llm/anthropic/utils/message_inputs.ts +15 -0
- package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +122 -0
- package/src/llm/bedrock/utils/message_inputs.ts +13 -0
- package/src/specs/langfuse-instrumentation.test.ts +64 -0
- package/src/specs/langfuse-routing.integration.test.ts +49 -4
- package/src/specs/langfuse-tool-output-tracing.test.ts +5 -4
- package/src/specs/langfuse-trace-shaping.test.ts +80 -9
- package/src/specs/subagent.test.ts +180 -0
- package/src/tools/BashExecutor.ts +9 -8
- package/src/tools/CodeExecutor.ts +9 -7
- package/src/tools/ToolSearch.ts +81 -23
- package/src/tools/__tests__/BashExecutor.test.ts +16 -5
- package/src/tools/__tests__/CodeExecutor.stateful.test.ts +17 -6
- package/src/tools/__tests__/ToolSearch.test.ts +151 -0
- package/src/tools/__tests__/hitl.test.ts +114 -1
- package/src/tools/search/source-processing.test.ts +57 -0
- package/src/tools/search/tool.ts +13 -1
- package/src/tools/search/types.ts +2 -0
- 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":"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"}
|
|
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":"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"}
|
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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` —
|
|
22
|
-
*
|
|
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
|
|
47
|
-
*
|
|
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
|
-
*
|
|
70
|
+
* Explicit rules take precedence over fallback modes:
|
|
74
71
|
*
|
|
75
72
|
* 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).
|
|
76
|
-
* 2. `
|
|
73
|
+
* 2. `ask` rule match → `'ask'`.
|
|
77
74
|
* 3. `allow` rule match → `'allow'`.
|
|
78
|
-
* 4. `
|
|
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
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
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
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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` —
|
|
24
|
-
*
|
|
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
|
|
50
|
-
*
|
|
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
|
-
*
|
|
115
|
+
* Explicit rules take precedence over fallback modes:
|
|
119
116
|
*
|
|
120
117
|
* 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).
|
|
121
|
-
* 2. `
|
|
118
|
+
* 2. `ask` rule match → `'ask'`.
|
|
122
119
|
* 3. `allow` rule match → `'allow'`.
|
|
123
|
-
* 4. `
|
|
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 (
|
|
172
|
-
return '
|
|
168
|
+
if (askMatch(toolName)) {
|
|
169
|
+
return 'ask';
|
|
173
170
|
}
|
|
174
171
|
if (allowMatch(toolName)) {
|
|
175
172
|
return 'allow';
|
|
176
173
|
}
|
|
177
|
-
if (
|
|
178
|
-
return '
|
|
174
|
+
if (mode === 'bypass') {
|
|
175
|
+
return 'allow';
|
|
179
176
|
}
|
|
180
177
|
if (mode === 'dontAsk') {
|
|
181
178
|
return 'deny';
|
package/src/instrumentation.ts
CHANGED
|
@@ -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
|
|
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();
|