@librechat/agents 3.2.57 → 3.2.58

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.
Files changed (73) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +7 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +2 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  6. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  7. package/dist/cjs/langfuseTraceShaping.cjs +172 -0
  8. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -0
  9. package/dist/cjs/run.cjs +2 -2
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/tools/search/keenable-search.cjs +68 -0
  12. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -0
  13. package/dist/cjs/tools/search/rerankers.cjs +28 -11
  14. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  15. package/dist/cjs/tools/search/search.cjs +30 -4
  16. package/dist/cjs/tools/search/search.cjs.map +1 -1
  17. package/dist/cjs/tools/search/tool.cjs +14 -6
  18. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  19. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +12 -1
  20. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  21. package/dist/cjs/utils/title.cjs +9 -9
  22. package/dist/cjs/utils/title.cjs.map +1 -1
  23. package/dist/esm/agents/AgentContext.mjs +7 -1
  24. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +2 -2
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/langfuseToolOutputTracing.mjs +4 -0
  28. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  29. package/dist/esm/langfuseTraceShaping.mjs +171 -0
  30. package/dist/esm/langfuseTraceShaping.mjs.map +1 -0
  31. package/dist/esm/run.mjs +2 -2
  32. package/dist/esm/run.mjs.map +1 -1
  33. package/dist/esm/tools/search/keenable-search.mjs +66 -0
  34. package/dist/esm/tools/search/keenable-search.mjs.map +1 -0
  35. package/dist/esm/tools/search/rerankers.mjs +28 -11
  36. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  37. package/dist/esm/tools/search/search.mjs +30 -4
  38. package/dist/esm/tools/search/search.mjs.map +1 -1
  39. package/dist/esm/tools/search/tool.mjs +14 -6
  40. package/dist/esm/tools/search/tool.mjs.map +1 -1
  41. package/dist/esm/tools/subagent/SubagentExecutor.mjs +12 -1
  42. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  43. package/dist/esm/utils/title.mjs +9 -9
  44. package/dist/esm/utils/title.mjs.map +1 -1
  45. package/dist/types/langfuseTraceShaping.d.ts +20 -0
  46. package/dist/types/tools/search/keenable-search.d.ts +4 -0
  47. package/dist/types/tools/search/rerankers.d.ts +7 -2
  48. package/dist/types/tools/search/types.d.ts +38 -1
  49. package/dist/types/types/graph.d.ts +22 -0
  50. package/package.json +1 -1
  51. package/src/agents/AgentContext.ts +9 -0
  52. package/src/agents/__tests__/AgentContext.test.ts +40 -0
  53. package/src/graphs/Graph.ts +25 -2
  54. package/src/graphs/__tests__/composition.smoke.test.ts +53 -0
  55. package/src/langfuseToolOutputTracing.ts +11 -0
  56. package/src/langfuseTraceShaping.ts +280 -0
  57. package/src/llm/anthropic/inherited-stream-events.spec.ts +6 -3
  58. package/src/run.ts +3 -3
  59. package/src/specs/langfuse-routing.integration.test.ts +1 -1
  60. package/src/specs/langfuse-trace-shaping.test.ts +194 -0
  61. package/src/tools/__tests__/SubagentExecutor.test.ts +44 -0
  62. package/src/tools/__tests__/hitl.test.ts +85 -0
  63. package/src/tools/search/jina-reranker.test.ts +70 -1
  64. package/src/tools/search/keenable-search.ts +98 -0
  65. package/src/tools/search/keenable.test.ts +183 -0
  66. package/src/tools/search/rerankers.ts +41 -4
  67. package/src/tools/search/search.ts +58 -2
  68. package/src/tools/search/source-processing.test.ts +86 -0
  69. package/src/tools/search/tool.ts +29 -4
  70. package/src/tools/search/types.ts +42 -1
  71. package/src/tools/subagent/SubagentExecutor.ts +11 -0
  72. package/src/types/graph.ts +22 -0
  73. package/src/utils/title.ts +9 -9
@@ -42,6 +42,46 @@ describe('AgentContext', () => {
42
42
  schema: { type: 'object', properties: {} },
43
43
  }) as unknown as t.GenericTool;
44
44
 
45
+ describe('fromConfig — host-supplied graphTools', () => {
46
+ it('copies graphTools onto the context without aliasing the host array', () => {
47
+ const askTool = createMockTool('ask_user_question');
48
+ const hostGraphTools = [askTool];
49
+ const ctx = createBasicContext({
50
+ agentConfig: { graphTools: hostGraphTools },
51
+ });
52
+
53
+ expect(ctx.graphTools).toEqual([askTool]);
54
+ expect(ctx.graphTools).not.toBe(hostGraphTools);
55
+
56
+ /** The SDK pushes graph-managed tools (handoff/subagent) into this
57
+ * array later — that must never mutate the host's input. */
58
+ (ctx.graphTools as t.GenericTool[]).push(createMockTool('subagent'));
59
+ expect(hostGraphTools).toHaveLength(1);
60
+ });
61
+
62
+ it('leaves graphTools undefined when the host supplies none (or an empty list)', () => {
63
+ expect(createBasicContext().graphTools).toBeUndefined();
64
+ expect(
65
+ createBasicContext({ agentConfig: { graphTools: [] } }).graphTools
66
+ ).toBeUndefined();
67
+ });
68
+
69
+ it('binds host graphTools to the model in event-driven mode', () => {
70
+ const askTool = createMockTool('ask_user_question');
71
+ const ctx = createBasicContext({
72
+ agentConfig: {
73
+ graphTools: [askTool],
74
+ toolDefinitions: [{ name: 'echo', description: 'host event tool' }],
75
+ },
76
+ });
77
+
78
+ const bound = ctx.getToolsForBinding() ?? [];
79
+ const names = (bound as Array<{ name?: string }>).map((t) => t.name);
80
+ expect(names).toContain('ask_user_question');
81
+ expect(names).toContain('echo');
82
+ });
83
+ });
84
+
45
85
  describe('System Runnable - Lazy Creation', () => {
46
86
  it('creates system runnable on first access', () => {
47
87
  const ctx = createBasicContext({
@@ -1140,7 +1140,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1140
1140
  getToolCount(): number {
1141
1141
  const context = this.agentContexts.get(this.defaultAgentId);
1142
1142
  return (
1143
- (context?.tools?.length ?? 0) + (context?.toolDefinitions?.length ?? 0)
1143
+ (context?.tools?.length ?? 0) +
1144
+ (context?.toolDefinitions?.length ?? 0) +
1145
+ /**
1146
+ * Graph-managed + host-supplied direct tools (handoff, subagent,
1147
+ * `AgentInputs.graphTools`) are bound to the model and token-accounted,
1148
+ * so a count that omits them under-reports the run's tool surface
1149
+ * (Codex #289 P3).
1150
+ */
1151
+ (context?.graphTools?.length ?? 0)
1144
1152
  );
1145
1153
  }
1146
1154
 
@@ -1286,10 +1294,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1286
1294
  graphTools && graphTools.length > 0
1287
1295
  ? [...baseTools, ...graphTools]
1288
1296
  : baseTools;
1297
+ /**
1298
+ * ToolNode treats a supplied `toolMap` as authoritative (it only derives
1299
+ * one from `tools` when the param is undefined), so when graphTools force
1300
+ * us to build a merged map here, an absent `currentToolMap` must be
1301
+ * seeded from the BASE tools first — otherwise ordinary tools stay bound
1302
+ * to the model but vanish from the execution map and every call to them
1303
+ * fails as an unknown tool (Codex #289 round 2).
1304
+ */
1289
1305
  const traditionalToolMap =
1290
1306
  graphTools && graphTools.length > 0
1291
1307
  ? new Map([
1292
- ...(currentToolMap ?? new Map()),
1308
+ ...(currentToolMap ??
1309
+ new Map(
1310
+ baseTools
1311
+ .filter(
1312
+ (t): t is t.GenericTool & { name: string } => 'name' in t
1313
+ )
1314
+ .map((t) => [t.name, t] as [string, t.GenericTool])
1315
+ )),
1293
1316
  ...graphTools
1294
1317
  .filter((t): t is t.GenericTool & { name: string } => 'name' in t)
1295
1318
  .map((t) => [t.name, t] as [string, t.GenericTool]),
@@ -73,6 +73,59 @@ const expectCompiledWorkflow = (
73
73
  };
74
74
 
75
75
  describe('LangGraph composition smoke tests', () => {
76
+ it('getToolCount includes direct graph tools (host-supplied graphTools) alongside instances and definitions', () => {
77
+ const askLikeTool = { name: 'ask_user_question' } as unknown as NonNullable<
78
+ t.AgentInputs['graphTools']
79
+ >[number];
80
+ const graph = new StandardGraph({
81
+ runId: 'toolcount-smoke',
82
+ agents: [
83
+ {
84
+ ...makeAgent('agent'),
85
+ toolDefinitions: [
86
+ { name: 'evt1', description: 'event tool one' },
87
+ { name: 'evt2', description: 'event tool two' },
88
+ ],
89
+ graphTools: [askLikeTool],
90
+ },
91
+ ],
92
+ });
93
+ // 2 schema-only event tools + 1 in-process direct tool — all bound to the
94
+ // model and token-accounted, so all must be counted (Codex #289 P3).
95
+ expect(graph.getToolCount()).toBe(3);
96
+ });
97
+
98
+ it('keeps ordinary tools executable when graphTools are added without a host toolMap (traditional mode)', () => {
99
+ type HostTool = NonNullable<t.AgentInputs['graphTools']>[number];
100
+ const echoTool = { name: 'echo_tool' } as unknown as HostTool;
101
+ const askLikeTool = { name: 'ask_user_question' } as unknown as HostTool;
102
+ const graph = new StandardGraph({
103
+ runId: 'toolmap-merge-smoke',
104
+ agents: [
105
+ {
106
+ ...makeAgent('agent'),
107
+ tools: [echoTool],
108
+ graphTools: [askLikeTool],
109
+ },
110
+ ],
111
+ });
112
+ const agentContext = graph.agentContexts.get('agent');
113
+ const node = graph.initializeTools({
114
+ currentTools: agentContext?.tools,
115
+ currentToolMap: undefined,
116
+ agentContext,
117
+ });
118
+ /**
119
+ * ToolNode treats a provided toolMap as authoritative — if the merged map
120
+ * built for graphTools drops the base tools, they stay bound to the model
121
+ * but every call fails as an unknown tool (Codex #289 round 2).
122
+ */
123
+ const toolMap = (node as unknown as { toolMap: Map<string, unknown> })
124
+ .toolMap;
125
+ expect(toolMap.has('echo_tool')).toBe(true);
126
+ expect(toolMap.has('ask_user_question')).toBe(true);
127
+ });
128
+
76
129
  it('clears run-scoped eager tool state on reset', () => {
77
130
  const graph = new StandardGraph({
78
131
  runId: 'standard-eager-reset',
@@ -16,6 +16,10 @@ import {
16
16
  resolveLangfuseConfig,
17
17
  resolveToolOutputTracingConfig,
18
18
  } from '@/langfuseConfig';
19
+ import {
20
+ shapeLangfuseSpan,
21
+ shouldDropLangfuseSpan,
22
+ } from '@/langfuseTraceShaping';
19
23
  import { resolveToolOutputTracingConfigForSpan } from '@/langfuseRuntimeScope';
20
24
 
21
25
  export { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT, resolveLangfuseConfig };
@@ -407,6 +411,9 @@ class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
407
411
  }
408
412
 
409
413
  onStart(span: Span, parentContext: Context): void {
414
+ if (shouldDropLangfuseSpan(span.name)) {
415
+ return;
416
+ }
410
417
  const config =
411
418
  resolveToolOutputTracingConfigForSpan(parentContext) ??
412
419
  this.fallbackConfig;
@@ -417,7 +424,11 @@ class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
417
424
  }
418
425
 
419
426
  onEnd(span: ReadableSpan): void {
427
+ if (shouldDropLangfuseSpan(span.name)) {
428
+ return;
429
+ }
420
430
  classifyLangfuseToolNodeSpan(span);
431
+ shapeLangfuseSpan(span);
421
432
  const config = this.spanConfigs.get(span) ?? this.fallbackConfig;
422
433
  if (config != null) {
423
434
  redactLangfuseSpanToolOutputs(span, config);
@@ -0,0 +1,280 @@
1
+ import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
2
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
+
4
+ const LANGGRAPH_START_NODE = '__start__';
5
+ const ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';
6
+ const LANGGRAPH_AGENT_NODE_PREFIX = 'agent=';
7
+ const LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';
8
+ const TOOL_BATCH_RUN_NAME = 'tool_batch';
9
+ const AGENT_NODE_SPAN_NAME = 'agent';
10
+
11
+ type MutableSpan = ReadableSpan & {
12
+ name: string;
13
+ attributes: Record<string, unknown>;
14
+ };
15
+
16
+ type SerializedToolCall = {
17
+ name: string;
18
+ args: unknown;
19
+ id?: string;
20
+ };
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return value != null && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+
26
+ function parseAttributeValue(value: unknown): unknown {
27
+ if (typeof value !== 'string') {
28
+ return value;
29
+ }
30
+ const trimmed = value.trim();
31
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
32
+ return value;
33
+ }
34
+ try {
35
+ return JSON.parse(value) as unknown;
36
+ } catch {
37
+ return value;
38
+ }
39
+ }
40
+
41
+ function getMessageArray(
42
+ value: unknown
43
+ ): Record<string, unknown>[] | undefined {
44
+ if (Array.isArray(value)) {
45
+ const records = value.filter(isRecord);
46
+ return records.length > 0 ? records : undefined;
47
+ }
48
+ if (!isRecord(value)) {
49
+ return undefined;
50
+ }
51
+ return (
52
+ getMessageArray(value.messages) ??
53
+ getMessageArray(value.input) ??
54
+ getMessageArray(value.output)
55
+ );
56
+ }
57
+
58
+ function getMessageRole(message: Record<string, unknown>): string | undefined {
59
+ const id = message.id;
60
+ if (Array.isArray(id)) {
61
+ const className = id[id.length - 1];
62
+ if (typeof className === 'string') {
63
+ if (className.includes('Human')) {
64
+ return 'user';
65
+ }
66
+ if (className.includes('AI')) {
67
+ return 'assistant';
68
+ }
69
+ if (className.includes('System')) {
70
+ return 'system';
71
+ }
72
+ if (className.includes('Tool')) {
73
+ return 'tool';
74
+ }
75
+ }
76
+ }
77
+ const rawRole =
78
+ message.type ?? message._type ?? message.role ?? message.sender;
79
+ if (typeof rawRole !== 'string') {
80
+ const kwargs = message.kwargs;
81
+ return isRecord(kwargs) ? getMessageRole(kwargs) : undefined;
82
+ }
83
+ const normalized = rawRole.toLowerCase();
84
+ if (normalized === 'human') {
85
+ return 'user';
86
+ }
87
+ if (normalized === 'ai') {
88
+ return 'assistant';
89
+ }
90
+ return normalized;
91
+ }
92
+
93
+ function getMessageText(message: Record<string, unknown>): string | undefined {
94
+ const content =
95
+ message.content ??
96
+ (isRecord(message.kwargs) ? message.kwargs.content : undefined) ??
97
+ (isRecord(message.data) ? message.data.content : undefined);
98
+ if (typeof content === 'string') {
99
+ return content;
100
+ }
101
+ if (!Array.isArray(content)) {
102
+ return undefined;
103
+ }
104
+ const text = content
105
+ .filter(isRecord)
106
+ .map((part) => (typeof part.text === 'string' ? part.text : ''))
107
+ .join('');
108
+ return text === '' ? undefined : text;
109
+ }
110
+
111
+ function findLastMessageText(value: unknown, role: string): string | undefined {
112
+ const messages = getMessageArray(value);
113
+ if (messages == null) {
114
+ return undefined;
115
+ }
116
+ for (let i = messages.length - 1; i >= 0; i--) {
117
+ if (getMessageRole(messages[i]) !== role) {
118
+ continue;
119
+ }
120
+ const text = getMessageText(messages[i]);
121
+ if (text != null && text.trim() !== '') {
122
+ return text;
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ function normalizeToolCall(value: unknown): SerializedToolCall | undefined {
129
+ if (!isRecord(value)) {
130
+ return undefined;
131
+ }
132
+ const fn = value.function;
133
+ if (isRecord(fn) && typeof fn.name === 'string') {
134
+ return {
135
+ name: fn.name,
136
+ args: parseAttributeValue(fn.arguments),
137
+ ...(typeof value.id === 'string' ? { id: value.id } : {}),
138
+ };
139
+ }
140
+ if (typeof value.name !== 'string') {
141
+ return undefined;
142
+ }
143
+ return {
144
+ name: value.name,
145
+ args: value.args ?? {},
146
+ ...(typeof value.id === 'string' ? { id: value.id } : {}),
147
+ };
148
+ }
149
+
150
+ function getMessageToolCalls(
151
+ message: Record<string, unknown>
152
+ ): SerializedToolCall[] {
153
+ const rawCalls =
154
+ message.tool_calls ??
155
+ (isRecord(message.kwargs) ? message.kwargs.tool_calls : undefined) ??
156
+ (isRecord(message.additional_kwargs)
157
+ ? message.additional_kwargs.tool_calls
158
+ : undefined) ??
159
+ (isRecord(message.data) ? message.data.tool_calls : undefined);
160
+ if (!Array.isArray(rawCalls)) {
161
+ return [];
162
+ }
163
+ const calls: SerializedToolCall[] = [];
164
+ for (const rawCall of rawCalls) {
165
+ const call = normalizeToolCall(rawCall);
166
+ if (call != null) {
167
+ calls.push(call);
168
+ }
169
+ }
170
+ return calls;
171
+ }
172
+
173
+ /** Latest assistant turn's tool calls — the calls this tool node is executing. */
174
+ function findPendingToolCalls(value: unknown): SerializedToolCall[] {
175
+ const messages = getMessageArray(value);
176
+ if (messages == null) {
177
+ return [];
178
+ }
179
+ for (let i = messages.length - 1; i >= 0; i--) {
180
+ if (getMessageRole(messages[i]) !== 'assistant') {
181
+ continue;
182
+ }
183
+ const calls = getMessageToolCalls(messages[i]);
184
+ if (calls.length > 0) {
185
+ return calls;
186
+ }
187
+ }
188
+ return [];
189
+ }
190
+
191
+ function getRootSpanParentId(span: ReadableSpan): string | undefined {
192
+ const legacyParent = (span as { parentSpanId?: string }).parentSpanId;
193
+ if (typeof legacyParent === 'string' && legacyParent !== '') {
194
+ return legacyParent;
195
+ }
196
+ const parentContext = (span as { parentSpanContext?: { spanId?: string } })
197
+ .parentSpanContext;
198
+ const spanId = parentContext?.spanId;
199
+ return typeof spanId === 'string' && spanId !== '' ? spanId : undefined;
200
+ }
201
+
202
+ function isRootSpan(span: ReadableSpan): boolean {
203
+ return getRootSpanParentId(span) == null;
204
+ }
205
+
206
+ /**
207
+ * LangGraph plumbing observations that add noise without information:
208
+ * the duplicated `__start__` channel-seed nodes and anonymous
209
+ * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).
210
+ */
211
+ export function shouldDropLangfuseSpan(spanName: string): boolean {
212
+ return (
213
+ spanName === LANGGRAPH_START_NODE || spanName === ANONYMOUS_LAMBDA_NAME
214
+ );
215
+ }
216
+
217
+ function shapeToolNodeSpan(span: MutableSpan): void {
218
+ const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
219
+ const calls = findPendingToolCalls(
220
+ parseAttributeValue(span.attributes[inputKey])
221
+ );
222
+ if (calls.length === 0) {
223
+ return;
224
+ }
225
+ span.name = [...new Set(calls.map((call) => call.name))].join(', ');
226
+ span.attributes[inputKey] = JSON.stringify(
227
+ calls.map(({ name, args }) => ({ name, args }))
228
+ );
229
+ }
230
+
231
+ function shapeRootSpan(span: MutableSpan): void {
232
+ const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
233
+ const outputKey = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
234
+ const question = findLastMessageText(
235
+ parseAttributeValue(span.attributes[inputKey]),
236
+ 'user'
237
+ );
238
+ const answer = findLastMessageText(
239
+ parseAttributeValue(span.attributes[outputKey]),
240
+ 'assistant'
241
+ );
242
+ if (question != null) {
243
+ span.attributes[inputKey] = question;
244
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = question;
245
+ }
246
+ if (answer != null) {
247
+ span.attributes[outputKey] = answer;
248
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = answer;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Reshapes spans per Langfuse-team feedback before export:
254
+ * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
255
+ * (`provider__model`) — strip it so switching models doesn't break
256
+ * name-based logic (item 1).
257
+ * - Tool node spans are renamed to the actual tool name(s) and their
258
+ * input scoped to the pending tool-call args instead of the whole
259
+ * chat history (items 3 & 4).
260
+ * - The root span (and trace) input/output become the user question and
261
+ * assistant response so the session view reads as a conversation
262
+ * (item 2).
263
+ */
264
+ export function shapeLangfuseSpan(span: ReadableSpan): void {
265
+ const mutable = span as MutableSpan;
266
+ if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {
267
+ mutable.name = AGENT_NODE_SPAN_NAME;
268
+ return;
269
+ }
270
+ if (
271
+ mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX) ||
272
+ mutable.name === TOOL_BATCH_RUN_NAME
273
+ ) {
274
+ shapeToolNodeSpan(mutable);
275
+ return;
276
+ }
277
+ if (isRootSpan(span)) {
278
+ shapeRootSpan(mutable);
279
+ }
280
+ }
@@ -32,7 +32,6 @@ import { HumanMessage } from '@langchain/core/messages';
32
32
  import { ChatModelStream } from '@langchain/core/language_models/stream';
33
33
  import type { BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
34
34
  import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
35
- import type { Stream } from '@anthropic-ai/sdk/streaming';
36
35
  import type {
37
36
  AnthropicMessageStreamEvent,
38
37
  AnthropicRequestOptions,
@@ -57,10 +56,14 @@ class MockStreamChatAnthropic extends ChatAnthropic {
57
56
  this.mockEvents = mockEvents;
58
57
  }
59
58
 
59
+ // Inherit the base method's own Stream identity instead of importing
60
+ // @anthropic-ai/sdk/streaming here: the SDK ships dual .d.ts/.d.mts types,
61
+ // and an independent import can resolve to the other format's identity
62
+ // depending on compile order (flaky TS2416 in sharded CI).
60
63
  protected override async createStreamWithRetry(
61
64
  request: AnthropicStreamingMessageCreateParams,
62
65
  _options?: AnthropicRequestOptions
63
- ): Promise<Stream<AnthropicMessageStreamEvent>> {
66
+ ): ReturnType<ChatAnthropic['createStreamWithRetry']> {
64
67
  this.capturedRequest = request;
65
68
  const events = this.mockEvents;
66
69
  return {
@@ -70,7 +73,7 @@ class MockStreamChatAnthropic extends ChatAnthropic {
70
73
  yield event as AnthropicMessageStreamEvent;
71
74
  }
72
75
  },
73
- } as unknown as Stream<AnthropicMessageStreamEvent>;
76
+ } as unknown as Awaited<ReturnType<ChatAnthropic['createStreamWithRetry']>>;
74
77
  }
75
78
  }
76
79
 
package/src/run.ts CHANGED
@@ -1403,7 +1403,7 @@ export class Run<_T extends t.BaseGraphState> {
1403
1403
  inputText,
1404
1404
  skipLanguage,
1405
1405
  }),
1406
- }).withConfig({ runName: 'ConvoTransform' });
1406
+ }).withConfig({ runName: 'PrepareTitleInput' });
1407
1407
 
1408
1408
  const titleChain =
1409
1409
  titleMethod === TitleMethod.COMPLETION
@@ -1412,10 +1412,10 @@ export class Run<_T extends t.BaseGraphState> {
1412
1412
 
1413
1413
  /** Pipes `convoTemplate` -> `transformer` -> `titleChain` */
1414
1414
  const fullChain = convoTemplate
1415
- .withConfig({ runName: 'ConvoTemplate' })
1415
+ .withConfig({ runName: 'FormatConversation' })
1416
1416
  .pipe(convoToTitleInput)
1417
1417
  .pipe(titleChain)
1418
- .withConfig({ runName: 'TitleChain' });
1418
+ .withConfig({ runName: 'GenerateConversationTitle' });
1419
1419
 
1420
1420
  const invokeConfig = Object.assign({}, chainOptions, {
1421
1421
  run_id: this.id,
@@ -469,7 +469,7 @@ describe('Langfuse per-run routing integration', () => {
469
469
  expectNamedSpansUseTraceId({
470
470
  starts,
471
471
  traceId: titleTraceId,
472
- names: [`LibreChat Title: Parent ${tenantId}`, 'CompletionTitleChain'],
472
+ names: [`LibreChat Title: Parent ${tenantId}`, 'GenerateTitle'],
473
473
  });
474
474
  expectNoCrossTenantTrace({
475
475
  tenantId,