@librechat/agents 3.2.64 → 3.2.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/cjs/graphs/Graph.cjs +3 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +73 -9
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +41 -9
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +3 -1
  9. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/utils/stream_events.cjs +337 -0
  11. package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -0
  12. package/dist/cjs/tools/search/crw-scraper.cjs +165 -0
  13. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -0
  14. package/dist/cjs/tools/search/crw-search.cjs +105 -0
  15. package/dist/cjs/tools/search/crw-search.cjs.map +1 -0
  16. package/dist/cjs/tools/search/search.cjs +4 -2
  17. package/dist/cjs/tools/search/search.cjs.map +1 -1
  18. package/dist/cjs/tools/search/tool.cjs +15 -3
  19. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +3 -0
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/llm/anthropic/index.mjs +73 -9
  23. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  24. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  25. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +41 -9
  26. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  27. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +3 -2
  28. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  29. package/dist/esm/llm/anthropic/utils/stream_events.mjs +337 -0
  30. package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -0
  31. package/dist/esm/tools/search/crw-scraper.mjs +163 -0
  32. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -0
  33. package/dist/esm/tools/search/crw-search.mjs +103 -0
  34. package/dist/esm/tools/search/crw-search.mjs.map +1 -0
  35. package/dist/esm/tools/search/search.mjs +4 -2
  36. package/dist/esm/tools/search/search.mjs.map +1 -1
  37. package/dist/esm/tools/search/tool.mjs +15 -3
  38. package/dist/esm/tools/search/tool.mjs.map +1 -1
  39. package/dist/types/llm/anthropic/index.d.ts +2 -0
  40. package/dist/types/llm/anthropic/types.d.ts +2 -0
  41. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -2
  42. package/dist/types/llm/anthropic/utils/stream_events.d.ts +25 -0
  43. package/dist/types/tools/search/crw-scraper.d.ts +41 -0
  44. package/dist/types/tools/search/crw-search.d.ts +4 -0
  45. package/dist/types/tools/search/types.d.ts +88 -3
  46. package/package.json +1 -1
  47. package/src/graphs/Graph.ts +15 -0
  48. package/src/llm/anthropic/index.ts +134 -10
  49. package/src/llm/anthropic/inherited-content-utils.spec.ts +10 -5
  50. package/src/llm/anthropic/inherited-stream-events.spec.ts +513 -9
  51. package/src/llm/anthropic/llm.spec.ts +100 -16
  52. package/src/llm/anthropic/types.ts +3 -0
  53. package/src/llm/anthropic/utils/message_inputs.ts +60 -3
  54. package/src/llm/anthropic/utils/message_outputs.ts +10 -2
  55. package/src/llm/anthropic/utils/stream_events.ts +471 -0
  56. package/src/tools/search/crw-scraper.ts +244 -0
  57. package/src/tools/search/crw-search.ts +167 -0
  58. package/src/tools/search/crw.test.ts +836 -0
  59. package/src/tools/search/search.ts +7 -1
  60. package/src/tools/search/tool.ts +23 -3
  61. package/src/tools/search/types.ts +103 -3
@@ -0,0 +1,337 @@
1
+ import { getAnthropicUsageMetadata } from "./message_outputs.mjs";
2
+ //#region src/llm/anthropic/utils/stream_events.ts
3
+ /**
4
+ * Convert an async iterable of raw Anthropic stream events into
5
+ * LangChain `ChatModelStreamEvent`s with typed deltas.
6
+ */
7
+ async function* convertAnthropicStream(source, options = {}) {
8
+ const shouldStreamUsage = options.streamUsage ?? true;
9
+ const blockAccumulators = /* @__PURE__ */ new Map();
10
+ let usageSnapshot;
11
+ let eventUsage;
12
+ let stopReason = null;
13
+ for await (const data of source) switch (data.type) {
14
+ case "message_start": {
15
+ const { usage, id, model } = data.message;
16
+ if (shouldStreamUsage) {
17
+ eventUsage = {
18
+ inputTokens: usage.input_tokens,
19
+ cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,
20
+ cacheReadInputTokens: usage.cache_read_input_tokens ?? 0,
21
+ outputTokens: usage.output_tokens
22
+ };
23
+ usageSnapshot = buildUsageSnapshot(eventUsage);
24
+ }
25
+ yield {
26
+ event: "message-start",
27
+ id,
28
+ ...usageSnapshot ? { usage: usageSnapshot } : {}
29
+ };
30
+ yield {
31
+ event: "provider",
32
+ provider: "anthropic",
33
+ name: "message_start",
34
+ payload: {
35
+ model,
36
+ id
37
+ }
38
+ };
39
+ break;
40
+ }
41
+ case "message_delta":
42
+ stopReason = data.delta.stop_reason;
43
+ if (shouldStreamUsage) {
44
+ eventUsage = updateEventUsage(eventUsage, data.usage);
45
+ usageSnapshot = buildUsageSnapshot(eventUsage);
46
+ yield {
47
+ event: "usage",
48
+ usage: usageSnapshot
49
+ };
50
+ }
51
+ if ("context_management" in data.delta && data.delta.context_management != null) yield {
52
+ event: "provider",
53
+ provider: "anthropic",
54
+ name: "context_management",
55
+ payload: data.delta.context_management
56
+ };
57
+ break;
58
+ case "message_stop":
59
+ yield {
60
+ event: "message-finish",
61
+ reason: mapStopReason(stopReason),
62
+ ...usageSnapshot ? { usage: usageSnapshot } : {},
63
+ responseMetadata: { model_provider: "anthropic" }
64
+ };
65
+ break;
66
+ case "error": {
67
+ const streamError = new Error(data.error.message);
68
+ streamError.name = data.error.type;
69
+ throw streamError;
70
+ }
71
+ case "content_block_start": {
72
+ const { index, content_block } = data;
73
+ const mapped = mapBlockToContentBlock(content_block, index);
74
+ blockAccumulators.set(index, { ...mapped });
75
+ yield {
76
+ event: "content-block-start",
77
+ index,
78
+ content: mapped
79
+ };
80
+ break;
81
+ }
82
+ case "content_block_delta": {
83
+ const { index, delta } = data;
84
+ const acc = blockAccumulators.get(index);
85
+ if (!acc) break;
86
+ const { contentDelta, accumulated } = applyAnthropicDelta(acc, delta);
87
+ blockAccumulators.set(index, accumulated);
88
+ yield {
89
+ event: "content-block-delta",
90
+ index,
91
+ delta: contentDelta
92
+ };
93
+ break;
94
+ }
95
+ case "content_block_stop": {
96
+ const { index } = data;
97
+ const acc = blockAccumulators.get(index);
98
+ if (!acc) break;
99
+ yield {
100
+ event: "content-block-finish",
101
+ index,
102
+ content: finalizeBlock(acc)
103
+ };
104
+ blockAccumulators.delete(index);
105
+ break;
106
+ }
107
+ default: {
108
+ const providerData = data;
109
+ yield {
110
+ event: "provider",
111
+ provider: "anthropic",
112
+ name: providerData.type,
113
+ payload: providerData
114
+ };
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ function mapStopReason(stopReason) {
120
+ switch (stopReason) {
121
+ case "end_turn":
122
+ case "stop_sequence": return "stop";
123
+ case "tool_use": return "tool_use";
124
+ case "max_tokens":
125
+ case "model_context_window_exceeded": return "length";
126
+ case "refusal": return "content_filter";
127
+ default: return "stop";
128
+ }
129
+ }
130
+ function updateEventUsage(previous, current) {
131
+ return {
132
+ inputTokens: getCumulativeUsageValue(current.input_tokens, previous?.inputTokens),
133
+ cacheCreationInputTokens: getCumulativeUsageValue(current.cache_creation_input_tokens, previous?.cacheCreationInputTokens),
134
+ cacheReadInputTokens: getCumulativeUsageValue(current.cache_read_input_tokens, previous?.cacheReadInputTokens),
135
+ outputTokens: getCumulativeUsageValue(current.output_tokens, previous?.outputTokens)
136
+ };
137
+ }
138
+ function getCumulativeUsageValue(current, previous) {
139
+ return Math.max(previous ?? 0, current ?? 0);
140
+ }
141
+ function buildUsageSnapshot(usage) {
142
+ const metadata = getAnthropicUsageMetadata({
143
+ input_tokens: usage.inputTokens,
144
+ output_tokens: usage.outputTokens,
145
+ cache_creation_input_tokens: usage.cacheCreationInputTokens,
146
+ cache_read_input_tokens: usage.cacheReadInputTokens
147
+ });
148
+ if (metadata == null) throw new Error("Anthropic usage metadata was not created");
149
+ return metadata;
150
+ }
151
+ function getStringField(record, key) {
152
+ const value = record[key];
153
+ return typeof value === "string" ? value : "";
154
+ }
155
+ function getArrayField(record, key) {
156
+ const value = record[key];
157
+ return Array.isArray(value) ? value : [];
158
+ }
159
+ function mapBlockToContentBlock(block, index) {
160
+ switch (block.type) {
161
+ case "text": return {
162
+ type: "text",
163
+ text: block.text,
164
+ ...block.citations != null ? { citations: block.citations } : {},
165
+ index
166
+ };
167
+ case "thinking": return {
168
+ type: "reasoning",
169
+ reasoning: block.thinking,
170
+ index
171
+ };
172
+ case "redacted_thinking": return {
173
+ ...block,
174
+ index
175
+ };
176
+ case "tool_use": return {
177
+ type: "tool_call_chunk",
178
+ id: block.id,
179
+ name: block.name,
180
+ args: "",
181
+ index
182
+ };
183
+ case "server_tool_use": return {
184
+ type: "server_tool_call_chunk",
185
+ id: block.id,
186
+ name: block.name,
187
+ args: "",
188
+ index
189
+ };
190
+ case "web_search_tool_result": return {
191
+ ...block,
192
+ index
193
+ };
194
+ case "compaction": return {
195
+ ...block,
196
+ index
197
+ };
198
+ default: return {
199
+ type: "non_standard",
200
+ value: { ...block },
201
+ index
202
+ };
203
+ }
204
+ }
205
+ /**
206
+ * Map an Anthropic content_block_delta to a content block delta
207
+ * and update the accumulated state.
208
+ */
209
+ function applyAnthropicDelta(accumulated, delta) {
210
+ const rawDelta = delta;
211
+ switch (delta.type) {
212
+ case "text_delta": return {
213
+ contentDelta: {
214
+ type: "text-delta",
215
+ text: delta.text
216
+ },
217
+ accumulated: {
218
+ ...accumulated,
219
+ text: getStringField(accumulated, "text") + delta.text
220
+ }
221
+ };
222
+ case "thinking_delta": return {
223
+ contentDelta: {
224
+ type: "reasoning-delta",
225
+ reasoning: delta.thinking
226
+ },
227
+ accumulated: {
228
+ ...accumulated,
229
+ reasoning: getStringField(accumulated, "reasoning") + delta.thinking
230
+ }
231
+ };
232
+ case "input_json_delta": {
233
+ const newArgs = getStringField(accumulated, "args") + delta.partial_json;
234
+ return {
235
+ contentDelta: {
236
+ type: "block-delta",
237
+ fields: {
238
+ type: getStringField(accumulated, "type"),
239
+ args: newArgs
240
+ }
241
+ },
242
+ accumulated: {
243
+ ...accumulated,
244
+ args: newArgs
245
+ }
246
+ };
247
+ }
248
+ case "citations_delta": {
249
+ const citations = [...getArrayField(accumulated, "citations"), delta.citation];
250
+ return {
251
+ contentDelta: {
252
+ type: "block-delta",
253
+ fields: {
254
+ type: getStringField(accumulated, "type"),
255
+ citations
256
+ }
257
+ },
258
+ accumulated: {
259
+ ...accumulated,
260
+ citations
261
+ }
262
+ };
263
+ }
264
+ case "signature_delta": return {
265
+ contentDelta: {
266
+ type: "block-delta",
267
+ fields: {
268
+ type: getStringField(accumulated, "type"),
269
+ signature: delta.signature
270
+ }
271
+ },
272
+ accumulated: {
273
+ ...accumulated,
274
+ signature: delta.signature
275
+ }
276
+ };
277
+ case "compaction_delta": {
278
+ const previousContent = accumulated.content;
279
+ const content = delta.content == null ? previousContent ?? null : getStringField(accumulated, "content") + delta.content;
280
+ return {
281
+ contentDelta: {
282
+ type: "block-delta",
283
+ fields: {
284
+ type: "compaction",
285
+ content,
286
+ encrypted_content: delta.encrypted_content
287
+ }
288
+ },
289
+ accumulated: {
290
+ ...accumulated,
291
+ content,
292
+ encrypted_content: delta.encrypted_content
293
+ }
294
+ };
295
+ }
296
+ default: return {
297
+ contentDelta: {
298
+ type: "block-delta",
299
+ fields: {
300
+ type: getStringField(accumulated, "type"),
301
+ ...rawDelta
302
+ }
303
+ },
304
+ accumulated
305
+ };
306
+ }
307
+ }
308
+ function finalizeBlock(accumulated) {
309
+ if (accumulated.type === "tool_call_chunk" || accumulated.type === "server_tool_call_chunk") {
310
+ const finalType = accumulated.type === "tool_call_chunk" ? "tool_call" : "server_tool_call";
311
+ const args = getStringField(accumulated, "args");
312
+ let parsedArgs;
313
+ try {
314
+ parsedArgs = JSON.parse(args || "{}");
315
+ } catch {
316
+ return {
317
+ type: "invalid_tool_call",
318
+ id: getStringField(accumulated, "id"),
319
+ name: getStringField(accumulated, "name"),
320
+ args,
321
+ error: "Failed to parse tool call arguments as JSON"
322
+ };
323
+ }
324
+ return {
325
+ type: finalType,
326
+ id: getStringField(accumulated, "id"),
327
+ name: getStringField(accumulated, "name"),
328
+ args: parsedArgs
329
+ };
330
+ }
331
+ const { index: _index, ...rest } = accumulated;
332
+ return rest;
333
+ }
334
+ //#endregion
335
+ export { convertAnthropicStream };
336
+
337
+ //# sourceMappingURL=stream_events.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream_events.mjs","names":[],"sources":["../../../../../src/llm/anthropic/utils/stream_events.ts"],"sourcesContent":["/**\n * Converts a raw Anthropic SSE event stream into LangChain ChatModelStreamEvents.\n *\n * @module\n */\n\nimport type {\n ChatModelStreamEvent,\n ContentBlockDelta,\n FinishReason,\n} from '@langchain/core/language_models/event';\nimport type { ContentBlock, UsageMetadata } from '@langchain/core/messages';\nimport type {\n AnthropicCompactionBlock,\n AnthropicCompactionContentBlockDelta,\n AnthropicMessageStreamEvent,\n} from '../types';\nimport type { AnthropicUsageData } from './message_outputs';\nimport { getAnthropicUsageMetadata } from './message_outputs';\n\ntype AnthropicContentBlock =\n | Extract<\n AnthropicMessageStreamEvent,\n { type: 'content_block_start' }\n >['content_block']\n | AnthropicCompactionBlock;\n\ntype AnthropicContentDelta =\n | Extract<\n AnthropicMessageStreamEvent,\n { type: 'content_block_delta' }\n >['delta']\n | AnthropicCompactionContentBlockDelta;\n\ninterface AnthropicStreamErrorEvent {\n type: 'error';\n error: {\n type: string;\n message: string;\n };\n request_id?: string | null;\n}\n\ntype AnthropicStreamInputEvent =\n | AnthropicMessageStreamEvent\n | AnthropicStreamErrorEvent;\n\ntype BlockAccumulator = Record<string, unknown>;\n\ninterface AnthropicEventUsage {\n inputTokens: number;\n cacheCreationInputTokens: number;\n cacheReadInputTokens: number;\n outputTokens: number;\n}\n\n// ─── Public API ─────────────────────────────────────────────────\n\nexport interface ConvertAnthropicStreamOptions {\n streamUsage?: boolean;\n}\n\n/**\n * Convert an async iterable of raw Anthropic stream events into\n * LangChain `ChatModelStreamEvent`s with typed deltas.\n */\nexport async function* convertAnthropicStream(\n source: AsyncIterable<AnthropicStreamInputEvent>,\n options: ConvertAnthropicStreamOptions = {}\n): AsyncGenerator<ChatModelStreamEvent> {\n const shouldStreamUsage = options.streamUsage ?? true;\n\n // Track accumulated state per content block (for finalization)\n const blockAccumulators = new Map<number, BlockAccumulator>();\n let usageSnapshot: UsageMetadata | undefined;\n let eventUsage: AnthropicEventUsage | undefined;\n let stopReason: string | null = null;\n\n for await (const data of source) {\n switch (data.type) {\n // ── Message lifecycle ──────────────────────────────────\n case 'message_start': {\n const { usage, id, model } = data.message;\n if (shouldStreamUsage) {\n eventUsage = {\n inputTokens: usage.input_tokens,\n cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,\n cacheReadInputTokens: usage.cache_read_input_tokens ?? 0,\n outputTokens: usage.output_tokens,\n };\n usageSnapshot = buildUsageSnapshot(eventUsage);\n }\n yield {\n event: 'message-start' as const,\n id,\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n };\n yield {\n event: 'provider' as const,\n provider: 'anthropic',\n name: 'message_start',\n payload: { model, id },\n };\n break;\n }\n\n case 'message_delta': {\n stopReason = data.delta.stop_reason;\n if (shouldStreamUsage) {\n eventUsage = updateEventUsage(eventUsage, data.usage);\n usageSnapshot = buildUsageSnapshot(eventUsage);\n yield { event: 'usage' as const, usage: usageSnapshot };\n }\n if (\n 'context_management' in data.delta &&\n data.delta.context_management != null\n ) {\n yield {\n event: 'provider' as const,\n provider: 'anthropic',\n name: 'context_management',\n payload: data.delta.context_management,\n };\n }\n break;\n }\n\n case 'message_stop': {\n const finishEvent = {\n event: 'message-finish' as const,\n reason: mapStopReason(stopReason),\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n responseMetadata: { model_provider: 'anthropic' },\n };\n yield finishEvent;\n break;\n }\n\n case 'error': {\n const streamError = new Error(data.error.message);\n streamError.name = data.error.type;\n throw streamError;\n }\n\n // ── Content block lifecycle ───────────────────────────\n case 'content_block_start': {\n const { index, content_block } = data;\n const mapped = mapBlockToContentBlock(content_block, index);\n blockAccumulators.set(index, { ...mapped });\n yield {\n event: 'content-block-start' as const,\n index,\n content: mapped as unknown as ContentBlock,\n };\n break;\n }\n\n case 'content_block_delta': {\n const { index, delta } = data;\n const acc = blockAccumulators.get(index);\n if (!acc) break;\n\n const { contentDelta, accumulated } = applyAnthropicDelta(acc, delta);\n blockAccumulators.set(index, accumulated);\n\n yield {\n event: 'content-block-delta' as const,\n index,\n delta: contentDelta,\n };\n break;\n }\n\n case 'content_block_stop': {\n const { index } = data;\n const acc = blockAccumulators.get(index);\n if (!acc) break;\n\n const finalized = finalizeBlock(acc);\n yield {\n event: 'content-block-finish' as const,\n index,\n content: finalized,\n };\n blockAccumulators.delete(index);\n break;\n }\n\n // ── Unhandled → provider passthrough ───────────────────\n default: {\n const providerData = data as AnthropicMessageStreamEvent;\n yield {\n event: 'provider' as const,\n provider: 'anthropic',\n name: providerData.type,\n payload: providerData,\n };\n break;\n }\n }\n }\n}\n\n// ─── Internal helpers ───────────────────────────────────────────\n\nfunction mapStopReason(stopReason: string | null | undefined): FinishReason {\n switch (stopReason) {\n case 'end_turn':\n case 'stop_sequence':\n return 'stop';\n case 'tool_use':\n return 'tool_use';\n case 'max_tokens':\n case 'model_context_window_exceeded':\n return 'length';\n case 'refusal':\n return 'content_filter';\n default:\n return 'stop';\n }\n}\n\nfunction updateEventUsage(\n previous: AnthropicEventUsage | undefined,\n current: AnthropicUsageData\n): AnthropicEventUsage {\n return {\n inputTokens: getCumulativeUsageValue(\n current.input_tokens,\n previous?.inputTokens\n ),\n cacheCreationInputTokens: getCumulativeUsageValue(\n current.cache_creation_input_tokens,\n previous?.cacheCreationInputTokens\n ),\n cacheReadInputTokens: getCumulativeUsageValue(\n current.cache_read_input_tokens,\n previous?.cacheReadInputTokens\n ),\n outputTokens: getCumulativeUsageValue(\n current.output_tokens,\n previous?.outputTokens\n ),\n };\n}\n\nfunction getCumulativeUsageValue(\n current: number | null | undefined,\n previous: number | undefined\n): number {\n return Math.max(previous ?? 0, current ?? 0);\n}\n\nfunction buildUsageSnapshot(usage: AnthropicEventUsage): UsageMetadata {\n const metadata = getAnthropicUsageMetadata({\n input_tokens: usage.inputTokens,\n output_tokens: usage.outputTokens,\n cache_creation_input_tokens: usage.cacheCreationInputTokens,\n cache_read_input_tokens: usage.cacheReadInputTokens,\n });\n if (metadata == null) {\n throw new Error('Anthropic usage metadata was not created');\n }\n return metadata;\n}\n\nfunction getStringField(record: BlockAccumulator, key: string): string {\n const value = record[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction getArrayField(record: BlockAccumulator, key: string): unknown[] {\n const value = record[key];\n return Array.isArray(value) ? value : [];\n}\n\nfunction mapBlockToContentBlock(\n block: AnthropicContentBlock,\n index: number\n): BlockAccumulator {\n switch (block.type) {\n case 'text':\n return {\n type: 'text' as const,\n text: block.text,\n ...(block.citations != null ? { citations: block.citations } : {}),\n index,\n };\n case 'thinking':\n return {\n type: 'reasoning' as const,\n reasoning: block.thinking,\n index,\n };\n case 'redacted_thinking':\n return { ...block, index };\n case 'tool_use':\n return {\n type: 'tool_call_chunk' as const,\n id: block.id,\n name: block.name,\n args: '',\n index,\n };\n case 'server_tool_use':\n return {\n type: 'server_tool_call_chunk' as const,\n id: block.id,\n name: block.name,\n args: '',\n index,\n };\n case 'web_search_tool_result':\n return { ...block, index };\n case 'compaction':\n return { ...block, index };\n default:\n return { type: 'non_standard' as const, value: { ...block }, index };\n }\n}\n\n/**\n * Map an Anthropic content_block_delta to a content block delta\n * and update the accumulated state.\n */\nfunction applyAnthropicDelta(\n accumulated: BlockAccumulator,\n delta: AnthropicContentDelta\n): {\n contentDelta: ContentBlockDelta;\n accumulated: BlockAccumulator;\n} {\n const rawDelta = delta as unknown as Record<string, unknown>;\n switch (delta.type) {\n case 'text_delta':\n return {\n contentDelta: { type: 'text-delta' as const, text: delta.text },\n accumulated: {\n ...accumulated,\n text: getStringField(accumulated, 'text') + delta.text,\n },\n };\n\n case 'thinking_delta':\n return {\n contentDelta: {\n type: 'reasoning-delta' as const,\n reasoning: delta.thinking,\n },\n accumulated: {\n ...accumulated,\n reasoning: getStringField(accumulated, 'reasoning') + delta.thinking,\n },\n };\n\n case 'input_json_delta': {\n const newArgs = getStringField(accumulated, 'args') + delta.partial_json;\n return {\n contentDelta: {\n type: 'block-delta' as const,\n fields: {\n type: getStringField(accumulated, 'type'),\n args: newArgs,\n },\n },\n accumulated: { ...accumulated, args: newArgs },\n };\n }\n\n case 'citations_delta': {\n const citations = [\n ...getArrayField(accumulated, 'citations'),\n delta.citation,\n ];\n return {\n contentDelta: {\n type: 'block-delta' as const,\n fields: {\n type: getStringField(accumulated, 'type'),\n citations,\n },\n },\n accumulated: {\n ...accumulated,\n citations,\n },\n };\n }\n\n case 'signature_delta':\n return {\n contentDelta: {\n type: 'block-delta' as const,\n fields: {\n type: getStringField(accumulated, 'type'),\n signature: delta.signature,\n },\n },\n accumulated: { ...accumulated, signature: delta.signature },\n };\n\n case 'compaction_delta': {\n const previousContent = accumulated.content;\n const content =\n delta.content == null\n ? (previousContent ?? null)\n : getStringField(accumulated, 'content') + delta.content;\n return {\n contentDelta: {\n type: 'block-delta' as const,\n fields: {\n type: 'compaction',\n content,\n encrypted_content: delta.encrypted_content,\n },\n },\n accumulated: {\n ...accumulated,\n content,\n encrypted_content: delta.encrypted_content,\n },\n };\n }\n\n default:\n return {\n contentDelta: {\n type: 'block-delta' as const,\n fields: {\n type: getStringField(accumulated, 'type'),\n ...rawDelta,\n },\n },\n accumulated,\n };\n }\n}\n\nfunction finalizeBlock(accumulated: BlockAccumulator): ContentBlock {\n if (\n accumulated.type === 'tool_call_chunk' ||\n accumulated.type === 'server_tool_call_chunk'\n ) {\n const finalType =\n accumulated.type === 'tool_call_chunk'\n ? ('tool_call' as const)\n : ('server_tool_call' as const);\n const args = getStringField(accumulated, 'args');\n let parsedArgs: unknown;\n try {\n parsedArgs = JSON.parse(args || '{}');\n } catch {\n return {\n type: 'invalid_tool_call' as const,\n id: getStringField(accumulated, 'id'),\n name: getStringField(accumulated, 'name'),\n args,\n error: 'Failed to parse tool call arguments as JSON',\n } as ContentBlock.Tools.InvalidToolCall;\n }\n return {\n type: finalType,\n id: getStringField(accumulated, 'id'),\n name: getStringField(accumulated, 'name'),\n args: parsedArgs,\n } as ContentBlock;\n }\n\n const { index: _index, ...rest } = accumulated;\n return rest as ContentBlock;\n}\n"],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ import { createDefaultLogger } from "./utils.mjs";
2
+ import { processContent } from "./content.mjs";
3
+ import axios from "axios";
4
+ //#region src/tools/search/crw-scraper.ts
5
+ /** HTTP headroom over the payload render budget: fastCRW's queue/verification
6
+ * overhead is not counted against `timeout`, so the client must wait longer. */
7
+ const CRW_TIMEOUT_BUFFER = 5e3;
8
+ /**
9
+ * fastCRW scraper. Firecrawl-compatible web scraper; single binary;
10
+ * self-host or cloud. Posts to {base}/v1/scrape.
11
+ */
12
+ var CrwScraper = class {
13
+ apiKey;
14
+ apiUrl;
15
+ defaultFormats;
16
+ timeout;
17
+ logger;
18
+ onlyMainContent;
19
+ includeTags;
20
+ excludeTags;
21
+ waitFor;
22
+ headers;
23
+ renderJs;
24
+ cssSelector;
25
+ xpath;
26
+ proxy;
27
+ stealth;
28
+ constructor(config = {}) {
29
+ this.apiKey = config.apiKey ?? process.env.CRW_API_KEY ?? "";
30
+ const baseUrl = config.apiUrl ?? process.env.CRW_API_URL ?? "https://api.fastcrw.com";
31
+ this.apiUrl = `${baseUrl.replace(/\/+$/, "")}/v1/scrape`;
32
+ this.defaultFormats = config.formats ?? ["markdown", "html"];
33
+ this.timeout = config.timeout ?? 7500;
34
+ this.logger = config.logger || createDefaultLogger();
35
+ this.onlyMainContent = config.onlyMainContent;
36
+ this.includeTags = config.includeTags;
37
+ this.excludeTags = config.excludeTags;
38
+ this.waitFor = config.waitFor;
39
+ this.headers = config.headers;
40
+ this.renderJs = config.renderJs;
41
+ this.cssSelector = config.cssSelector;
42
+ this.xpath = config.xpath;
43
+ this.proxy = config.proxy;
44
+ this.stealth = config.stealth;
45
+ if (!this.apiKey) this.logger.warn("CRW_API_KEY is not set. Scraping will not work.");
46
+ this.logger.debug(`CRW scraper initialized with API URL: ${this.apiUrl}`);
47
+ }
48
+ async scrapeUrl(url, options = {}) {
49
+ try {
50
+ const payloadTimeout = options.timeout ?? this.timeout;
51
+ const payload = omitUndefined({
52
+ url,
53
+ formats: options.formats ?? this.defaultFormats,
54
+ onlyMainContent: options.onlyMainContent ?? this.onlyMainContent,
55
+ includeTags: options.includeTags ?? this.includeTags,
56
+ excludeTags: options.excludeTags ?? this.excludeTags,
57
+ waitFor: options.waitFor ?? this.waitFor,
58
+ headers: options.headers ?? this.headers,
59
+ renderJs: options.renderJs ?? this.renderJs,
60
+ cssSelector: options.cssSelector ?? this.cssSelector,
61
+ xpath: options.xpath ?? this.xpath,
62
+ proxy: options.proxy ?? this.proxy,
63
+ stealth: options.stealth ?? this.stealth,
64
+ timeout: payloadTimeout,
65
+ deadlineMs: Math.max(1, Math.min(payloadTimeout, 6e4))
66
+ });
67
+ const headers = { "Content-Type": "application/json" };
68
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
69
+ return [url, normalizeCrwResponse((await axios.post(this.apiUrl, payload, {
70
+ headers,
71
+ timeout: payloadTimeout + CRW_TIMEOUT_BUFFER
72
+ })).data)];
73
+ } catch (error) {
74
+ return [url, {
75
+ success: false,
76
+ error: `fastCRW API request failed: ${error instanceof Error ? error.message : String(error)}`
77
+ }];
78
+ }
79
+ }
80
+ /**
81
+ * Extract content from scrape response. Mirrors FirecrawlScraper — reads
82
+ * response.data.*, which normalizeCrwResponse guarantees, preserving the
83
+ * processContent ref-markers used by the reranker. Parameter is typed as the
84
+ * NARROWER t.CrwScrapeResponse, exactly like TavilyScraper/FirecrawlScraper:
85
+ * TS class-method bivariance accepts this against BaseScraper's
86
+ * AnyScraperResponse, AND it lets us read response.data.plainText (a
87
+ * CrwScrapeResponse-only field) with no cast.
88
+ */
89
+ extractContent(response) {
90
+ if (!response.success || !response.data) return ["", void 0];
91
+ const htmlSource = response.data.html ?? response.data.rawHtml;
92
+ if (response.data.markdown != null && htmlSource != null) try {
93
+ const { markdown, ...rest } = processContent(htmlSource, response.data.markdown);
94
+ return [markdown, rest];
95
+ } catch (error) {
96
+ this.logger.error("Error processing content:", error);
97
+ return [response.data.markdown, void 0];
98
+ }
99
+ else if (response.data.markdown != null) return [response.data.markdown, void 0];
100
+ if (response.data.html != null) return [response.data.html, void 0];
101
+ if (response.data.rawHtml != null) return [response.data.rawHtml, void 0];
102
+ if (response.data.plainText != null) return [response.data.plainText, void 0];
103
+ return ["", void 0];
104
+ }
105
+ extractMetadata(response) {
106
+ if (!response.success || !response.data || !response.data.metadata) return {};
107
+ return response.data.metadata;
108
+ }
109
+ };
110
+ /**
111
+ * Create a fastCRW scraper instance
112
+ * @param config Scraper configuration
113
+ * @returns fastCRW scraper instance
114
+ */
115
+ const createCrwScraper = (config = {}) => new CrwScraper(config);
116
+ /**
117
+ * fastCRW cloud nests scrape fields under `data` ({success, data: {markdown,
118
+ * ...}}, live-verified 2026-07-02), matching Firecrawl. Prefer the nested
119
+ * container and fall back to top-level fields for self-host/legacy responses.
120
+ */
121
+ function normalizeCrwResponse(raw) {
122
+ if (raw == null) return {
123
+ success: false,
124
+ error: "Empty fastCRW response"
125
+ };
126
+ if (raw.success === false) return {
127
+ success: false,
128
+ error: raw.error_code != null ? `[${raw.error_code}] ${raw.error ?? "Unknown error"}` : raw.error ?? "fastCRW scrape failed",
129
+ error_code: raw.error_code
130
+ };
131
+ const data = raw.data ?? raw;
132
+ return {
133
+ success: true,
134
+ data: {
135
+ markdown: stripBase64DataUris(data.markdown),
136
+ html: stripBase64DataUris(data.html),
137
+ rawHtml: stripBase64DataUris(data.rawHtml),
138
+ plainText: stripBase64DataUris(data.plainText),
139
+ screenshot: data.screenshot,
140
+ links: data.links,
141
+ metadata: data.metadata
142
+ }
143
+ };
144
+ }
145
+ /**
146
+ * Replace inline base64 data-URI payloads (typically images) in scraped text
147
+ * with a short placeholder. Such payloads can be hundreds of KB; the content
148
+ * processor builds a per-link RegExp from each URL, and one that large overflows
149
+ * the engine's pattern-size limit ("Invalid regular expression"). Firecrawl
150
+ * drops them via removeBase64Images — mirror that so image-heavy pages stay
151
+ * processable (and don't bloat the LLM context with base64 noise).
152
+ */
153
+ function stripBase64DataUris(text) {
154
+ if (text == null) return text;
155
+ return text.replace(/data:[\w.+-]+\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g, "data:base64-content-removed");
156
+ }
157
+ function omitUndefined(obj) {
158
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
159
+ }
160
+ //#endregion
161
+ export { createCrwScraper };
162
+
163
+ //# sourceMappingURL=crw-scraper.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crw-scraper.mjs","names":[],"sources":["../../../../src/tools/search/crw-scraper.ts"],"sourcesContent":["import axios from 'axios';\nimport type * as t from './types';\nimport { createDefaultLogger } from './utils';\nimport { processContent } from './content';\n\n/** HTTP headroom over the payload render budget: fastCRW's queue/verification\n * overhead is not counted against `timeout`, so the client must wait longer. */\nconst CRW_TIMEOUT_BUFFER = 5000;\n\n/**\n * fastCRW scraper. Firecrawl-compatible web scraper; single binary;\n * self-host or cloud. Posts to {base}/v1/scrape.\n */\nexport class CrwScraper implements t.BaseScraper {\n private apiKey: string;\n private apiUrl: string;\n private defaultFormats: string[];\n private timeout: number;\n private logger: t.Logger;\n private onlyMainContent?: boolean;\n private includeTags?: string[];\n private excludeTags?: string[];\n private waitFor?: number;\n private headers?: Record<string, string>;\n private renderJs?: boolean | null;\n private cssSelector?: string;\n private xpath?: string;\n private proxy?: string;\n private stealth?: boolean;\n\n constructor(config: t.CrwScraperConfig = {}) {\n this.apiKey = config.apiKey ?? process.env.CRW_API_KEY ?? '';\n\n const baseUrl =\n config.apiUrl ?? process.env.CRW_API_URL ?? 'https://api.fastcrw.com';\n this.apiUrl = `${baseUrl.replace(/\\/+$/, '')}/v1/scrape`;\n\n this.defaultFormats = config.formats ?? ['markdown', 'html'];\n this.timeout = config.timeout ?? 7500;\n this.logger = config.logger || createDefaultLogger();\n\n this.onlyMainContent = config.onlyMainContent;\n this.includeTags = config.includeTags;\n this.excludeTags = config.excludeTags;\n this.waitFor = config.waitFor;\n this.headers = config.headers;\n this.renderJs = config.renderJs;\n this.cssSelector = config.cssSelector;\n this.xpath = config.xpath;\n this.proxy = config.proxy;\n this.stealth = config.stealth;\n\n // Self-host fastCRW may run without auth, so a missing key is only a\n // warning — unlike Firecrawl/Tavily, scrapeUrl does NOT early-return on it.\n if (!this.apiKey) {\n this.logger.warn('CRW_API_KEY is not set. Scraping will not work.');\n }\n this.logger.debug(`CRW scraper initialized with API URL: ${this.apiUrl}`);\n }\n\n async scrapeUrl(\n url: string,\n options: t.CrwScrapeOptions = {}\n ): Promise<[string, t.CrwScrapeResponse]> {\n try {\n const payloadTimeout = options.timeout ?? this.timeout;\n const payload = omitUndefined({\n url,\n formats: options.formats ?? this.defaultFormats,\n onlyMainContent: options.onlyMainContent ?? this.onlyMainContent,\n includeTags: options.includeTags ?? this.includeTags,\n excludeTags: options.excludeTags ?? this.excludeTags,\n waitFor: options.waitFor ?? this.waitFor,\n headers: options.headers ?? this.headers,\n renderJs: options.renderJs ?? this.renderJs,\n cssSelector: options.cssSelector ?? this.cssSelector,\n xpath: options.xpath ?? this.xpath,\n proxy: options.proxy ?? this.proxy,\n stealth: options.stealth ?? this.stealth,\n // Cloud honors `timeout` (live-verified); the published OpenAPI\n // documents `deadlineMs` (1..60000) instead. Send both.\n timeout: payloadTimeout,\n deadlineMs: Math.max(1, Math.min(payloadTimeout, 60000)),\n });\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (this.apiKey) {\n headers.Authorization = `Bearer ${this.apiKey}`;\n }\n\n const response = await axios.post<t.CrwRawScrapeResponse>(\n this.apiUrl,\n payload,\n {\n headers,\n timeout: payloadTimeout + CRW_TIMEOUT_BUFFER,\n }\n );\n\n return [url, normalizeCrwResponse(response.data)];\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n return [\n url,\n {\n success: false,\n error: `fastCRW API request failed: ${errorMessage}`,\n },\n ];\n }\n }\n\n /**\n * Extract content from scrape response. Mirrors FirecrawlScraper — reads\n * response.data.*, which normalizeCrwResponse guarantees, preserving the\n * processContent ref-markers used by the reranker. Parameter is typed as the\n * NARROWER t.CrwScrapeResponse, exactly like TavilyScraper/FirecrawlScraper:\n * TS class-method bivariance accepts this against BaseScraper's\n * AnyScraperResponse, AND it lets us read response.data.plainText (a\n * CrwScrapeResponse-only field) with no cast.\n */\n extractContent(\n response: t.CrwScrapeResponse\n ): [string, undefined | t.References] {\n if (!response.success || !response.data) {\n return ['', undefined];\n }\n\n const htmlSource = response.data.html ?? response.data.rawHtml;\n if (response.data.markdown != null && htmlSource != null) {\n try {\n const { markdown, ...rest } = processContent(\n htmlSource,\n response.data.markdown\n );\n return [markdown, rest];\n } catch (error) {\n this.logger.error('Error processing content:', error);\n return [response.data.markdown, undefined];\n }\n } else if (response.data.markdown != null) {\n return [response.data.markdown, undefined];\n }\n\n // Fall back to HTML content\n if (response.data.html != null) {\n return [response.data.html, undefined];\n }\n\n // Fall back to raw HTML content\n if (response.data.rawHtml != null) {\n return [response.data.rawHtml, undefined];\n }\n\n // CRW-only fallback (no Firecrawl equivalent): plain-text body.\n if (response.data.plainText != null) {\n return [response.data.plainText, undefined];\n }\n\n return ['', undefined];\n }\n\n extractMetadata(response: t.CrwScrapeResponse): t.ScrapeMetadata {\n if (!response.success || !response.data || !response.data.metadata) {\n return {};\n }\n\n return response.data.metadata;\n }\n}\n\n/**\n * Create a fastCRW scraper instance\n * @param config Scraper configuration\n * @returns fastCRW scraper instance\n */\nexport const createCrwScraper = (config: t.CrwScraperConfig = {}): CrwScraper =>\n new CrwScraper(config);\n\n/**\n * fastCRW cloud nests scrape fields under `data` ({success, data: {markdown,\n * ...}}, live-verified 2026-07-02), matching Firecrawl. Prefer the nested\n * container and fall back to top-level fields for self-host/legacy responses.\n */\nfunction normalizeCrwResponse(\n raw: t.CrwRawScrapeResponse | null | undefined\n): t.CrwScrapeResponse {\n if (raw == null) {\n return { success: false, error: 'Empty fastCRW response' };\n }\n if (raw.success === false) {\n return {\n success: false,\n error:\n raw.error_code != null\n ? `[${raw.error_code}] ${raw.error ?? 'Unknown error'}`\n : (raw.error ?? 'fastCRW scrape failed'),\n error_code: raw.error_code,\n };\n }\n const data = raw.data ?? raw;\n return {\n success: true,\n data: {\n // Strip inline base64 image payloads from text content (not `screenshot`,\n // which is base64 by design and never reaches the content processor).\n markdown: stripBase64DataUris(data.markdown),\n html: stripBase64DataUris(data.html),\n rawHtml: stripBase64DataUris(data.rawHtml),\n plainText: stripBase64DataUris(data.plainText),\n screenshot: data.screenshot,\n links: data.links,\n metadata: data.metadata,\n },\n };\n}\n\n/**\n * Replace inline base64 data-URI payloads (typically images) in scraped text\n * with a short placeholder. Such payloads can be hundreds of KB; the content\n * processor builds a per-link RegExp from each URL, and one that large overflows\n * the engine's pattern-size limit (\"Invalid regular expression\"). Firecrawl\n * drops them via removeBase64Images — mirror that so image-heavy pages stay\n * processable (and don't bloat the LLM context with base64 noise).\n */\nfunction stripBase64DataUris(text: string | undefined): string | undefined {\n if (text == null) {\n return text;\n }\n return text.replace(\n /data:[\\w.+-]+\\/[\\w.+-]+;base64,[A-Za-z0-9+/=]+/g,\n 'data:base64-content-removed'\n );\n}\n\n// Helper function to clean up payload for fastCRW\nfunction omitUndefined<T extends object>(obj: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(obj).filter(([, v]) => v !== undefined)\n ) as Partial<T>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ import axios from "axios";
2
+ //#region src/tools/search/crw-search.ts
3
+ const DEFAULT_CRW_TIMEOUT = 15e3;
4
+ const getHostname = (link) => {
5
+ try {
6
+ return new URL(link).hostname;
7
+ } catch {
8
+ return link;
9
+ }
10
+ };
11
+ /** The published OpenAPI (docs.fastcrw.com) documents `data` as a flat
12
+ * SearchResult[] with a `category` field; route rows into source groups. */
13
+ const splitByCategory = (rows) => {
14
+ const groups = {
15
+ web: [],
16
+ images: [],
17
+ news: []
18
+ };
19
+ for (const row of rows) if (row.category === "news") groups.news.push(row);
20
+ else if (row.category === "images") groups.images.push({
21
+ ...row,
22
+ imageUrl: row.url
23
+ });
24
+ else groups.web.push(row);
25
+ return groups;
26
+ };
27
+ const createCrwAPI = (apiKey, apiUrl, options) => {
28
+ const base = (apiUrl ?? process.env.CRW_API_URL ?? "https://api.fastcrw.com").replace(/\/+$/, "");
29
+ const config = {
30
+ apiKey: apiKey ?? process.env.CRW_API_KEY,
31
+ apiUrl: `${base}/v1/search`,
32
+ timeout: options?.timeout ?? DEFAULT_CRW_TIMEOUT
33
+ };
34
+ const getSources = async ({ query, date, numResults = 8, type }) => {
35
+ if (!query.trim()) return {
36
+ success: false,
37
+ error: "Query cannot be empty"
38
+ };
39
+ try {
40
+ const limit = Math.min(Math.max(1, options?.maxResults ?? numResults), 20);
41
+ let sources;
42
+ if (type === "images") sources = ["images"];
43
+ else if (type === "news") sources = ["news"];
44
+ else sources = options?.includeImages === true ? ["web", "images"] : ["web"];
45
+ const payload = {
46
+ query,
47
+ limit,
48
+ sources
49
+ };
50
+ if (date != null) payload.tbs = `qdr:${date}`;
51
+ const headers = { "Content-Type": "application/json" };
52
+ if (config.apiKey != null && config.apiKey !== "") headers.Authorization = `Bearer ${config.apiKey}`;
53
+ const body = (await axios.post(config.apiUrl, payload, {
54
+ headers,
55
+ timeout: config.timeout
56
+ })).data;
57
+ if (body.success === false) return {
58
+ success: false,
59
+ error: `fastCRW search failed: ${body.error_code != null ? `[${body.error_code}] ` : ""}${body.error ?? "Unknown error"}`
60
+ };
61
+ const container = body.data ?? {};
62
+ const data = Array.isArray(container) ? splitByCategory(container) : container.results ?? container;
63
+ return {
64
+ success: true,
65
+ data: {
66
+ organic: (data.web ?? []).filter((r) => r.url != null && r.url !== "").map((r) => ({
67
+ title: r.title ?? "",
68
+ link: r.url,
69
+ snippet: r.description ?? r.snippet ?? "",
70
+ position: r.position
71
+ })),
72
+ images: (data.images ?? []).filter((r) => r.imageUrl != null && r.imageUrl !== "").map((r) => ({
73
+ title: r.title,
74
+ imageUrl: r.imageUrl,
75
+ link: r.url,
76
+ position: r.position
77
+ })),
78
+ topStories: [],
79
+ videos: [],
80
+ news: (data.news ?? []).filter((r) => r.url != null && r.url !== "").map((r) => ({
81
+ title: r.title ?? "",
82
+ link: r.url,
83
+ snippet: r.description ?? r.snippet ?? "",
84
+ date: r.publishedDate,
85
+ source: getHostname(r.url),
86
+ position: r.position
87
+ })),
88
+ relatedSearches: []
89
+ }
90
+ };
91
+ } catch (error) {
92
+ return {
93
+ success: false,
94
+ error: `fastCRW search request failed: ${error instanceof Error ? error.message : String(error)}`
95
+ };
96
+ }
97
+ };
98
+ return { getSources };
99
+ };
100
+ //#endregion
101
+ export { createCrwAPI };
102
+
103
+ //# sourceMappingURL=crw-search.mjs.map