@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
|
@@ -8,8 +8,11 @@ import type * as t from '@/types';
|
|
|
8
8
|
import {
|
|
9
9
|
getLangfuseRuntimeConfig,
|
|
10
10
|
getLangfuseRuntimeToolOutputTracingConfig,
|
|
11
|
+
getLangfuseScopeAgentId,
|
|
12
|
+
getLangfuseScopeRunId,
|
|
11
13
|
getTraceIdSeed,
|
|
12
14
|
hasLangfuseRuntimeContextValue,
|
|
15
|
+
replaceLangfuseRuntimeContext,
|
|
13
16
|
runWithLangfuseRuntimeContext,
|
|
14
17
|
} from '@/langfuseRuntimeContext';
|
|
15
18
|
import {
|
|
@@ -24,6 +27,16 @@ export type ResolveLangfuseRuntimeScopeParams = {
|
|
|
24
27
|
runLangfuse?: t.LangfuseConfig;
|
|
25
28
|
langfuseOverlay?: t.LangfuseConfig;
|
|
26
29
|
traceIdSeed?: string;
|
|
30
|
+
runId?: string;
|
|
31
|
+
agentId?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type LangfuseRuntimeScopeOptions = {
|
|
35
|
+
/** Replace the surrounding scope entirely instead of merging with it:
|
|
36
|
+
* fields absent from `scope` are CLEARED rather than inherited. Required
|
|
37
|
+
* when rejecting a foreign concurrent run's scope, whose explicit values
|
|
38
|
+
* must not survive merge inheritance. */
|
|
39
|
+
replace?: boolean;
|
|
27
40
|
};
|
|
28
41
|
|
|
29
42
|
const langfuseToolOutputTracingConfigKey = createContextKey(
|
|
@@ -33,6 +46,8 @@ const langfuseConfigKey = createContextKey('librechat.langfuse.config');
|
|
|
33
46
|
const langfuseTraceIdSeedKey = createContextKey(
|
|
34
47
|
'librechat.langfuse.trace-id-seed'
|
|
35
48
|
);
|
|
49
|
+
const langfuseScopeRunIdKey = createContextKey('librechat.langfuse.run-id');
|
|
50
|
+
const langfuseScopeAgentIdKey = createContextKey('librechat.langfuse.agent-id');
|
|
36
51
|
|
|
37
52
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
38
53
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -75,6 +90,32 @@ export function resolveTraceIdSeedForSpan(
|
|
|
75
90
|
return getTraceIdSeed() ?? getOtelTraceIdSeed(activeContext);
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
function getOtelScopeRunId(activeContext: Context): string | undefined {
|
|
94
|
+
const value = activeContext.getValue(langfuseScopeRunIdKey);
|
|
95
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getOtelScopeAgentId(activeContext: Context): string | undefined {
|
|
99
|
+
const value = activeContext.getValue(langfuseScopeAgentIdKey);
|
|
100
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The run identity the active scope belongs to (see
|
|
104
|
+
* `LangfuseRuntimeContext.runId`), or `undefined` for unstamped scopes. */
|
|
105
|
+
export function resolveLangfuseScopeRunId(
|
|
106
|
+
activeContext: Context
|
|
107
|
+
): string | undefined {
|
|
108
|
+
return getLangfuseScopeRunId() ?? getOtelScopeRunId(activeContext);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The agent identity of a per-agent overlay scope (see
|
|
112
|
+
* `LangfuseRuntimeContext.agentId`), or `undefined` for run-level scopes. */
|
|
113
|
+
export function resolveLangfuseScopeAgentId(
|
|
114
|
+
activeContext: Context
|
|
115
|
+
): string | undefined {
|
|
116
|
+
return getLangfuseScopeAgentId() ?? getOtelScopeAgentId(activeContext);
|
|
117
|
+
}
|
|
118
|
+
|
|
78
119
|
export function resolveToolOutputTracingConfigForSpan(
|
|
79
120
|
activeContext: Context
|
|
80
121
|
): ResolvedLangfuseToolOutputTracingConfig | undefined {
|
|
@@ -84,45 +125,77 @@ export function resolveToolOutputTracingConfigForSpan(
|
|
|
84
125
|
);
|
|
85
126
|
}
|
|
86
127
|
|
|
128
|
+
function setOrClearContextValue(
|
|
129
|
+
activeContext: Context,
|
|
130
|
+
key: Parameters<Context['setValue']>[0],
|
|
131
|
+
value: unknown,
|
|
132
|
+
replace: boolean
|
|
133
|
+
): Context {
|
|
134
|
+
if (value != null) {
|
|
135
|
+
return activeContext.setValue(key, value);
|
|
136
|
+
}
|
|
137
|
+
return replace ? activeContext.deleteValue(key) : activeContext;
|
|
138
|
+
}
|
|
139
|
+
|
|
87
140
|
export function withLangfuseRuntimeScope<T>(
|
|
88
141
|
scope: LangfuseRuntimeScope,
|
|
89
|
-
action: () => T
|
|
142
|
+
action: () => T,
|
|
143
|
+
options?: LangfuseRuntimeScopeOptions
|
|
90
144
|
): T {
|
|
91
|
-
|
|
145
|
+
const replace = options?.replace === true;
|
|
146
|
+
if (!replace && !hasLangfuseRuntimeContextValue(scope)) {
|
|
92
147
|
return action();
|
|
93
148
|
}
|
|
94
149
|
|
|
95
150
|
let activeContext = context.active();
|
|
96
|
-
|
|
97
|
-
activeContext
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
151
|
+
activeContext = setOrClearContextValue(
|
|
152
|
+
activeContext,
|
|
153
|
+
langfuseConfigKey,
|
|
154
|
+
scope.langfuse,
|
|
155
|
+
replace
|
|
156
|
+
);
|
|
157
|
+
activeContext = setOrClearContextValue(
|
|
158
|
+
activeContext,
|
|
159
|
+
langfuseToolOutputTracingConfigKey,
|
|
160
|
+
scope.toolOutputTracing,
|
|
161
|
+
replace
|
|
162
|
+
);
|
|
163
|
+
activeContext = setOrClearContextValue(
|
|
164
|
+
activeContext,
|
|
165
|
+
langfuseTraceIdSeedKey,
|
|
166
|
+
hasText(scope.traceIdSeed) ? scope.traceIdSeed : undefined,
|
|
167
|
+
replace
|
|
168
|
+
);
|
|
169
|
+
activeContext = setOrClearContextValue(
|
|
170
|
+
activeContext,
|
|
171
|
+
langfuseScopeRunIdKey,
|
|
172
|
+
hasText(scope.runId) ? scope.runId : undefined,
|
|
173
|
+
replace
|
|
174
|
+
);
|
|
175
|
+
activeContext = setOrClearContextValue(
|
|
176
|
+
activeContext,
|
|
177
|
+
langfuseScopeAgentIdKey,
|
|
178
|
+
hasText(scope.agentId) ? scope.agentId : undefined,
|
|
179
|
+
replace
|
|
180
|
+
);
|
|
111
181
|
|
|
112
182
|
// Span processors receive the OTel parent context in `onStart`, while
|
|
113
183
|
// LangChain callback handlers may run outside that context and need ALS.
|
|
114
184
|
// The trace id generator reads the seed from ALS or OTel context so SDK
|
|
115
185
|
// callbacks that preserve only one of those contexts still keep trace/score
|
|
116
186
|
// cohesion.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
187
|
+
const runScoped = replace
|
|
188
|
+
? replaceLangfuseRuntimeContext
|
|
189
|
+
: runWithLangfuseRuntimeContext;
|
|
190
|
+
return runScoped(scope, () => context.with(activeContext, action));
|
|
120
191
|
}
|
|
121
192
|
|
|
122
193
|
export function resolveLangfuseRuntimeScope({
|
|
123
194
|
runLangfuse,
|
|
124
195
|
langfuseOverlay,
|
|
125
196
|
traceIdSeed,
|
|
197
|
+
runId,
|
|
198
|
+
agentId,
|
|
126
199
|
}: ResolveLangfuseRuntimeScopeParams): LangfuseRuntimeScope {
|
|
127
200
|
const langfuse = resolveLangfuseConfig(runLangfuse, langfuseOverlay);
|
|
128
201
|
const toolOutputTracing = !hasToolOutputTracingConfig(
|
|
@@ -131,5 +204,5 @@ export function resolveLangfuseRuntimeScope({
|
|
|
131
204
|
)
|
|
132
205
|
? undefined
|
|
133
206
|
: resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay);
|
|
134
|
-
return { langfuse, traceIdSeed, toolOutputTracing };
|
|
207
|
+
return { langfuse, traceIdSeed, toolOutputTracing, runId, agentId };
|
|
135
208
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
|
|
3
|
+
import type { Span } from '@opentelemetry/api';
|
|
4
|
+
import type * as t from '@/types';
|
|
5
|
+
import {
|
|
6
|
+
hasLangfuseConfigCredentials,
|
|
7
|
+
hasLangfuseEnvCredentials,
|
|
8
|
+
hasLangfuseEnvConfig,
|
|
9
|
+
} from '@/langfuseConfig';
|
|
10
|
+
import { isPresent } from '@/utils/misc';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Spans created through the Langfuse tracer provider, keyed by the export
|
|
14
|
+
* destination they were routed to. Callback handlers use this to distinguish
|
|
15
|
+
* safe ambient parents from spans a root observation must not inherit:
|
|
16
|
+
*
|
|
17
|
+
* - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP
|
|
18
|
+
* server spans on the global provider) are never exported to Langfuse —
|
|
19
|
+
* inheriting one orphans the trace root (its root/trace input-output
|
|
20
|
+
* shaping is skipped), collapses concurrent runs inside one request
|
|
21
|
+
* context into a single merged trace, and bypasses the seeded
|
|
22
|
+
* deterministic trace id generator.
|
|
23
|
+
* - Langfuse-managed spans bound to a *different* destination (another
|
|
24
|
+
* tenant's project) would leave the new trace dangling in its own
|
|
25
|
+
* destination while inheriting the other tenant's trace id.
|
|
26
|
+
*
|
|
27
|
+
* Only a managed span whose destination matches the starting run's resolved
|
|
28
|
+
* destination is a safe parent — that is the sanctioned way for hosts to
|
|
29
|
+
* group runs under their own Langfuse observations.
|
|
30
|
+
*/
|
|
31
|
+
const managedSpanDestinations = new WeakMap<Span, string>();
|
|
32
|
+
|
|
33
|
+
export function registerLangfuseManagedSpan(
|
|
34
|
+
span: Span,
|
|
35
|
+
destinationKey: string
|
|
36
|
+
): void {
|
|
37
|
+
managedSpanDestinations.set(span, destinationKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getLangfuseManagedSpanDestination(
|
|
41
|
+
span: Span
|
|
42
|
+
): string | undefined {
|
|
43
|
+
return managedSpanDestinations.get(span);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveLangfuseEnvironment(
|
|
47
|
+
langfuse?: t.LangfuseConfig
|
|
48
|
+
): string | undefined {
|
|
49
|
+
const candidates = [
|
|
50
|
+
langfuse?.environment,
|
|
51
|
+
process.env.LANGFUSE_TRACING_ENVIRONMENT,
|
|
52
|
+
process.env.NODE_ENV,
|
|
53
|
+
];
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
if (candidate != null && candidate.trim() !== '') {
|
|
56
|
+
return candidate.trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getLangfuseSpanProcessorParams(
|
|
63
|
+
langfuse?: t.LangfuseConfig
|
|
64
|
+
): LangfuseSpanProcessorParams | undefined {
|
|
65
|
+
if (langfuse?.enabled === false) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const environment = resolveLangfuseEnvironment(langfuse);
|
|
69
|
+
if (hasLangfuseConfigCredentials(langfuse)) {
|
|
70
|
+
return {
|
|
71
|
+
publicKey: langfuse.publicKey,
|
|
72
|
+
secretKey: langfuse.secretKey,
|
|
73
|
+
...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
|
|
74
|
+
...(isPresent(environment) ? { environment } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (hasLangfuseEnvConfig()) {
|
|
78
|
+
const baseUrl =
|
|
79
|
+
langfuse?.baseUrl ??
|
|
80
|
+
process.env.LANGFUSE_BASE_URL ??
|
|
81
|
+
process.env.LANGFUSE_BASEURL;
|
|
82
|
+
return {
|
|
83
|
+
publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
|
|
84
|
+
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
|
|
85
|
+
...(isPresent(baseUrl) ? { baseUrl } : {}),
|
|
86
|
+
...(isPresent(environment) ? { environment } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
|
|
90
|
+
return {
|
|
91
|
+
publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
|
|
92
|
+
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
|
|
93
|
+
baseUrl: langfuse.baseUrl,
|
|
94
|
+
...(isPresent(environment) ? { environment } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hashCacheKeyValue(value: string | undefined): string | undefined {
|
|
101
|
+
return isPresent(value)
|
|
102
|
+
? createHash('sha256').update(value, 'utf8').digest('hex')
|
|
103
|
+
: undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Identity of an export destination (project credentials + endpoint +
|
|
108
|
+
* environment) only. Processor-level policies like `toolOutputTracing` are
|
|
109
|
+
* deliberately excluded: two spans exporting to the same project under
|
|
110
|
+
* different redaction settings still share a destination and may parent one
|
|
111
|
+
* another.
|
|
112
|
+
*/
|
|
113
|
+
export function getLangfuseDestinationKey(
|
|
114
|
+
params: LangfuseSpanProcessorParams
|
|
115
|
+
): string {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
publicKey: params.publicKey,
|
|
118
|
+
secretKeyHash: hashCacheKeyValue(params.secretKey),
|
|
119
|
+
baseUrl: params.baseUrl,
|
|
120
|
+
environment: params.environment,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The export destination a run with this config resolves to, or `undefined`
|
|
125
|
+
* when no Langfuse destination is configured. */
|
|
126
|
+
export function resolveLangfuseDestinationKey(
|
|
127
|
+
langfuse?: t.LangfuseConfig
|
|
128
|
+
): string | undefined {
|
|
129
|
+
const params = getLangfuseSpanProcessorParams(langfuse);
|
|
130
|
+
return params == null ? undefined : getLangfuseDestinationKey(params);
|
|
131
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
|
|
2
2
|
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
import { Constants } from '@/common';
|
|
3
4
|
|
|
4
5
|
const LANGGRAPH_START_NODE = '__start__';
|
|
5
6
|
const ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';
|
|
@@ -10,8 +11,12 @@ const TOOL_DISPATCH_SPAN_NAME = 'tool-dispatch';
|
|
|
10
11
|
const GENERATION_SPAN_NAME = 'llm';
|
|
11
12
|
const ROOT_OBSERVATION_TYPE = 'agent';
|
|
12
13
|
const CHAIN_OBSERVATION_TYPE = 'chain';
|
|
14
|
+
const TOOL_OBSERVATION_TYPE = 'tool';
|
|
13
15
|
const AGENT_TRACE_TAG = 'agent';
|
|
14
16
|
const TITLE_TRACE_TAG = 'title';
|
|
17
|
+
const EPHEMERAL_AGENT_SENDER_SEPARATOR = '___';
|
|
18
|
+
const EPHEMERAL_AGENT_INDEX_SEPARATOR = '____';
|
|
19
|
+
const OBSERVATION_METADATA_LANGGRAPH_NODE = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;
|
|
15
20
|
|
|
16
21
|
type MutableSpan = ReadableSpan & {
|
|
17
22
|
name: string;
|
|
@@ -175,17 +180,112 @@ function getMessageToolCalls(
|
|
|
175
180
|
return calls;
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Id-bearing `invalid_tool_calls` on the assistant turn. ToolNode pairs these
|
|
185
|
+
* with synthesized error results (and an invalid-only turn is routed on them
|
|
186
|
+
* alone), so the tool-dispatch span must count them as part of the executing
|
|
187
|
+
* batch — otherwise an invalid-only dispatch finds zero calls and the span
|
|
188
|
+
* keeps the full serialized graph state as its input, and a mixed dispatch
|
|
189
|
+
* silently omits the malformed call. `args` stays the raw unparsed string.
|
|
190
|
+
*/
|
|
191
|
+
/** Tool-result ids present in the serialized state — ToolNode's
|
|
192
|
+
* `!toolMessageIds.has(id)` execution filter, mirrored for the span. */
|
|
193
|
+
function getToolResultIds(
|
|
194
|
+
messages: Record<string, unknown>[]
|
|
195
|
+
): Set<string> {
|
|
196
|
+
const ids = new Set<string>();
|
|
197
|
+
for (const message of messages) {
|
|
198
|
+
if (getMessageRole(message) !== 'tool') {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const rawId =
|
|
202
|
+
message.tool_call_id ??
|
|
203
|
+
(isRecord(message.kwargs) ? message.kwargs.tool_call_id : undefined) ??
|
|
204
|
+
(isRecord(message.data) ? message.data.tool_call_id : undefined);
|
|
205
|
+
if (typeof rawId === 'string' && rawId !== '') {
|
|
206
|
+
ids.add(rawId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return ids;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getMessageInvalidToolCalls(
|
|
213
|
+
message: Record<string, unknown>,
|
|
214
|
+
answeredIds: ReadonlySet<string>
|
|
215
|
+
): SerializedToolCall[] {
|
|
216
|
+
const rawCalls =
|
|
217
|
+
message.invalid_tool_calls ??
|
|
218
|
+
(isRecord(message.kwargs) ? message.kwargs.invalid_tool_calls : undefined) ??
|
|
219
|
+
(isRecord(message.data) ? message.data.invalid_tool_calls : undefined);
|
|
220
|
+
if (!Array.isArray(rawCalls)) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
const calls: SerializedToolCall[] = [];
|
|
224
|
+
for (const rawCall of rawCalls) {
|
|
225
|
+
// Same attribution predicate ToolNode executes with (id-bearing,
|
|
226
|
+
// non-server) so the span never claims calls the node deliberately skips.
|
|
227
|
+
if (
|
|
228
|
+
!isRecord(rawCall) ||
|
|
229
|
+
typeof rawCall.id !== 'string' ||
|
|
230
|
+
rawCall.id === '' ||
|
|
231
|
+
answeredIds.has(rawCall.id) ||
|
|
232
|
+
rawCall.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
|
|
233
|
+
) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Same name fallback ToolNode synthesizes with — a nameless attributable
|
|
237
|
+
// call still gets a result, so it must still count in the span input.
|
|
238
|
+
const call = normalizeToolCall(
|
|
239
|
+
typeof rawCall.name === 'string' && rawCall.name !== ''
|
|
240
|
+
? rawCall
|
|
241
|
+
: { ...rawCall, name: 'unknown' }
|
|
242
|
+
);
|
|
243
|
+
if (call != null) {
|
|
244
|
+
calls.push(call);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return calls;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The serialized message's own id (uuid), NOT the LC-serialization type id
|
|
251
|
+
* array that `message.id` carries in constructor dumps. */
|
|
252
|
+
function getSerializedMessageId(
|
|
253
|
+
message: Record<string, unknown>
|
|
254
|
+
): string | undefined {
|
|
255
|
+
const kwargsId = isRecord(message.kwargs) ? message.kwargs.id : undefined;
|
|
256
|
+
const dataId = isRecord(message.data) ? message.data.id : undefined;
|
|
257
|
+
const rawId = message.id;
|
|
258
|
+
const id = kwargsId ?? dataId ?? rawId;
|
|
259
|
+
return typeof id === 'string' && id !== '' ? id : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
178
262
|
/** Latest assistant turn's tool calls — the calls this tool node is executing. */
|
|
179
263
|
function findPendingToolCalls(value: unknown): SerializedToolCall[] {
|
|
180
264
|
const messages = getMessageArray(value);
|
|
181
265
|
if (messages == null) {
|
|
182
266
|
return [];
|
|
183
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Invalid calls count only where ToolNode's own gate lets them execute:
|
|
270
|
+
* the messages-state form (a bare-array state means the node returns a
|
|
271
|
+
* plain output list and skips invalid handling) with an id-bearing
|
|
272
|
+
* assistant message (no id, no reducer upsert). Mirrors
|
|
273
|
+
* `canPromoteInvalidCalls` so the span never reports skipped calls.
|
|
274
|
+
*/
|
|
275
|
+
const invalidCallsApply = !Array.isArray(value);
|
|
276
|
+
const answeredIds = invalidCallsApply
|
|
277
|
+
? getToolResultIds(messages)
|
|
278
|
+
: undefined;
|
|
184
279
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
185
280
|
if (getMessageRole(messages[i]) !== 'assistant') {
|
|
186
281
|
continue;
|
|
187
282
|
}
|
|
188
|
-
const calls =
|
|
283
|
+
const calls = [
|
|
284
|
+
...getMessageToolCalls(messages[i]),
|
|
285
|
+
...(invalidCallsApply && getSerializedMessageId(messages[i]) != null
|
|
286
|
+
? getMessageInvalidToolCalls(messages[i], answeredIds!)
|
|
287
|
+
: []),
|
|
288
|
+
];
|
|
189
289
|
if (calls.length > 0) {
|
|
190
290
|
return calls;
|
|
191
291
|
}
|
|
@@ -254,11 +354,20 @@ function shapeRootSpan(span: MutableSpan): void {
|
|
|
254
354
|
parseAttributeValue(span.attributes[outputKey]),
|
|
255
355
|
'assistant'
|
|
256
356
|
);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
357
|
+
/** A generation that IS the trace root — a bare `model.invoke` with no
|
|
358
|
+
* wrapping chain, i.e. the activity-label path — is also the only
|
|
359
|
+
* observation carrying its own prompt: reducing its observation input
|
|
360
|
+
* would discard the SystemMessage from the one place it is traced.
|
|
361
|
+
* Chain/agent roots keep the full reduction; their child generations
|
|
362
|
+
* still record the complete prompt. Trace-level input/output reduce
|
|
363
|
+
* either way, so the trace list keeps showing question and answer. */
|
|
364
|
+
if (!isGenerationSpan(span)) {
|
|
365
|
+
if (question != null) {
|
|
366
|
+
span.attributes[inputKey] = question;
|
|
367
|
+
}
|
|
368
|
+
if (answer != null) {
|
|
369
|
+
span.attributes[outputKey] = answer;
|
|
370
|
+
}
|
|
262
371
|
}
|
|
263
372
|
const traceInput = question ?? span.attributes[inputKey];
|
|
264
373
|
const traceOutput = answer ?? span.attributes[outputKey];
|
|
@@ -275,6 +384,81 @@ function isGenerationSpan(span: MutableSpan): boolean {
|
|
|
275
384
|
return typeof type === 'string' && type.toLowerCase() === 'generation';
|
|
276
385
|
}
|
|
277
386
|
|
|
387
|
+
function isToolSpan(span: MutableSpan): boolean {
|
|
388
|
+
const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
|
|
389
|
+
return (
|
|
390
|
+
typeof type === 'string' && type.toLowerCase() === TOOL_OBSERVATION_TYPE
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Whether the span is a LangGraph node execution whose node id is the span
|
|
396
|
+
* name — the shape of the outer workflow-agent node. `@langfuse/tracing`
|
|
397
|
+
* flattens object metadata to per-key attributes with string values stored
|
|
398
|
+
* raw, so LangGraph's `langgraph_node` arrives directly comparable.
|
|
399
|
+
*/
|
|
400
|
+
function isWorkflowNodeSpan(span: MutableSpan): boolean {
|
|
401
|
+
return span.attributes[OBSERVATION_METADATA_LANGGRAPH_NODE] === span.name;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* LibreChat ephemeral agents are identified as
|
|
406
|
+
* `endpoint__model___sender[____index]` (`__` encodes `:`; see LibreChat's
|
|
407
|
+
* `encodeEphemeralAgentId`), and the outer workflow node carries that id as
|
|
408
|
+
* its span name — so switching models renames the span (item 1). Returns the
|
|
409
|
+
* stable human `sender` segment only when the name matches that encoding:
|
|
410
|
+
* the `endpoint__model` prefix must contain both segments and — unlike
|
|
411
|
+
* display names such as `LibreChat Agent: Ops___EU` — never contains
|
|
412
|
+
* whitespace, so legitimate names that merely embed `___` are left alone.
|
|
413
|
+
*/
|
|
414
|
+
function extractEphemeralAgentSender(name: string): string | undefined {
|
|
415
|
+
let workingId = name;
|
|
416
|
+
const indexSeparator = workingId.lastIndexOf(EPHEMERAL_AGENT_INDEX_SEPARATOR);
|
|
417
|
+
if (
|
|
418
|
+
indexSeparator !== -1 &&
|
|
419
|
+
/^\d+$/.test(
|
|
420
|
+
workingId.slice(indexSeparator + EPHEMERAL_AGENT_INDEX_SEPARATOR.length)
|
|
421
|
+
)
|
|
422
|
+
) {
|
|
423
|
+
workingId = workingId.slice(0, indexSeparator);
|
|
424
|
+
}
|
|
425
|
+
const senderSeparator = workingId.indexOf(EPHEMERAL_AGENT_SENDER_SEPARATOR);
|
|
426
|
+
if (senderSeparator <= 0) {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
const encodedPrefix = workingId.slice(0, senderSeparator);
|
|
430
|
+
if (/\s/.test(encodedPrefix)) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
const prefixParts = encodedPrefix.split('__');
|
|
434
|
+
if (prefixParts.length < 2 || prefixParts.some((part) => part === '')) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
const sender = workingId
|
|
438
|
+
.slice(senderSeparator + EPHEMERAL_AGENT_SENDER_SEPARATOR.length)
|
|
439
|
+
.replace(/__/g, ':');
|
|
440
|
+
return sender === '' ? undefined : sender;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Workflow-agent node spans are always children of the run's root chain and
|
|
444
|
+
* always carry `langgraph_node` metadata equal to their name, so host-named
|
|
445
|
+
* run roots (`LibreChat Agent: <name>`) and ordinary chains that merely look
|
|
446
|
+
* like encoded ids (`pipeline__stage___EU`) are never rename candidates.
|
|
447
|
+
* Successful decodes become `agent` observations, matching the inner
|
|
448
|
+
* `agent=<id>` node shaping. */
|
|
449
|
+
function shapeEphemeralAgentNodeSpan(span: MutableSpan): void {
|
|
450
|
+
if (isToolSpan(span) || isRootSpan(span) || !isWorkflowNodeSpan(span)) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const sender = extractEphemeralAgentSender(span.name);
|
|
454
|
+
if (sender == null) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
span.name = sender;
|
|
458
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
|
|
459
|
+
ROOT_OBSERVATION_TYPE;
|
|
460
|
+
}
|
|
461
|
+
|
|
278
462
|
function hasTraceTag(span: MutableSpan, expectedTag: string): boolean {
|
|
279
463
|
const tags = parseAttributeValue(
|
|
280
464
|
span.attributes[LangfuseOtelSpanAttributes.TRACE_TAGS]
|
|
@@ -304,7 +488,8 @@ function shapeRootObservationType(span: MutableSpan): void {
|
|
|
304
488
|
* Reshapes spans per Langfuse-team feedback before export:
|
|
305
489
|
* - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
|
|
306
490
|
* (`provider__model`) — strip it so switching models doesn't break
|
|
307
|
-
* name-based logic (item 1).
|
|
491
|
+
* name-based logic (item 1). The outer workflow node is named with the
|
|
492
|
+
* bare agent id; ephemeral ids reduce to their stable sender name.
|
|
308
493
|
* - LLM generation spans keep the provider client class name (`ChatOpenAI`,
|
|
309
494
|
* `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
|
|
310
495
|
* name reflects the operation, not the model (the model stays on the
|
|
@@ -324,6 +509,8 @@ export function shapeLangfuseSpan(span: ReadableSpan): void {
|
|
|
324
509
|
shapeToolNodeSpan(mutable);
|
|
325
510
|
} else if (isGenerationSpan(mutable)) {
|
|
326
511
|
mutable.name = GENERATION_SPAN_NAME;
|
|
512
|
+
} else {
|
|
513
|
+
shapeEphemeralAgentNodeSpan(mutable);
|
|
327
514
|
}
|
|
328
515
|
if (!isRootSpan(span)) {
|
|
329
516
|
return;
|
|
@@ -388,6 +388,44 @@ function _ensureMessageContents(
|
|
|
388
388
|
return updatedMsgs as (SystemMessage | HumanMessage | AIMessage)[];
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Anthropic requires `tool_use.input` to be a JSON object; anything else is
|
|
393
|
+
* rejected with a 400 (`tool_use.input: Input should be an object`).
|
|
394
|
+
* History can carry non-object inputs: streaming leaves the raw partial-JSON
|
|
395
|
+
* string on the content block, and upstream context projections can leave
|
|
396
|
+
* `null` (observed live on a summarization-retained tool call replayed after
|
|
397
|
+
* compaction). A string is parsed when it forms complete JSON; every other
|
|
398
|
+
* shape degrades to `{}` — the call already executed, so the replayed input
|
|
399
|
+
* is informational and an empty object is the safe representation.
|
|
400
|
+
*/
|
|
401
|
+
/** True for `{}` — no own enumerable keys on a non-array, non-null object. */
|
|
402
|
+
function isEmptyPlainObject(value: unknown): boolean {
|
|
403
|
+
return (
|
|
404
|
+
typeof value === 'object' &&
|
|
405
|
+
value !== null &&
|
|
406
|
+
!Array.isArray(value) &&
|
|
407
|
+
Object.keys(value).length === 0
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function coerceAnthropicToolUseInput(
|
|
412
|
+
input: unknown
|
|
413
|
+
): Record<string, unknown> {
|
|
414
|
+
let candidate: unknown = input;
|
|
415
|
+
if (typeof candidate === 'string') {
|
|
416
|
+
try {
|
|
417
|
+
candidate = JSON.parse(candidate);
|
|
418
|
+
} catch {
|
|
419
|
+
return {};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return typeof candidate === 'object' &&
|
|
423
|
+
candidate !== null &&
|
|
424
|
+
!Array.isArray(candidate)
|
|
425
|
+
? (candidate as Record<string, unknown>)
|
|
426
|
+
: {};
|
|
427
|
+
}
|
|
428
|
+
|
|
391
429
|
export function _convertLangChainToolCallToAnthropic(
|
|
392
430
|
toolCall: ToolCall
|
|
393
431
|
): AnthropicToolResponse {
|
|
@@ -401,7 +439,7 @@ export function _convertLangChainToolCallToAnthropic(
|
|
|
401
439
|
type: isServerTool ? 'server_tool_use' : 'tool_use',
|
|
402
440
|
id: isServerTool ? toolCall.id : normalizeAnthropicToolCallId(toolCall.id),
|
|
403
441
|
name: toolCall.name,
|
|
404
|
-
input: toolCall.args,
|
|
442
|
+
input: coerceAnthropicToolUseInput(toolCall.args),
|
|
405
443
|
};
|
|
406
444
|
}
|
|
407
445
|
|
|
@@ -649,19 +687,11 @@ function _formatContent(message: BaseMessage) {
|
|
|
649
687
|
'name' in contentPart
|
|
650
688
|
) {
|
|
651
689
|
const rawPart = contentPart as Record<string, unknown>;
|
|
652
|
-
let input = rawPart.input ?? rawPart.args;
|
|
653
|
-
if (typeof input === 'string') {
|
|
654
|
-
try {
|
|
655
|
-
input = JSON.parse(input);
|
|
656
|
-
} catch {
|
|
657
|
-
input = {};
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
690
|
const corrected: AnthropicServerToolUseBlockParam = {
|
|
661
691
|
type: 'server_tool_use',
|
|
662
692
|
id: rawPart.id as string,
|
|
663
693
|
name: (rawPart.name ?? 'web_search') as 'web_search',
|
|
664
|
-
input: (input ??
|
|
694
|
+
input: coerceAnthropicToolUseInput(rawPart.input ?? rawPart.args),
|
|
665
695
|
};
|
|
666
696
|
return corrected;
|
|
667
697
|
}
|
|
@@ -916,10 +946,16 @@ function _formatContent(message: BaseMessage) {
|
|
|
916
946
|
// Core's streaming aggregation can leave the inline tool_use input empty
|
|
917
947
|
// (the assembled arguments live in `message.tool_calls` or, for persisted
|
|
918
948
|
// messages, in sibling input_json_delta blocks). Restore it when missing.
|
|
949
|
+
// An empty plain object counts as missing too: context-pressure
|
|
950
|
+
// truncation degrades an inline input to `{}` while a small `args`
|
|
951
|
+
// mirror survives intact, and a genuinely empty call's mirror is also
|
|
952
|
+
// `{}`, so preferring the mirror never loses information.
|
|
919
953
|
if (
|
|
920
954
|
contentPartCopy.type === 'tool_use' &&
|
|
921
955
|
typeof contentPartCopy.id === 'string' &&
|
|
922
|
-
(contentPartCopy.input === '' ||
|
|
956
|
+
(contentPartCopy.input === '' ||
|
|
957
|
+
contentPartCopy.input == null ||
|
|
958
|
+
isEmptyPlainObject(contentPartCopy.input))
|
|
923
959
|
) {
|
|
924
960
|
const matchingToolCall = isAIMessage(message)
|
|
925
961
|
? message.tool_calls?.find(
|
|
@@ -949,14 +985,29 @@ function _formatContent(message: BaseMessage) {
|
|
|
949
985
|
}
|
|
950
986
|
}
|
|
951
987
|
|
|
952
|
-
if (
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
988
|
+
if (
|
|
989
|
+
contentPartCopy.type === 'tool_use' ||
|
|
990
|
+
contentPartCopy.type === 'server_tool_use'
|
|
991
|
+
) {
|
|
992
|
+
/**
|
|
993
|
+
* The API requires `input` to be a JSON object. Streaming leaves the
|
|
994
|
+
* raw partial-JSON string here, and context-pressure truncation can
|
|
995
|
+
* leave `null` on BOTH the block and its `tool_calls` mirror — so
|
|
996
|
+
* the restore above may itself restore a non-object. Coerce last,
|
|
997
|
+
* after every restore path has run.
|
|
998
|
+
*/
|
|
999
|
+
contentPartCopy.input = coerceAnthropicToolUseInput(
|
|
1000
|
+
'input' in contentPartCopy ? contentPartCopy.input : undefined
|
|
1001
|
+
);
|
|
1002
|
+
} else if (
|
|
1003
|
+
'input' in contentPartCopy &&
|
|
1004
|
+
typeof contentPartCopy.input === 'string'
|
|
1005
|
+
) {
|
|
1006
|
+
// Non-tool_use tool blocks keep their legacy string handling.
|
|
1007
|
+
try {
|
|
1008
|
+
contentPartCopy.input = JSON.parse(contentPartCopy.input);
|
|
1009
|
+
} catch {
|
|
1010
|
+
contentPartCopy.input = {};
|
|
960
1011
|
}
|
|
961
1012
|
}
|
|
962
1013
|
|