@librechat/agents 3.2.57 → 3.2.59
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 +7 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +2 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +172 -0
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -0
- package/dist/cjs/main.cjs +7 -0
- package/dist/cjs/run.cjs +2 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +2 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +58 -9
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +4 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +57 -7
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -3
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +19 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +18 -1
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +68 -0
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -0
- package/dist/cjs/tools/search/rerankers.cjs +28 -11
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -4
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +14 -6
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +12 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +9 -9
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +7 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +2 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +4 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +171 -0
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -0
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/run.mjs +2 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +2 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +56 -10
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +4 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +54 -8
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -3
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +20 -2
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +18 -2
- package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +66 -0
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -0
- package/dist/esm/tools/search/rerankers.mjs +28 -11
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -4
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +14 -6
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +12 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +9 -9
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/langfuseTraceShaping.d.ts +20 -0
- package/dist/types/tools/BashExecutor.d.ts +13 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +3 -0
- package/dist/types/tools/eagerEventExecution.d.ts +8 -0
- package/dist/types/tools/search/keenable-search.d.ts +4 -0
- package/dist/types/tools/search/rerankers.d.ts +7 -2
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/types/graph.d.ts +22 -0
- package/dist/types/types/tools.d.ts +58 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +9 -0
- package/src/agents/__tests__/AgentContext.test.ts +40 -0
- package/src/graphs/Graph.ts +25 -2
- package/src/graphs/__tests__/composition.smoke.test.ts +53 -0
- package/src/langfuseToolOutputTracing.ts +11 -0
- package/src/langfuseTraceShaping.ts +280 -0
- package/src/llm/anthropic/inherited-stream-events.spec.ts +6 -3
- package/src/run.ts +3 -3
- package/src/specs/langfuse-routing.integration.test.ts +1 -1
- package/src/specs/langfuse-trace-shaping.test.ts +194 -0
- package/src/stream.ts +17 -1
- package/src/tools/BashExecutor.ts +107 -14
- package/src/tools/BashProgrammaticToolCalling.ts +20 -1
- package/src/tools/CodeExecutor.ts +113 -9
- package/src/tools/ProgrammaticToolCalling.ts +27 -1
- package/src/tools/ToolNode.ts +36 -4
- package/src/tools/__tests__/BashExecutor.test.ts +39 -0
- package/src/tools/__tests__/CodeExecutor.stateful.test.ts +113 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +44 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +86 -0
- package/src/tools/__tests__/eagerEventExecution.session.test.ts +92 -0
- package/src/tools/__tests__/hitl.test.ts +85 -0
- package/src/tools/eagerEventExecution.ts +32 -5
- package/src/tools/search/jina-reranker.test.ts +70 -1
- package/src/tools/search/keenable-search.ts +98 -0
- package/src/tools/search/keenable.test.ts +183 -0
- package/src/tools/search/rerankers.ts +41 -4
- package/src/tools/search/search.ts +58 -2
- package/src/tools/search/source-processing.test.ts +86 -0
- package/src/tools/search/tool.ts +29 -4
- package/src/tools/search/types.ts +42 -1
- package/src/tools/subagent/SubagentExecutor.ts +11 -0
- package/src/types/graph.ts +22 -0
- package/src/types/tools.ts +65 -0
- package/src/utils/title.ts +9 -9
|
@@ -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
|
-
):
|
|
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
|
|
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: '
|
|
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: '
|
|
1415
|
+
.withConfig({ runName: 'FormatConversation' })
|
|
1416
1416
|
.pipe(convoToTitleInput)
|
|
1417
1417
|
.pipe(titleChain)
|
|
1418
|
-
.withConfig({ runName: '
|
|
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}`, '
|
|
472
|
+
names: [`LibreChat Title: Parent ${tenantId}`, 'GenerateTitle'],
|
|
473
473
|
});
|
|
474
474
|
expectNoCrossTenantTrace({
|
|
475
475
|
tenantId,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
|
|
2
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
import {
|
|
4
|
+
shapeLangfuseSpan,
|
|
5
|
+
shouldDropLangfuseSpan,
|
|
6
|
+
} from '@/langfuseTraceShaping';
|
|
7
|
+
|
|
8
|
+
type TestSpan = ReadableSpan & {
|
|
9
|
+
name: string;
|
|
10
|
+
attributes: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function createSpan(
|
|
14
|
+
name: string,
|
|
15
|
+
attributes: Record<string, unknown> = {},
|
|
16
|
+
parentSpanId?: string
|
|
17
|
+
): TestSpan {
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
attributes,
|
|
21
|
+
...(parentSpanId != null ? { parentSpanId } : {}),
|
|
22
|
+
} as unknown as TestSpan;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const INPUT = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
|
|
26
|
+
const OUTPUT = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
|
|
27
|
+
const TRACE_INPUT = LangfuseOtelSpanAttributes.TRACE_INPUT;
|
|
28
|
+
const TRACE_OUTPUT = LangfuseOtelSpanAttributes.TRACE_OUTPUT;
|
|
29
|
+
|
|
30
|
+
describe('shouldDropLangfuseSpan', () => {
|
|
31
|
+
it('drops langgraph __start__ seed spans', () => {
|
|
32
|
+
expect(shouldDropLangfuseSpan('__start__')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('drops anonymous RunnableLambda pass-throughs', () => {
|
|
36
|
+
expect(shouldDropLangfuseSpan('RunnableLambda')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('keeps named observations', () => {
|
|
40
|
+
expect(shouldDropLangfuseSpan('GenerateTitle')).toBe(false);
|
|
41
|
+
expect(shouldDropLangfuseSpan('agent=openAI__gpt-5.4')).toBe(false);
|
|
42
|
+
expect(shouldDropLangfuseSpan('ChatOpenAI')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('shapeLangfuseSpan', () => {
|
|
47
|
+
it('strips the ephemeral agent id (provider__model) from agent node names', () => {
|
|
48
|
+
const span = createSpan('agent=openAI__gpt-5.4', {}, 'parent-1');
|
|
49
|
+
shapeLangfuseSpan(span);
|
|
50
|
+
expect(span.name).toBe('agent');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renames tool node spans to the pending tool names and scopes input to args', () => {
|
|
54
|
+
const messages = [
|
|
55
|
+
{ type: 'human', content: 'hello' },
|
|
56
|
+
{
|
|
57
|
+
type: 'ai',
|
|
58
|
+
content: '',
|
|
59
|
+
tool_calls: [
|
|
60
|
+
{
|
|
61
|
+
name: 'get_service_details',
|
|
62
|
+
args: { path: 'organizations/1' },
|
|
63
|
+
id: 'call_1',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
const span = createSpan(
|
|
69
|
+
'tools=openAI__gpt-5.4',
|
|
70
|
+
{ [INPUT]: JSON.stringify({ messages }) },
|
|
71
|
+
'parent-1'
|
|
72
|
+
);
|
|
73
|
+
shapeLangfuseSpan(span);
|
|
74
|
+
expect(span.name).toBe('get_service_details');
|
|
75
|
+
expect(JSON.parse(span.attributes[INPUT] as string)).toEqual([
|
|
76
|
+
{ name: 'get_service_details', args: { path: 'organizations/1' } },
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('joins multiple pending tool names and dedupes repeats', () => {
|
|
81
|
+
const messages = [
|
|
82
|
+
{
|
|
83
|
+
type: 'ai',
|
|
84
|
+
tool_calls: [
|
|
85
|
+
{ name: 'web_search', args: { q: 'a' }, id: '1' },
|
|
86
|
+
{ name: 'web_search', args: { q: 'b' }, id: '2' },
|
|
87
|
+
{ name: 'execute_code', args: { code: '1+1' }, id: '3' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
const span = createSpan(
|
|
92
|
+
'tool_batch',
|
|
93
|
+
{ [INPUT]: JSON.stringify({ messages }) },
|
|
94
|
+
'parent-1'
|
|
95
|
+
);
|
|
96
|
+
shapeLangfuseSpan(span);
|
|
97
|
+
expect(span.name).toBe('web_search, execute_code');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('reads tool calls from serialized langchain message kwargs', () => {
|
|
101
|
+
const messages = [
|
|
102
|
+
{
|
|
103
|
+
lc: 1,
|
|
104
|
+
type: 'constructor',
|
|
105
|
+
id: ['langchain_core', 'messages', 'AIMessage'],
|
|
106
|
+
kwargs: {
|
|
107
|
+
content: '',
|
|
108
|
+
tool_calls: [{ name: 'lookup', args: { id: 7 }, id: 'call_7' }],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const span = createSpan(
|
|
113
|
+
'tools=agent_abc',
|
|
114
|
+
{ [INPUT]: JSON.stringify({ messages }) },
|
|
115
|
+
'parent-1'
|
|
116
|
+
);
|
|
117
|
+
shapeLangfuseSpan(span);
|
|
118
|
+
expect(span.name).toBe('lookup');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('leaves tool node spans untouched when no tool calls are found', () => {
|
|
122
|
+
const original = JSON.stringify({
|
|
123
|
+
messages: [{ type: 'human', content: 'hi' }],
|
|
124
|
+
});
|
|
125
|
+
const span = createSpan(
|
|
126
|
+
'tools=agent_abc',
|
|
127
|
+
{ [INPUT]: original },
|
|
128
|
+
'parent-1'
|
|
129
|
+
);
|
|
130
|
+
shapeLangfuseSpan(span);
|
|
131
|
+
expect(span.name).toBe('tools=agent_abc');
|
|
132
|
+
expect(span.attributes[INPUT]).toBe(original);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('sets root span and trace input/output to the question and answer', () => {
|
|
136
|
+
const span = createSpan('LibreChat Agent', {
|
|
137
|
+
[INPUT]: JSON.stringify({
|
|
138
|
+
messages: [
|
|
139
|
+
{ type: 'system', content: 'You are helpful.' },
|
|
140
|
+
{ type: 'human', content: 'What is ClickHouse?' },
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
143
|
+
[OUTPUT]: JSON.stringify({
|
|
144
|
+
messages: [
|
|
145
|
+
{ type: 'human', content: 'What is ClickHouse?' },
|
|
146
|
+
{ type: 'ai', content: 'A columnar OLAP database.' },
|
|
147
|
+
],
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
shapeLangfuseSpan(span);
|
|
151
|
+
expect(span.attributes[INPUT]).toBe('What is ClickHouse?');
|
|
152
|
+
expect(span.attributes[TRACE_INPUT]).toBe('What is ClickHouse?');
|
|
153
|
+
expect(span.attributes[OUTPUT]).toBe('A columnar OLAP database.');
|
|
154
|
+
expect(span.attributes[TRACE_OUTPUT]).toBe('A columnar OLAP database.');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('extracts answer text from content part arrays', () => {
|
|
158
|
+
const span = createSpan('LibreChat Agent', {
|
|
159
|
+
[INPUT]: JSON.stringify([{ type: 'human', content: 'hi' }]),
|
|
160
|
+
[OUTPUT]: JSON.stringify({
|
|
161
|
+
messages: [
|
|
162
|
+
{
|
|
163
|
+
id: ['langchain_core', 'messages', 'AIMessage'],
|
|
164
|
+
kwargs: {
|
|
165
|
+
content: [
|
|
166
|
+
{ type: 'text', text: 'Hello ' },
|
|
167
|
+
{ type: 'text', text: 'there.' },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
shapeLangfuseSpan(span);
|
|
175
|
+
expect(span.attributes[INPUT]).toBe('hi');
|
|
176
|
+
expect(span.attributes[OUTPUT]).toBe('Hello there.');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not rewrite non-root spans with message payloads', () => {
|
|
180
|
+
const original = JSON.stringify({
|
|
181
|
+
messages: [{ type: 'human', content: 'hi' }],
|
|
182
|
+
});
|
|
183
|
+
const span = createSpan('ChatOpenAI', { [INPUT]: original }, 'parent-1');
|
|
184
|
+
shapeLangfuseSpan(span);
|
|
185
|
+
expect(span.attributes[INPUT]).toBe(original);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('preserves root attributes when extraction finds nothing', () => {
|
|
189
|
+
const span = createSpan('LibreChat Agent', { [INPUT]: 'plain text' });
|
|
190
|
+
shapeLangfuseSpan(span);
|
|
191
|
+
expect(span.attributes[INPUT]).toBe('plain text');
|
|
192
|
+
expect(span.attributes[TRACE_INPUT]).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
});
|
package/src/stream.ts
CHANGED
|
@@ -142,7 +142,18 @@ function isEagerExecutionExcludedTool(
|
|
|
142
142
|
// side-effecting: never prestart it speculatively (a revised/superseded turn
|
|
143
143
|
// would leave the write applied). Implies exclusion without the host having
|
|
144
144
|
// to also list the name in excludeToolNames.
|
|
145
|
-
|
|
145
|
+
if (graph.codeSessionToolNames?.includes(name) === true) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
// With stateful sessions on, execute_code/bash run against a DURABLE warm
|
|
149
|
+
// runtime. Speculative prestart there is unsafe: if the model revises the
|
|
150
|
+
// args, ToolNode discards the eager result but the mutation has already
|
|
151
|
+
// landed in the session workspace, corrupting later runs. Stateless mode
|
|
152
|
+
// uses a throwaway VM per call, so eager prestart stays safe there.
|
|
153
|
+
return (
|
|
154
|
+
graph.toolExecution?.sandbox?.statefulSessions === true &&
|
|
155
|
+
CODE_EXECUTION_TOOLS.has(name)
|
|
156
|
+
);
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
function isDirectGraphTool(
|
|
@@ -668,6 +679,11 @@ function createEagerToolExecutionPlan(args: {
|
|
|
668
679
|
return undefined;
|
|
669
680
|
}
|
|
670
681
|
|
|
682
|
+
/* No runtimeSessionHint here on purpose: the eager path is speculative, and
|
|
683
|
+
* code-session tools (the only ones that carry a hint) are excluded from
|
|
684
|
+
* eager prestart entirely when stateful sessions are on — see
|
|
685
|
+
* isEagerExecutionExcludedTool. The durable runtime is only ever touched by
|
|
686
|
+
* the committed ToolNode path. */
|
|
671
687
|
const plan = buildToolExecutionRequestPlan({
|
|
672
688
|
toolCalls: candidateToolCalls.map((toolCall) => ({
|
|
673
689
|
id: toolCall.id,
|