@librechat/agents 3.3.8 → 3.3.10
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 +4 -0
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +66 -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 +57 -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/agents/AgentContext.mjs +4 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +67 -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 +57 -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/agents/AgentContext.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +24 -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/run.d.ts +7 -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/agents/AgentContext.ts +5 -0
- package/src/graphs/Graph.ts +108 -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 +91 -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/discovered-tools.test.ts +217 -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
|
@@ -1708,6 +1708,11 @@ export class AgentContext {
|
|
|
1708
1708
|
this.totalTokensFresh = false;
|
|
1709
1709
|
}
|
|
1710
1710
|
|
|
1711
|
+
/** Returns a snapshot of the deferred tools discovered in this context. */
|
|
1712
|
+
getDiscoveredTools(): string[] {
|
|
1713
|
+
return Array.from(this.discoveredToolNames);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1711
1716
|
/**
|
|
1712
1717
|
* Marks tools as discovered via tool search.
|
|
1713
1718
|
* Discovered tools will be included in the next model binding.
|
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';
|
|
@@ -639,6 +643,10 @@ export abstract class Graph<
|
|
|
639
643
|
currentToolMap?: t.ToolMap;
|
|
640
644
|
}): CustomToolNode<T> | ToolNode<T>;
|
|
641
645
|
abstract getRunMessages(): BaseMessage[] | undefined;
|
|
646
|
+
/** Returns a snapshot of deferred tools discovered by this graph. */
|
|
647
|
+
getDiscoveredTools(_agentId?: string): string[] {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
642
650
|
abstract getContentParts(): t.MessageContentComplex[] | undefined;
|
|
643
651
|
abstract generateStepId(stepKey: string): [string, number];
|
|
644
652
|
abstract getKeyList(
|
|
@@ -762,6 +770,15 @@ export abstract class Graph<
|
|
|
762
770
|
new Map();
|
|
763
771
|
eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
|
|
764
772
|
new Map();
|
|
773
|
+
/**
|
|
774
|
+
* Per-run eager prestart circuit breaker, shared by reference with every
|
|
775
|
+
* ToolNode this graph compiles. When a prestarted execution's args turn
|
|
776
|
+
* out to differ from the final request, ToolNode records the tool name
|
|
777
|
+
* here and the stream handler stops prestarting that tool for the rest of
|
|
778
|
+
* the run — the retry then executes normally instead of re-diverging in a
|
|
779
|
+
* loop (LibreChat#14371).
|
|
780
|
+
*/
|
|
781
|
+
eagerEventToolSuppressions: Set<string> = new Set();
|
|
765
782
|
/**
|
|
766
783
|
* Run-scoped execution backend for built-in code tools. Defaults to the
|
|
767
784
|
* remote Code API sandbox when unset.
|
|
@@ -811,6 +828,7 @@ export abstract class Graph<
|
|
|
811
828
|
this.eagerEventToolExecutions.clear();
|
|
812
829
|
this.clearEagerEventToolUsageCounts();
|
|
813
830
|
this.eagerEventToolCallChunks.clear();
|
|
831
|
+
this.eagerEventToolSuppressions.clear();
|
|
814
832
|
this.toolExecution = undefined;
|
|
815
833
|
this.handlerDispatchedEventCounts.clear();
|
|
816
834
|
/**
|
|
@@ -992,11 +1010,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
992
1010
|
messages: BaseMessage[] = [];
|
|
993
1011
|
/** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
|
|
994
1012
|
private cachedRunMessages?: BaseMessage[];
|
|
1013
|
+
/** Per-agent discovery snapshots preserved before contexts are reset on cleanup. */
|
|
1014
|
+
private cachedDiscoveredTools?: Map<string, string[]>;
|
|
995
1015
|
/** Ids of AI turns the agent node returned THIS run; see isRunProducedMessage. */
|
|
996
1016
|
protected runProducedAiMessageIds = new Set<string>();
|
|
997
1017
|
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
998
1018
|
private originalToolContentCheckpointScope?: string;
|
|
999
1019
|
runId: string | undefined;
|
|
1020
|
+
/**
|
|
1021
|
+
* Identity used to stamp Langfuse runtime scopes and handlers (see
|
|
1022
|
+
* `LangfuseRuntimeContext.runId`). Carries an opaque per-instance
|
|
1023
|
+
* component: public run ids are unrestricted and may repeat across
|
|
1024
|
+
* concurrently executing runs (retries, duplicate submissions,
|
|
1025
|
+
* tenant-local message ids), and equal stamps would let those runs adopt
|
|
1026
|
+
* each other's scopes. One graph instance = one execution's stamp, shared
|
|
1027
|
+
* by the stream handler and every graph-level scope of that execution.
|
|
1028
|
+
*/
|
|
1029
|
+
readonly langfuseScopeRunId: string;
|
|
1000
1030
|
/**
|
|
1001
1031
|
* Boundary between historical messages (loaded from conversation state)
|
|
1002
1032
|
* and messages produced during the current run. Set once in the state
|
|
@@ -1081,6 +1111,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1081
1111
|
}: t.StandardGraphInput) {
|
|
1082
1112
|
super();
|
|
1083
1113
|
this.runId = runId;
|
|
1114
|
+
this.langfuseScopeRunId = `${runId ?? 'graph'}:${nanoid()}`;
|
|
1084
1115
|
this.signal = signal;
|
|
1085
1116
|
this.langfuse = langfuse;
|
|
1086
1117
|
this.subagentUsageSink = subagentUsageSink;
|
|
@@ -1112,6 +1143,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1112
1143
|
resetValues(keepContent?: boolean, checkpointScope?: string): void {
|
|
1113
1144
|
this.messages = [];
|
|
1114
1145
|
this.cachedRunMessages = undefined;
|
|
1146
|
+
this.cachedDiscoveredTools = undefined;
|
|
1115
1147
|
this.config = resetIfNotEmpty(this.config, undefined);
|
|
1116
1148
|
if (keepContent !== true) {
|
|
1117
1149
|
this.contentData = resetIfNotEmpty(this.contentData, []);
|
|
@@ -1129,6 +1161,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1129
1161
|
this.eagerEventToolExecutions.clear();
|
|
1130
1162
|
this.clearEagerEventToolUsageCounts();
|
|
1131
1163
|
this.eagerEventToolCallChunks.clear();
|
|
1164
|
+
this.eagerEventToolSuppressions.clear();
|
|
1132
1165
|
this.handlerDispatchedStepIds = resetIfNotEmpty(
|
|
1133
1166
|
this.handlerDispatchedStepIds,
|
|
1134
1167
|
new Set()
|
|
@@ -1177,6 +1210,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1177
1210
|
|
|
1178
1211
|
override clearHeavyState(): void {
|
|
1179
1212
|
this.cachedRunMessages = this.messages.slice(this.startIndex);
|
|
1213
|
+
this.cachedDiscoveredTools = new Map(
|
|
1214
|
+
Array.from(this.agentContexts, ([agentId, context]) => [
|
|
1215
|
+
agentId,
|
|
1216
|
+
context.getDiscoveredTools(),
|
|
1217
|
+
])
|
|
1218
|
+
);
|
|
1180
1219
|
super.clearHeavyState();
|
|
1181
1220
|
this.messages = [];
|
|
1182
1221
|
this.overrideModel = undefined;
|
|
@@ -1471,6 +1510,32 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1471
1510
|
return this.messages.slice(this.startIndex);
|
|
1472
1511
|
}
|
|
1473
1512
|
|
|
1513
|
+
override getDiscoveredTools(agentId?: string): string[] {
|
|
1514
|
+
if (agentId != null) {
|
|
1515
|
+
const current =
|
|
1516
|
+
this.agentContexts.get(agentId)?.getDiscoveredTools() ?? [];
|
|
1517
|
+
if (current.length > 0 || this.cachedDiscoveredTools == null) {
|
|
1518
|
+
return current;
|
|
1519
|
+
}
|
|
1520
|
+
return [...(this.cachedDiscoveredTools.get(agentId) ?? [])];
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const discoveredTools = new Set<string>();
|
|
1524
|
+
for (const context of this.agentContexts.values()) {
|
|
1525
|
+
for (const toolName of context.getDiscoveredTools()) {
|
|
1526
|
+
discoveredTools.add(toolName);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (discoveredTools.size === 0 && this.cachedDiscoveredTools != null) {
|
|
1530
|
+
for (const snapshot of this.cachedDiscoveredTools.values()) {
|
|
1531
|
+
for (const toolName of snapshot) {
|
|
1532
|
+
discoveredTools.add(toolName);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return Array.from(discoveredTools);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1474
1539
|
/**
|
|
1475
1540
|
* True when THIS RUN produced `message` — the provenance the handoff cue
|
|
1476
1541
|
* gate needs. Tracked as an id set rather than inferred from `startIndex`
|
|
@@ -1657,6 +1722,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1657
1722
|
eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
|
|
1658
1723
|
agentContext?.agentId
|
|
1659
1724
|
),
|
|
1725
|
+
eagerEventToolSuppressions: this.eagerEventToolSuppressions,
|
|
1660
1726
|
toolExecution: this.toolExecution,
|
|
1661
1727
|
directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
|
|
1662
1728
|
interruptingToolNames:
|
|
@@ -3057,6 +3123,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3057
3123
|
metadata: {
|
|
3058
3124
|
...(config.metadata ?? {}),
|
|
3059
3125
|
...traceMetadata,
|
|
3126
|
+
/** Canonical agent identity, stamped OUTSIDE trace-metadata
|
|
3127
|
+
* filtering: `createLangfuseTraceMetadata` drops values over its
|
|
3128
|
+
* length cap, but scope trust (`isForeignScope`) needs the id
|
|
3129
|
+
* verbatim regardless of length. */
|
|
3130
|
+
agentId,
|
|
3060
3131
|
},
|
|
3061
3132
|
};
|
|
3062
3133
|
initializeLangfuseTracing(langfuse);
|
|
@@ -3069,6 +3140,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3069
3140
|
tags: ['librechat', 'agent'],
|
|
3070
3141
|
traceIdSeed:
|
|
3071
3142
|
langfuse?.deterministicTraceId === true ? this.runId : undefined,
|
|
3143
|
+
runId: this.langfuseScopeRunId,
|
|
3144
|
+
toolOutputTracing: hasToolOutputTracingConfig(
|
|
3145
|
+
this.langfuse,
|
|
3146
|
+
agentContext.langfuse
|
|
3147
|
+
)
|
|
3148
|
+
? resolveToolOutputTracingConfig(
|
|
3149
|
+
this.langfuse,
|
|
3150
|
+
agentContext.langfuse
|
|
3151
|
+
)
|
|
3152
|
+
: undefined,
|
|
3072
3153
|
});
|
|
3073
3154
|
if (langfuseHandler != null) {
|
|
3074
3155
|
invokeConfig = {
|
|
@@ -3089,6 +3170,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3089
3170
|
resolveLangfuseRuntimeScope({
|
|
3090
3171
|
runLangfuse: this.langfuse,
|
|
3091
3172
|
langfuseOverlay: agentContext.langfuse,
|
|
3173
|
+
runId: this.langfuseScopeRunId,
|
|
3174
|
+
agentId,
|
|
3092
3175
|
}),
|
|
3093
3176
|
() =>
|
|
3094
3177
|
attemptInvoke(
|
|
@@ -3251,6 +3334,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3251
3334
|
resolveLangfuseRuntimeScope({
|
|
3252
3335
|
runLangfuse: this.langfuse,
|
|
3253
3336
|
langfuseOverlay: agentContext.langfuse,
|
|
3337
|
+
runId: this.langfuseScopeRunId,
|
|
3338
|
+
agentId,
|
|
3254
3339
|
}),
|
|
3255
3340
|
() =>
|
|
3256
3341
|
tryFallbackProviders({
|
|
@@ -3673,7 +3758,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3673
3758
|
* the same run — `haltRun` is first-write-wins — is left alone.
|
|
3674
3759
|
*/
|
|
3675
3760
|
const halt = this.hookRegistry.getHaltSignal(runId);
|
|
3676
|
-
if (
|
|
3761
|
+
if (
|
|
3762
|
+
result.preventContinuation === true &&
|
|
3763
|
+
halt?.source === 'PreemptBoundary'
|
|
3764
|
+
) {
|
|
3677
3765
|
this.preemptHaltReason = halt.reason;
|
|
3678
3766
|
this.hookRegistry.clearHaltSignal(runId);
|
|
3679
3767
|
}
|
|
@@ -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
|
);
|