@librechat/agents 3.4.2 → 3.4.4

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 (75) hide show
  1. package/dist/cjs/graphs/Graph.cjs +27 -14
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  4. package/dist/cjs/instrumentation.cjs +3 -3
  5. package/dist/cjs/langfuse.cjs +3 -3
  6. package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
  8. package/dist/cjs/main.cjs +5 -1
  9. package/dist/cjs/messages/assistantPhase.cjs +59 -0
  10. package/dist/cjs/messages/assistantPhase.cjs.map +1 -0
  11. package/dist/cjs/messages/index.cjs +1 -0
  12. package/dist/cjs/prompts/activityLabel.cjs +76 -0
  13. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  14. package/dist/cjs/run.cjs +200 -10
  15. package/dist/cjs/run.cjs.map +1 -1
  16. package/dist/cjs/session/AgentSession.cjs +1 -1
  17. package/dist/cjs/stream.cjs +45 -8
  18. package/dist/cjs/stream.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +3 -3
  20. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +81 -6
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  22. package/dist/cjs/utils/callbacks.cjs +8 -0
  23. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  24. package/dist/esm/graphs/Graph.mjs +27 -14
  25. package/dist/esm/graphs/Graph.mjs.map +1 -1
  26. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  27. package/dist/esm/instrumentation.mjs +3 -3
  28. package/dist/esm/langfuse.mjs +3 -3
  29. package/dist/esm/langfuseRuntimeScope.mjs +1 -1
  30. package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
  31. package/dist/esm/main.mjs +3 -2
  32. package/dist/esm/messages/assistantPhase.mjs +57 -0
  33. package/dist/esm/messages/assistantPhase.mjs.map +1 -0
  34. package/dist/esm/messages/index.mjs +1 -0
  35. package/dist/esm/prompts/activityLabel.mjs +74 -1
  36. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  37. package/dist/esm/run.mjs +202 -12
  38. package/dist/esm/run.mjs.map +1 -1
  39. package/dist/esm/session/AgentSession.mjs +1 -1
  40. package/dist/esm/stream.mjs +45 -8
  41. package/dist/esm/stream.mjs.map +1 -1
  42. package/dist/esm/tools/ToolNode.mjs +3 -3
  43. package/dist/esm/tools/subagent/SubagentExecutor.mjs +81 -6
  44. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  45. package/dist/esm/utils/callbacks.mjs +8 -1
  46. package/dist/esm/utils/callbacks.mjs.map +1 -1
  47. package/dist/types/messages/assistantPhase.d.ts +22 -0
  48. package/dist/types/messages/index.d.ts +1 -0
  49. package/dist/types/prompts/activityLabel.d.ts +21 -1
  50. package/dist/types/run.d.ts +15 -2
  51. package/dist/types/types/activityLabel.d.ts +63 -0
  52. package/dist/types/types/assistantPhase.d.ts +6 -0
  53. package/dist/types/types/graph.d.ts +8 -1
  54. package/dist/types/types/index.d.ts +1 -0
  55. package/dist/types/types/stream.d.ts +11 -0
  56. package/dist/types/utils/callbacks.d.ts +1 -0
  57. package/package.json +3 -3
  58. package/src/graphs/Graph.ts +33 -9
  59. package/src/graphs/__tests__/Graph.reasoning.test.ts +57 -0
  60. package/src/messages/assistantPhase.test.ts +75 -0
  61. package/src/messages/assistantPhase.ts +91 -0
  62. package/src/messages/index.ts +1 -0
  63. package/src/prompts/activityLabel.ts +177 -1
  64. package/src/run.ts +403 -21
  65. package/src/specs/activity-label-prompt.test.ts +123 -1
  66. package/src/specs/activity-phase-label.test.ts +306 -0
  67. package/src/stream.ts +69 -12
  68. package/src/tools/__tests__/SubagentExecutor.test.ts +436 -0
  69. package/src/tools/subagent/SubagentExecutor.ts +160 -8
  70. package/src/types/activityLabel.ts +65 -0
  71. package/src/types/assistantPhase.ts +6 -0
  72. package/src/types/graph.ts +8 -0
  73. package/src/types/index.ts +1 -0
  74. package/src/types/stream.ts +9 -0
  75. package/src/utils/callbacks.ts +21 -0
@@ -0,0 +1,306 @@
1
+ import { AIMessage, HumanMessage } from '@langchain/core/messages';
2
+ import { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT } from '@/langfuseToolOutputTracing';
3
+ import { Providers } from '@/common';
4
+ import { Run } from '@/run';
5
+
6
+ const invoke = jest.fn();
7
+
8
+ jest.mock('@/llm/init', () => ({
9
+ initializeModel: jest.fn(() => ({ invoke })),
10
+ }));
11
+
12
+ async function createRun(): Promise<Run<never>> {
13
+ return Run.create({
14
+ runId: 'phase-run',
15
+ graphConfig: {
16
+ type: 'standard',
17
+ agents: [
18
+ {
19
+ agentId: 'agent-1',
20
+ name: 'Phase Agent',
21
+ provider: Providers.OPENAI,
22
+ clientOptions: { model: 'gpt-4.1-mini' },
23
+ tools: [],
24
+ },
25
+ ],
26
+ },
27
+ });
28
+ }
29
+
30
+ describe('generateActivityPhaseLabel', () => {
31
+ beforeEach(() => {
32
+ invoke.mockReset();
33
+ invoke.mockResolvedValue(
34
+ new AIMessage('"Fixed session refresh handling and verified auth tests."')
35
+ );
36
+ });
37
+
38
+ it('does not spend a model call on one logical activity', async () => {
39
+ const run = await createRun();
40
+
41
+ await expect(
42
+ run.generateActivityPhaseLabel({
43
+ provider: Providers.OPENAI,
44
+ activities: [{ label: 'Inspected session refresh middleware' }],
45
+ })
46
+ ).resolves.toEqual({});
47
+ expect(invoke).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it('summarizes two activities and normalizes the persisted row', async () => {
51
+ const run = await createRun();
52
+ if (run.Graph != null) {
53
+ run.Graph.messages = [
54
+ new HumanMessage({
55
+ content: [
56
+ {
57
+ type: 'input_text',
58
+ text: 'Why is session refresh failing?',
59
+ } as never,
60
+ ],
61
+ }),
62
+ new HumanMessage({
63
+ content: 'Internal routing instructions',
64
+ additional_kwargs: {
65
+ role: 'user',
66
+ isMeta: true,
67
+ source: 'routing',
68
+ },
69
+ }),
70
+ ];
71
+ }
72
+ const handleChainStart = jest.fn();
73
+ const handleChainEnd = jest.fn();
74
+
75
+ await expect(
76
+ run.generateActivityPhaseLabel({
77
+ provider: Providers.OPENAI,
78
+ activities: [
79
+ { label: 'Inspected session refresh middleware' },
80
+ { label: 'Fixed refresh token validation' },
81
+ ],
82
+ assistantContext: ['I am checking the auth path.'],
83
+ closingTextPhase: 'final_answer',
84
+ chainOptions: {
85
+ callbacks: [{ handleChainStart, handleChainEnd }],
86
+ configurable: {
87
+ requestBody: { parentMessageId: 'parent-message-1' },
88
+ },
89
+ },
90
+ })
91
+ ).resolves.toEqual({
92
+ label: 'Fixed session refresh handling and verified auth tests',
93
+ });
94
+ expect(invoke).toHaveBeenCalledTimes(1);
95
+ const messages = invoke.mock.calls[0][0] as AIMessage[];
96
+ expect(String(messages[1].content)).toContain(
97
+ 'Inspected session refresh middleware'
98
+ );
99
+ expect(String(messages[1].content)).toContain(
100
+ 'Fixed refresh token validation'
101
+ );
102
+ expect(String(messages[1].content)).not.toContain(
103
+ 'Why is session refresh failing?'
104
+ );
105
+ const modelConfig = invoke.mock.calls[0][1] as {
106
+ callbacks?: { getParentRunId?: () => string | undefined };
107
+ tags?: string[];
108
+ metadata?: Record<string, unknown>;
109
+ };
110
+ expect(modelConfig.callbacks?.getParentRunId?.()).toEqual(
111
+ expect.any(String)
112
+ );
113
+ expect(modelConfig.tags).toEqual(
114
+ expect.arrayContaining(['activity-phase', 'agent'])
115
+ );
116
+ expect(modelConfig.metadata).toEqual(
117
+ expect.objectContaining({
118
+ agentId: 'agent-1',
119
+ agentName: 'Phase Agent',
120
+ parentMessageId: 'parent-message-1',
121
+ })
122
+ );
123
+ expect(handleChainStart.mock.calls[0]?.[1]).toEqual(
124
+ expect.objectContaining({
125
+ messages: expect.arrayContaining([
126
+ expect.objectContaining({
127
+ content: 'Why is session refresh failing?',
128
+ }),
129
+ ]),
130
+ })
131
+ );
132
+ expect(JSON.stringify(handleChainStart.mock.calls[0]?.[1])).not.toContain(
133
+ 'Internal routing instructions'
134
+ );
135
+ expect(handleChainEnd.mock.calls[0]?.[0]).toEqual(
136
+ expect.objectContaining({
137
+ messages: expect.arrayContaining([
138
+ expect.objectContaining({
139
+ content: 'Fixed session refresh handling and verified auth tests',
140
+ }),
141
+ ]),
142
+ })
143
+ );
144
+ });
145
+
146
+ it('does not call the model when retained activities have no evidence', async () => {
147
+ const run = await createRun();
148
+
149
+ await expect(
150
+ run.generateActivityPhaseLabel({
151
+ provider: Providers.OPENAI,
152
+ activities: [
153
+ ...Array.from({ length: 12 }, () => ({ status: 'success' as const })),
154
+ { label: 'Evidence beyond the prompt cap' },
155
+ ],
156
+ })
157
+ ).resolves.toEqual({});
158
+ expect(invoke).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('preserves the phase parent when retrying without a failing callback', async () => {
162
+ invoke.mockRejectedValueOnce(new Error('event stream callback failed'));
163
+ const run = await createRun();
164
+
165
+ await expect(
166
+ run.generateActivityPhaseLabel({
167
+ provider: Providers.OPENAI,
168
+ activities: [
169
+ { label: 'Inspected session refresh middleware' },
170
+ { label: 'Fixed refresh token validation' },
171
+ ],
172
+ chainOptions: {
173
+ callbacks: [{ handleChainStart: jest.fn() }],
174
+ },
175
+ })
176
+ ).resolves.toEqual({
177
+ label: 'Fixed session refresh handling and verified auth tests',
178
+ });
179
+
180
+ expect(invoke).toHaveBeenCalledTimes(2);
181
+ const firstCallbacks = invoke.mock.calls[0][1].callbacks as {
182
+ getParentRunId: () => string | undefined;
183
+ handlers: unknown[];
184
+ };
185
+ const retryCallbacks = invoke.mock.calls[1][1].callbacks as {
186
+ getParentRunId: () => string | undefined;
187
+ handlers: unknown[];
188
+ };
189
+ expect(firstCallbacks.getParentRunId()).toEqual(expect.any(String));
190
+ expect(retryCallbacks.getParentRunId()).toBe(
191
+ firstCallbacks.getParentRunId()
192
+ );
193
+ expect(retryCallbacks.handlers).toHaveLength(0);
194
+ });
195
+
196
+ it('applies every agent redaction policy when any activity is unattributed', async () => {
197
+ const run = await Run.create({
198
+ runId: 'mixed-attribution-phase-run',
199
+ graphConfig: {
200
+ type: 'multi-agent',
201
+ agents: [
202
+ {
203
+ agentId: 'agent-1',
204
+ provider: Providers.OPENAI,
205
+ clientOptions: { model: 'gpt-4.1-mini' },
206
+ tools: [],
207
+ },
208
+ {
209
+ agentId: 'agent-2',
210
+ provider: Providers.OPENAI,
211
+ clientOptions: { model: 'gpt-4.1-mini' },
212
+ tools: [],
213
+ langfuse: {
214
+ toolOutputTracing: { redactedToolNames: ['secret_tool'] },
215
+ },
216
+ },
217
+ ],
218
+ edges: [],
219
+ },
220
+ });
221
+
222
+ await run.generateActivityPhaseLabel({
223
+ provider: Providers.OPENAI,
224
+ activities: [
225
+ { agentId: 'agent-1', label: 'Inspected public session behavior' },
226
+ {
227
+ entries: [
228
+ {
229
+ toolName: 'secret_tool',
230
+ toolInput: { key: 'public-key' },
231
+ toolOutput: 'STRICT_AGENT_SECRET',
232
+ status: 'success',
233
+ },
234
+ ],
235
+ },
236
+ ],
237
+ });
238
+
239
+ const messages = invoke.mock.calls[0][0] as AIMessage[];
240
+ expect(String(messages[1].content)).not.toContain('STRICT_AGENT_SECRET');
241
+ expect(String(messages[1].content)).toContain(
242
+ LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
243
+ );
244
+ });
245
+
246
+ it('applies every policy when omitted activities have no complete agent list', async () => {
247
+ const run = await Run.create({
248
+ runId: 'omitted-attribution-phase-run',
249
+ graphConfig: {
250
+ type: 'multi-agent',
251
+ agents: [
252
+ {
253
+ agentId: 'agent-1',
254
+ provider: Providers.OPENAI,
255
+ clientOptions: { model: 'gpt-4.1-mini' },
256
+ tools: [],
257
+ },
258
+ {
259
+ agentId: 'agent-2',
260
+ provider: Providers.OPENAI,
261
+ clientOptions: { model: 'gpt-4.1-mini' },
262
+ tools: [],
263
+ langfuse: {
264
+ toolOutputTracing: { redactedToolNames: ['secret_tool'] },
265
+ },
266
+ },
267
+ ],
268
+ edges: [],
269
+ },
270
+ });
271
+
272
+ await run.generateActivityPhaseLabel({
273
+ provider: Providers.OPENAI,
274
+ activities: [
275
+ {
276
+ agentId: 'agent-1',
277
+ entries: [
278
+ {
279
+ toolName: 'public_lookup',
280
+ toolInput: { id: 'one' },
281
+ toolOutput: 'public-one',
282
+ status: 'success',
283
+ },
284
+ ],
285
+ },
286
+ {
287
+ agentId: 'agent-1',
288
+ entries: [
289
+ {
290
+ toolName: 'public_lookup',
291
+ toolInput: { id: 'two' },
292
+ toolOutput: 'public-two',
293
+ status: 'success',
294
+ },
295
+ ],
296
+ },
297
+ ],
298
+ totalActivityCount: 3,
299
+ assistantContext: ['OMITTED_AGENT_SECRET'],
300
+ });
301
+
302
+ const messages = invoke.mock.calls[0][0] as AIMessage[];
303
+ expect(String(messages[1].content)).not.toContain('OMITTED_AGENT_SECRET');
304
+ expect(String(messages[1].content)).toContain('public-one');
305
+ });
306
+ });
package/src/stream.ts CHANGED
@@ -3,8 +3,19 @@ import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
3
3
  import type { ChatOpenAIReasoningSummary } from '@langchain/openai';
4
4
  import type { AIMessageChunk } from '@langchain/core/messages';
5
5
  import type { AgentContext } from '@/agents/AgentContext';
6
+ import type { RunBreakerScope } from '@/llm/streamLimits';
6
7
  import type { StandardGraph } from '@/graphs';
7
8
  import type * as t from '@/types';
9
+ import {
10
+ claimStreamLimitCharge,
11
+ combineCompleteToolCalls,
12
+ enforceCompleteToolCallArgLimit,
13
+ enforceStreamedToolCallArgLimit,
14
+ enforceStreamDeltaEventLimit,
15
+ requiresStreamLimitAccounting,
16
+ StreamLimitExceededError,
17
+ STREAM_LIMIT_EPOCH_KEY,
18
+ } from '@/llm/streamLimits';
8
19
  import {
9
20
  getStreamedToolCallSeal,
10
21
  getStreamedToolCallAdapter,
@@ -21,6 +32,10 @@ import {
21
32
  CODE_EXECUTION_TOOLS,
22
33
  LOCAL_CODING_BUNDLE_NAMES,
23
34
  } from '@/common';
35
+ import {
36
+ getMessageCreationContentMetadata,
37
+ splitAssistantTextContentByPhase,
38
+ } from '@/messages/assistantPhase';
24
39
  import {
25
40
  buildToolExecutionRequestPlan,
26
41
  coerceRecordArgs,
@@ -39,17 +54,6 @@ import {
39
54
  calculateMaxToolResultChars,
40
55
  truncateToolResultContent,
41
56
  } from '@/utils/truncation';
42
- import type { RunBreakerScope } from '@/llm/streamLimits';
43
- import {
44
- claimStreamLimitCharge,
45
- combineCompleteToolCalls,
46
- enforceCompleteToolCallArgLimit,
47
- enforceStreamedToolCallArgLimit,
48
- enforceStreamDeltaEventLimit,
49
- requiresStreamLimitAccounting,
50
- StreamLimitExceededError,
51
- STREAM_LIMIT_EPOCH_KEY,
52
- } from '@/llm/streamLimits';
53
57
  import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
54
58
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
55
59
  import { safeDispatchCustomEvent } from '@/utils/events';
@@ -521,10 +525,14 @@ function shouldStartFreshMessageStepAfterGoogleServerSideTool({
521
525
  async function dispatchMessageCreationStep({
522
526
  graph,
523
527
  stepKey,
528
+ content,
529
+ contentType,
524
530
  metadata,
525
531
  }: {
526
532
  graph: StandardGraph;
527
533
  stepKey: string;
534
+ content?: string | t.MessageContentComplex[];
535
+ contentType?: ContentTypes.TEXT | ContentTypes.THINK;
528
536
  metadata?: Record<string, unknown>;
529
537
  }): Promise<string> {
530
538
  const messageId = getMessageId(stepKey, graph, true) ?? '';
@@ -534,6 +542,7 @@ async function dispatchMessageCreationStep({
534
542
  type: StepTypes.MESSAGE_CREATION,
535
543
  message_creation: {
536
544
  message_id: messageId,
545
+ ...getMessageCreationContentMetadata(content, contentType),
537
546
  },
538
547
  },
539
548
  metadata
@@ -555,6 +564,7 @@ async function dispatchMessageContentParts({
555
564
  const currentStepId = await dispatchMessageCreationStep({
556
565
  graph,
557
566
  stepKey,
567
+ content: [contentPart],
558
568
  metadata,
559
569
  });
560
570
  if (isGoogleServerSideToolContentPart(contentPart)) {
@@ -587,6 +597,8 @@ async function dispatchReasoningContentParts({
587
597
  const currentStepId = await dispatchMessageCreationStep({
588
598
  graph,
589
599
  stepKey,
600
+ content,
601
+ contentType: ContentTypes.THINK,
590
602
  metadata,
591
603
  });
592
604
  await graph.dispatchReasoningDelta(
@@ -1811,14 +1823,53 @@ export class ChatModelStreamHandler implements t.EventHandler {
1811
1823
  return;
1812
1824
  }
1813
1825
 
1826
+ if (Array.isArray(content) && content.every(isTextContentPart)) {
1827
+ const contentGroups = splitAssistantTextContentByPhase(content);
1828
+ const currentStepId = graph.stepKeyIds?.get(stepKey)?.at(-1);
1829
+ const currentStep =
1830
+ currentStepId == null ? undefined : graph.getRunStep(currentStepId);
1831
+ const currentPhase =
1832
+ currentStep?.stepDetails.type === StepTypes.MESSAGE_CREATION
1833
+ ? currentStep.stepDetails.message_creation.phase
1834
+ : undefined;
1835
+ const nextPhase = getMessageCreationContentMetadata(
1836
+ contentGroups[0]
1837
+ ).phase;
1838
+ const phaseChanged =
1839
+ currentPhase != null &&
1840
+ nextPhase != null &&
1841
+ currentPhase !== nextPhase;
1842
+ if (contentGroups.length > 1 || phaseChanged) {
1843
+ for (const contentGroup of contentGroups) {
1844
+ const currentStepId = await dispatchMessageCreationStep({
1845
+ graph,
1846
+ stepKey,
1847
+ content: contentGroup,
1848
+ metadata,
1849
+ });
1850
+ await graph.dispatchMessageDelta(
1851
+ currentStepId,
1852
+ { content: contentGroup },
1853
+ metadata
1854
+ );
1855
+ }
1856
+ return;
1857
+ }
1858
+ }
1859
+
1814
1860
  const message_id = getMessageId(stepKey, graph) ?? '';
1815
1861
  if (message_id) {
1862
+ const fallbackContentType =
1863
+ agentContext.currentTokenType === ContentTypes.TEXT
1864
+ ? ContentTypes.TEXT
1865
+ : ContentTypes.THINK;
1816
1866
  await graph.dispatchRunStep(
1817
1867
  stepKey,
1818
1868
  {
1819
1869
  type: StepTypes.MESSAGE_CREATION,
1820
1870
  message_creation: {
1821
1871
  message_id,
1872
+ ...getMessageCreationContentMetadata(content, fallbackContentType),
1822
1873
  },
1823
1874
  },
1824
1875
  metadata
@@ -1835,7 +1886,12 @@ export class ChatModelStreamHandler implements t.EventHandler {
1835
1886
  content,
1836
1887
  })
1837
1888
  ) {
1838
- stepId = await dispatchMessageCreationStep({ graph, stepKey, metadata });
1889
+ stepId = await dispatchMessageCreationStep({
1890
+ graph,
1891
+ stepKey,
1892
+ content,
1893
+ metadata,
1894
+ });
1839
1895
  runStep = graph.getRunStep(stepId);
1840
1896
  }
1841
1897
  if (!runStep) {
@@ -1906,6 +1962,7 @@ hasToolCallChunks: ${hasToolCallChunks}
1906
1962
  type: StepTypes.MESSAGE_CREATION,
1907
1963
  message_creation: {
1908
1964
  message_id,
1965
+ content_type: ContentTypes.TEXT,
1909
1966
  },
1910
1967
  },
1911
1968
  metadata