@librechat/agents 3.3.3 → 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/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- 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 +793 -111
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.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/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +331 -5
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.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 +36 -5
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +290 -60
- 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 +4 -1
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- 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 +35 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +56 -63
- 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/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.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 +798 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.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/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +332 -8
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.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 +14 -11
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +290 -61
- 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 +4 -1
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- 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 +34 -11
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +56 -63
- 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/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +139 -2
- 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 +34 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -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/messages/prune.d.ts +16 -12
- 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 +1 -26
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- 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 +1261 -198
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
- 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/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
- package/src/llm/bedrock/utils/message_inputs.ts +100 -24
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +611 -8
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +454 -91
- package/src/messages/formatAgentMessages.steer.test.ts +267 -0
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -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/messages/prune.ts +996 -183
- 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/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.dispatch.test.ts +63 -0
- package/src/stream.ts +59 -23
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +150 -99
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- 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/tools/toolOutputReferences.ts +6 -0
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +1 -42
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
- 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
|
@@ -113,6 +113,54 @@ describe('ToolNode tool output references', () => {
|
|
|
113
113
|
|
|
114
114
|
expect(capturedArgs).toEqual(['raw {{tool0turn0}}']);
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
it('bounds structured ToolMessage output before its first model call', async () => {
|
|
118
|
+
const structuredTool = tool(
|
|
119
|
+
async () =>
|
|
120
|
+
new ToolMessage({
|
|
121
|
+
status: 'success',
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'json',
|
|
125
|
+
rows: Array.from({ length: 20 }, (_, index) => ({
|
|
126
|
+
id: index,
|
|
127
|
+
value: `${'x'.repeat(100)}-${index}`,
|
|
128
|
+
})),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
artifact: { source: 'clickhouse' },
|
|
132
|
+
metadata: { requestId: 'request-1' },
|
|
133
|
+
name: 'run_select_query',
|
|
134
|
+
tool_call_id: 'c1',
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
name: 'run_select_query',
|
|
138
|
+
description: 'returns structured rows',
|
|
139
|
+
schema: z.object({ command: z.string() }),
|
|
140
|
+
}
|
|
141
|
+
) as unknown as StructuredToolInterface;
|
|
142
|
+
const node = new ToolNode({
|
|
143
|
+
tools: [structuredTool],
|
|
144
|
+
maxToolResultChars: 200,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const [msg] = await invokeBatch(node, [
|
|
148
|
+
{
|
|
149
|
+
id: 'c1',
|
|
150
|
+
name: 'run_select_query',
|
|
151
|
+
command: 'SELECT * FROM events',
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
expect(typeof msg.content).toBe('string');
|
|
156
|
+
expect(msg.content).toContain('truncated');
|
|
157
|
+
expect((msg.content as string).length).toBeLessThanOrEqual(200);
|
|
158
|
+
expect(msg.tool_call_id).toBe('c1');
|
|
159
|
+
expect(msg.name).toBe('run_select_query');
|
|
160
|
+
expect(msg.status).toBe('success');
|
|
161
|
+
expect(msg.artifact).toEqual({ source: 'clickhouse' });
|
|
162
|
+
expect(msg.metadata).toEqual({ requestId: 'request-1' });
|
|
163
|
+
});
|
|
116
164
|
});
|
|
117
165
|
|
|
118
166
|
describe('enabled', () => {
|
|
@@ -364,6 +412,67 @@ describe('ToolNode tool output references', () => {
|
|
|
364
412
|
).toBe(raw);
|
|
365
413
|
});
|
|
366
414
|
|
|
415
|
+
it('preserves structured output exactly up to the registry limit', async () => {
|
|
416
|
+
const structured = {
|
|
417
|
+
rows: Array.from({ length: 10 }, (_, index) => ({
|
|
418
|
+
id: index,
|
|
419
|
+
value: `row-${index}-${'x'.repeat(100)}`,
|
|
420
|
+
})),
|
|
421
|
+
};
|
|
422
|
+
const exact = JSON.stringify(structured);
|
|
423
|
+
const capturedArgs: string[] = [];
|
|
424
|
+
let callCount = 0;
|
|
425
|
+
const structuredTool = createEchoTool({
|
|
426
|
+
capturedArgs: [],
|
|
427
|
+
outputs: ['unused'],
|
|
428
|
+
name: 'structured',
|
|
429
|
+
});
|
|
430
|
+
(
|
|
431
|
+
structuredTool as unknown as {
|
|
432
|
+
invoke: (input: {
|
|
433
|
+
args: { command: string };
|
|
434
|
+
}) => Promise<typeof structured | string>;
|
|
435
|
+
}
|
|
436
|
+
).invoke = async (input) => {
|
|
437
|
+
capturedArgs.push(input.args.command);
|
|
438
|
+
return callCount++ === 0 ? structured : 'done';
|
|
439
|
+
};
|
|
440
|
+
const node = new ToolNode({
|
|
441
|
+
tools: [structuredTool],
|
|
442
|
+
maxToolResultChars: 100,
|
|
443
|
+
toolOutputReferences: {
|
|
444
|
+
enabled: true,
|
|
445
|
+
maxOutputSize: exact.length,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const [first] = await invokeBatch(
|
|
450
|
+
node,
|
|
451
|
+
[{ id: 'c1', name: 'structured', command: 'first' }],
|
|
452
|
+
'structured-raw-preservation'
|
|
453
|
+
);
|
|
454
|
+
await invokeBatch(
|
|
455
|
+
node,
|
|
456
|
+
[
|
|
457
|
+
{
|
|
458
|
+
id: 'c2',
|
|
459
|
+
name: 'structured',
|
|
460
|
+
command: '{{tool0turn0}}',
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
'structured-raw-preservation'
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect((first.content as string).length).toBeLessThanOrEqual(100);
|
|
467
|
+
expect(first.content).toContain('truncated');
|
|
468
|
+
expect(capturedArgs[1]).toBe(exact);
|
|
469
|
+
expect(
|
|
470
|
+
node
|
|
471
|
+
._unsafeGetToolOutputRegistry()!
|
|
472
|
+
.get('structured-raw-preservation', 'tool0turn0')
|
|
473
|
+
).toBe(exact);
|
|
474
|
+
});
|
|
475
|
+
|
|
367
476
|
it('uses each batch\'s own turn when ToolNode is invoked concurrently within a run', async () => {
|
|
368
477
|
const gates: Record<string, () => void> = {};
|
|
369
478
|
const slowTool = tool(
|
|
@@ -823,7 +932,7 @@ describe('ToolNode tool output references', () => {
|
|
|
823
932
|
expect(JSON.parse(stepCompletedArgs[1]).command).toBe('echo STORED');
|
|
824
933
|
});
|
|
825
934
|
|
|
826
|
-
it('records unresolved refs
|
|
935
|
+
it('records unresolved refs on multipart ToolMessage content within the cap', async () => {
|
|
827
936
|
const complexTool = tool(
|
|
828
937
|
async () =>
|
|
829
938
|
new ToolMessage({
|
|
@@ -855,8 +964,8 @@ describe('ToolNode tool output references', () => {
|
|
|
855
964
|
|
|
856
965
|
expect(Array.isArray(msg.content)).toBe(true);
|
|
857
966
|
const blocks = msg.content as Array<{ type: string; text?: string }>;
|
|
858
|
-
//
|
|
859
|
-
//
|
|
967
|
+
// In-budget multi-part content stays intact; the lazy transform handles
|
|
968
|
+
// the unresolved-refs warning at request time.
|
|
860
969
|
expect(blocks).toHaveLength(2);
|
|
861
970
|
expect(blocks[0].type).toBe('text');
|
|
862
971
|
expect(blocks[0].text).toBe('data');
|
|
@@ -90,6 +90,38 @@ describe('annotateMessagesForLLM', () => {
|
|
|
90
90
|
expect(out[0]).not.toBe(tm);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it('does not annotate native computer screenshot strings', () => {
|
|
94
|
+
const registry = new ToolOutputReferenceRegistry();
|
|
95
|
+
registry.set('r1', 'tool0turn0', 'stored-raw');
|
|
96
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
97
|
+
const tm = makeToolMessage({
|
|
98
|
+
content: screenshot,
|
|
99
|
+
additional_kwargs: {
|
|
100
|
+
type: 'computer_call_output',
|
|
101
|
+
_refKey: 'tool0turn0',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const out = annotateMessagesForLLM([tm], registry, 'r1');
|
|
106
|
+
|
|
107
|
+
expect(out[0].content).toBe(screenshot);
|
|
108
|
+
expect((out[0] as ToolMessage).additional_kwargs.type).toBe(
|
|
109
|
+
'computer_call_output'
|
|
110
|
+
);
|
|
111
|
+
expect((out[0] as ToolMessage).additional_kwargs._refKey).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('leaves native computer screenshots reference-equal without ref metadata', () => {
|
|
115
|
+
const registry = new ToolOutputReferenceRegistry();
|
|
116
|
+
const tm = makeToolMessage({
|
|
117
|
+
content: 'data:image/png;base64,AAAA',
|
|
118
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
119
|
+
});
|
|
120
|
+
const messages = [tm];
|
|
121
|
+
|
|
122
|
+
expect(annotateMessagesForLLM(messages, registry, 'r1')).toBe(messages);
|
|
123
|
+
});
|
|
124
|
+
|
|
93
125
|
it('leaves content untouched but strips framework metadata when _refKey is stale', () => {
|
|
94
126
|
/**
|
|
95
127
|
* Stale `_refKey` (not in registry) doesn't trigger annotation,
|
|
@@ -20,7 +20,7 @@ import { ToolNode } from '../ToolNode';
|
|
|
20
20
|
*/
|
|
21
21
|
function createDirectTool(
|
|
22
22
|
name: string,
|
|
23
|
-
impl: (args: Record<string, unknown>) =>
|
|
23
|
+
impl: (args: Record<string, unknown>) => unknown | Promise<unknown>
|
|
24
24
|
): StructuredToolInterface {
|
|
25
25
|
return tool(async (args: Record<string, unknown>) => impl(args), {
|
|
26
26
|
name,
|
|
@@ -147,6 +147,217 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
147
147
|
expect(String(message.content)).toBe('ran:ls');
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
it('serializes bigint values in direct structured tool output', async () => {
|
|
151
|
+
const query = createDirectTool('query', () => 'unused');
|
|
152
|
+
(
|
|
153
|
+
query as unknown as {
|
|
154
|
+
invoke: () => Promise<Array<Record<string, unknown>>>;
|
|
155
|
+
}
|
|
156
|
+
).invoke = async () => [{ rowsRead: BigInt(42), status: 'complete' }];
|
|
157
|
+
const node = new ToolNode({
|
|
158
|
+
tools: [query],
|
|
159
|
+
eventDrivenMode: true,
|
|
160
|
+
directToolNames: new Set(['query']),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await node.invoke({
|
|
164
|
+
messages: [aiCall('call_bigint', 'query', {})],
|
|
165
|
+
});
|
|
166
|
+
const [message] = toolMessages(result);
|
|
167
|
+
|
|
168
|
+
expect(message.status).toBe('success');
|
|
169
|
+
expect(message.content).toBe('[{"rowsRead":"42","status":"complete"}]');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('bounds a large structured result without invoking toJSON', async () => {
|
|
173
|
+
let toJSONCalls = 0;
|
|
174
|
+
const structuredOutput = {
|
|
175
|
+
rows: [{ value: 'x'.repeat(20_000) }],
|
|
176
|
+
toJSON() {
|
|
177
|
+
toJSONCalls++;
|
|
178
|
+
return 'y'.repeat(100_000);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
const query = createDirectTool('large_query', () => 'unused');
|
|
182
|
+
(
|
|
183
|
+
query as unknown as {
|
|
184
|
+
invoke: () => Promise<typeof structuredOutput>;
|
|
185
|
+
}
|
|
186
|
+
).invoke = async () => structuredOutput;
|
|
187
|
+
const node = new ToolNode({
|
|
188
|
+
tools: [query],
|
|
189
|
+
eventDrivenMode: true,
|
|
190
|
+
directToolNames: new Set(['large_query']),
|
|
191
|
+
maxToolResultChars: 200,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await node.invoke({
|
|
195
|
+
messages: [aiCall('call_large', 'large_query', {})],
|
|
196
|
+
});
|
|
197
|
+
const [message] = toolMessages(result);
|
|
198
|
+
|
|
199
|
+
expect(toJSONCalls).toBe(0);
|
|
200
|
+
expect(typeof message.content).toBe('string');
|
|
201
|
+
expect((message.content as string).length).toBeLessThanOrEqual(200);
|
|
202
|
+
expect(message.content).toContain('truncated');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('keeps native computer screenshots intact despite the generic result cap', async () => {
|
|
206
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
207
|
+
const contents: ToolMessage['content'][] = [
|
|
208
|
+
screenshot,
|
|
209
|
+
[{ type: 'computer_screenshot', image_url: screenshot }],
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < contents.length; i++) {
|
|
213
|
+
const name = `computer_output_${i}`;
|
|
214
|
+
const computerOutput = new ToolMessage({
|
|
215
|
+
content: contents[i],
|
|
216
|
+
tool_call_id: `computer-call-${i}`,
|
|
217
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
218
|
+
});
|
|
219
|
+
const computer = createDirectTool(name, () => 'unused');
|
|
220
|
+
(
|
|
221
|
+
computer as unknown as {
|
|
222
|
+
invoke: () => Promise<ToolMessage>;
|
|
223
|
+
}
|
|
224
|
+
).invoke = async () => computerOutput;
|
|
225
|
+
const node = new ToolNode({
|
|
226
|
+
tools: [computer],
|
|
227
|
+
eventDrivenMode: true,
|
|
228
|
+
directToolNames: new Set([name]),
|
|
229
|
+
maxToolResultChars: 80,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await node.invoke({
|
|
233
|
+
messages: [aiCall(`computer-call-${i}`, name, {})],
|
|
234
|
+
});
|
|
235
|
+
const [message] = toolMessages(result);
|
|
236
|
+
|
|
237
|
+
expect(message).toBe(computerOutput);
|
|
238
|
+
expect(message.content).toBe(contents[i]);
|
|
239
|
+
expect(message.additional_kwargs.type).toBe('computer_call_output');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('preserves a valid PostToolUse computer screenshot replacement', async () => {
|
|
244
|
+
const original = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
245
|
+
const replacement = `data:image/png;base64,${'B'.repeat(2_000)}`;
|
|
246
|
+
const computerOutput = new ToolMessage({
|
|
247
|
+
content: original,
|
|
248
|
+
tool_call_id: 'computer-hook-valid',
|
|
249
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
250
|
+
});
|
|
251
|
+
const computer = createDirectTool('computer_hook_valid', () => 'unused');
|
|
252
|
+
(
|
|
253
|
+
computer as unknown as {
|
|
254
|
+
invoke: () => Promise<ToolMessage>;
|
|
255
|
+
}
|
|
256
|
+
).invoke = async () => computerOutput;
|
|
257
|
+
const registry = new HookRegistry();
|
|
258
|
+
registry.register('PostToolUse', {
|
|
259
|
+
hooks: [
|
|
260
|
+
async (): Promise<PostToolUseHookOutput> => ({
|
|
261
|
+
updatedOutput: replacement,
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
const node = new ToolNode({
|
|
266
|
+
tools: [computer],
|
|
267
|
+
eventDrivenMode: true,
|
|
268
|
+
hookRegistry: registry,
|
|
269
|
+
directToolNames: new Set(['computer_hook_valid']),
|
|
270
|
+
maxToolResultChars: 80,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = await node.invoke({
|
|
274
|
+
messages: [aiCall('computer-hook-valid', 'computer_hook_valid', {})],
|
|
275
|
+
});
|
|
276
|
+
const [message] = toolMessages(result);
|
|
277
|
+
|
|
278
|
+
expect(message.content).toBe(replacement);
|
|
279
|
+
expect(message.additional_kwargs.type).toBe('computer_call_output');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('rejects an invalid PostToolUse computer output replacement', async () => {
|
|
283
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
284
|
+
const computerOutput = new ToolMessage({
|
|
285
|
+
content: screenshot,
|
|
286
|
+
tool_call_id: 'computer-hook-invalid',
|
|
287
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
288
|
+
});
|
|
289
|
+
const computer = createDirectTool('computer_hook_invalid', () => 'unused');
|
|
290
|
+
(
|
|
291
|
+
computer as unknown as {
|
|
292
|
+
invoke: () => Promise<ToolMessage>;
|
|
293
|
+
}
|
|
294
|
+
).invoke = async () => computerOutput;
|
|
295
|
+
const registry = new HookRegistry();
|
|
296
|
+
registry.register('PostToolUse', {
|
|
297
|
+
hooks: [
|
|
298
|
+
async (): Promise<PostToolUseHookOutput> => ({
|
|
299
|
+
updatedOutput: 'redacted screenshot',
|
|
300
|
+
}),
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
const node = new ToolNode({
|
|
304
|
+
tools: [computer],
|
|
305
|
+
eventDrivenMode: true,
|
|
306
|
+
hookRegistry: registry,
|
|
307
|
+
directToolNames: new Set(['computer_hook_invalid']),
|
|
308
|
+
maxToolResultChars: 80,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await expect(
|
|
312
|
+
node.invoke({
|
|
313
|
+
messages: [
|
|
314
|
+
aiCall('computer-hook-invalid', 'computer_hook_invalid', {}),
|
|
315
|
+
],
|
|
316
|
+
})
|
|
317
|
+
).rejects.toThrow(
|
|
318
|
+
'PostToolUse updatedOutput for a computer call must be a valid screenshot'
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('caps returned and thrown direct-tool errors before storing them', async () => {
|
|
323
|
+
const returnedError = createDirectTool(
|
|
324
|
+
'returned_error',
|
|
325
|
+
() =>
|
|
326
|
+
new ToolMessage({
|
|
327
|
+
status: 'error',
|
|
328
|
+
content: `returned:${'x'.repeat(2_000)}`,
|
|
329
|
+
tool_call_id: 'returned',
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
const thrownError = createDirectTool('thrown_error', () => {
|
|
333
|
+
throw new Error(`thrown:${'y'.repeat(2_000)}`);
|
|
334
|
+
});
|
|
335
|
+
const node = new ToolNode({
|
|
336
|
+
tools: [returnedError, thrownError],
|
|
337
|
+
eventDrivenMode: true,
|
|
338
|
+
directToolNames: new Set(['returned_error', 'thrown_error']),
|
|
339
|
+
maxToolResultChars: 200,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const returned = toolMessages(
|
|
343
|
+
await node.invoke({
|
|
344
|
+
messages: [aiCall('returned', 'returned_error', {})],
|
|
345
|
+
})
|
|
346
|
+
)[0];
|
|
347
|
+
const thrown = toolMessages(
|
|
348
|
+
await node.invoke({
|
|
349
|
+
messages: [aiCall('thrown', 'thrown_error', {})],
|
|
350
|
+
})
|
|
351
|
+
)[0];
|
|
352
|
+
|
|
353
|
+
expect(returned.status).toBe('error');
|
|
354
|
+
expect(String(returned.content).length).toBeLessThanOrEqual(200);
|
|
355
|
+
expect(returned.content).toContain('[truncated:');
|
|
356
|
+
expect(thrown.status).toBe('error');
|
|
357
|
+
expect(String(thrown.content).length).toBeLessThanOrEqual(200);
|
|
358
|
+
expect(thrown.content).toContain('[truncated:');
|
|
359
|
+
});
|
|
360
|
+
|
|
150
361
|
it('executingAgentId defaults to agentId for direct callers, and an explicit value wins', async () => {
|
|
151
362
|
const echo = createDirectTool('echo', (args) => `ran:${args.command}`);
|
|
152
363
|
const captured: Array<string | undefined> = [];
|
|
@@ -247,6 +458,37 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
247
458
|
expect(message.status).toBe('success');
|
|
248
459
|
});
|
|
249
460
|
|
|
461
|
+
it('caps PostToolUse updatedOutput after the hook rewrite', async () => {
|
|
462
|
+
const echo = createDirectTool('echo', () => 'ORIGINAL');
|
|
463
|
+
const replacement = 'R'.repeat(500);
|
|
464
|
+
|
|
465
|
+
const registry = new HookRegistry();
|
|
466
|
+
registry.register('PostToolUse', {
|
|
467
|
+
hooks: [
|
|
468
|
+
async (): Promise<PostToolUseHookOutput> => ({
|
|
469
|
+
updatedOutput: replacement,
|
|
470
|
+
}),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const node = new ToolNode({
|
|
475
|
+
tools: [echo],
|
|
476
|
+
eventDrivenMode: true,
|
|
477
|
+
hookRegistry: registry,
|
|
478
|
+
directToolNames: new Set(['echo']),
|
|
479
|
+
maxToolResultChars: 50,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const result = await node.invoke({
|
|
483
|
+
messages: [aiCall('call_5_capped', 'echo', { command: 'x' })],
|
|
484
|
+
});
|
|
485
|
+
const [message] = toolMessages(result);
|
|
486
|
+
|
|
487
|
+
expect(typeof message.content).toBe('string');
|
|
488
|
+
expect((message.content as string).length).toBeLessThanOrEqual(50);
|
|
489
|
+
expect(message.content).not.toBe(replacement);
|
|
490
|
+
});
|
|
491
|
+
|
|
250
492
|
it('PostToolUseFailure observes errors thrown by the tool', async () => {
|
|
251
493
|
const failing = createDirectTool('boom', () => {
|
|
252
494
|
throw new Error('kaboom');
|
|
@@ -376,11 +618,16 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
376
618
|
|
|
377
619
|
// Patch the tool's `func` to record the turn the body sees via the
|
|
378
620
|
// standard LangChain config.toolCall.turn channel.
|
|
379
|
-
const originalFunc = (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
621
|
+
const originalFunc = (
|
|
622
|
+
echo as unknown as {
|
|
623
|
+
func: (input: unknown, config: unknown) => Promise<string>;
|
|
624
|
+
}
|
|
625
|
+
).func;
|
|
626
|
+
(
|
|
627
|
+
echo as unknown as {
|
|
628
|
+
func: (input: unknown, config: unknown) => Promise<string>;
|
|
629
|
+
}
|
|
630
|
+
).func = async (input, config): Promise<string> => {
|
|
384
631
|
const t = (config as { toolCall?: { turn?: number } } | undefined)
|
|
385
632
|
?.toolCall?.turn;
|
|
386
633
|
if (typeof t === 'number') bodyTurns.push(t);
|
|
@@ -424,7 +671,8 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
424
671
|
hooks: [
|
|
425
672
|
async (): Promise<PreToolUseHookOutput> => ({
|
|
426
673
|
decision: 'allow',
|
|
427
|
-
additionalContext:
|
|
674
|
+
additionalContext:
|
|
675
|
+
'POLICY-NOTE: writes here require approval next time',
|
|
428
676
|
}),
|
|
429
677
|
],
|
|
430
678
|
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createKeenableScraper } from './keenable-scraper';
|
|
3
|
+
|
|
4
|
+
jest.mock('axios');
|
|
5
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
6
|
+
|
|
7
|
+
const sampleResponse = {
|
|
8
|
+
data: {
|
|
9
|
+
url: 'https://example.com/',
|
|
10
|
+
title: 'Example Domain',
|
|
11
|
+
content:
|
|
12
|
+
'# Example Domain\n\nThis domain is for use in documentation examples.',
|
|
13
|
+
description: 'An example page.',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('Keenable scraper', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
delete process.env.KEENABLE_API_KEY;
|
|
21
|
+
delete process.env.KEENABLE_FETCH_URL;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns an error for empty URLs without calling the API', async () => {
|
|
25
|
+
const scraper = createKeenableScraper();
|
|
26
|
+
const [url, response] = await scraper.scrapeUrl(' ');
|
|
27
|
+
|
|
28
|
+
expect(url).toBe(' ');
|
|
29
|
+
expect(response.success).toBe(false);
|
|
30
|
+
expect(mockedAxios.get).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('hits the public endpoint and omits the API key header when keyless', async () => {
|
|
34
|
+
mockedAxios.get.mockResolvedValueOnce(sampleResponse);
|
|
35
|
+
|
|
36
|
+
const scraper = createKeenableScraper();
|
|
37
|
+
const [, response] = await scraper.scrapeUrl('https://example.com');
|
|
38
|
+
|
|
39
|
+
expect(mockedAxios.get).toHaveBeenCalledWith(
|
|
40
|
+
'https://api.keenable.ai/v1/fetch/public',
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
params: { url: 'https://example.com' },
|
|
43
|
+
headers: expect.objectContaining({ 'X-Keenable-Title': 'LibreChat' }),
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
const headers = mockedAxios.get.mock.calls[0][1]?.headers as Record<
|
|
47
|
+
string,
|
|
48
|
+
string
|
|
49
|
+
>;
|
|
50
|
+
expect(headers['X-API-Key']).toBeUndefined();
|
|
51
|
+
expect(response.success).toBe(true);
|
|
52
|
+
expect(response.data?.content).toContain('# Example Domain');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('hits the keyed endpoint and sends the API key header when a key is set', async () => {
|
|
56
|
+
mockedAxios.get.mockResolvedValueOnce(sampleResponse);
|
|
57
|
+
|
|
58
|
+
const scraper = createKeenableScraper({ apiKey: 'secret-key' });
|
|
59
|
+
await scraper.scrapeUrl('https://example.com');
|
|
60
|
+
|
|
61
|
+
expect(mockedAxios.get).toHaveBeenCalledWith(
|
|
62
|
+
'https://api.keenable.ai/v1/fetch',
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
headers: expect.objectContaining({ 'X-API-Key': 'secret-key' }),
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('honors a custom attribution title and fetch URL override', async () => {
|
|
70
|
+
mockedAxios.get.mockResolvedValueOnce(sampleResponse);
|
|
71
|
+
|
|
72
|
+
const scraper = createKeenableScraper({
|
|
73
|
+
apiUrl: 'https://keenable.internal/v1/fetch/public',
|
|
74
|
+
attributionTitle: 'MyApp',
|
|
75
|
+
});
|
|
76
|
+
await scraper.scrapeUrl('https://example.com');
|
|
77
|
+
|
|
78
|
+
expect(mockedAxios.get).toHaveBeenCalledWith(
|
|
79
|
+
'https://keenable.internal/v1/fetch/public',
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
headers: expect.objectContaining({ 'X-Keenable-Title': 'MyApp' }),
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('reports failure when the API returns no content', async () => {
|
|
87
|
+
mockedAxios.get.mockResolvedValueOnce({
|
|
88
|
+
data: { url: 'https://x.test', content: '' },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const scraper = createKeenableScraper();
|
|
92
|
+
const [, response] = await scraper.scrapeUrl('https://x.test');
|
|
93
|
+
|
|
94
|
+
expect(response.success).toBe(false);
|
|
95
|
+
expect(response.error).toMatch(/no content/i);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('reports failure with a message when the request throws', async () => {
|
|
99
|
+
mockedAxios.get.mockRejectedValueOnce(new Error('network down'));
|
|
100
|
+
|
|
101
|
+
const scraper = createKeenableScraper();
|
|
102
|
+
const [, response] = await scraper.scrapeUrl('https://x.test');
|
|
103
|
+
|
|
104
|
+
expect(response.success).toBe(false);
|
|
105
|
+
expect(response.error).toContain('network down');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('scrapes multiple URLs concurrently', async () => {
|
|
109
|
+
mockedAxios.get
|
|
110
|
+
.mockResolvedValueOnce({ data: { content: 'a', url: 'https://a.test' } })
|
|
111
|
+
.mockResolvedValueOnce({ data: { content: 'b', url: 'https://b.test' } });
|
|
112
|
+
|
|
113
|
+
const scraper = createKeenableScraper();
|
|
114
|
+
const results = await scraper.scrapeUrls([
|
|
115
|
+
'https://a.test',
|
|
116
|
+
'https://b.test',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
expect(results).toHaveLength(2);
|
|
120
|
+
expect(results[0][1].data?.content).toBe('a');
|
|
121
|
+
expect(results[1][1].data?.content).toBe('b');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('extractContent returns the markdown with no structured references', () => {
|
|
125
|
+
const scraper = createKeenableScraper();
|
|
126
|
+
const [content, references] = scraper.extractContent({
|
|
127
|
+
success: true,
|
|
128
|
+
data: { content: '# Title\n\nBody' },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(content).toBe('# Title\n\nBody');
|
|
132
|
+
expect(references).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('extractMetadata surfaces title, description and url', () => {
|
|
136
|
+
const scraper = createKeenableScraper();
|
|
137
|
+
const metadata = scraper.extractMetadata({
|
|
138
|
+
success: true,
|
|
139
|
+
data: {
|
|
140
|
+
content: 'x',
|
|
141
|
+
title: 'Example Domain',
|
|
142
|
+
description: 'An example page.',
|
|
143
|
+
url: 'https://example.com/',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(metadata).toEqual({
|
|
148
|
+
title: 'Example Domain',
|
|
149
|
+
description: 'An example page.',
|
|
150
|
+
url: 'https://example.com/',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|