@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
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
3
|
import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
|
|
4
4
|
import { CallbackManager } from '@langchain/core/callbacks/manager';
|
|
5
|
+
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
5
6
|
import { context as otelContext, trace as otelTrace } from '@opentelemetry/api';
|
|
7
|
+
import {
|
|
8
|
+
Command,
|
|
9
|
+
END,
|
|
10
|
+
GraphInterrupt,
|
|
11
|
+
MemorySaver,
|
|
12
|
+
MessagesAnnotation,
|
|
13
|
+
NodeInterrupt,
|
|
14
|
+
ParentCommand,
|
|
15
|
+
START,
|
|
16
|
+
StateGraph,
|
|
17
|
+
isGraphInterrupt,
|
|
18
|
+
isInterrupted,
|
|
19
|
+
} from '@langchain/langgraph';
|
|
6
20
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
7
21
|
import type * as t from '@/types';
|
|
8
22
|
import { handleConverseStreamMetadata } from '@/llm/bedrock/utils/message_outputs';
|
|
9
23
|
import { traceIdFromSeed } from '@/langfuseRuntimeContext';
|
|
10
24
|
import { Constants, Providers } from '@/common';
|
|
25
|
+
import { ToolNode } from '@/tools/ToolNode';
|
|
26
|
+
import { askUserQuestion } from '@/hitl';
|
|
11
27
|
import { Run } from '@/run';
|
|
12
28
|
|
|
13
29
|
const mockProcessorStarts: Array<{
|
|
@@ -15,6 +31,8 @@ const mockProcessorStarts: Array<{
|
|
|
15
31
|
traceId: string;
|
|
16
32
|
}> = [];
|
|
17
33
|
const mockSpanAttributeSets: Array<Record<string, unknown>> = [];
|
|
34
|
+
let mockSpansStarted = 0;
|
|
35
|
+
let mockSpansEnded = 0;
|
|
18
36
|
let mockProviderInput:
|
|
19
37
|
| {
|
|
20
38
|
spanProcessors?: Array<{
|
|
@@ -29,6 +47,7 @@ let mockProviderInput:
|
|
|
29
47
|
| undefined;
|
|
30
48
|
|
|
31
49
|
const createMockSpan = (traceIdOverride?: string) => {
|
|
50
|
+
mockSpansStarted += 1;
|
|
32
51
|
const traceId =
|
|
33
52
|
traceIdOverride ??
|
|
34
53
|
mockProviderInput?.idGenerator?.generateTraceId() ??
|
|
@@ -36,6 +55,7 @@ const createMockSpan = (traceIdOverride?: string) => {
|
|
|
36
55
|
const spanId = mockProviderInput?.idGenerator?.generateSpanId() ?? 'span-id';
|
|
37
56
|
const span = {
|
|
38
57
|
end: jest.fn(() => {
|
|
58
|
+
mockSpansEnded += 1;
|
|
39
59
|
for (const processor of mockProviderInput?.spanProcessors ?? []) {
|
|
40
60
|
processor.onEnd?.(span);
|
|
41
61
|
}
|
|
@@ -104,6 +124,8 @@ describe('Langfuse callback composition', () => {
|
|
|
104
124
|
jest.clearAllMocks();
|
|
105
125
|
mockProcessorStarts.length = 0;
|
|
106
126
|
mockSpanAttributeSets.length = 0;
|
|
127
|
+
mockSpansStarted = 0;
|
|
128
|
+
mockSpansEnded = 0;
|
|
107
129
|
delete process.env.LANGFUSE_PUBLIC_KEY;
|
|
108
130
|
delete process.env.LANGFUSE_SECRET_KEY;
|
|
109
131
|
delete process.env.LANGFUSE_BASE_URL;
|
|
@@ -430,6 +452,311 @@ describe('Langfuse callback composition', () => {
|
|
|
430
452
|
);
|
|
431
453
|
});
|
|
432
454
|
|
|
455
|
+
it('ends nested GraphInterrupt control flow without reporting an error', async () => {
|
|
456
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
457
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
458
|
+
const langfuse = {
|
|
459
|
+
publicKey: 'pk-graph-interrupt',
|
|
460
|
+
secretKey: 'sk-graph-interrupt',
|
|
461
|
+
};
|
|
462
|
+
initializeLangfuseTracing(langfuse);
|
|
463
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
464
|
+
const runId = 'test-langfuse-graph-interrupt';
|
|
465
|
+
const sensitiveQuestion = 'sensitive question payload';
|
|
466
|
+
|
|
467
|
+
await handler?.handleChainStart(
|
|
468
|
+
{ lc: 1, type: 'not_implemented', id: ['InterruptedChain'] },
|
|
469
|
+
{ input: 'ask' },
|
|
470
|
+
runId,
|
|
471
|
+
'parent-run'
|
|
472
|
+
);
|
|
473
|
+
await handler?.handleChainError(
|
|
474
|
+
new GraphInterrupt([
|
|
475
|
+
{
|
|
476
|
+
value: {
|
|
477
|
+
type: 'ask_user_question',
|
|
478
|
+
question: sensitiveQuestion,
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
]),
|
|
482
|
+
runId,
|
|
483
|
+
'parent-run'
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
expect(mockSpanAttributeSets).not.toContainEqual(
|
|
487
|
+
expect.objectContaining({
|
|
488
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
expect(mockSpanAttributeSets).toContainEqual(
|
|
492
|
+
expect.objectContaining({
|
|
493
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify({
|
|
494
|
+
controlFlow: 'GraphInterrupt',
|
|
495
|
+
}),
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
expect(JSON.stringify(mockSpanAttributeSets)).not.toContain(
|
|
499
|
+
sensitiveQuestion
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('ends nested GraphInterrupt tool control flow without reporting an error', async () => {
|
|
504
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
505
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
506
|
+
const langfuse = {
|
|
507
|
+
publicKey: 'pk-tool-graph-interrupt',
|
|
508
|
+
secretKey: 'sk-tool-graph-interrupt',
|
|
509
|
+
};
|
|
510
|
+
initializeLangfuseTracing(langfuse);
|
|
511
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
512
|
+
const runId = 'test-langfuse-tool-graph-interrupt';
|
|
513
|
+
const sensitiveQuestion = 'sensitive tool question payload';
|
|
514
|
+
|
|
515
|
+
await handler?.handleToolStart(
|
|
516
|
+
{ lc: 1, type: 'not_implemented', id: ['AskUserQuestion'] },
|
|
517
|
+
'{}',
|
|
518
|
+
runId,
|
|
519
|
+
'parent-run'
|
|
520
|
+
);
|
|
521
|
+
await handler?.handleToolError(
|
|
522
|
+
new GraphInterrupt([
|
|
523
|
+
{
|
|
524
|
+
value: {
|
|
525
|
+
type: 'ask_user_question',
|
|
526
|
+
question: sensitiveQuestion,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
]),
|
|
530
|
+
runId,
|
|
531
|
+
'parent-run'
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
expect(mockSpanAttributeSets).not.toContainEqual(
|
|
535
|
+
expect.objectContaining({
|
|
536
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
expect(mockSpanAttributeSets).toContainEqual(
|
|
540
|
+
expect.objectContaining({
|
|
541
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify({
|
|
542
|
+
controlFlow: 'GraphInterrupt',
|
|
543
|
+
}),
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
expect(JSON.stringify(mockSpanAttributeSets)).not.toContain(
|
|
547
|
+
sensitiveQuestion
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('normalizes a real ask_user_question pause and preserves resume behavior', async () => {
|
|
552
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
553
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
554
|
+
const langfuse = {
|
|
555
|
+
publicKey: 'pk-ask-user-question',
|
|
556
|
+
secretKey: 'sk-ask-user-question',
|
|
557
|
+
};
|
|
558
|
+
initializeLangfuseTracing(langfuse);
|
|
559
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
560
|
+
expect(handler).toBeDefined();
|
|
561
|
+
|
|
562
|
+
const sensitiveQuestion = 'Should the workflow continue?';
|
|
563
|
+
const askTool = tool(
|
|
564
|
+
async () => {
|
|
565
|
+
const { answer } = askUserQuestion({ question: sensitiveQuestion });
|
|
566
|
+
return `answered: ${answer}`;
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: 'ask_user_question',
|
|
570
|
+
description: 'suspends to collect a human answer',
|
|
571
|
+
schema: z.object({}).passthrough(),
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
const toolNode = new ToolNode({
|
|
575
|
+
tools: [askTool],
|
|
576
|
+
eventDrivenMode: true,
|
|
577
|
+
directToolNames: new Set(['ask_user_question']),
|
|
578
|
+
interruptingToolNames: new Set(['ask_user_question']),
|
|
579
|
+
});
|
|
580
|
+
const graph = new StateGraph(MessagesAnnotation)
|
|
581
|
+
.addNode('agent', () => ({
|
|
582
|
+
messages: [
|
|
583
|
+
new AIMessage({
|
|
584
|
+
content: '',
|
|
585
|
+
tool_calls: [
|
|
586
|
+
{ id: 'ask-call', name: 'ask_user_question', args: {} },
|
|
587
|
+
],
|
|
588
|
+
}),
|
|
589
|
+
],
|
|
590
|
+
}))
|
|
591
|
+
.addNode('tools', toolNode)
|
|
592
|
+
.addEdge(START, 'agent')
|
|
593
|
+
.addEdge('agent', 'tools')
|
|
594
|
+
.addEdge('tools', END)
|
|
595
|
+
.compile({ checkpointer: new MemorySaver() });
|
|
596
|
+
const chainErrorSpy = jest.spyOn(handler!, 'handleChainError');
|
|
597
|
+
const toolErrorSpy = jest.spyOn(handler!, 'handleToolError');
|
|
598
|
+
const config = {
|
|
599
|
+
callbacks: [handler!],
|
|
600
|
+
configurable: { thread_id: 'ask-user-question-langfuse' },
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const first = await graph.invoke({ messages: [] }, config);
|
|
604
|
+
|
|
605
|
+
expect(isInterrupted<t.HumanInterruptPayload>(first)).toBe(true);
|
|
606
|
+
expect(
|
|
607
|
+
(
|
|
608
|
+
first as {
|
|
609
|
+
__interrupt__?: Array<{
|
|
610
|
+
value?: t.HumanInterruptPayload;
|
|
611
|
+
}>;
|
|
612
|
+
}
|
|
613
|
+
).__interrupt__?.[0]?.value
|
|
614
|
+
).toMatchObject({
|
|
615
|
+
type: 'ask_user_question',
|
|
616
|
+
question: { question: sensitiveQuestion },
|
|
617
|
+
});
|
|
618
|
+
expect(
|
|
619
|
+
chainErrorSpy.mock.calls.some(
|
|
620
|
+
([error, , parentRunId]) =>
|
|
621
|
+
isGraphInterrupt(error) && parentRunId != null
|
|
622
|
+
)
|
|
623
|
+
).toBe(true);
|
|
624
|
+
expect(
|
|
625
|
+
toolErrorSpy.mock.calls.some(
|
|
626
|
+
([error, , parentRunId]) =>
|
|
627
|
+
isGraphInterrupt(error) && parentRunId != null
|
|
628
|
+
)
|
|
629
|
+
).toBe(true);
|
|
630
|
+
const controlFlowOutputs = mockSpanAttributeSets.filter(
|
|
631
|
+
(attributes) =>
|
|
632
|
+
attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] ===
|
|
633
|
+
JSON.stringify({ controlFlow: 'GraphInterrupt' })
|
|
634
|
+
);
|
|
635
|
+
expect(controlFlowOutputs.length).toBeGreaterThanOrEqual(2);
|
|
636
|
+
expect(mockSpanAttributeSets).not.toContainEqual(
|
|
637
|
+
expect.objectContaining({
|
|
638
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
639
|
+
})
|
|
640
|
+
);
|
|
641
|
+
expect(mockSpanAttributeSets).not.toContainEqual(
|
|
642
|
+
expect.objectContaining({
|
|
643
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE]:
|
|
644
|
+
expect.stringContaining('GraphInterrupt'),
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
expect(mockSpansEnded).toBe(mockSpansStarted);
|
|
648
|
+
|
|
649
|
+
const second = await graph.invoke(
|
|
650
|
+
new Command({ resume: { answer: 'yes' } }),
|
|
651
|
+
config
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
expect(isInterrupted<t.HumanInterruptPayload>(second)).toBe(false);
|
|
655
|
+
const messages = (second as { messages: ToolMessage[] }).messages;
|
|
656
|
+
expect(
|
|
657
|
+
messages.find(
|
|
658
|
+
(message) =>
|
|
659
|
+
message instanceof ToolMessage && message.name === 'ask_user_question'
|
|
660
|
+
)?.content
|
|
661
|
+
).toBe('answered: yes');
|
|
662
|
+
expect(mockSpanAttributeSets).not.toContainEqual(
|
|
663
|
+
expect.objectContaining({
|
|
664
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
665
|
+
})
|
|
666
|
+
);
|
|
667
|
+
expect(mockSpansEnded).toBe(mockSpansStarted);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('defensively reports a synthetic root GraphInterrupt as an error', async () => {
|
|
671
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
672
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
673
|
+
const langfuse = {
|
|
674
|
+
publicKey: 'pk-root-graph-interrupt',
|
|
675
|
+
secretKey: 'sk-root-graph-interrupt',
|
|
676
|
+
};
|
|
677
|
+
initializeLangfuseTracing(langfuse);
|
|
678
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
679
|
+
const runId = 'test-langfuse-root-graph-interrupt';
|
|
680
|
+
|
|
681
|
+
await handler?.handleChainStart(
|
|
682
|
+
{ lc: 1, type: 'not_implemented', id: ['RootChain'] },
|
|
683
|
+
{ input: 'root interrupt' },
|
|
684
|
+
runId
|
|
685
|
+
);
|
|
686
|
+
await handler?.handleChainError(new GraphInterrupt(), runId);
|
|
687
|
+
|
|
688
|
+
expect(mockSpanAttributeSets).toContainEqual(
|
|
689
|
+
expect.objectContaining({
|
|
690
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
691
|
+
})
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('normalizes nested NodeInterrupt control flow', async () => {
|
|
696
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
697
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
698
|
+
const langfuse = {
|
|
699
|
+
publicKey: 'pk-node-interrupt',
|
|
700
|
+
secretKey: 'sk-node-interrupt',
|
|
701
|
+
};
|
|
702
|
+
initializeLangfuseTracing(langfuse);
|
|
703
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
704
|
+
const runId = 'test-langfuse-node-interrupt';
|
|
705
|
+
|
|
706
|
+
await handler?.handleChainStart(
|
|
707
|
+
{ lc: 1, type: 'not_implemented', id: ['InterruptedChain'] },
|
|
708
|
+
{ input: 'ask' },
|
|
709
|
+
runId,
|
|
710
|
+
'parent-run'
|
|
711
|
+
);
|
|
712
|
+
await handler?.handleChainError(
|
|
713
|
+
new NodeInterrupt('pause'),
|
|
714
|
+
runId,
|
|
715
|
+
'parent-run'
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
expect(mockSpanAttributeSets).not.toContainEqual(
|
|
719
|
+
expect.objectContaining({
|
|
720
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
expect(mockSpanAttributeSets).toContainEqual(
|
|
724
|
+
expect.objectContaining({
|
|
725
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify({
|
|
726
|
+
controlFlow: 'GraphInterrupt',
|
|
727
|
+
}),
|
|
728
|
+
})
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('continues reporting genuine nested tool failures as errors', async () => {
|
|
733
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
734
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
735
|
+
const langfuse = {
|
|
736
|
+
publicKey: 'pk-genuine-tool-error',
|
|
737
|
+
secretKey: 'sk-genuine-tool-error',
|
|
738
|
+
};
|
|
739
|
+
initializeLangfuseTracing(langfuse);
|
|
740
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
741
|
+
const runId = 'test-langfuse-genuine-tool-error';
|
|
742
|
+
|
|
743
|
+
await handler?.handleToolStart(
|
|
744
|
+
{ lc: 1, type: 'not_implemented', id: ['FailingTool'] },
|
|
745
|
+
'{}',
|
|
746
|
+
runId,
|
|
747
|
+
'parent-run'
|
|
748
|
+
);
|
|
749
|
+
await handler?.handleToolError(new Error('tool boom'), runId, 'parent-run');
|
|
750
|
+
|
|
751
|
+
expect(mockSpanAttributeSets).toContainEqual(
|
|
752
|
+
expect.objectContaining({
|
|
753
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
|
|
754
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE]:
|
|
755
|
+
'Error: tool boom',
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
});
|
|
759
|
+
|
|
433
760
|
it('reports a ParentCommand that escapes the root graph as an error', async () => {
|
|
434
761
|
const { createLangfuseHandler } = await import('@/langfuse');
|
|
435
762
|
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
@@ -512,6 +839,29 @@ describe('Langfuse callback composition', () => {
|
|
|
512
839
|
).resolves.toBeUndefined();
|
|
513
840
|
});
|
|
514
841
|
|
|
842
|
+
it('delegates null tool errors without throwing during classification', async () => {
|
|
843
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
844
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
845
|
+
const langfuse = {
|
|
846
|
+
publicKey: 'pk-null-tool-error',
|
|
847
|
+
secretKey: 'sk-null-tool-error',
|
|
848
|
+
};
|
|
849
|
+
initializeLangfuseTracing(langfuse);
|
|
850
|
+
const handler = createLangfuseHandler({ langfuse });
|
|
851
|
+
const runId = 'test-langfuse-null-tool-error';
|
|
852
|
+
|
|
853
|
+
await handler?.handleToolStart(
|
|
854
|
+
{ lc: 1, type: 'not_implemented', id: ['NullErrorTool'] },
|
|
855
|
+
'{}',
|
|
856
|
+
runId,
|
|
857
|
+
'parent-run'
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
await expect(
|
|
861
|
+
handler?.handleToolError(null, runId, 'parent-run')
|
|
862
|
+
).resolves.toBeUndefined();
|
|
863
|
+
});
|
|
864
|
+
|
|
515
865
|
it('traces a completed two-agent handoff without false error spans', async () => {
|
|
516
866
|
const langfuse = {
|
|
517
867
|
publicKey: 'pk-handoff',
|