@librechat/agents 3.3.4 → 3.3.5
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/common/constants.cjs +21 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/events.cjs +10 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +456 -7
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +25 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +8 -0
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +1 -3
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +268 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/preempt.cjs +132 -0
- package/dist/cjs/llm/preempt.cjs.map +1 -0
- package/dist/cjs/main.cjs +17 -3
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- package/dist/cjs/messages/format.cjs +72 -0
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +63 -0
- package/dist/cjs/messages/handoffCue.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +3 -0
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/run.cjs +80 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +18 -9
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +28 -7
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -23
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +9 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/common/constants.mjs +19 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/events.mjs +10 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +455 -6
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +25 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +9 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +1 -3
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +270 -6
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/preempt.mjs +131 -0
- package/dist/esm/llm/preempt.mjs.map +1 -0
- package/dist/esm/main.mjs +8 -5
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- package/dist/esm/messages/format.mjs +72 -0
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +61 -0
- package/dist/esm/messages/handoffCue.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +3 -0
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/run.mjs +80 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +19 -10
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +27 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -23
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +9 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +138 -1
- package/dist/types/hooks/HookRegistry.d.ts +15 -0
- package/dist/types/hooks/index.d.ts +12 -1
- package/dist/types/hooks/types.d.ts +45 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +7 -0
- package/dist/types/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -0
- package/dist/types/messages/handoffCue.d.ts +40 -0
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/injected.d.ts +3 -0
- package/dist/types/run.d.ts +7 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +0 -7
- package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
- package/dist/types/tools/search/types.d.ts +31 -2
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/run.d.ts +65 -0
- package/dist/types/types/stream.d.ts +0 -25
- package/package.json +1 -1
- package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
- package/src/common/constants.ts +21 -0
- package/src/events.ts +15 -1
- package/src/graphs/Graph.ts +568 -3
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
- package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
- package/src/hooks/HookRegistry.ts +40 -0
- package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
- package/src/hooks/index.ts +16 -2
- package/src/hooks/types.ts +47 -3
- package/src/index.ts +1 -1
- package/src/langfuse.ts +26 -1
- package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
- package/src/llm/bedrock/utils/message_inputs.ts +14 -11
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.ts +417 -8
- package/src/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- package/src/messages/format.ts +91 -0
- package/src/messages/formatAgentMessages.steer.test.ts +267 -0
- package/src/messages/handoffCue.test.ts +96 -0
- package/src/messages/handoffCue.ts +78 -0
- package/src/messages/index.ts +3 -0
- package/src/messages/injected.test.ts +90 -0
- package/src/messages/injected.ts +74 -0
- package/src/run.ts +91 -6
- package/src/scripts/preempt-probe.ts +330 -0
- package/src/scripts/preempt-scenarios.ts +388 -0
- package/src/session/handlers.ts +32 -12
- package/src/specs/handoffCue.test.ts +165 -0
- package/src/specs/langfuse-callbacks.test.ts +352 -2
- package/src/specs/preemptSeal.test.ts +309 -0
- package/src/stream.dispatch.test.ts +63 -0
- package/src/stream.ts +36 -8
- package/src/tools/ToolNode.ts +3 -31
- package/src/tools/search/keenable-scraper.test.ts +153 -0
- package/src/tools/search/keenable-scraper.ts +137 -0
- package/src/tools/search/tool.ts +13 -2
- package/src/tools/search/types.ts +50 -3
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +0 -41
- package/dist/cjs/splitStream.cjs +0 -151
- package/dist/cjs/splitStream.cjs.map +0 -1
- package/dist/esm/splitStream.mjs +0 -150
- package/dist/esm/splitStream.mjs.map +0 -1
- package/dist/types/mockStream.d.ts +0 -32
- package/dist/types/splitStream.d.ts +0 -37
- package/src/mockStream.ts +0 -99
- package/src/splitStream.ts +0 -234
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createDefaultLogger } from "./utils.mjs";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
//#region src/tools/search/keenable-scraper.ts
|
|
4
|
+
const DEFAULT_KEENABLE_SCRAPE_TIMEOUT = 15e3;
|
|
5
|
+
/** Keyed and keyless fetch endpoints. Keenable reads any URL as clean markdown
|
|
6
|
+
* without a key via the public endpoint; a key only lifts rate limits. */
|
|
7
|
+
const KEENABLE_FETCH_API_URL = "https://api.keenable.ai/v1/fetch";
|
|
8
|
+
const KEENABLE_FETCH_PUBLIC_URL = "https://api.keenable.ai/v1/fetch/public";
|
|
9
|
+
var KeenableScraper = class {
|
|
10
|
+
apiKey;
|
|
11
|
+
apiUrl;
|
|
12
|
+
timeout;
|
|
13
|
+
attributionTitle;
|
|
14
|
+
logger;
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
const resolvedKey = config.apiKey ?? process.env.KEENABLE_API_KEY;
|
|
17
|
+
this.apiKey = resolvedKey != null && resolvedKey !== "" ? resolvedKey : void 0;
|
|
18
|
+
this.apiUrl = config.apiUrl ?? process.env.KEENABLE_FETCH_URL ?? (this.apiKey != null ? KEENABLE_FETCH_API_URL : KEENABLE_FETCH_PUBLIC_URL);
|
|
19
|
+
this.timeout = config.timeout ?? DEFAULT_KEENABLE_SCRAPE_TIMEOUT;
|
|
20
|
+
this.attributionTitle = config.attributionTitle ?? "LibreChat";
|
|
21
|
+
this.logger = config.logger || createDefaultLogger();
|
|
22
|
+
}
|
|
23
|
+
buildHeaders() {
|
|
24
|
+
/** X-Keenable-Title is used for traffic attribution and required on the
|
|
25
|
+
* keyless endpoint; the key only lifts rate limits, so it is sent only when
|
|
26
|
+
* present. */
|
|
27
|
+
const headers = { "X-Keenable-Title": this.attributionTitle };
|
|
28
|
+
if (this.apiKey != null) headers["X-API-Key"] = this.apiKey;
|
|
29
|
+
return headers;
|
|
30
|
+
}
|
|
31
|
+
async scrapeUrl(url, options = {}) {
|
|
32
|
+
if (!url || !url.trim()) return [url, {
|
|
33
|
+
success: false,
|
|
34
|
+
error: "URL cannot be empty"
|
|
35
|
+
}];
|
|
36
|
+
try {
|
|
37
|
+
const data = (await axios.get(this.apiUrl, {
|
|
38
|
+
params: { url },
|
|
39
|
+
headers: this.buildHeaders(),
|
|
40
|
+
timeout: options.timeout ?? this.timeout
|
|
41
|
+
})).data;
|
|
42
|
+
const content = data.content ?? "";
|
|
43
|
+
if (!content) return [url, {
|
|
44
|
+
success: false,
|
|
45
|
+
error: "Keenable Fetch returned no content"
|
|
46
|
+
}];
|
|
47
|
+
return [url, {
|
|
48
|
+
success: true,
|
|
49
|
+
data: {
|
|
50
|
+
content,
|
|
51
|
+
title: data.title,
|
|
52
|
+
description: data.description,
|
|
53
|
+
url: data.url ?? url
|
|
54
|
+
}
|
|
55
|
+
}];
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return [url, {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Keenable Fetch API request failed: ${error instanceof Error ? error.message : String(error)}`
|
|
60
|
+
}];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async scrapeUrls(urls, options = {}) {
|
|
64
|
+
/** Keenable fetch is single-URL; run the batch concurrently. */
|
|
65
|
+
return Promise.all(urls.map((url) => this.scrapeUrl(url, options)));
|
|
66
|
+
}
|
|
67
|
+
extractContent(response) {
|
|
68
|
+
if (!response.success || !response.data) return ["", void 0];
|
|
69
|
+
/** Keenable returns clean markdown with no separate media arrays, so there
|
|
70
|
+
* are no structured references to surface. */
|
|
71
|
+
return [response.data.content, void 0];
|
|
72
|
+
}
|
|
73
|
+
extractMetadata(response) {
|
|
74
|
+
if (!response.success || !response.data) return {};
|
|
75
|
+
const metadata = {};
|
|
76
|
+
if (response.data.title != null) metadata.title = response.data.title;
|
|
77
|
+
if (response.data.description != null && response.data.description !== "") metadata.description = response.data.description;
|
|
78
|
+
if (response.data.url != null) metadata.url = response.data.url;
|
|
79
|
+
return metadata;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const createKeenableScraper = (config = {}) => {
|
|
83
|
+
return new KeenableScraper(config);
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { createKeenableScraper };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=keenable-scraper.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keenable-scraper.mjs","names":[],"sources":["../../../../src/tools/search/keenable-scraper.ts"],"sourcesContent":["import axios from 'axios';\nimport type * as t from './types';\nimport { createDefaultLogger } from './utils';\n\nconst DEFAULT_KEENABLE_SCRAPE_TIMEOUT = 15000;\n\n/** Keyed and keyless fetch endpoints. Keenable reads any URL as clean markdown\n * without a key via the public endpoint; a key only lifts rate limits. */\nconst KEENABLE_FETCH_API_URL = 'https://api.keenable.ai/v1/fetch';\nconst KEENABLE_FETCH_PUBLIC_URL = 'https://api.keenable.ai/v1/fetch/public';\n\nexport class KeenableScraper implements t.BaseScraper {\n private apiKey: string | undefined;\n private apiUrl: string;\n private timeout: number;\n private attributionTitle: string;\n private logger: t.Logger;\n\n constructor(config: t.KeenableScraperConfig = {}) {\n const resolvedKey = config.apiKey ?? process.env.KEENABLE_API_KEY;\n this.apiKey =\n resolvedKey != null && resolvedKey !== '' ? resolvedKey : undefined;\n this.apiUrl =\n config.apiUrl ??\n process.env.KEENABLE_FETCH_URL ??\n (this.apiKey != null\n ? KEENABLE_FETCH_API_URL\n : KEENABLE_FETCH_PUBLIC_URL);\n this.timeout = config.timeout ?? DEFAULT_KEENABLE_SCRAPE_TIMEOUT;\n this.attributionTitle = config.attributionTitle ?? 'LibreChat';\n this.logger = config.logger || createDefaultLogger();\n }\n\n private buildHeaders(): Record<string, string> {\n /** X-Keenable-Title is used for traffic attribution and required on the\n * keyless endpoint; the key only lifts rate limits, so it is sent only when\n * present. */\n const headers: Record<string, string> = {\n 'X-Keenable-Title': this.attributionTitle,\n };\n if (this.apiKey != null) {\n headers['X-API-Key'] = this.apiKey;\n }\n return headers;\n }\n\n async scrapeUrl(\n url: string,\n options: t.KeenableScrapeOptions = {}\n ): Promise<[string, t.KeenableScrapeResponse]> {\n if (!url || !url.trim()) {\n return [url, { success: false, error: 'URL cannot be empty' }];\n }\n\n try {\n const response = await axios.get<t.KeenableFetchResult>(this.apiUrl, {\n params: { url },\n headers: this.buildHeaders(),\n timeout: options.timeout ?? this.timeout,\n });\n\n const data = response.data;\n const content = data.content ?? '';\n if (!content) {\n return [\n url,\n { success: false, error: 'Keenable Fetch returned no content' },\n ];\n }\n\n return [\n url,\n {\n success: true,\n data: {\n content,\n title: data.title,\n description: data.description,\n url: data.url ?? url,\n },\n },\n ];\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n return [\n url,\n {\n success: false,\n error: `Keenable Fetch API request failed: ${errorMessage}`,\n },\n ];\n }\n }\n\n async scrapeUrls(\n urls: string[],\n options: t.KeenableScrapeOptions = {}\n ): Promise<Array<[string, t.KeenableScrapeResponse]>> {\n /** Keenable fetch is single-URL; run the batch concurrently. */\n return Promise.all(urls.map((url) => this.scrapeUrl(url, options)));\n }\n\n extractContent(\n response: t.KeenableScrapeResponse\n ): [string, undefined | t.References] {\n if (!response.success || !response.data) {\n return ['', undefined];\n }\n /** Keenable returns clean markdown with no separate media arrays, so there\n * are no structured references to surface. */\n return [response.data.content, undefined];\n }\n\n extractMetadata(response: t.KeenableScrapeResponse): t.GenericScrapeMetadata {\n if (!response.success || !response.data) {\n return {};\n }\n const metadata: t.GenericScrapeMetadata = {};\n if (response.data.title != null) {\n metadata.title = response.data.title;\n }\n if (response.data.description != null && response.data.description !== '') {\n metadata.description = response.data.description;\n }\n if (response.data.url != null) {\n metadata.url = response.data.url;\n }\n return metadata;\n }\n}\n\nexport const createKeenableScraper = (\n config: t.KeenableScraperConfig = {}\n): KeenableScraper => {\n return new KeenableScraper(config);\n};\n"],"mappings":";;;AAIA,MAAM,kCAAkC;;;AAIxC,MAAM,yBAAyB;AAC/B,MAAM,4BAA4B;AAElC,IAAa,kBAAb,MAAsD;CACpD;CACA;CACA;CACA;CACA;CAEA,YAAY,SAAkC,CAAC,GAAG;EAChD,MAAM,cAAc,OAAO,UAAU,QAAQ,IAAI;EACjD,KAAK,SACH,eAAe,QAAQ,gBAAgB,KAAK,cAAc,KAAA;EAC5D,KAAK,SACH,OAAO,UACP,QAAQ,IAAI,uBACX,KAAK,UAAU,OACZ,yBACA;EACN,KAAK,UAAU,OAAO,WAAW;EACjC,KAAK,mBAAmB,OAAO,oBAAoB;EACnD,KAAK,SAAS,OAAO,UAAU,oBAAoB;CACrD;CAEA,eAA+C;;;;EAI7C,MAAM,UAAkC,EACtC,oBAAoB,KAAK,iBAC3B;EACA,IAAI,KAAK,UAAU,MACjB,QAAQ,eAAe,KAAK;EAE9B,OAAO;CACT;CAEA,MAAM,UACJ,KACA,UAAmC,CAAC,GACS;EAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,GACpB,OAAO,CAAC,KAAK;GAAE,SAAS;GAAO,OAAO;EAAsB,CAAC;EAG/D,IAAI;GAOF,MAAM,QAAO,MANU,MAAM,IAA2B,KAAK,QAAQ;IACnE,QAAQ,EAAE,IAAI;IACd,SAAS,KAAK,aAAa;IAC3B,SAAS,QAAQ,WAAW,KAAK;GACnC,CAAC,EAAA,CAEqB;GACtB,MAAM,UAAU,KAAK,WAAW;GAChC,IAAI,CAAC,SACH,OAAO,CACL,KACA;IAAE,SAAS;IAAO,OAAO;GAAqC,CAChE;GAGF,OAAO,CACL,KACA;IACE,SAAS;IACT,MAAM;KACJ;KACA,OAAO,KAAK;KACZ,aAAa,KAAK;KAClB,KAAK,KAAK,OAAO;IACnB;GACF,CACF;EACF,SAAS,OAAO;GAGd,OAAO,CACL,KACA;IACE,SAAS;IACT,OAAO,sCALT,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAMrD,CACF;EACF;CACF;CAEA,MAAM,WACJ,MACA,UAAmC,CAAC,GACgB;;EAEpD,OAAO,QAAQ,IAAI,KAAK,KAAK,QAAQ,KAAK,UAAU,KAAK,OAAO,CAAC,CAAC;CACpE;CAEA,eACE,UACoC;EACpC,IAAI,CAAC,SAAS,WAAW,CAAC,SAAS,MACjC,OAAO,CAAC,IAAI,KAAA,CAAS;;;EAIvB,OAAO,CAAC,SAAS,KAAK,SAAS,KAAA,CAAS;CAC1C;CAEA,gBAAgB,UAA6D;EAC3E,IAAI,CAAC,SAAS,WAAW,CAAC,SAAS,MACjC,OAAO,CAAC;EAEV,MAAM,WAAoC,CAAC;EAC3C,IAAI,SAAS,KAAK,SAAS,MACzB,SAAS,QAAQ,SAAS,KAAK;EAEjC,IAAI,SAAS,KAAK,eAAe,QAAQ,SAAS,KAAK,gBAAgB,IACrE,SAAS,cAAc,SAAS,KAAK;EAEvC,IAAI,SAAS,KAAK,OAAO,MACvB,SAAS,MAAM,SAAS,KAAK;EAE/B,OAAO;CACT;AACF;AAEA,MAAa,yBACX,SAAkC,CAAC,MACf;CACpB,OAAO,IAAI,gBAAgB,MAAM;AACnC"}
|
|
@@ -4,6 +4,7 @@ import { createDefaultLogger } from "./utils.mjs";
|
|
|
4
4
|
import { formatResultsForLLM } from "./format.mjs";
|
|
5
5
|
import { WebSearchToolDescription, WebSearchToolName, countrySchema, dateSchema, imagesSchema, newsSchema, querySchema, videosSchema } from "./schema.mjs";
|
|
6
6
|
import { createSearchAPI, createSourceProcessor } from "./search.mjs";
|
|
7
|
+
import { createKeenableScraper } from "./keenable-scraper.mjs";
|
|
7
8
|
import { createSerperScraper } from "./serper-scraper.mjs";
|
|
8
9
|
import { createTavilyScraper } from "./tavily-scraper.mjs";
|
|
9
10
|
import { createFirecrawlScraper } from "./firecrawl.mjs";
|
|
@@ -181,7 +182,7 @@ function createTool({ schema, search, maxOutputChars, onSearchResults: _onSearch
|
|
|
181
182
|
});
|
|
182
183
|
}
|
|
183
184
|
const createSearchTool = (config = {}) => {
|
|
184
|
-
const { searchProvider = "serper", serperApiKey, searxngInstanceUrl, searxngApiKey, tavilyApiKey, tavilySearchUrl, tavilyExtractUrl, tavilySearchOptions, keenableApiKey, keenableApiUrl, keenableSearchOptions, rerankerType = "cohere", rerankerTimeout, topResults = 5, maxContentLength, chunkSize, chunkOverlap, mainExpandBy, separatorExpandBy, maxOutputChars, strategies = ["no_extraction"], filterContent = true, safeSearch = 1, scraperProvider = "firecrawl", firecrawlApiKey, firecrawlApiUrl, firecrawlVersion, firecrawlOptions, serperScraperOptions, tavilyScraperOptions, crwApiKey, crwApiUrl, crwSearchOptions, crwScraperOptions, scraperTimeout, jinaApiKey, jinaApiUrl, cohereApiKey, onSearchResults: _onSearchResults, onGetHighlights } = config;
|
|
185
|
+
const { searchProvider = "serper", serperApiKey, searxngInstanceUrl, searxngApiKey, tavilyApiKey, tavilySearchUrl, tavilyExtractUrl, tavilySearchOptions, keenableApiKey, keenableApiUrl, keenableSearchOptions, keenableScraperOptions, rerankerType = "cohere", rerankerTimeout, topResults = 5, maxContentLength, chunkSize, chunkOverlap, mainExpandBy, separatorExpandBy, maxOutputChars, strategies = ["no_extraction"], filterContent = true, safeSearch = 1, scraperProvider = "firecrawl", firecrawlApiKey, firecrawlApiUrl, firecrawlVersion, firecrawlOptions, serperScraperOptions, tavilyScraperOptions, crwApiKey, crwApiUrl, crwSearchOptions, crwScraperOptions, scraperTimeout, jinaApiKey, jinaApiUrl, cohereApiKey, onSearchResults: _onSearchResults, onGetHighlights } = config;
|
|
185
186
|
const logger = config.logger || createDefaultLogger();
|
|
186
187
|
const effectiveTavilySearchOptions = searchProvider === "tavily" && config.safeSearch != null ? {
|
|
187
188
|
...tavilySearchOptions,
|
|
@@ -238,6 +239,13 @@ const createSearchTool = (config = {}) => {
|
|
|
238
239
|
formats: crwScraperOptions?.formats ?? ["markdown", "rawHtml"],
|
|
239
240
|
logger
|
|
240
241
|
});
|
|
242
|
+
else if (scraperProvider === "keenable") scraperInstance = createKeenableScraper({
|
|
243
|
+
...keenableScraperOptions,
|
|
244
|
+
apiKey: keenableScraperOptions?.apiKey ?? keenableApiKey,
|
|
245
|
+
timeout: scraperTimeout ?? keenableScraperOptions?.timeout,
|
|
246
|
+
attributionTitle: keenableScraperOptions?.attributionTitle ?? keenableSearchOptions?.attributionTitle,
|
|
247
|
+
logger
|
|
248
|
+
});
|
|
241
249
|
else scraperInstance = createFirecrawlScraper({
|
|
242
250
|
...firecrawlOptions,
|
|
243
251
|
apiKey: firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY,
|
|
@@ -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 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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"}
|
|
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 { createKeenableScraper } from './keenable-scraper';\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 keenableScraperOptions,\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: 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 if (scraperProvider === 'keenable') {\n scraperInstance = createKeenableScraper({\n ...keenableScraperOptions,\n apiKey: keenableScraperOptions?.apiKey ?? keenableApiKey,\n timeout: scraperTimeout ?? keenableScraperOptions?.timeout,\n attributionTitle:\n keenableScraperOptions?.attributionTitle ??\n keenableSearchOptions?.attributionTitle,\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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"}
|
|
@@ -11,3 +11,21 @@ export declare const ANTHROPIC_TOOL_TOKEN_MULTIPLIER = 2.6;
|
|
|
11
11
|
* Empirically calibrated at ~1.4× the raw JSON token count.
|
|
12
12
|
*/
|
|
13
13
|
export declare const DEFAULT_TOOL_TOKEN_MULTIPLIER = 1.4;
|
|
14
|
+
/**
|
|
15
|
+
* Default ceiling on cooperative stream seals per run. Each seal costs one
|
|
16
|
+
* extra superstep, so this also sizes the recursion-limit headroom a
|
|
17
|
+
* preemption-enabled run reserves.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_MAX_SEALS = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Per-hook timeout for `PreemptBoundary`, deliberately far above
|
|
22
|
+
* `DEFAULT_HOOK_TIMEOUT_MS`. A host drain has already popped its queue and
|
|
23
|
+
* persisted content by the time this fires, so a timeout does not cancel the
|
|
24
|
+
* work — it only loses the messages the run was about to inject.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS = 120000;
|
|
27
|
+
/**
|
|
28
|
+
* Default LangGraph recursion limit for a run. Callers may override it via
|
|
29
|
+
* the stream config; a preemption-enabled run adds its seal budget on top.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_RECURSION_LIMIT = 50;
|
|
@@ -46,6 +46,20 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
|
|
|
46
46
|
reasoningStepHasDeltas: Set<string>;
|
|
47
47
|
protected handlerDispatchedEventCounts: Map<string, number>;
|
|
48
48
|
signal?: AbortSignal;
|
|
49
|
+
/**
|
|
50
|
+
* The abort signal the CALLER handed to the current `processStream` call,
|
|
51
|
+
* assigned unconditionally — including back to `undefined` — on every call.
|
|
52
|
+
*
|
|
53
|
+
* Kept separate from {@link signal} on purpose. That field is construction
|
|
54
|
+
* state with its own consumers (model-call config, subagent parentSignal),
|
|
55
|
+
* so adopting a per-call signal into it would leak one call's controller
|
|
56
|
+
* into the next — `clearHeavyState()` is skipped on HITL interrupts, so a
|
|
57
|
+
* host that aborts a finished request's controller would poison the resumed
|
|
58
|
+
* run's model calls and boundary drains with an already-aborted signal.
|
|
59
|
+
* Boundary dispatch composes the two instead; see
|
|
60
|
+
* `StandardGraph.dispatchPreemptBoundary`.
|
|
61
|
+
*/
|
|
62
|
+
callerSignal?: AbortSignal;
|
|
49
63
|
/** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
|
|
50
64
|
invokedToolIds?: Set<string>;
|
|
51
65
|
handlerRegistry: HandlerRegistry | undefined;
|
|
@@ -178,6 +192,8 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
|
|
|
178
192
|
messages: BaseMessage[];
|
|
179
193
|
/** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
|
|
180
194
|
private cachedRunMessages?;
|
|
195
|
+
/** Ids of AI turns the agent node returned THIS run; see isRunProducedMessage. */
|
|
196
|
+
protected runProducedAiMessageIds: Set<string>;
|
|
181
197
|
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
182
198
|
private originalToolContentCheckpointScope?;
|
|
183
199
|
runId: string | undefined;
|
|
@@ -203,9 +219,105 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
|
|
|
203
219
|
subagentUsageSink?: t.SubagentUsageSink;
|
|
204
220
|
/** See {@link t.StandardGraphInput.subagentScope}. */
|
|
205
221
|
subagentScope: boolean;
|
|
206
|
-
|
|
222
|
+
/** See {@link t.StandardGraphInput.preemption}. */
|
|
223
|
+
preemption?: t.StreamPreemption;
|
|
224
|
+
/**
|
|
225
|
+
* Seals charged against `preemption.maxSeals`. Per-turn: cleared by both
|
|
226
|
+
* reset paths so a fresh turn gets a fresh budget, while a HITL resume —
|
|
227
|
+
* which skips `resetValues` — keeps what it had left.
|
|
228
|
+
*/
|
|
229
|
+
private preemptSealBudgetUsed;
|
|
230
|
+
/**
|
|
231
|
+
* Seals honored over the graph's lifetime. Reported by
|
|
232
|
+
* {@link getPreemptStats}, so it deliberately SURVIVES `clearHeavyState()`
|
|
233
|
+
* — a host reads it after `processStream` returns, which is strictly after
|
|
234
|
+
* cleanup runs.
|
|
235
|
+
*/
|
|
236
|
+
preemptSealCount: number;
|
|
237
|
+
/** Boundaries that produced nothing to inject, so the turn stopped early. */
|
|
238
|
+
preemptEmptyBoundaries: number;
|
|
239
|
+
/**
|
|
240
|
+
* Set between claiming a seal and resolving its boundary. `MultiAgentGraph`
|
|
241
|
+
* fans parallel agents through this one instance against a single host
|
|
242
|
+
* request, so without a one-at-a-time gate several streams would each seal
|
|
243
|
+
* for the same queued message and every loser would take the
|
|
244
|
+
* nothing-to-inject path and cut its answer short.
|
|
245
|
+
*/
|
|
246
|
+
private preemptSealInFlight;
|
|
247
|
+
/**
|
|
248
|
+
* True when a seal ended the turn without a resume. The assistant turn is
|
|
249
|
+
* real and kept, but it is not the answer the model intended to finish —
|
|
250
|
+
* hosts persist it as unfinished rather than complete.
|
|
251
|
+
*/
|
|
252
|
+
preemptIncomplete: boolean;
|
|
253
|
+
/**
|
|
254
|
+
* `stopReason` from a `PreemptBoundary` hook that halted the turn.
|
|
255
|
+
*
|
|
256
|
+
* Clearing the registry halt is what keeps the sealed turn alive, but the
|
|
257
|
+
* registry held the only copy of the reason — so it is captured here first.
|
|
258
|
+
* Without it `getHaltReason()` returns undefined and a host records a
|
|
259
|
+
* hook-halted turn as an ordinary completion.
|
|
260
|
+
*/
|
|
261
|
+
preemptHaltReason: string | undefined;
|
|
262
|
+
/**
|
|
263
|
+
* Agent IDs whose next superstep must return to the agent node. Keyed by
|
|
264
|
+
* agent because `MultiAgentGraph` routes every parallel agent through this
|
|
265
|
+
* same instance, and a single field would let one agent's boundary resume
|
|
266
|
+
* another's turn.
|
|
267
|
+
*/
|
|
268
|
+
pendingPreemptReturn: Set<string>;
|
|
269
|
+
constructor({ runId, signal, agents, langfuse, tokenCounter, indexTokenCountMap, calibrationRatio, subagentUsageSink, subagentScope, preemption, }: t.StandardGraphInput);
|
|
207
270
|
resetValues(keepContent?: boolean, checkpointScope?: string): void;
|
|
208
271
|
clearHeavyState(): void;
|
|
272
|
+
/**
|
|
273
|
+
* Per-turn seal budget and routing markers. Cleared by both reset paths so
|
|
274
|
+
* a new turn starts with a full budget and no stale resume marker.
|
|
275
|
+
*
|
|
276
|
+
* The REPORTED counters are deliberately not touched here — see
|
|
277
|
+
* {@link resetPreemptTotals}.
|
|
278
|
+
*/
|
|
279
|
+
private resetPreemptTurnState;
|
|
280
|
+
/**
|
|
281
|
+
* Lifetime seal totals, cleared only when a genuinely new run starts.
|
|
282
|
+
* `clearHeavyState()` must NOT call this: it runs in `processStream`'s
|
|
283
|
+
* `finally`, so zeroing here would make {@link getPreemptStats} and
|
|
284
|
+
* `preemptIncomplete` unreadable for every caller of the method that just
|
|
285
|
+
* produced them.
|
|
286
|
+
*/
|
|
287
|
+
private resetPreemptTotals;
|
|
288
|
+
/**
|
|
289
|
+
* True when the host has requested a cooperative seal AND this graph may
|
|
290
|
+
* honor it. Read once per streamed chunk, so it stays property reads plus
|
|
291
|
+
* one host callback — no I/O, no allocation.
|
|
292
|
+
*
|
|
293
|
+
* Non-mutating: a true result only means a seal is worth evaluating. The
|
|
294
|
+
* budget is taken by {@link claimPreemptSeal} once the accumulated chunk is
|
|
295
|
+
* known to be safe, so a chunk that cannot seal never spends budget.
|
|
296
|
+
*
|
|
297
|
+
* Subagent scopes never seal: a steer targets the top-level conversation,
|
|
298
|
+
* and a child run must finish so its parent sees a complete result.
|
|
299
|
+
*/
|
|
300
|
+
/** Internal seal preconditions only — no host callback, no side effects. */
|
|
301
|
+
private canClaimPreemptSeal;
|
|
302
|
+
shouldPreemptStream(): boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Takes the seal slot, or returns false if another stream already holds it.
|
|
305
|
+
*
|
|
306
|
+
* Assumes the caller already polled `shouldPreemptStream()` for THIS chunk,
|
|
307
|
+
* and deliberately does not poll the host again — `StreamPreemption`
|
|
308
|
+
* documents `shouldPreempt` as once per chunk, and a host that consumes a
|
|
309
|
+
* pending flag on read would lose the request to a second call.
|
|
310
|
+
*
|
|
311
|
+
* The guard and both mutations remain one synchronous body, which is what
|
|
312
|
+
* makes this safe under a parallel `MultiAgentGraph`: several agents share
|
|
313
|
+
* one graph and can each see the poll as true, but no `await` can split the
|
|
314
|
+
* claim, so only one takes the slot. The loser keeps streaming normally
|
|
315
|
+
* rather than sealing for a message it would never receive.
|
|
316
|
+
*/
|
|
317
|
+
claimPreemptSeal(): boolean;
|
|
318
|
+
/** Releases the seal slot once its boundary has resolved, win or lose. */
|
|
319
|
+
releasePreemptSeal(): void;
|
|
320
|
+
getPreemptStats(): t.PreemptStats;
|
|
209
321
|
getRunStep(stepId: string): t.RunStep | undefined;
|
|
210
322
|
getAgentContext(metadata: Record<string, unknown> | undefined): AgentContext;
|
|
211
323
|
getStepBaseKey(metadata: Record<string, unknown> | undefined): string;
|
|
@@ -217,6 +329,17 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
|
|
|
217
329
|
private getBaseKeyList;
|
|
218
330
|
checkKeyList(keyList: (string | number | undefined)[]): boolean;
|
|
219
331
|
getRunMessages(): BaseMessage[] | undefined;
|
|
332
|
+
/**
|
|
333
|
+
* True when THIS RUN produced `message` — the provenance the handoff cue
|
|
334
|
+
* gate needs. Tracked as an id set rather than inferred from `startIndex`
|
|
335
|
+
* arithmetic: summarization's remove-all compaction rewrites the live
|
|
336
|
+
* array and leaves `startIndex` stale, so index-based run/host
|
|
337
|
+
* discrimination silently breaks right after a mid-run summarize. Ids
|
|
338
|
+
* survive compaction (retained messages keep theirs), host-supplied
|
|
339
|
+
* prefill messages are never in the set, and membership is O(1) per
|
|
340
|
+
* model call.
|
|
341
|
+
*/
|
|
342
|
+
isRunProducedMessage(message: BaseMessage): boolean;
|
|
220
343
|
getContentParts(): t.MessageContentComplex[] | undefined;
|
|
221
344
|
getCalibrationRatio(): number;
|
|
222
345
|
getResolvedInstructionOverhead(): number | undefined;
|
|
@@ -256,6 +379,20 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
|
|
|
256
379
|
*/
|
|
257
380
|
private beginOverflowRecovery;
|
|
258
381
|
createCallModel(agentId?: string): (state: t.AgentSubgraphState, config?: RunnableConfig) => Promise<Partial<t.AgentSubgraphState>>;
|
|
382
|
+
/**
|
|
383
|
+
* Fires `PreemptBoundary` after a sealed turn and returns whatever the
|
|
384
|
+
* hooks asked to inject, converted through the same `convertInjectedMessages`
|
|
385
|
+
* the tool boundary uses so the two sites cannot emit different shapes.
|
|
386
|
+
*
|
|
387
|
+
* Never throws: a drain that fails or times out costs the injection, not the
|
|
388
|
+
* run. The caller treats an empty result as "nothing to resume with".
|
|
389
|
+
*
|
|
390
|
+
* `preventContinuation` is surfaced alongside the messages rather than left
|
|
391
|
+
* to the registry halt signal, which `processStream` only polls between
|
|
392
|
+
* stream events — by then the self-loop it was meant to prevent has already
|
|
393
|
+
* issued another model call, and a direct graph consumer never polls it.
|
|
394
|
+
*/
|
|
395
|
+
private dispatchPreemptBoundary;
|
|
259
396
|
createAgentNode(agentId: string): t.CompiledAgentWorfklow;
|
|
260
397
|
createWorkflow(): t.CompiledStateWorkflow;
|
|
261
398
|
/**
|
|
@@ -120,5 +120,20 @@ export declare class HookRegistry {
|
|
|
120
120
|
hasResultAlteringHooks(sessionId?: string): boolean;
|
|
121
121
|
/** True if at least one matcher exists for `event` (global + session). */
|
|
122
122
|
hasHookFor(event: HookEvent, sessionId?: string): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* True when at least one matcher for `event` would actually DISPATCH on a
|
|
125
|
+
* query-less call — a wildcard pattern with a non-empty `hooks` array.
|
|
126
|
+
*
|
|
127
|
+
* `hasHookFor` answers "is one registered", which is not the same question:
|
|
128
|
+
* a matcher carrying a pattern is inert for events that supply no
|
|
129
|
+
* `matchQuery`, so a caller using registration as a proxy for "something
|
|
130
|
+
* will run" can act on a hook that never fires. Mirrors the two skips in
|
|
131
|
+
* `executeHooks` (pattern mismatch, empty `hooks`).
|
|
132
|
+
*
|
|
133
|
+
* Non-allocating on purpose — `StandardGraph.canClaimPreemptSeal` reads it
|
|
134
|
+
* once per streamed chunk, where `getMatchers`' defensive `slice()` would
|
|
135
|
+
* allocate on every delta.
|
|
136
|
+
*/
|
|
137
|
+
hasDispatchableHookFor(event: HookEvent, sessionId?: string): boolean;
|
|
123
138
|
private ensureSessionBucket;
|
|
124
139
|
}
|
|
@@ -8,11 +8,22 @@ export { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';
|
|
|
8
8
|
* be consumed by an SDK version that would silently drop it.
|
|
9
9
|
*/
|
|
10
10
|
export declare const HOOK_INJECTED_MESSAGES_CAPABLE = true;
|
|
11
|
+
/**
|
|
12
|
+
* Feature probe for hosts: this SDK dispatches `PreemptBoundary`, so a
|
|
13
|
+
* cooperative mid-generation seal can drain into the run.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately separate from {@link HOOK_INJECTED_MESSAGES_CAPABLE} — an SDK
|
|
16
|
+
* version can support `injectedMessages` at the tool boundary and know
|
|
17
|
+
* nothing about preemption. A host that probed the wrong flag would arm an
|
|
18
|
+
* interrupt control whose seal request is silently ignored, which reads to
|
|
19
|
+
* the user as a dead button rather than as an unsupported feature.
|
|
20
|
+
*/
|
|
21
|
+
export declare const HOOK_PREEMPT_BOUNDARY_CAPABLE = true;
|
|
11
22
|
export { matchesQuery, hasNestedQuantifier, MAX_PATTERN_LENGTH, MAX_CACHE_SIZE, } from './matchers';
|
|
12
23
|
export { createToolPolicyHook } from './createToolPolicyHook';
|
|
13
24
|
export type { ToolPolicyMode, ToolPolicyConfig } from './createToolPolicyHook';
|
|
14
25
|
export { createWorkspacePolicyHook } from './createWorkspacePolicyHook';
|
|
15
26
|
export type { OutsideAccessPolicy, WorkspacePolicyConfig, PathExtractor, } from './createWorkspacePolicyHook';
|
|
16
27
|
export { HOOK_EVENTS } from './types';
|
|
17
|
-
export type { HookEvent, HookInput, HookOutput, HookCallback, HookMatcher, HooksByEvent, HookInputByEvent, HookOutputByEvent, BaseHookInput, BaseHookOutput, ToolDecision, StopDecision, AggregatedHookResult, RunStartHookInput, UserPromptSubmitHookInput, PreToolUseHookInput, PostToolUseHookInput, PostToolUseFailureHookInput, PostToolBatchHookInput, PostToolBatchEntry, PermissionDeniedHookInput, SubagentStartHookInput, SubagentStopHookInput, StopHookInput, StopFailureHookInput, PreCompactHookInput, PostCompactHookInput, RunStartHookOutput, UserPromptSubmitHookOutput, PreToolUseHookOutput, PostToolUseHookOutput, PostToolUseFailureHookOutput, PostToolBatchHookOutput, PermissionDeniedHookOutput, SubagentStartHookOutput, SubagentStopHookOutput, StopHookOutput, StopFailureHookOutput, PreCompactHookOutput, PostCompactHookOutput, } from './types';
|
|
28
|
+
export type { HookEvent, HookInput, HookOutput, HookCallback, HookMatcher, HooksByEvent, HookInputByEvent, HookOutputByEvent, BaseHookInput, BaseHookOutput, ToolDecision, StopDecision, AggregatedHookResult, RunStartHookInput, UserPromptSubmitHookInput, PreToolUseHookInput, PostToolUseHookInput, PostToolUseFailureHookInput, PostToolBatchHookInput, PostToolBatchEntry, PreemptBoundaryHookInput, PermissionDeniedHookInput, SubagentStartHookInput, SubagentStopHookInput, StopHookInput, StopFailureHookInput, PreCompactHookInput, PostCompactHookInput, RunStartHookOutput, UserPromptSubmitHookOutput, PreToolUseHookOutput, PostToolUseHookOutput, PostToolUseFailureHookOutput, PostToolBatchHookOutput, PreemptBoundaryHookOutput, PermissionDeniedHookOutput, SubagentStartHookOutput, SubagentStopHookOutput, StopHookOutput, StopFailureHookOutput, PreCompactHookOutput, PostCompactHookOutput, } from './types';
|
|
18
29
|
export type { ExecuteHooksOptions } from './executeHooks';
|
|
@@ -8,7 +8,7 @@ import type { InjectedMessage } from '@/types/tools';
|
|
|
8
8
|
* `docs/hooks-design-report.md` §3.2 for the mapping to existing
|
|
9
9
|
* `@librechat/agents` emission points.
|
|
10
10
|
*/
|
|
11
|
-
export declare const HOOK_EVENTS: readonly ["RunStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "PermissionDenied", "SubagentStart", "SubagentStop", "Stop", "StopFailure", "PreCompact", "PostCompact"];
|
|
11
|
+
export declare const HOOK_EVENTS: readonly ["RunStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "PreemptBoundary", "PermissionDenied", "SubagentStart", "SubagentStop", "Stop", "StopFailure", "PreCompact", "PostCompact"];
|
|
12
12
|
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
13
13
|
/** Tool-gating decision; executeHooks folds with `deny > ask > allow` precedence. */
|
|
14
14
|
export type ToolDecision = 'allow' | 'deny' | 'ask';
|
|
@@ -117,6 +117,38 @@ export interface PostToolBatchHookInput extends BaseHookInput {
|
|
|
117
117
|
/** All tool calls (and their outcomes) from this batch, in batch order. */
|
|
118
118
|
entries: PostToolBatchEntry[];
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Fires when a cooperative preemption seals the model stream mid-generation
|
|
122
|
+
* — the second injection boundary, and the only one that exists during a
|
|
123
|
+
* long text answer with no tool calls in it.
|
|
124
|
+
*
|
|
125
|
+
* Order: fires after the model stream is sealed and BEFORE the next model
|
|
126
|
+
* call. The `injectedMessages` a hook returns here are appended verbatim and
|
|
127
|
+
* the agent node self-loops. Returning nothing is a valid outcome (the host's
|
|
128
|
+
* queue was cancelled or already drained): the run stops honestly rather than
|
|
129
|
+
* self-looping into an empty turn.
|
|
130
|
+
*
|
|
131
|
+
* The sealed turn is NOT yet observable from graph state when this fires.
|
|
132
|
+
* Dispatch happens inside the agent node, and only the outer graph's reducer
|
|
133
|
+
* writes `StandardGraph.messages` — which cannot run until the node returns.
|
|
134
|
+
* A hook calling `Run.getRunMessages()` here sees the state as of the last
|
|
135
|
+
* completed superstep, so the sealed text is absent. `PostToolBatch` behaves
|
|
136
|
+
* the same way: a tool-boundary hook cannot see the assistant turn that
|
|
137
|
+
* requested the tool. This is a property of the single-node outer graph, not
|
|
138
|
+
* of preemption, and committing first would mean returning from the node and
|
|
139
|
+
* re-entering — precisely what the self-loop exists to avoid.
|
|
140
|
+
*
|
|
141
|
+
* A hook that needs the sealed text should therefore not go looking for it in
|
|
142
|
+
* graph state. Decide from the host's own queue, or read it after the run.
|
|
143
|
+
*
|
|
144
|
+
* Requires `RunConfig.preemption`. This event is deliberately NOT
|
|
145
|
+
* result-altering, so registering it never disables eager tool execution.
|
|
146
|
+
*/
|
|
147
|
+
export interface PreemptBoundaryHookInput extends BaseHookInput {
|
|
148
|
+
hook_event_name: 'PreemptBoundary';
|
|
149
|
+
/** 1-based index of this seal within the run. */
|
|
150
|
+
sealCount: number;
|
|
151
|
+
}
|
|
120
152
|
export interface PermissionDeniedHookInput extends BaseHookInput {
|
|
121
153
|
hook_event_name: 'PermissionDenied';
|
|
122
154
|
toolName: string;
|
|
@@ -170,7 +202,7 @@ export interface PostCompactHookInput extends BaseHookInput {
|
|
|
170
202
|
messagesAfterCount: number;
|
|
171
203
|
}
|
|
172
204
|
/** Discriminated union of every hook input shape. */
|
|
173
|
-
export type HookInput = RunStartHookInput | UserPromptSubmitHookInput | PreToolUseHookInput | PostToolUseHookInput | PostToolUseFailureHookInput | PostToolBatchHookInput | PermissionDeniedHookInput | SubagentStartHookInput | SubagentStopHookInput | StopHookInput | StopFailureHookInput | PreCompactHookInput | PostCompactHookInput;
|
|
205
|
+
export type HookInput = RunStartHookInput | UserPromptSubmitHookInput | PreToolUseHookInput | PostToolUseHookInput | PostToolUseFailureHookInput | PostToolBatchHookInput | PreemptBoundaryHookInput | PermissionDeniedHookInput | SubagentStartHookInput | SubagentStopHookInput | StopHookInput | StopFailureHookInput | PreCompactHookInput | PostCompactHookInput;
|
|
174
206
|
/** Compile-time map from event name to its input shape. */
|
|
175
207
|
export type HookInputByEvent = {
|
|
176
208
|
RunStart: RunStartHookInput;
|
|
@@ -179,6 +211,7 @@ export type HookInputByEvent = {
|
|
|
179
211
|
PostToolUse: PostToolUseHookInput;
|
|
180
212
|
PostToolUseFailure: PostToolUseFailureHookInput;
|
|
181
213
|
PostToolBatch: PostToolBatchHookInput;
|
|
214
|
+
PreemptBoundary: PreemptBoundaryHookInput;
|
|
182
215
|
PermissionDenied: PermissionDeniedHookInput;
|
|
183
216
|
SubagentStart: SubagentStartHookInput;
|
|
184
217
|
SubagentStop: SubagentStopHookInput;
|
|
@@ -196,14 +229,18 @@ export interface BaseHookOutput {
|
|
|
196
229
|
additionalContext?: string;
|
|
197
230
|
/**
|
|
198
231
|
* Messages to inject into graph state, one `HumanMessage` per entry
|
|
199
|
-
* (converted via `
|
|
232
|
+
* (converted via `convertInjectedMessages`, which preserves
|
|
200
233
|
* `role`/`source`/`isMeta` in `additional_kwargs`). Unlike
|
|
201
234
|
* `additionalContext` — which is consolidated across hooks into a single
|
|
202
235
|
* system-flavored message — each entry keeps its own identity and role,
|
|
203
236
|
* making this the channel for injecting verbatim user speech (e.g. a
|
|
204
237
|
* mid-run steering message). Accumulated across hooks in registration
|
|
205
|
-
* order.
|
|
206
|
-
*
|
|
238
|
+
* order.
|
|
239
|
+
*
|
|
240
|
+
* Consumed at exactly two dispatch sites, both of which run the same
|
|
241
|
+
* converter so the emitted shapes cannot drift: `PostToolBatch` (the tool
|
|
242
|
+
* boundary) and `PreemptBoundary` (a cooperative mid-generation seal).
|
|
243
|
+
* Every other event ignores the field.
|
|
207
244
|
*/
|
|
208
245
|
injectedMessages?: InjectedMessage[];
|
|
209
246
|
/** True to prevent the next model turn. Any hook can set this. */
|
|
@@ -305,6 +342,7 @@ export interface PostToolUseHookOutput extends BaseHookOutput {
|
|
|
305
342
|
}
|
|
306
343
|
export type PostToolUseFailureHookOutput = BaseHookOutput;
|
|
307
344
|
export type PostToolBatchHookOutput = BaseHookOutput;
|
|
345
|
+
export type PreemptBoundaryHookOutput = BaseHookOutput;
|
|
308
346
|
export type PermissionDeniedHookOutput = BaseHookOutput;
|
|
309
347
|
export interface SubagentStartHookOutput extends BaseHookOutput {
|
|
310
348
|
decision?: ToolDecision;
|
|
@@ -326,6 +364,7 @@ export type HookOutputByEvent = {
|
|
|
326
364
|
PostToolUse: PostToolUseHookOutput;
|
|
327
365
|
PostToolUseFailure: PostToolUseFailureHookOutput;
|
|
328
366
|
PostToolBatch: PostToolBatchHookOutput;
|
|
367
|
+
PreemptBoundary: PreemptBoundaryHookOutput;
|
|
329
368
|
PermissionDenied: PermissionDeniedHookOutput;
|
|
330
369
|
SubagentStart: SubagentStartHookOutput;
|
|
331
370
|
SubagentStop: SubagentStopHookOutput;
|
|
@@ -335,7 +374,7 @@ export type HookOutputByEvent = {
|
|
|
335
374
|
PostCompact: PostCompactHookOutput;
|
|
336
375
|
};
|
|
337
376
|
/** Superset output shape used by the executor's fold loop. */
|
|
338
|
-
export type HookOutput = RunStartHookOutput | UserPromptSubmitHookOutput | PreToolUseHookOutput | PostToolUseHookOutput | PostToolUseFailureHookOutput | PostToolBatchHookOutput | PermissionDeniedHookOutput | SubagentStartHookOutput | SubagentStopHookOutput | StopHookOutput | StopFailureHookOutput | PreCompactHookOutput | PostCompactHookOutput;
|
|
377
|
+
export type HookOutput = RunStartHookOutput | UserPromptSubmitHookOutput | PreToolUseHookOutput | PostToolUseHookOutput | PostToolUseFailureHookOutput | PostToolBatchHookOutput | PreemptBoundaryHookOutput | PermissionDeniedHookOutput | SubagentStartHookOutput | SubagentStopHookOutput | StopHookOutput | StopFailureHookOutput | PreCompactHookOutput | PostCompactHookOutput;
|
|
339
378
|
/**
|
|
340
379
|
* A hook callback is a plain async function registered against a specific
|
|
341
380
|
* event. The `signal` is always supplied by `executeHooks` and combines the
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './run';
|
|
2
2
|
export * from './stream';
|
|
3
|
-
export * from './splitStream';
|
|
4
3
|
export * from './events';
|
|
5
4
|
export * from './messages';
|
|
6
5
|
export * from './graphs';
|
|
@@ -45,4 +44,5 @@ export { getChatModelClass } from './llm/providers';
|
|
|
45
44
|
export { FakeChatModel, createFakeStreamingLLM } from './llm/fake';
|
|
46
45
|
export { initializeModel } from './llm/init';
|
|
47
46
|
export { attemptInvoke, tryFallbackProviders } from './llm/invoke';
|
|
47
|
+
export { canSealPreempt } from './llm/preempt';
|
|
48
48
|
export { isThinkingEnabled, getMaxOutputTokensKey } from './llm/request';
|
|
@@ -54,6 +54,13 @@ export declare function projectMessagesForProvider({ model, messages, provider,
|
|
|
54
54
|
maxToolResultChars?: number;
|
|
55
55
|
callOptions?: unknown;
|
|
56
56
|
}): BaseMessage[];
|
|
57
|
+
/**
|
|
58
|
+
* The serving model's id, read through the same wrapper stack
|
|
59
|
+
* `collectModelCallbackSources` walks — `bindTools` returns a
|
|
60
|
+
* `RunnableBinding` and a system runnable pipes a `RunnableSequence`, and
|
|
61
|
+
* neither exposes the chat model's `model` at the top level.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveServingModelId(model: unknown): string | undefined;
|
|
57
64
|
/**
|
|
58
65
|
* Invokes a chat model with the given messages, handling both streaming and
|
|
59
66
|
* non-streaming paths.
|