@librechat/agents 3.4.5 → 3.4.6

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 (81) hide show
  1. package/dist/cjs/common/enum.cjs +2 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +268 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuseTraceShaping.cjs +9 -6
  6. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  7. package/dist/cjs/run.cjs +147 -14
  8. package/dist/cjs/run.cjs.map +1 -1
  9. package/dist/cjs/session/AgentSession.cjs +93 -31
  10. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  11. package/dist/cjs/session/handlers.cjs +10 -0
  12. package/dist/cjs/session/handlers.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +17 -3
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/summarization/node.cjs +1 -0
  16. package/dist/cjs/summarization/node.cjs.map +1 -1
  17. package/dist/cjs/tools/ToolNode.cjs +4 -2
  18. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  19. package/dist/cjs/tools/handlers.cjs +1 -0
  20. package/dist/cjs/tools/handlers.cjs.map +1 -1
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +28 -0
  22. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  23. package/dist/cjs/utils/handlers.cjs +3 -0
  24. package/dist/cjs/utils/handlers.cjs.map +1 -1
  25. package/dist/esm/common/enum.mjs +2 -0
  26. package/dist/esm/common/enum.mjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +268 -14
  28. package/dist/esm/graphs/Graph.mjs.map +1 -1
  29. package/dist/esm/langfuseTraceShaping.mjs +9 -6
  30. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  31. package/dist/esm/run.mjs +147 -14
  32. package/dist/esm/run.mjs.map +1 -1
  33. package/dist/esm/session/AgentSession.mjs +93 -31
  34. package/dist/esm/session/AgentSession.mjs.map +1 -1
  35. package/dist/esm/session/handlers.mjs +10 -0
  36. package/dist/esm/session/handlers.mjs.map +1 -1
  37. package/dist/esm/stream.mjs +17 -3
  38. package/dist/esm/stream.mjs.map +1 -1
  39. package/dist/esm/summarization/node.mjs +1 -0
  40. package/dist/esm/summarization/node.mjs.map +1 -1
  41. package/dist/esm/tools/ToolNode.mjs +4 -2
  42. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  43. package/dist/esm/tools/handlers.mjs +1 -0
  44. package/dist/esm/tools/handlers.mjs.map +1 -1
  45. package/dist/esm/tools/subagent/SubagentExecutor.mjs +28 -0
  46. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  47. package/dist/esm/utils/handlers.mjs +3 -0
  48. package/dist/esm/utils/handlers.mjs.map +1 -1
  49. package/dist/types/common/enum.d.ts +2 -0
  50. package/dist/types/graphs/Graph.d.ts +87 -0
  51. package/dist/types/langfuseTraceShaping.d.ts +4 -3
  52. package/dist/types/run.d.ts +9 -1
  53. package/dist/types/session/types.d.ts +1 -1
  54. package/dist/types/summarization/node.d.ts +6 -0
  55. package/dist/types/types/graph.d.ts +3 -3
  56. package/dist/types/types/stream.d.ts +57 -0
  57. package/dist/types/utils/handlers.d.ts +1 -0
  58. package/package.json +1 -1
  59. package/src/aggregator.test.ts +127 -0
  60. package/src/common/enum.ts +2 -0
  61. package/src/graphs/Graph.ts +365 -7
  62. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +385 -0
  63. package/src/langfuseTraceShaping.ts +14 -7
  64. package/src/run.ts +219 -41
  65. package/src/session/AgentSession.ts +105 -13
  66. package/src/session/handlers.ts +14 -2
  67. package/src/session/types.ts +1 -0
  68. package/src/specs/activity-label-observability.live.test.ts +262 -0
  69. package/src/specs/activity-label-observability.test.ts +167 -0
  70. package/src/specs/langfuse-trace-shaping.test.ts +32 -0
  71. package/src/specs/run-step-timestamps.test.ts +414 -0
  72. package/src/stream.ts +29 -6
  73. package/src/summarization/node.ts +11 -0
  74. package/src/tools/ToolNode.ts +2 -0
  75. package/src/tools/__tests__/SubagentExecutor.test.ts +52 -10
  76. package/src/tools/__tests__/handlers.test.ts +2 -0
  77. package/src/tools/handlers.ts +1 -0
  78. package/src/tools/subagent/SubagentExecutor.ts +46 -4
  79. package/src/types/graph.ts +9 -6
  80. package/src/types/stream.ts +65 -12
  81. package/src/utils/handlers.ts +13 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Live provider + Langfuse export verification for activity-label traces.
3
+ *
4
+ * Run with:
5
+ * RUN_ACTIVITY_LABEL_LANGFUSE_LIVE=1 OPENAI_API_KEY=... \
6
+ * LANGFUSE_PUBLIC_KEY=... LANGFUSE_SECRET_KEY=... LANGFUSE_BASE_URL=... \
7
+ * LANGFUSE_FORCE_FLUSH_ON_DISPOSE=true \
8
+ * npm test -- activity-label-observability.live.test.ts --runInBand
9
+ */
10
+ import { config as dotenvConfig } from 'dotenv';
11
+ dotenvConfig();
12
+
13
+ import { HumanMessage } from '@langchain/core/messages';
14
+ import { describe, expect, it, jest } from '@jest/globals';
15
+ import { Providers } from '@/common';
16
+ import { Run } from '@/run';
17
+
18
+ type LangfuseMetadata = {
19
+ sourceRunId?: string;
20
+ responseId?: string;
21
+ activityIndex?: string | number;
22
+ phaseIndex?: string | number;
23
+ activityCount?: string | number;
24
+ };
25
+
26
+ type LangfuseObservation = {
27
+ id: string;
28
+ parentObservationId?: string | null;
29
+ type: string;
30
+ name: string;
31
+ model?: string | null;
32
+ usage?: { input?: number; output?: number; total?: number };
33
+ };
34
+
35
+ type LangfuseTrace = {
36
+ id: string;
37
+ name: string;
38
+ tags?: string[];
39
+ metadata?: LangfuseMetadata;
40
+ observations?: LangfuseObservation[];
41
+ };
42
+
43
+ type LangfuseTraceList = {
44
+ data: LangfuseTrace[];
45
+ };
46
+
47
+ const shouldRunLive =
48
+ process.env.RUN_ACTIVITY_LABEL_LANGFUSE_LIVE === '1' &&
49
+ (process.env.OPENAI_API_KEY ?? '') !== '' &&
50
+ (process.env.LANGFUSE_PUBLIC_KEY ?? '') !== '' &&
51
+ (process.env.LANGFUSE_SECRET_KEY ?? '') !== '' &&
52
+ (process.env.LANGFUSE_BASE_URL ?? '') !== '';
53
+
54
+ const describeIfLive = shouldRunLive ? describe : describe.skip;
55
+
56
+ function langfuseHeaders(): HeadersInit {
57
+ const credentials = Buffer.from(
58
+ `${process.env.LANGFUSE_PUBLIC_KEY}:${process.env.LANGFUSE_SECRET_KEY}`
59
+ ).toString('base64');
60
+ return { Authorization: `Basic ${credentials}` };
61
+ }
62
+
63
+ async function requestLangfuse(path: string): Promise<Response> {
64
+ const baseUrl = process.env.LANGFUSE_BASE_URL?.replace(/\/$/, '');
65
+ return fetch(`${baseUrl}${path}`, {
66
+ headers: langfuseHeaders(),
67
+ });
68
+ }
69
+
70
+ async function getJson<T>(path: string): Promise<T> {
71
+ const response = await requestLangfuse(path);
72
+ if (!response.ok) {
73
+ throw new Error(`Langfuse request failed: ${response.status}`);
74
+ }
75
+ return (await response.json()) as T;
76
+ }
77
+
78
+ async function getJsonIfPresent<T>(path: string): Promise<T | undefined> {
79
+ const response = await requestLangfuse(path);
80
+ if (response.status === 404) {
81
+ return undefined;
82
+ }
83
+ if (!response.ok) {
84
+ throw new Error(`Langfuse request failed: ${response.status}`);
85
+ }
86
+ return (await response.json()) as T;
87
+ }
88
+
89
+ async function findExportedTraces(
90
+ sourceRunId: string,
91
+ fromTimestamp: string
92
+ ): Promise<{ label: LangfuseTrace; phase: LangfuseTrace }> {
93
+ const query = new URLSearchParams({
94
+ limit: '100',
95
+ fromTimestamp,
96
+ });
97
+ for (let attempt = 0; attempt < 20; attempt += 1) {
98
+ const list = await getJson<LangfuseTraceList>(
99
+ `/api/public/traces?${query.toString()}`
100
+ );
101
+ const candidates = list.data.filter(
102
+ (trace) => trace.metadata?.sourceRunId === sourceRunId
103
+ );
104
+ const labelSummary = candidates.find(
105
+ (trace) => trace.tags?.includes('activity-label') === true
106
+ );
107
+ const phaseSummary = candidates.find(
108
+ (trace) => trace.tags?.includes('activity-phase') === true
109
+ );
110
+ if (labelSummary != null && phaseSummary != null) {
111
+ const [label, phase] = await Promise.all([
112
+ getJsonIfPresent<LangfuseTrace>(
113
+ `/api/public/traces/${labelSummary.id}`
114
+ ),
115
+ getJsonIfPresent<LangfuseTrace>(
116
+ `/api/public/traces/${phaseSummary.id}`
117
+ ),
118
+ ]);
119
+ if (label != null && phase != null) {
120
+ const labelReady =
121
+ label.observations?.some(
122
+ (observation) => observation.type === 'GENERATION'
123
+ ) === true;
124
+ const phaseRootReady =
125
+ phase.observations?.some(
126
+ (observation) =>
127
+ observation.parentObservationId == null &&
128
+ observation.type === 'CHAIN'
129
+ ) === true;
130
+ const phaseGenerationReady =
131
+ phase.observations?.some(
132
+ (observation) => observation.type === 'GENERATION'
133
+ ) === true;
134
+ if (labelReady && phaseRootReady && phaseGenerationReady) {
135
+ return { label, phase };
136
+ }
137
+ }
138
+ }
139
+ await new Promise((resolve) => setTimeout(resolve, 3000));
140
+ }
141
+ throw new Error(`Timed out waiting for activity traces from ${sourceRunId}`);
142
+ }
143
+
144
+ describeIfLive('activity label Langfuse export (live)', () => {
145
+ jest.setTimeout(120_000);
146
+
147
+ it('exports stable, correlated traces with correct observation types', async () => {
148
+ const startedAt = new Date(Date.now() - 5000).toISOString();
149
+ const sourceRunId = `activity-label-live-${Date.now()}`;
150
+ const sessionId = `${sourceRunId}-session`;
151
+ const model = process.env.ACTIVITY_LABEL_LIVE_MODEL ?? 'gpt-4.1-mini';
152
+ const run = await Run.create({
153
+ runId: sourceRunId,
154
+ graphConfig: {
155
+ type: 'standard',
156
+ agents: [
157
+ {
158
+ agentId: 'activity-label-live-agent',
159
+ name: 'Volatile Agent Display Name',
160
+ provider: Providers.OPENAI,
161
+ clientOptions: {
162
+ apiKey: process.env.OPENAI_API_KEY,
163
+ model,
164
+ },
165
+ tools: [],
166
+ },
167
+ ],
168
+ },
169
+ });
170
+ if (run.Graph != null) {
171
+ run.Graph.messages = [
172
+ new HumanMessage('Verify activity-label Langfuse trace semantics'),
173
+ ];
174
+ }
175
+ const chainOptions = {
176
+ configurable: {
177
+ thread_id: sessionId,
178
+ user_id: 'activity-label-live-user',
179
+ requestBody: { parentMessageId: 'activity-label-live-parent' },
180
+ },
181
+ };
182
+
183
+ await run.generateActivityLabel({
184
+ provider: Providers.OPENAI,
185
+ agentId: 'activity-label-live-agent',
186
+ clientOptions: {
187
+ apiKey: process.env.OPENAI_API_KEY,
188
+ model,
189
+ },
190
+ entries: [
191
+ {
192
+ toolName: 'inspect_runtime',
193
+ toolInput: { target: 'trace-shape' },
194
+ toolOutput: { status: 'verified' },
195
+ status: 'success',
196
+ },
197
+ ],
198
+ chainOptions,
199
+ });
200
+ await run.generateActivityPhaseLabel({
201
+ provider: Providers.OPENAI,
202
+ clientOptions: {
203
+ apiKey: process.env.OPENAI_API_KEY,
204
+ model,
205
+ },
206
+ activities: [
207
+ { label: 'Inspected the activity-label trace shape' },
208
+ { label: 'Verified stable correlation metadata' },
209
+ ],
210
+ sourceRunId,
211
+ responseId: sourceRunId,
212
+ phaseIndex: 0,
213
+ chainOptions,
214
+ });
215
+
216
+ const { label, phase } = await findExportedTraces(sourceRunId, startedAt);
217
+ expect(label).toMatchObject({
218
+ name: 'LibreChat Activity Label',
219
+ metadata: {
220
+ sourceRunId,
221
+ responseId: sourceRunId,
222
+ activityIndex: 0,
223
+ },
224
+ });
225
+ expect(phase).toMatchObject({
226
+ name: 'LibreChat Activity Phase',
227
+ metadata: {
228
+ sourceRunId,
229
+ responseId: sourceRunId,
230
+ phaseIndex: 0,
231
+ activityCount: 2,
232
+ },
233
+ });
234
+
235
+ const labelGeneration = label.observations?.find(
236
+ (observation) => observation.type === 'GENERATION'
237
+ );
238
+ expect(labelGeneration).toMatchObject({
239
+ parentObservationId: null,
240
+ name: 'llm',
241
+ model: expect.stringContaining(model),
242
+ });
243
+ expect(labelGeneration?.usage?.total).toBeGreaterThan(0);
244
+
245
+ const phaseRoot = phase.observations?.find(
246
+ (observation) => observation.parentObservationId == null
247
+ );
248
+ expect(phaseRoot).toMatchObject({
249
+ type: 'CHAIN',
250
+ name: 'summarize-activity-phase',
251
+ });
252
+ const phaseGeneration = phase.observations?.find(
253
+ (observation) => observation.type === 'GENERATION'
254
+ );
255
+ expect(phaseGeneration).toMatchObject({
256
+ parentObservationId: phaseRoot?.id,
257
+ name: 'llm',
258
+ model: expect.stringContaining(model),
259
+ });
260
+ expect(phaseGeneration?.usage?.total).toBeGreaterThan(0);
261
+ });
262
+ });
@@ -0,0 +1,167 @@
1
+ import { CallbackHandler } from '@langfuse/langchain';
2
+ import { propagateAttributes } from '@langfuse/tracing';
3
+ import { AIMessage, HumanMessage } from '@langchain/core/messages';
4
+ import { Providers } from '@/common';
5
+ import { Run } from '@/run';
6
+
7
+ const invoke = jest.fn();
8
+
9
+ jest.mock('@/llm/init', () => ({
10
+ initializeModel: jest.fn(() => ({ invoke })),
11
+ }));
12
+
13
+ jest.mock('@langfuse/langchain', () => ({
14
+ CallbackHandler: jest.fn().mockImplementation(() => ({})),
15
+ }));
16
+
17
+ jest.mock('@langfuse/tracing', () => ({
18
+ ...jest.requireActual('@langfuse/tracing'),
19
+ propagateAttributes: jest.fn((_params, action: () => unknown) => action()),
20
+ }));
21
+
22
+ const MockedCallbackHandler = CallbackHandler as jest.MockedClass<
23
+ typeof CallbackHandler
24
+ >;
25
+ const MockedPropagateAttributes = propagateAttributes as jest.MockedFunction<
26
+ typeof propagateAttributes
27
+ >;
28
+
29
+ async function createRun(): Promise<Run<never>> {
30
+ const run = await Run.create({
31
+ runId: 'response-1',
32
+ graphConfig: {
33
+ type: 'standard',
34
+ agents: [
35
+ {
36
+ agentId: 'agent-1',
37
+ name: 'Changing Model Name',
38
+ provider: Providers.OPENAI,
39
+ clientOptions: { model: 'gpt-4.1-mini' },
40
+ tools: [],
41
+ },
42
+ ],
43
+ },
44
+ });
45
+ if (run.Graph != null) {
46
+ run.Graph.messages = [new HumanMessage('Why did the run fail?')];
47
+ }
48
+ return run;
49
+ }
50
+
51
+ describe('activity label observability', () => {
52
+ const originalEnv = process.env;
53
+
54
+ beforeEach(() => {
55
+ jest.clearAllMocks();
56
+ invoke.mockResolvedValue(new AIMessage('Resolved the failing run'));
57
+ process.env = {
58
+ ...originalEnv,
59
+ LANGFUSE_SECRET_KEY: 'sk-test',
60
+ LANGFUSE_PUBLIC_KEY: 'pk-test',
61
+ LANGFUSE_BASE_URL: 'https://langfuse.test',
62
+ };
63
+ });
64
+
65
+ afterEach(() => {
66
+ process.env = originalEnv;
67
+ });
68
+
69
+ it('uses a stable trace name and source-run metadata for batch labels', async () => {
70
+ const run = await createRun();
71
+
72
+ await run.generateActivityLabel({
73
+ provider: Providers.OPENAI,
74
+ entries: [
75
+ {
76
+ toolName: 'inspect_status',
77
+ toolInput: { id: 'one' },
78
+ toolOutput: { fixed: true },
79
+ status: 'success',
80
+ },
81
+ ],
82
+ chainOptions: {
83
+ configurable: {
84
+ thread_id: 'thread-1',
85
+ user_id: 'user-1',
86
+ requestBody: { parentMessageId: 'parent-1' },
87
+ },
88
+ },
89
+ });
90
+
91
+ expect(MockedCallbackHandler).toHaveBeenCalledTimes(1);
92
+ expect(MockedCallbackHandler.mock.calls[0][0]).toMatchObject({
93
+ traceMetadata: {
94
+ messageId: 'activity-label-response-1',
95
+ parentMessageId: 'parent-1',
96
+ agentId: 'agent-1',
97
+ agentName: 'Changing Model Name',
98
+ sourceRunId: 'response-1',
99
+ responseId: 'response-1',
100
+ activityIndex: '0',
101
+ },
102
+ tags: ['librechat', 'activity-label'],
103
+ });
104
+ expect(MockedPropagateAttributes.mock.calls[0][0]).toMatchObject({
105
+ traceName: 'LibreChat Activity Label',
106
+ metadata: {
107
+ sourceRunId: 'response-1',
108
+ responseId: 'response-1',
109
+ activityIndex: '0',
110
+ },
111
+ });
112
+ expect(invoke.mock.calls[0][1]).toMatchObject({
113
+ runName: 'LibreChat Activity Label',
114
+ tags: ['librechat', 'activity-label'],
115
+ metadata: {
116
+ sourceRunId: 'response-1',
117
+ responseId: 'response-1',
118
+ activityIndex: 0,
119
+ parentMessageId: 'parent-1',
120
+ agentId: 'agent-1',
121
+ agentName: 'Changing Model Name',
122
+ },
123
+ });
124
+ });
125
+
126
+ it('uses a stable trace name for phase summaries', async () => {
127
+ const run = await createRun();
128
+
129
+ await run.generateActivityPhaseLabel({
130
+ provider: Providers.OPENAI,
131
+ activities: [
132
+ { label: 'Inspected session refresh behavior' },
133
+ { label: 'Fixed refresh token validation' },
134
+ ],
135
+ sourceRunId: 'response-1',
136
+ responseId: 'response-1',
137
+ phaseIndex: 0,
138
+ chainOptions: {
139
+ configurable: {
140
+ thread_id: 'thread-1',
141
+ user_id: 'user-1',
142
+ requestBody: { parentMessageId: 'parent-1' },
143
+ },
144
+ },
145
+ });
146
+
147
+ expect(MockedCallbackHandler).toHaveBeenCalledTimes(1);
148
+ expect(MockedCallbackHandler.mock.calls[0][0]).toMatchObject({
149
+ tags: ['librechat', 'activity-phase', 'agent-run-summary', 'agent'],
150
+ traceMetadata: {
151
+ sourceRunId: 'response-1',
152
+ responseId: 'response-1',
153
+ phaseIndex: '0',
154
+ activityCount: '2',
155
+ },
156
+ });
157
+ expect(MockedPropagateAttributes.mock.calls[0][0]).toMatchObject({
158
+ traceName: 'LibreChat Activity Phase',
159
+ metadata: {
160
+ sourceRunId: 'response-1',
161
+ responseId: 'response-1',
162
+ phaseIndex: '0',
163
+ activityCount: '2',
164
+ },
165
+ });
166
+ });
167
+ });
@@ -486,6 +486,38 @@ describe('shapeLangfuseSpan', () => {
486
486
  expect(span.attributes[TRACE_OUTPUT]).toBe('Conversation title');
487
487
  });
488
488
 
489
+ it('marks activity-phase roots as chains even when tagged as agent work', () => {
490
+ const span = createSpan('summarize-activity-phase', {
491
+ [TRACE_TAGS]: JSON.stringify([
492
+ 'librechat',
493
+ 'activity-phase',
494
+ 'agent-run-summary',
495
+ 'agent',
496
+ ]),
497
+ [INPUT]: 'What changed?',
498
+ [OUTPUT]: 'Reconciled the implementation and verified the fix',
499
+ });
500
+
501
+ shapeLangfuseSpan(span);
502
+
503
+ expect(span.attributes[OBSERVATION_TYPE]).toBe('chain');
504
+ expect(span.attributes[TRACE_INPUT]).toBe('What changed?');
505
+ expect(span.attributes[TRACE_OUTPUT]).toBe(
506
+ 'Reconciled the implementation and verified the fix'
507
+ );
508
+ });
509
+
510
+ it('does not treat a user-supplied activity-phase tag as an operation type', () => {
511
+ const span = createSpan('LibreChat Agent', {
512
+ [TRACE_TAGS]: JSON.stringify(['librechat', 'agent', 'activity-phase']),
513
+ [INPUT]: 'Run an ordinary agent',
514
+ });
515
+
516
+ shapeLangfuseSpan(span);
517
+
518
+ expect(span.attributes[OBSERVATION_TYPE]).toBe('agent');
519
+ });
520
+
489
521
  it('does not classify untagged root spans as agents', () => {
490
522
  const span = createSpan('Custom root', { [INPUT]: 'input' });
491
523