@librechat/agents 3.1.95 → 3.1.97
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 +54 -21
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +120 -9
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +30 -226
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +465 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -0
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +142 -69
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +29 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +20 -8
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +10 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +56 -23
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +118 -9
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +28 -224
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +457 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -0
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/run.mjs +144 -71
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +29 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +20 -8
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +10 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +5 -1
- package/dist/types/instrumentation.d.ts +5 -1
- package/dist/types/langfuse.d.ts +6 -28
- package/dist/types/langfuseToolOutputTracing.d.ts +20 -0
- package/dist/types/run.d.ts +5 -1
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +4 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +2 -0
- package/dist/types/types/graph.d.ts +30 -0
- package/dist/types/types/run.d.ts +6 -0
- package/dist/types/types/tools.d.ts +7 -0
- package/package.json +2 -1
- package/src/graphs/Graph.ts +90 -34
- package/src/instrumentation.ts +172 -11
- package/src/langfuse.ts +59 -324
- package/src/langfuseToolOutputTracing.ts +683 -0
- package/src/run.ts +190 -87
- package/src/specs/langfuse-callbacks.test.ts +178 -1
- package/src/specs/langfuse-config.test.ts +112 -76
- package/src/specs/langfuse-instrumentation.test.ts +283 -0
- package/src/specs/langfuse-metadata.test.ts +54 -1
- package/src/specs/langfuse-tool-output-tracing.test.ts +588 -0
- package/src/tools/BashProgrammaticToolCalling.ts +39 -5
- package/src/tools/ToolNode.ts +28 -7
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +54 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +72 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +32 -0
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +41 -0
- package/src/tools/subagent/SubagentExecutor.ts +11 -6
- package/src/types/graph.ts +32 -0
- package/src/types/run.ts +6 -0
- package/src/types/tools.ts +7 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { AIMessage, ToolMessage, HumanMessage } from '@langchain/core/messages';
|
|
2
|
+
import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
|
|
3
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
4
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
5
|
+
import type { TPayload } from '@/types';
|
|
6
|
+
import {
|
|
7
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
8
|
+
redactLangfuseSpanToolOutputs,
|
|
9
|
+
resolveLangfuseConfig,
|
|
10
|
+
shouldTraceToolNodeForLangfuse,
|
|
11
|
+
type ResolvedLangfuseToolOutputTracingConfig,
|
|
12
|
+
} from '@/langfuseToolOutputTracing';
|
|
13
|
+
import { formatAgentMessages } from '@/messages/format';
|
|
14
|
+
import { ContentTypes } from '@/common';
|
|
15
|
+
|
|
16
|
+
type SerializedLangfuseChatMessage = {
|
|
17
|
+
content: BaseMessage['content'];
|
|
18
|
+
role?: string;
|
|
19
|
+
additional_kwargs?: BaseMessage['additional_kwargs'];
|
|
20
|
+
tool_calls?:
|
|
21
|
+
| NonNullable<AIMessage['tool_calls']>
|
|
22
|
+
| NonNullable<BaseMessage['additional_kwargs']['tool_calls']>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type RedactedMessage = {
|
|
26
|
+
role?: string;
|
|
27
|
+
content?: string;
|
|
28
|
+
tool_calls?: Array<{
|
|
29
|
+
id?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
args?: {
|
|
32
|
+
query?: string;
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function createSpan(
|
|
38
|
+
name: string,
|
|
39
|
+
attributes: Record<string, unknown>
|
|
40
|
+
): ReadableSpan {
|
|
41
|
+
return { name, attributes } as unknown as ReadableSpan;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createConfig(
|
|
45
|
+
overrides: Partial<ResolvedLangfuseToolOutputTracingConfig> = {}
|
|
46
|
+
): ResolvedLangfuseToolOutputTracingConfig {
|
|
47
|
+
return {
|
|
48
|
+
enabled: true,
|
|
49
|
+
redactedToolNames: new Set<string>(),
|
|
50
|
+
redactedToolNameMatchMode: 'exact',
|
|
51
|
+
redactionText: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function serializeMessageForLangfuse(
|
|
57
|
+
message: BaseMessage
|
|
58
|
+
): SerializedLangfuseChatMessage {
|
|
59
|
+
if (message instanceof HumanMessage) {
|
|
60
|
+
return { content: message.content, role: 'user' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (message instanceof AIMessage) {
|
|
64
|
+
const response: SerializedLangfuseChatMessage = {
|
|
65
|
+
content: message.content,
|
|
66
|
+
role: 'assistant',
|
|
67
|
+
};
|
|
68
|
+
if (message.tool_calls != null && message.tool_calls.length > 0) {
|
|
69
|
+
response.tool_calls = message.tool_calls;
|
|
70
|
+
}
|
|
71
|
+
if (message.additional_kwargs.tool_calls != null) {
|
|
72
|
+
response.tool_calls = message.additional_kwargs.tool_calls;
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (message instanceof ToolMessage) {
|
|
78
|
+
return {
|
|
79
|
+
content: message.content,
|
|
80
|
+
additional_kwargs: message.additional_kwargs,
|
|
81
|
+
role: message.name,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return message.name != null
|
|
86
|
+
? { content: message.content, role: message.name }
|
|
87
|
+
: { content: message.content };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readJsonAttribute<T>(span: ReadableSpan, key: string): T {
|
|
91
|
+
return JSON.parse(span.attributes[key] as string) as T;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('Langfuse tool output tracing redaction', () => {
|
|
95
|
+
const originalEnv = process.env;
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
process.env = { ...originalEnv };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
process.env = originalEnv;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('enables ToolNode tracing only when Langfuse is active by default', () => {
|
|
106
|
+
delete process.env.LANGFUSE_SECRET_KEY;
|
|
107
|
+
delete process.env.LANGFUSE_PUBLIC_KEY;
|
|
108
|
+
delete process.env.LANGFUSE_BASE_URL;
|
|
109
|
+
|
|
110
|
+
expect(shouldTraceToolNodeForLangfuse({})).toBe(false);
|
|
111
|
+
expect(
|
|
112
|
+
shouldTraceToolNodeForLangfuse({
|
|
113
|
+
runLangfuse: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
publicKey: 'pk-run',
|
|
116
|
+
secretKey: 'sk-run',
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
).toBe(true);
|
|
120
|
+
expect(
|
|
121
|
+
shouldTraceToolNodeForLangfuse({
|
|
122
|
+
agentLangfuse: {
|
|
123
|
+
enabled: true,
|
|
124
|
+
publicKey: 'pk-agent',
|
|
125
|
+
secretKey: 'sk-agent',
|
|
126
|
+
baseUrl: 'https://langfuse.test',
|
|
127
|
+
toolNodeTracing: { enabled: true },
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
).toBe(true);
|
|
131
|
+
|
|
132
|
+
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
|
|
133
|
+
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
|
|
134
|
+
process.env.LANGFUSE_BASE_URL = 'https://langfuse.test';
|
|
135
|
+
|
|
136
|
+
expect(shouldTraceToolNodeForLangfuse({})).toBe(true);
|
|
137
|
+
expect(
|
|
138
|
+
shouldTraceToolNodeForLangfuse({
|
|
139
|
+
runLangfuse: { toolNodeTracing: { enabled: true } },
|
|
140
|
+
})
|
|
141
|
+
).toBe(true);
|
|
142
|
+
expect(
|
|
143
|
+
shouldTraceToolNodeForLangfuse({
|
|
144
|
+
runLangfuse: { toolNodeTracing: { enabled: false } },
|
|
145
|
+
})
|
|
146
|
+
).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('lets agent Langfuse enablement override disabled run defaults for ToolNode tracing', () => {
|
|
150
|
+
delete process.env.LANGFUSE_SECRET_KEY;
|
|
151
|
+
delete process.env.LANGFUSE_PUBLIC_KEY;
|
|
152
|
+
delete process.env.LANGFUSE_BASE_URL;
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
shouldTraceToolNodeForLangfuse({
|
|
156
|
+
runLangfuse: {
|
|
157
|
+
enabled: false,
|
|
158
|
+
},
|
|
159
|
+
agentLangfuse: {
|
|
160
|
+
enabled: true,
|
|
161
|
+
publicKey: 'pk-agent',
|
|
162
|
+
secretKey: 'sk-agent',
|
|
163
|
+
baseUrl: 'https://langfuse.test',
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('keeps ToolNode tracing disabled when resolved Langfuse is disabled', () => {
|
|
170
|
+
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
|
|
171
|
+
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
|
|
172
|
+
|
|
173
|
+
expect(
|
|
174
|
+
shouldTraceToolNodeForLangfuse({
|
|
175
|
+
runLangfuse: {
|
|
176
|
+
enabled: false,
|
|
177
|
+
toolNodeTracing: { enabled: true },
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('redacts raw tool observation output when tool output tracing is disabled', () => {
|
|
184
|
+
const span = createSpan('execute_sql', {
|
|
185
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
|
|
186
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: '{"query":"select 1"}',
|
|
187
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: 'secret rows',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
191
|
+
|
|
192
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
|
|
193
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
194
|
+
);
|
|
195
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]).toBe(
|
|
196
|
+
'{"query":"select 1"}'
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('redacts ToolMessage content inside serialized generation inputs', () => {
|
|
201
|
+
const messages = [
|
|
202
|
+
{ role: 'user', content: 'show tables' },
|
|
203
|
+
{
|
|
204
|
+
role: 'execute_sql',
|
|
205
|
+
content: 'private query result',
|
|
206
|
+
additional_kwargs: {},
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
const span = createSpan('gpt-4o', {
|
|
210
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
211
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(messages),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
215
|
+
|
|
216
|
+
const redacted = JSON.parse(
|
|
217
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] as string
|
|
218
|
+
) as Array<{ role: string; content: string }>;
|
|
219
|
+
expect(redacted[0].content).toBe('show tables');
|
|
220
|
+
expect(redacted[1].content).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('redacts only configured tool names when output tracing stays enabled', () => {
|
|
224
|
+
const messages = [
|
|
225
|
+
{ role: 'execute_sql', content: 'private query result' },
|
|
226
|
+
{ role: 'bash', content: 'public build log' },
|
|
227
|
+
];
|
|
228
|
+
const span = createSpan('LangGraph', {
|
|
229
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify({
|
|
230
|
+
messages,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
redactLangfuseSpanToolOutputs(
|
|
235
|
+
span,
|
|
236
|
+
createConfig({
|
|
237
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const redacted = JSON.parse(
|
|
242
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] as string
|
|
243
|
+
) as { messages: Array<{ role: string; content: string }> };
|
|
244
|
+
expect(redacted.messages[0].content).toBe(
|
|
245
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
246
|
+
);
|
|
247
|
+
expect(redacted.messages[1].content).toBe('public build log');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('uses nested ToolMessage names instead of generic tool role', () => {
|
|
251
|
+
const messages = [
|
|
252
|
+
{
|
|
253
|
+
role: 'tool',
|
|
254
|
+
content: 'private query result',
|
|
255
|
+
kwargs: {
|
|
256
|
+
name: 'execute_sql',
|
|
257
|
+
tool_call_id: 'call_1',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
const span = createSpan('gpt-4o', {
|
|
262
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
263
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(messages),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
redactLangfuseSpanToolOutputs(
|
|
267
|
+
span,
|
|
268
|
+
createConfig({
|
|
269
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const redacted = readJsonAttribute<Array<{ content: string }>>(
|
|
274
|
+
span,
|
|
275
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
276
|
+
);
|
|
277
|
+
expect(redacted[0].content).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('maps tool_call_id to the preceding tool call name for allowlisted redaction', () => {
|
|
281
|
+
const messages = [
|
|
282
|
+
{
|
|
283
|
+
role: 'assistant',
|
|
284
|
+
content: '',
|
|
285
|
+
tool_calls: [
|
|
286
|
+
{
|
|
287
|
+
id: 'call_sql',
|
|
288
|
+
name: 'execute_sql',
|
|
289
|
+
args: { query: 'select * from private_table' },
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
role: 'tool',
|
|
295
|
+
tool_call_id: 'call_sql',
|
|
296
|
+
content: 'sensitive row output',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
role: 'tool',
|
|
300
|
+
tool_call_id: 'call_bash',
|
|
301
|
+
content: 'public build log',
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
const span = createSpan('gpt-4o', {
|
|
305
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
306
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(messages),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
redactLangfuseSpanToolOutputs(
|
|
310
|
+
span,
|
|
311
|
+
createConfig({
|
|
312
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const redacted = readJsonAttribute<RedactedMessage[]>(
|
|
317
|
+
span,
|
|
318
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
319
|
+
);
|
|
320
|
+
expect(redacted[0].tool_calls?.[0]?.args?.query).toBe(
|
|
321
|
+
'select * from private_table'
|
|
322
|
+
);
|
|
323
|
+
expect(redacted[1].content).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
324
|
+
expect(redacted[2].content).toBe('public build log');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('does not redact partial tool name matches by default', () => {
|
|
328
|
+
const span = createSpan('clickhouse_execute_sql_prod', {
|
|
329
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
|
|
330
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: 'secret rows',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
redactLangfuseSpanToolOutputs(
|
|
334
|
+
span,
|
|
335
|
+
createConfig({
|
|
336
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
|
|
341
|
+
'secret rows'
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('redacts configured partial tool name matches when enabled', () => {
|
|
346
|
+
const span = createSpan('clickhouse_execute_sql_prod', {
|
|
347
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
|
|
348
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: 'secret rows',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
redactLangfuseSpanToolOutputs(
|
|
352
|
+
span,
|
|
353
|
+
createConfig({
|
|
354
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
355
|
+
redactedToolNameMatchMode: 'partial',
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
|
|
360
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('redacts prior tool outputs from multi-turn generation inputs', () => {
|
|
365
|
+
const messages = [
|
|
366
|
+
{ role: 'user', content: 'run the query' },
|
|
367
|
+
{
|
|
368
|
+
role: 'assistant',
|
|
369
|
+
content: '',
|
|
370
|
+
tool_calls: [
|
|
371
|
+
{
|
|
372
|
+
id: 'call_sql',
|
|
373
|
+
name: 'execute_sql',
|
|
374
|
+
args: { query: 'select * from private_table' },
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
role: 'execute_sql',
|
|
380
|
+
content: 'sensitive row output',
|
|
381
|
+
additional_kwargs: {},
|
|
382
|
+
},
|
|
383
|
+
{ role: 'assistant', content: 'I found the answer.' },
|
|
384
|
+
{ role: 'user', content: 'explain the first row' },
|
|
385
|
+
];
|
|
386
|
+
const span = createSpan('gpt-4o', {
|
|
387
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
388
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(messages),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
392
|
+
|
|
393
|
+
const redacted = readJsonAttribute<RedactedMessage[]>(
|
|
394
|
+
span,
|
|
395
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
396
|
+
);
|
|
397
|
+
expect(redacted[0].content).toBe('run the query');
|
|
398
|
+
expect(redacted[1].tool_calls?.[0]?.args?.query).toBe(
|
|
399
|
+
'select * from private_table'
|
|
400
|
+
);
|
|
401
|
+
expect(redacted[2].content).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
402
|
+
expect(redacted[3].content).toBe('I found the answer.');
|
|
403
|
+
expect(redacted[4].content).toBe('explain the first row');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('redacts tool outputs after formatAgentMessages rehydrates content parts', () => {
|
|
407
|
+
const payload: TPayload = [
|
|
408
|
+
{ role: 'user', content: 'show me the private numbers' },
|
|
409
|
+
{
|
|
410
|
+
role: 'assistant',
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: ContentTypes.TEXT,
|
|
414
|
+
[ContentTypes.TEXT]: 'I will query ClickHouse.',
|
|
415
|
+
tool_call_ids: ['call_sql'],
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
type: ContentTypes.TOOL_CALL,
|
|
419
|
+
tool_call: {
|
|
420
|
+
id: 'call_sql',
|
|
421
|
+
name: 'execute_sql',
|
|
422
|
+
args: '{"query":"select secret_value from prod"}',
|
|
423
|
+
output: 'secret_value: 12345',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
{ role: 'user', content: 'can you summarize it?' },
|
|
429
|
+
];
|
|
430
|
+
const { messages } = formatAgentMessages(
|
|
431
|
+
payload,
|
|
432
|
+
undefined,
|
|
433
|
+
new Set(['execute_sql'])
|
|
434
|
+
);
|
|
435
|
+
const serialized = messages.map(serializeMessageForLangfuse);
|
|
436
|
+
const span = createSpan('gpt-4o', {
|
|
437
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
438
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]:
|
|
439
|
+
JSON.stringify(serialized),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
redactLangfuseSpanToolOutputs(
|
|
443
|
+
span,
|
|
444
|
+
createConfig({
|
|
445
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
446
|
+
})
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const redacted = readJsonAttribute<RedactedMessage[]>(
|
|
450
|
+
span,
|
|
451
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
452
|
+
);
|
|
453
|
+
expect(redacted[1].tool_calls?.[0]?.args?.query).toBe(
|
|
454
|
+
'select secret_value from prod'
|
|
455
|
+
);
|
|
456
|
+
expect(redacted[2].role).toBe('execute_sql');
|
|
457
|
+
expect(redacted[2].content).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
458
|
+
expect(JSON.stringify(redacted)).not.toContain('secret_value: 12345');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('redacts constructor-serialized ToolMessages from rehydrated content parts', () => {
|
|
462
|
+
const payload: TPayload = [
|
|
463
|
+
{ role: 'user', content: 'show the stored result' },
|
|
464
|
+
{
|
|
465
|
+
role: 'assistant',
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: ContentTypes.TEXT,
|
|
469
|
+
[ContentTypes.TEXT]: 'I will query ClickHouse.',
|
|
470
|
+
tool_call_ids: ['call_sql'],
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
type: ContentTypes.TOOL_CALL,
|
|
474
|
+
tool_call: {
|
|
475
|
+
id: 'call_sql',
|
|
476
|
+
name: 'execute_sql',
|
|
477
|
+
args: '{"query":"select constructor_path from prod"}',
|
|
478
|
+
output: 'constructor path secret',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
];
|
|
484
|
+
const { messages } = formatAgentMessages(
|
|
485
|
+
payload,
|
|
486
|
+
undefined,
|
|
487
|
+
new Set(['execute_sql'])
|
|
488
|
+
);
|
|
489
|
+
const span = createSpan('gpt-4o', {
|
|
490
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
491
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
492
|
+
messages,
|
|
493
|
+
]),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
redactLangfuseSpanToolOutputs(
|
|
497
|
+
span,
|
|
498
|
+
createConfig({
|
|
499
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const redacted = span.attributes[
|
|
504
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
505
|
+
] as string;
|
|
506
|
+
expect(redacted).toContain('select constructor_path from prod');
|
|
507
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
508
|
+
expect(redacted).not.toContain('constructor path secret');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('redacts ToolMessage artifacts because they are tool output', () => {
|
|
512
|
+
const messages = [
|
|
513
|
+
{
|
|
514
|
+
id: ['langchain_core', 'messages', 'ToolMessage'],
|
|
515
|
+
kwargs: {
|
|
516
|
+
name: 'execute_sql',
|
|
517
|
+
tool_call_id: 'call_sql',
|
|
518
|
+
content: 'safe display content',
|
|
519
|
+
artifact: {
|
|
520
|
+
rows: ['artifact secret row'],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
const span = createSpan('gpt-4o', {
|
|
526
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
527
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(messages),
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
redactLangfuseSpanToolOutputs(
|
|
531
|
+
span,
|
|
532
|
+
createConfig({
|
|
533
|
+
redactedToolNames: new Set(['execute_sql']),
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const redacted = readJsonAttribute<
|
|
538
|
+
Array<{
|
|
539
|
+
kwargs: {
|
|
540
|
+
artifact: string;
|
|
541
|
+
content: string;
|
|
542
|
+
};
|
|
543
|
+
}>
|
|
544
|
+
>(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT);
|
|
545
|
+
expect(redacted[0].kwargs.content).toBe(
|
|
546
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
547
|
+
);
|
|
548
|
+
expect(redacted[0].kwargs.artifact).toBe(
|
|
549
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
550
|
+
);
|
|
551
|
+
expect(JSON.stringify(redacted)).not.toContain('artifact secret row');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('merges run Langfuse defaults with agent redaction overrides', () => {
|
|
555
|
+
const resolved = resolveLangfuseConfig(
|
|
556
|
+
{
|
|
557
|
+
enabled: true,
|
|
558
|
+
publicKey: 'pk-run',
|
|
559
|
+
secretKey: 'sk-run',
|
|
560
|
+
baseUrl: 'https://langfuse.test',
|
|
561
|
+
toolNodeTracing: { enabled: true },
|
|
562
|
+
toolOutputTracing: {
|
|
563
|
+
enabled: true,
|
|
564
|
+
redactionText: '[redacted]',
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
toolOutputTracing: {
|
|
569
|
+
enabled: false,
|
|
570
|
+
redactedToolNames: ['execute_sql'],
|
|
571
|
+
},
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
expect(resolved).toMatchObject({
|
|
576
|
+
enabled: true,
|
|
577
|
+
publicKey: 'pk-run',
|
|
578
|
+
secretKey: 'sk-run',
|
|
579
|
+
baseUrl: 'https://langfuse.test',
|
|
580
|
+
toolNodeTracing: { enabled: true },
|
|
581
|
+
toolOutputTracing: {
|
|
582
|
+
enabled: false,
|
|
583
|
+
redactedToolNames: ['execute_sql'],
|
|
584
|
+
redactionText: '[redacted]',
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
@@ -71,7 +71,7 @@ const CORE_RULES = `Rules:
|
|
|
71
71
|
- Tools are pre-defined as bash functions—DO NOT redefine them
|
|
72
72
|
- Each tool function accepts a JSON string argument
|
|
73
73
|
- Save tool output with raw=$(tool '{}'); printf '%s\n' "$raw" > /mnt/data/file.json; direct tool > file may be empty
|
|
74
|
-
-
|
|
74
|
+
- Tool stdout is normalized to one compact JSON value when possible; parse saved stdout once, then use fromjson? // . only for JSON-string fields
|
|
75
75
|
- Only echo/printf output returns to the model
|
|
76
76
|
- ${CODE_ARTIFACT_PATH_GUIDANCE}
|
|
77
77
|
- ${BASH_SHELL_GUIDANCE}
|
|
@@ -149,6 +149,38 @@ export const BashProgrammaticToolCallingDefinition = {
|
|
|
149
149
|
schema: BashProgrammaticToolCallingSchema,
|
|
150
150
|
} as const;
|
|
151
151
|
|
|
152
|
+
function maybeParseJsonResultString(result: unknown): unknown {
|
|
153
|
+
if (typeof result !== 'string') {
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const trimmed = result.trim();
|
|
158
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(trimmed) as unknown;
|
|
164
|
+
} catch {
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function normalizeBashToolResultsForReplay(
|
|
170
|
+
toolResults: t.PTCToolResult[]
|
|
171
|
+
): t.PTCToolResult[] {
|
|
172
|
+
return toolResults.map((toolResult) => {
|
|
173
|
+
if (toolResult.is_error) {
|
|
174
|
+
return toolResult;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...toolResult,
|
|
179
|
+
result: maybeParseJsonResultString(toolResult.result),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
// ============================================================================
|
|
153
185
|
// Helper Functions
|
|
154
186
|
// ============================================================================
|
|
@@ -355,10 +387,12 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
355
387
|
);
|
|
356
388
|
}
|
|
357
389
|
|
|
358
|
-
const toolResults =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
390
|
+
const toolResults = normalizeBashToolResultsForReplay(
|
|
391
|
+
await executeTools(
|
|
392
|
+
response.tool_calls ?? [],
|
|
393
|
+
toolMap,
|
|
394
|
+
Constants.BASH_PROGRAMMATIC_TOOL_CALLING
|
|
395
|
+
)
|
|
362
396
|
);
|
|
363
397
|
|
|
364
398
|
response = await makeRequest(
|