@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.
- 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/run.cjs +2 -2
- package/dist/cjs/run.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/run.mjs +2 -2
- package/dist/esm/run.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/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/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/tools/__tests__/SubagentExecutor.test.ts +44 -0
- package/src/tools/__tests__/hitl.test.ts +85 -0
- 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/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({
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -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) +
|
|
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 ??
|
|
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
|
-
):
|
|
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,
|