@librechat/agents 3.3.8 → 3.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +47 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +18 -48
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +174 -29
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs +12 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
- package/dist/cjs/langfuseTraceShaping.cjs +121 -4
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +5 -5
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +10 -10
- package/dist/cjs/messages/prune.cjs +13 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +24 -12
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +47 -22
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/messageSerialization.cjs +6 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -1
- package/dist/cjs/stream.cjs +21 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +5 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +253 -24
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +48 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +3 -2
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +18 -48
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +176 -28
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs +10 -1
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeContext.mjs +21 -3
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +39 -10
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +91 -0
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
- package/dist/esm/langfuseTraceShaping.mjs +121 -4
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +2 -2
- package/dist/esm/llm/invoke.mjs +5 -5
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +8 -8
- package/dist/esm/messages/prune.mjs +13 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +24 -12
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +47 -22
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/messageSerialization.mjs +6 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -1
- package/dist/esm/stream.mjs +21 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +5 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +254 -25
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +19 -0
- package/dist/types/hitl/askUserQuestion.d.ts +11 -1
- package/dist/types/langfuse.d.ts +16 -8
- package/dist/types/langfuseConfig.d.ts +6 -0
- package/dist/types/langfuseRuntimeContext.d.ts +27 -1
- package/dist/types/langfuseRuntimeScope.d.ts +17 -2
- package/dist/types/langfuseSpanRegistry.d.ts +17 -0
- package/dist/types/langfuseTraceShaping.d.ts +2 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
- package/dist/types/session/types.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/types/hitl.d.ts +8 -0
- package/dist/types/types/tools.d.ts +30 -0
- package/package.json +7 -4
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
- package/src/graphs/Graph.ts +69 -20
- package/src/graphs/MultiAgentGraph.ts +56 -2
- package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
- package/src/hitl/askUserQuestion.ts +14 -1
- package/src/instrumentation.ts +35 -77
- package/src/langfuse.ts +320 -43
- package/src/langfuseConfig.ts +24 -0
- package/src/langfuseRuntimeContext.ts +43 -1
- package/src/langfuseRuntimeScope.ts +94 -21
- package/src/langfuseSpanRegistry.ts +131 -0
- package/src/langfuseTraceShaping.ts +194 -7
- package/src/llm/anthropic/utils/message_inputs.ts +70 -19
- package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
- package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
- package/src/llm/bedrock/utils/message_inputs.ts +32 -7
- package/src/messages/prune.ts +12 -1
- package/src/prompts/activityLabel.ts +23 -6
- package/src/run.ts +76 -45
- package/src/scripts/activity-labels/captured.json +56 -0
- package/src/scripts/activity-labels/checks.cjs +205 -0
- package/src/scripts/activity-labels/corpus.cjs +473 -0
- package/src/scripts/activity-labels/report.cjs +203 -0
- package/src/scripts/activity-labels/rescore.cjs +102 -0
- package/src/scripts/activity-labels/run.ts +705 -0
- package/src/scripts/activity-labels/variants.ts +71 -0
- package/src/session/messageSerialization.ts +12 -1
- package/src/session/types.ts +1 -0
- package/src/specs/activity-label-prompt.test.ts +26 -10
- package/src/specs/agent-handoffs.test.ts +306 -0
- package/src/specs/langfuse-callbacks.test.ts +456 -0
- package/src/specs/langfuse-routing.integration.test.ts +138 -1
- package/src/specs/langfuse-span-registry.test.ts +70 -0
- package/src/specs/langfuse-trace-shaping.test.ts +294 -0
- package/src/specs/prune.test.ts +38 -1
- package/src/stream.ts +70 -6
- package/src/summarization/node.ts +5 -0
- package/src/tools/ToolNode.ts +400 -9
- package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
- package/src/tools/__tests__/hitl.test.ts +58 -0
- package/src/types/hitl.ts +8 -0
- package/src/types/tools.ts +35 -1
package/src/graphs/Graph.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
+
import { v4 } from 'uuid';
|
|
2
3
|
import { nanoid } from 'nanoid';
|
|
3
4
|
import { tool } from '@langchain/core/tools';
|
|
4
5
|
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
|
@@ -53,6 +54,14 @@ import {
|
|
|
53
54
|
appendPredecessorHandoffCue,
|
|
54
55
|
removePredecessorHandoffCue,
|
|
55
56
|
} from '@/messages';
|
|
57
|
+
import {
|
|
58
|
+
attemptInvoke,
|
|
59
|
+
tryFallbackProviders,
|
|
60
|
+
getFallbackErrorContext,
|
|
61
|
+
getFallbackOverflowCandidates,
|
|
62
|
+
projectMessagesForProvider,
|
|
63
|
+
resolveServingModelId,
|
|
64
|
+
} from '@/llm/invoke';
|
|
56
65
|
import {
|
|
57
66
|
resetIfNotEmpty,
|
|
58
67
|
isAnthropicLike,
|
|
@@ -64,14 +73,14 @@ import {
|
|
|
64
73
|
sleep,
|
|
65
74
|
} from '@/utils';
|
|
66
75
|
import {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
Constants,
|
|
77
|
+
GraphNodeKeys,
|
|
78
|
+
ContentTypes,
|
|
79
|
+
GraphEvents,
|
|
80
|
+
Providers,
|
|
81
|
+
StepTypes,
|
|
82
|
+
PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
|
|
83
|
+
} from '@/common';
|
|
75
84
|
import {
|
|
76
85
|
createLangfuseHandler,
|
|
77
86
|
createLangfuseTraceMetadata,
|
|
@@ -83,21 +92,16 @@ import {
|
|
|
83
92
|
planContextOverflowRecovery,
|
|
84
93
|
translateRecoveryBudget,
|
|
85
94
|
} from '@/llm/contextOverflowRecovery';
|
|
95
|
+
import {
|
|
96
|
+
hasToolOutputTracingConfig,
|
|
97
|
+
resolveLangfuseConfig,
|
|
98
|
+
resolveToolOutputTracingConfig,
|
|
99
|
+
} from '@/langfuseConfig';
|
|
86
100
|
import {
|
|
87
101
|
compactToolContent,
|
|
88
102
|
getToolContentCharLength,
|
|
89
103
|
serializeToolContentBounded,
|
|
90
104
|
} from '@/utils/toolContent';
|
|
91
|
-
import { resolveMaxSeals } from '@/llm/preempt';
|
|
92
|
-
import {
|
|
93
|
-
Constants,
|
|
94
|
-
GraphNodeKeys,
|
|
95
|
-
ContentTypes,
|
|
96
|
-
GraphEvents,
|
|
97
|
-
Providers,
|
|
98
|
-
StepTypes,
|
|
99
|
-
PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
|
|
100
|
-
} from '@/common';
|
|
101
105
|
import {
|
|
102
106
|
annotateMessagesForLLM,
|
|
103
107
|
ToolOutputReferenceRegistry,
|
|
@@ -125,12 +129,12 @@ import { shouldTriggerSummarization } from '@/summarization';
|
|
|
125
129
|
import { resolveLocalToolsForBinding } from '@/tools/local';
|
|
126
130
|
import { createSummarizeNode } from '@/summarization/node';
|
|
127
131
|
import { messagesStateReducer } from '@/messages/reducer';
|
|
128
|
-
import { resolveLangfuseConfig } from '@/langfuseConfig';
|
|
129
132
|
import { createSchemaOnlyTools } from '@/tools/schema';
|
|
130
133
|
import { AgentContext } from '@/agents/AgentContext';
|
|
131
134
|
import { createFakeStreamingLLM } from '@/llm/fake';
|
|
132
135
|
import { handleToolCalls } from '@/tools/handlers';
|
|
133
136
|
import { isThinkingEnabled } from '@/llm/request';
|
|
137
|
+
import { resolveMaxSeals } from '@/llm/preempt';
|
|
134
138
|
import { initializeModel } from '@/llm/init';
|
|
135
139
|
import { HandlerRegistry } from '@/events';
|
|
136
140
|
import { ChatOpenAI } from '@/llm/openai';
|
|
@@ -762,6 +766,15 @@ export abstract class Graph<
|
|
|
762
766
|
new Map();
|
|
763
767
|
eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
|
|
764
768
|
new Map();
|
|
769
|
+
/**
|
|
770
|
+
* Per-run eager prestart circuit breaker, shared by reference with every
|
|
771
|
+
* ToolNode this graph compiles. When a prestarted execution's args turn
|
|
772
|
+
* out to differ from the final request, ToolNode records the tool name
|
|
773
|
+
* here and the stream handler stops prestarting that tool for the rest of
|
|
774
|
+
* the run — the retry then executes normally instead of re-diverging in a
|
|
775
|
+
* loop (LibreChat#14371).
|
|
776
|
+
*/
|
|
777
|
+
eagerEventToolSuppressions: Set<string> = new Set();
|
|
765
778
|
/**
|
|
766
779
|
* Run-scoped execution backend for built-in code tools. Defaults to the
|
|
767
780
|
* remote Code API sandbox when unset.
|
|
@@ -811,6 +824,7 @@ export abstract class Graph<
|
|
|
811
824
|
this.eagerEventToolExecutions.clear();
|
|
812
825
|
this.clearEagerEventToolUsageCounts();
|
|
813
826
|
this.eagerEventToolCallChunks.clear();
|
|
827
|
+
this.eagerEventToolSuppressions.clear();
|
|
814
828
|
this.toolExecution = undefined;
|
|
815
829
|
this.handlerDispatchedEventCounts.clear();
|
|
816
830
|
/**
|
|
@@ -997,6 +1011,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
997
1011
|
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
998
1012
|
private originalToolContentCheckpointScope?: string;
|
|
999
1013
|
runId: string | undefined;
|
|
1014
|
+
/**
|
|
1015
|
+
* Identity used to stamp Langfuse runtime scopes and handlers (see
|
|
1016
|
+
* `LangfuseRuntimeContext.runId`). Carries an opaque per-instance
|
|
1017
|
+
* component: public run ids are unrestricted and may repeat across
|
|
1018
|
+
* concurrently executing runs (retries, duplicate submissions,
|
|
1019
|
+
* tenant-local message ids), and equal stamps would let those runs adopt
|
|
1020
|
+
* each other's scopes. One graph instance = one execution's stamp, shared
|
|
1021
|
+
* by the stream handler and every graph-level scope of that execution.
|
|
1022
|
+
*/
|
|
1023
|
+
readonly langfuseScopeRunId: string;
|
|
1000
1024
|
/**
|
|
1001
1025
|
* Boundary between historical messages (loaded from conversation state)
|
|
1002
1026
|
* and messages produced during the current run. Set once in the state
|
|
@@ -1081,6 +1105,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1081
1105
|
}: t.StandardGraphInput) {
|
|
1082
1106
|
super();
|
|
1083
1107
|
this.runId = runId;
|
|
1108
|
+
this.langfuseScopeRunId = `${runId ?? 'graph'}:${nanoid()}`;
|
|
1084
1109
|
this.signal = signal;
|
|
1085
1110
|
this.langfuse = langfuse;
|
|
1086
1111
|
this.subagentUsageSink = subagentUsageSink;
|
|
@@ -1129,6 +1154,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1129
1154
|
this.eagerEventToolExecutions.clear();
|
|
1130
1155
|
this.clearEagerEventToolUsageCounts();
|
|
1131
1156
|
this.eagerEventToolCallChunks.clear();
|
|
1157
|
+
this.eagerEventToolSuppressions.clear();
|
|
1132
1158
|
this.handlerDispatchedStepIds = resetIfNotEmpty(
|
|
1133
1159
|
this.handlerDispatchedStepIds,
|
|
1134
1160
|
new Set()
|
|
@@ -1657,6 +1683,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1657
1683
|
eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
|
|
1658
1684
|
agentContext?.agentId
|
|
1659
1685
|
),
|
|
1686
|
+
eagerEventToolSuppressions: this.eagerEventToolSuppressions,
|
|
1660
1687
|
toolExecution: this.toolExecution,
|
|
1661
1688
|
directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
|
|
1662
1689
|
interruptingToolNames:
|
|
@@ -3057,6 +3084,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3057
3084
|
metadata: {
|
|
3058
3085
|
...(config.metadata ?? {}),
|
|
3059
3086
|
...traceMetadata,
|
|
3087
|
+
/** Canonical agent identity, stamped OUTSIDE trace-metadata
|
|
3088
|
+
* filtering: `createLangfuseTraceMetadata` drops values over its
|
|
3089
|
+
* length cap, but scope trust (`isForeignScope`) needs the id
|
|
3090
|
+
* verbatim regardless of length. */
|
|
3091
|
+
agentId,
|
|
3060
3092
|
},
|
|
3061
3093
|
};
|
|
3062
3094
|
initializeLangfuseTracing(langfuse);
|
|
@@ -3069,6 +3101,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3069
3101
|
tags: ['librechat', 'agent'],
|
|
3070
3102
|
traceIdSeed:
|
|
3071
3103
|
langfuse?.deterministicTraceId === true ? this.runId : undefined,
|
|
3104
|
+
runId: this.langfuseScopeRunId,
|
|
3105
|
+
toolOutputTracing: hasToolOutputTracingConfig(
|
|
3106
|
+
this.langfuse,
|
|
3107
|
+
agentContext.langfuse
|
|
3108
|
+
)
|
|
3109
|
+
? resolveToolOutputTracingConfig(
|
|
3110
|
+
this.langfuse,
|
|
3111
|
+
agentContext.langfuse
|
|
3112
|
+
)
|
|
3113
|
+
: undefined,
|
|
3072
3114
|
});
|
|
3073
3115
|
if (langfuseHandler != null) {
|
|
3074
3116
|
invokeConfig = {
|
|
@@ -3089,6 +3131,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3089
3131
|
resolveLangfuseRuntimeScope({
|
|
3090
3132
|
runLangfuse: this.langfuse,
|
|
3091
3133
|
langfuseOverlay: agentContext.langfuse,
|
|
3134
|
+
runId: this.langfuseScopeRunId,
|
|
3135
|
+
agentId,
|
|
3092
3136
|
}),
|
|
3093
3137
|
() =>
|
|
3094
3138
|
attemptInvoke(
|
|
@@ -3251,6 +3295,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3251
3295
|
resolveLangfuseRuntimeScope({
|
|
3252
3296
|
runLangfuse: this.langfuse,
|
|
3253
3297
|
langfuseOverlay: agentContext.langfuse,
|
|
3298
|
+
runId: this.langfuseScopeRunId,
|
|
3299
|
+
agentId,
|
|
3254
3300
|
}),
|
|
3255
3301
|
() =>
|
|
3256
3302
|
tryFallbackProviders({
|
|
@@ -3673,7 +3719,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3673
3719
|
* the same run — `haltRun` is first-write-wins — is left alone.
|
|
3674
3720
|
*/
|
|
3675
3721
|
const halt = this.hookRegistry.getHaltSignal(runId);
|
|
3676
|
-
if (
|
|
3722
|
+
if (
|
|
3723
|
+
result.preventContinuation === true &&
|
|
3724
|
+
halt?.source === 'PreemptBoundary'
|
|
3725
|
+
) {
|
|
3677
3726
|
this.preemptHaltReason = halt.reason;
|
|
3678
3727
|
this.hookRegistry.clearHaltSignal(runId);
|
|
3679
3728
|
}
|
|
@@ -80,6 +80,46 @@ function extractLegacyHandoffInstructions(
|
|
|
80
80
|
return content.match(HANDOFF_INSTRUCTIONS_PATTERN)?.[1]?.trim() ?? null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Whether a tool name marks a handoff transfer (static or conditional). */
|
|
84
|
+
function isTransferToolName(name: unknown): boolean {
|
|
85
|
+
return (
|
|
86
|
+
typeof name === 'string' &&
|
|
87
|
+
(name.startsWith(Constants.LC_TRANSFER_TO_) ||
|
|
88
|
+
name === 'conditional_transfer')
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Drop transfer `tool_use` content blocks from an AI message's array content.
|
|
94
|
+
* Companion to the reception's tool-call filtering: array-content providers
|
|
95
|
+
* (Anthropic) serialize retained blocks verbatim, so a transfer block whose
|
|
96
|
+
* call/result the reception stripped — or a parallel sibling's transfer block,
|
|
97
|
+
* whose result never reaches this recipient's state — would replay as an
|
|
98
|
+
* unmatched `tool_use`. Matched by the gathered ids AND by transfer name
|
|
99
|
+
* (sibling blocks have no collectable id here). String content passes through.
|
|
100
|
+
*/
|
|
101
|
+
function filterTransferToolUseBlocks(
|
|
102
|
+
content: AIMessage['content'],
|
|
103
|
+
transferToolCallIds: ReadonlySet<string>
|
|
104
|
+
): AIMessage['content'] {
|
|
105
|
+
if (!Array.isArray(content)) {
|
|
106
|
+
return content;
|
|
107
|
+
}
|
|
108
|
+
return content.filter((block) => {
|
|
109
|
+
if (
|
|
110
|
+
typeof block !== 'object' ||
|
|
111
|
+
(block as { type?: string } | null)?.type !== 'tool_use'
|
|
112
|
+
) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const toolUse = block as { id?: string; name?: string };
|
|
116
|
+
if (toolUse.id != null && transferToolCallIds.has(toolUse.id)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return !isTransferToolName(toolUse.name);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
83
123
|
function isValidHandoffGroupId(value: unknown): value is number {
|
|
84
124
|
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
85
125
|
}
|
|
@@ -842,9 +882,23 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
842
882
|
remainingToolCalls.length > 0 ||
|
|
843
883
|
(typeof aiMsg.content === 'string' && aiMsg.content.trim())
|
|
844
884
|
) {
|
|
845
|
-
/**
|
|
885
|
+
/**
|
|
886
|
+
* Keep the message but without transfer tool calls — AND
|
|
887
|
+
* without their `tool_use` content blocks. Array-content
|
|
888
|
+
* providers (Anthropic) serialize the retained blocks
|
|
889
|
+
* verbatim, so a transfer block whose call/result this
|
|
890
|
+
* filter just stripped would reach the receiving agent as
|
|
891
|
+
* an unmatched `tool_use` and the provider rejects the
|
|
892
|
+
* request. Filtered by transfer NAME as well as the
|
|
893
|
+
* gathered ids: a parallel sibling's transfer block has no
|
|
894
|
+
* result in THIS recipient's state, so its id is never
|
|
895
|
+
* collected, but its name still marks it.
|
|
896
|
+
*/
|
|
846
897
|
const filteredAiMsg = new AIMessage({
|
|
847
|
-
content:
|
|
898
|
+
content: filterTransferToolUseBlocks(
|
|
899
|
+
aiMsg.content,
|
|
900
|
+
transferToolCallIds
|
|
901
|
+
),
|
|
848
902
|
tool_calls: remainingToolCalls,
|
|
849
903
|
id: aiMsg.id,
|
|
850
904
|
});
|
|
@@ -135,6 +135,7 @@ describe('LangGraph composition smoke tests', () => {
|
|
|
135
135
|
const usageCount = graph.eagerEventToolUsageCount;
|
|
136
136
|
const scopedUsageCount = graph.getEagerEventToolUsageCount('agent');
|
|
137
137
|
const chunks = graph.eagerEventToolCallChunks;
|
|
138
|
+
const suppressions = graph.eagerEventToolSuppressions;
|
|
138
139
|
|
|
139
140
|
graph.eagerEventToolExecutions.set(
|
|
140
141
|
'call_weather',
|
|
@@ -143,6 +144,7 @@ describe('LangGraph composition smoke tests', () => {
|
|
|
143
144
|
graph.eagerEventToolUsageCount.set('weather', 1);
|
|
144
145
|
scopedUsageCount.set('weather', 1);
|
|
145
146
|
graph.eagerEventToolCallChunks.set('0', { argsText: '{"city":"NYC"}' });
|
|
147
|
+
graph.eagerEventToolSuppressions.add('weather');
|
|
146
148
|
|
|
147
149
|
graph.resetValues();
|
|
148
150
|
|
|
@@ -150,10 +152,12 @@ describe('LangGraph composition smoke tests', () => {
|
|
|
150
152
|
expect(graph.eagerEventToolUsageCount).toBe(usageCount);
|
|
151
153
|
expect(graph.getEagerEventToolUsageCount('agent')).toBe(scopedUsageCount);
|
|
152
154
|
expect(graph.eagerEventToolCallChunks).toBe(chunks);
|
|
155
|
+
expect(graph.eagerEventToolSuppressions).toBe(suppressions);
|
|
153
156
|
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
154
157
|
expect(graph.eagerEventToolUsageCount.size).toBe(0);
|
|
155
158
|
expect(scopedUsageCount.size).toBe(0);
|
|
156
159
|
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
160
|
+
expect(graph.eagerEventToolSuppressions.size).toBe(0);
|
|
157
161
|
});
|
|
158
162
|
|
|
159
163
|
it('compiles and invokes the standard single-agent graph', async () => {
|
|
@@ -60,11 +60,24 @@ import type {
|
|
|
60
60
|
* ```
|
|
61
61
|
*/
|
|
62
62
|
export function askUserQuestion(
|
|
63
|
-
question: AskUserQuestionRequest
|
|
63
|
+
question: AskUserQuestionRequest,
|
|
64
|
+
options?: {
|
|
65
|
+
/**
|
|
66
|
+
* The calling tool's `tool_call_id`, surfaced on the interrupt payload
|
|
67
|
+
* as `tool_call_id` so hosts can attribute the question (and answer) to
|
|
68
|
+
* the exact tool-call content part. Tool bodies created with
|
|
69
|
+
* `tool(fn, …)` receive it as `config.toolCall.id` — LangChain stamps
|
|
70
|
+
* the full ToolCall onto the config when a tool is invoked with one.
|
|
71
|
+
* Optional: positional hosts and custom nodes can omit it.
|
|
72
|
+
*/
|
|
73
|
+
toolCallId?: string;
|
|
74
|
+
}
|
|
64
75
|
): AskUserQuestionResolution {
|
|
65
76
|
const payload: AskUserQuestionInterruptPayload = {
|
|
66
77
|
type: 'ask_user_question',
|
|
67
78
|
question,
|
|
79
|
+
...(options?.toolCallId != null &&
|
|
80
|
+
options.toolCallId !== '' && { tool_call_id: options.toolCallId }),
|
|
68
81
|
};
|
|
69
82
|
return interrupt<AskUserQuestionInterruptPayload, AskUserQuestionResolution>(
|
|
70
83
|
payload
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { setLangfuseTracerProvider } from '@langfuse/tracing';
|
|
3
3
|
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
4
4
|
import { context, ROOT_CONTEXT, createContextKey } from '@opentelemetry/api';
|
|
@@ -13,16 +13,16 @@ import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
|
|
|
13
13
|
import type { Context } from '@opentelemetry/api';
|
|
14
14
|
import type * as t from '@/types';
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from '@/langfuse';
|
|
16
|
+
getLangfuseDestinationKey,
|
|
17
|
+
getLangfuseSpanProcessorParams,
|
|
18
|
+
registerLangfuseManagedSpan,
|
|
19
|
+
} from '@/langfuseSpanRegistry';
|
|
21
20
|
import {
|
|
22
21
|
resolveLangfuseConfigForSpan,
|
|
23
22
|
resolveTraceIdSeedForSpan,
|
|
24
23
|
} from '@/langfuseRuntimeScope';
|
|
25
24
|
import { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';
|
|
25
|
+
import { createLibreChatTraceAttributes } from '@/langfuse';
|
|
26
26
|
import { traceIdFromSeed } from '@/langfuseRuntimeContext';
|
|
27
27
|
import { isPresent } from '@/utils/misc';
|
|
28
28
|
|
|
@@ -75,75 +75,15 @@ export function ensureOpenTelemetryContextManager(): void {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
process.env.LANGFUSE_TRACING_ENVIRONMENT,
|
|
84
|
-
process.env.NODE_ENV,
|
|
85
|
-
];
|
|
86
|
-
for (const candidate of candidates) {
|
|
87
|
-
if (candidate != null && candidate.trim() !== '') {
|
|
88
|
-
return candidate.trim();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getLangfuseSpanProcessorParams(
|
|
95
|
-
langfuse?: t.LangfuseConfig
|
|
96
|
-
): LangfuseSpanProcessorParams | undefined {
|
|
97
|
-
if (langfuse?.enabled === false) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
const environment = resolveLangfuseEnvironment(langfuse);
|
|
101
|
-
if (hasLangfuseConfigCredentials(langfuse)) {
|
|
102
|
-
return {
|
|
103
|
-
publicKey: langfuse.publicKey,
|
|
104
|
-
secretKey: langfuse.secretKey,
|
|
105
|
-
...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
|
|
106
|
-
...(isPresent(environment) ? { environment } : {}),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
if (hasLangfuseEnvConfig()) {
|
|
110
|
-
const baseUrl =
|
|
111
|
-
langfuse?.baseUrl ??
|
|
112
|
-
process.env.LANGFUSE_BASE_URL ??
|
|
113
|
-
process.env.LANGFUSE_BASEURL;
|
|
114
|
-
return {
|
|
115
|
-
publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
|
|
116
|
-
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
|
|
117
|
-
...(isPresent(baseUrl) ? { baseUrl } : {}),
|
|
118
|
-
...(isPresent(environment) ? { environment } : {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
|
|
122
|
-
return {
|
|
123
|
-
publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
|
|
124
|
-
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
|
|
125
|
-
baseUrl: langfuse.baseUrl,
|
|
126
|
-
...(isPresent(environment) ? { environment } : {}),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function hashCacheKeyValue(value: string | undefined): string | undefined {
|
|
133
|
-
return isPresent(value)
|
|
134
|
-
? createHash('sha256').update(value, 'utf8').digest('hex')
|
|
135
|
-
: undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function getLangfuseTracerProviderKey(
|
|
139
|
-
params: LangfuseSpanProcessorParams,
|
|
78
|
+
/** Cache key for processor instances: the destination plus processor-level
|
|
79
|
+
* policy (`toolOutputTracing` is baked into each processor's redaction
|
|
80
|
+
* behavior), unlike the pure destination identity used for span parenting. */
|
|
81
|
+
function getLangfuseProcessorCacheKey(
|
|
82
|
+
destinationKey: string,
|
|
140
83
|
langfuse?: t.LangfuseConfig
|
|
141
84
|
): string {
|
|
142
85
|
return JSON.stringify({
|
|
143
|
-
|
|
144
|
-
secretKeyHash: hashCacheKeyValue(params.secretKey),
|
|
145
|
-
baseUrl: params.baseUrl,
|
|
146
|
-
environment: params.environment,
|
|
86
|
+
destinationKey,
|
|
147
87
|
toolOutputTracing: langfuse?.toolOutputTracing,
|
|
148
88
|
});
|
|
149
89
|
}
|
|
@@ -160,25 +100,43 @@ class RoutingLangfuseSpanProcessor implements SpanProcessor {
|
|
|
160
100
|
if (params == null) {
|
|
161
101
|
return undefined;
|
|
162
102
|
}
|
|
103
|
+
return this.ensureProcessorForKey(
|
|
104
|
+
getLangfuseProcessorCacheKey(getLangfuseDestinationKey(params), langfuse),
|
|
105
|
+
params,
|
|
106
|
+
langfuse
|
|
107
|
+
);
|
|
108
|
+
}
|
|
163
109
|
|
|
164
|
-
|
|
165
|
-
|
|
110
|
+
private ensureProcessorForKey(
|
|
111
|
+
processorCacheKey: string,
|
|
112
|
+
params: LangfuseSpanProcessorParams,
|
|
113
|
+
langfuse?: t.LangfuseConfig
|
|
114
|
+
): SpanProcessor {
|
|
115
|
+
const existing = this.processors.get(processorCacheKey);
|
|
166
116
|
if (existing != null) {
|
|
167
117
|
return existing;
|
|
168
118
|
}
|
|
169
119
|
|
|
170
120
|
const processor = createLangfuseSpanProcessor(params, langfuse);
|
|
171
|
-
this.processors.set(
|
|
121
|
+
this.processors.set(processorCacheKey, processor);
|
|
172
122
|
return processor;
|
|
173
123
|
}
|
|
174
124
|
|
|
175
125
|
onStart(span: Span, parentContext: Context): void {
|
|
176
126
|
const langfuse = resolveLangfuseConfigForSpan(parentContext);
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
127
|
+
const params = getLangfuseSpanProcessorParams(langfuse);
|
|
128
|
+
if (params == null) {
|
|
179
129
|
return;
|
|
180
130
|
}
|
|
181
131
|
|
|
132
|
+
const destinationKey = getLangfuseDestinationKey(params);
|
|
133
|
+
const processor = this.ensureProcessorForKey(
|
|
134
|
+
getLangfuseProcessorCacheKey(destinationKey, langfuse),
|
|
135
|
+
params,
|
|
136
|
+
langfuse
|
|
137
|
+
);
|
|
138
|
+
registerLangfuseManagedSpan(span, destinationKey);
|
|
139
|
+
|
|
182
140
|
const librechatTraceAttributes = createLibreChatTraceAttributes(
|
|
183
141
|
langfuse?.librechatTraceAttributes ?? {}
|
|
184
142
|
);
|