@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,385 @@
1
+ // src/graphs/__tests__/Graph.closeRunStep.test.ts
2
+ import type * as t from '@/types';
3
+ import { GraphEvents, StepTypes, Providers } from '@/common';
4
+ import { HandlerRegistry } from '@/events';
5
+ import { StandardGraph } from '../Graph';
6
+
7
+ const makeAgent = (agentId: string): t.AgentInputs => ({
8
+ agentId,
9
+ provider: Providers.OPENAI,
10
+ instructions: `You are ${agentId}.`,
11
+ });
12
+
13
+ function createGraph(): {
14
+ graph: StandardGraph;
15
+ closed: t.RunStepClosedEvent[];
16
+ } {
17
+ const graph = new StandardGraph({
18
+ runId: 'run_1',
19
+ agents: [makeAgent('agent')],
20
+ });
21
+ const closed: t.RunStepClosedEvent[] = [];
22
+ const registry = new HandlerRegistry();
23
+ registry.register(GraphEvents.ON_RUN_STEP_CLOSED, {
24
+ handle: (_event, data): void => {
25
+ closed.push(data as t.RunStepClosedEvent);
26
+ },
27
+ });
28
+ graph.handlerRegistry = registry;
29
+ return { graph, closed };
30
+ }
31
+
32
+ function seedStep(
33
+ graph: StandardGraph,
34
+ id: string,
35
+ type: StepTypes = StepTypes.MESSAGE_CREATION
36
+ ): t.RunStep {
37
+ const index = graph.contentData.length;
38
+ const stepDetails: t.StepDetails =
39
+ type === StepTypes.TOOL_CALLS
40
+ ? { type: StepTypes.TOOL_CALLS, tool_calls: [] }
41
+ : {
42
+ type: StepTypes.MESSAGE_CREATION,
43
+ message_creation: { message_id: `msg_${id}` },
44
+ };
45
+ const step: t.RunStep = {
46
+ id,
47
+ type,
48
+ index,
49
+ stepIndex: index,
50
+ stepDetails,
51
+ usage: null,
52
+ created_at: 1_000,
53
+ status: 'in_progress',
54
+ };
55
+ graph.contentData.push(step);
56
+ graph.contentIndexMap.set(id, index);
57
+ return step;
58
+ }
59
+
60
+ describe('StandardGraph.closeRunStep', () => {
61
+ it('stamps the terminal status + timestamp and emits ON_RUN_STEP_CLOSED once', async () => {
62
+ const { graph, closed } = createGraph();
63
+ const step = seedStep(graph, 'step_a');
64
+
65
+ const first = await graph.closeRunStep('step_a', 'completed', {
66
+ at: 2_000,
67
+ });
68
+ expect(first).toBe(true);
69
+ expect(step.status).toBe('completed');
70
+ expect(step.completed_at).toBe(2_000);
71
+ expect(closed).toHaveLength(1);
72
+ expect(closed[0]).toMatchObject({
73
+ id: 'step_a',
74
+ index: 0,
75
+ type: StepTypes.MESSAGE_CREATION,
76
+ status: 'completed',
77
+ created_at: 1_000,
78
+ closed_at: 2_000,
79
+ });
80
+
81
+ const second = await graph.closeRunStep('step_a', 'completed', {
82
+ at: 3_000,
83
+ });
84
+ expect(second).toBe(false);
85
+ expect(step.completed_at).toBe(2_000);
86
+ expect(closed).toHaveLength(1);
87
+ });
88
+
89
+ it('keeps a terminal status immutable across later close attempts', async () => {
90
+ const { graph, closed } = createGraph();
91
+ const step = seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
92
+
93
+ await graph.closeRunStep('step_a', 'cancelled', { at: 2_000 });
94
+ const reclosed = await graph.closeRunStep('step_a', 'completed', {
95
+ at: 3_000,
96
+ });
97
+ expect(reclosed).toBe(false);
98
+ expect(step.status).toBe('cancelled');
99
+ expect(step.cancelled_at).toBe(2_000);
100
+ expect(step.completed_at).toBeUndefined();
101
+ expect(closed).toHaveLength(1);
102
+ });
103
+
104
+ it('keeps the stored stamp and the emitted event in agreement', async () => {
105
+ const { graph, closed } = createGraph();
106
+ const step = seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
107
+
108
+ await graph.closeRunStep('step_a', 'completed', { at: 2_000 });
109
+ const reclosed = await graph.closeRunStep('step_a', 'completed', {
110
+ at: 3_000,
111
+ });
112
+
113
+ /** No silent correction: RunStep.completed_at can never disagree with the
114
+ * closed_at already delivered to subscribers. */
115
+ expect(reclosed).toBe(false);
116
+ expect(step.completed_at).toBe(2_000);
117
+ expect(closed).toHaveLength(1);
118
+ expect(closed[0].closed_at).toBe(2_000);
119
+ });
120
+
121
+ it('tolerates empty and unknown step ids', async () => {
122
+ const { graph, closed } = createGraph();
123
+ expect(await graph.closeRunStep('', 'completed')).toBe(false);
124
+ expect(await graph.closeRunStep('step_missing', 'completed')).toBe(false);
125
+ expect(closed).toHaveLength(0);
126
+ });
127
+ });
128
+
129
+ describe('StandardGraph.recordStepCompletion', () => {
130
+ it('closes a TOOL_CALLS step only after every registered call completes', async () => {
131
+ const { graph, closed } = createGraph();
132
+ const step = seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
133
+ graph.registerPendingToolCall('call_1', 'step_a');
134
+ graph.registerPendingToolCall('call_2', 'step_a');
135
+
136
+ await graph.recordStepCompletion('step_a', { toolCallId: 'call_1' });
137
+ expect(step.status).toBe('in_progress');
138
+ expect(closed).toHaveLength(0);
139
+
140
+ await graph.recordStepCompletion('step_a', { toolCallId: 'call_2' });
141
+ expect(step.status).toBe('completed');
142
+ expect(typeof step.completed_at).toBe('number');
143
+ expect(closed).toHaveLength(1);
144
+ expect(closed[0].status).toBe('completed');
145
+ });
146
+
147
+ it('closes steps without pending tracking on the first completion', async () => {
148
+ const { graph, closed } = createGraph();
149
+ const step = seedStep(graph, 'step_a');
150
+
151
+ await graph.recordStepCompletion('step_a');
152
+ expect(step.status).toBe('completed');
153
+ expect(closed).toHaveLength(1);
154
+ });
155
+
156
+ it('absorbs duplicate completions without re-emitting', async () => {
157
+ const { graph, closed } = createGraph();
158
+ seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
159
+ graph.registerPendingToolCall('call_1', 'step_a');
160
+
161
+ await graph.recordStepCompletion('step_a', { toolCallId: 'call_1' });
162
+ await graph.recordStepCompletion('step_a', { toolCallId: 'call_1' });
163
+ expect(closed).toHaveLength(1);
164
+ });
165
+
166
+ it('leaves a closed step untouched if a late call registers and completes', async () => {
167
+ const { graph, closed } = createGraph();
168
+ const step = seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
169
+ graph.registerPendingToolCall('call_1', 'step_a');
170
+
171
+ await graph.recordStepCompletion('step_a', {
172
+ toolCallId: 'call_1',
173
+ at: 2_000,
174
+ });
175
+ expect(closed).toHaveLength(1);
176
+
177
+ graph.registerPendingToolCall('call_2', 'step_a');
178
+ await graph.recordStepCompletion('step_a', {
179
+ toolCallId: 'call_2',
180
+ at: 3_000,
181
+ });
182
+ /** First close wins outright — no second event and no divergence between
183
+ * the stored stamp and what subscribers were told. */
184
+ expect(closed).toHaveLength(1);
185
+ expect(step.completed_at).toBe(2_000);
186
+ expect(closed[0].closed_at).toBe(2_000);
187
+ });
188
+ it('closes a multi-call step at the latest producer timestamp', async () => {
189
+ const { graph, closed } = createGraph();
190
+ const step = seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
191
+ graph.registerPendingToolCall('call_1', 'step_a');
192
+ graph.registerPendingToolCall('call_2', 'step_a');
193
+
194
+ /** call_2 finished later, but its host handler was faster, so the
195
+ * earlier-finishing call_1 is what drains the pending set. */
196
+ await graph.recordStepCompletion('step_a', {
197
+ toolCallId: 'call_2',
198
+ at: 5_000,
199
+ });
200
+ await graph.recordStepCompletion('step_a', {
201
+ toolCallId: 'call_1',
202
+ at: 4_000,
203
+ });
204
+
205
+ expect(step.completed_at).toBe(5_000);
206
+ expect(closed).toHaveLength(1);
207
+ expect(closed[0].closed_at).toBe(5_000);
208
+ });
209
+
210
+ it('uses the producer\'s completion timestamp when one is supplied', async () => {
211
+ const { graph, closed } = createGraph();
212
+ const step = seedStep(graph, 'step_a', StepTypes.TOOL_CALLS);
213
+ graph.registerPendingToolCall('call_1', 'step_a');
214
+
215
+ await graph.recordStepCompletion('step_a', {
216
+ toolCallId: 'call_1',
217
+ at: 4_000,
218
+ });
219
+
220
+ /** A slow host handler must not inflate the recorded duration. */
221
+ expect(step.completed_at).toBe(4_000);
222
+ expect(closed).toHaveLength(1);
223
+ expect(closed[0].closed_at).toBe(4_000);
224
+ });
225
+ });
226
+
227
+ describe('StandardGraph.closeUnfinishedRunSteps', () => {
228
+ it('closes only non-terminal steps with the sweep status', async () => {
229
+ const { graph, closed } = createGraph();
230
+ const done = seedStep(graph, 'step_done', StepTypes.TOOL_CALLS);
231
+ const openMessage = seedStep(graph, 'step_msg');
232
+ const openTool = seedStep(graph, 'step_tool', StepTypes.TOOL_CALLS);
233
+ await graph.closeRunStep('step_done', 'completed', { at: 2_000 });
234
+ closed.length = 0;
235
+
236
+ await graph.closeUnfinishedRunSteps('cancelled', 5_000);
237
+
238
+ expect(done.completed_at).toBe(2_000);
239
+ expect(openMessage.status).toBe('cancelled');
240
+ expect(openMessage.cancelled_at).toBe(5_000);
241
+ expect(openTool.status).toBe('cancelled');
242
+ expect(openTool.cancelled_at).toBe(5_000);
243
+ expect(closed.map((event) => event.id)).toEqual(['step_msg', 'step_tool']);
244
+ expect(closed.every((event) => event.status === 'cancelled')).toBe(true);
245
+ expect(graph.pendingToolCallsByStep.size).toBe(0);
246
+ expect(graph.openMessageStepByAgent.size).toBe(0);
247
+ });
248
+
249
+ it('stamps failed_at for failed sweeps', async () => {
250
+ const { graph } = createGraph();
251
+ const step = seedStep(graph, 'step_a');
252
+
253
+ await graph.closeUnfinishedRunSteps('failed', 5_000);
254
+ expect(step.status).toBe('failed');
255
+ expect(step.failed_at).toBe(5_000);
256
+ expect(step.cancelled_at).toBeUndefined();
257
+ expect(step.completed_at).toBeUndefined();
258
+ });
259
+ it('keeps sweeping when a closure handler throws', async () => {
260
+ const graph = new StandardGraph({
261
+ runId: 'run_1',
262
+ agents: [makeAgent('agent')],
263
+ });
264
+ const seen: string[] = [];
265
+ const registry = new HandlerRegistry();
266
+ registry.register(GraphEvents.ON_RUN_STEP_CLOSED, {
267
+ handle: (_event, data): void => {
268
+ const event = data as t.RunStepClosedEvent;
269
+ seen.push(event.id);
270
+ if (event.id === 'step_1') {
271
+ throw new Error('host handler blew up');
272
+ }
273
+ },
274
+ });
275
+ graph.handlerRegistry = registry;
276
+ const first = seedStep(graph, 'step_1');
277
+ const second = seedStep(graph, 'step_2');
278
+ const third = seedStep(graph, 'step_3');
279
+
280
+ await graph.closeUnfinishedRunSteps('cancelled', 5_000);
281
+
282
+ expect(seen).toEqual(['step_1', 'step_2', 'step_3']);
283
+ for (const step of [first, second, third]) {
284
+ expect(step.status).toBe('cancelled');
285
+ expect(step.cancelled_at).toBe(5_000);
286
+ }
287
+ });
288
+ });
289
+
290
+ describe('StandardGraph.trackDispatchedRunStep', () => {
291
+ it('reserves distinct content indexes when parallel lanes dispatch concurrently', async () => {
292
+ const graph = new StandardGraph({
293
+ runId: 'run_1',
294
+ agents: [makeAgent('agent')],
295
+ });
296
+ /** A host handler that yields — the window in which two lanes could
297
+ * otherwise both read the same contentData.length. */
298
+ const registry = new HandlerRegistry();
299
+ registry.register(GraphEvents.ON_RUN_STEP_CLOSED, {
300
+ handle: async (): Promise<void> => {
301
+ await new Promise((resolve) => setTimeout(resolve, 5));
302
+ },
303
+ });
304
+ graph.handlerRegistry = registry;
305
+
306
+ const openA = seedStep(graph, 'open_a');
307
+ openA.agentId = 'agent_a';
308
+ const openB = seedStep(graph, 'open_b');
309
+ openB.agentId = 'agent_b';
310
+ graph.openMessageStepByAgent.set('agent_a', 'open_a');
311
+ graph.openMessageStepByAgent.set('agent_b', 'open_b');
312
+
313
+ const makeSuccessor = (id: string, agentId: string): t.RunStep => ({
314
+ id,
315
+ agentId,
316
+ type: StepTypes.MESSAGE_CREATION,
317
+ /** Both lanes computed the same stale index before dispatching. */
318
+ index: graph.contentData.length,
319
+ stepDetails: {
320
+ type: StepTypes.MESSAGE_CREATION,
321
+ message_creation: { message_id: `msg_${id}` },
322
+ },
323
+ usage: null,
324
+ created_at: 1_000,
325
+ status: 'in_progress',
326
+ });
327
+ const successorA = makeSuccessor('next_a', 'agent_a');
328
+ const successorB = makeSuccessor('next_b', 'agent_b');
329
+
330
+ const track = (
331
+ graph as unknown as {
332
+ trackDispatchedRunStep: (step: t.RunStep) => Promise<void>;
333
+ }
334
+ ).trackDispatchedRunStep.bind(graph);
335
+ await Promise.all([track(successorA), track(successorB)]);
336
+
337
+ expect(successorA.index).not.toBe(successorB.index);
338
+ expect(graph.getRunStep('next_a')).toBe(successorA);
339
+ expect(graph.getRunStep('next_b')).toBe(successorB);
340
+ expect(graph.contentData.filter((s) => s.id === 'next_a')).toHaveLength(1);
341
+ const indexes = graph.contentData.map((step) => step.index);
342
+ expect(new Set(indexes).size).toBe(indexes.length);
343
+ });
344
+ it('stamps created_at after the predecessor closure is delivered', async () => {
345
+ const graph = new StandardGraph({
346
+ runId: 'run_1',
347
+ agents: [makeAgent('agent')],
348
+ });
349
+ let predecessorClosedAt = 0;
350
+ const registry = new HandlerRegistry();
351
+ registry.register(GraphEvents.ON_RUN_STEP_CLOSED, {
352
+ handle: async (): Promise<void> => {
353
+ /** A slow predecessor handler must not be charged to the successor. */
354
+ await new Promise((resolve) => setTimeout(resolve, 20));
355
+ predecessorClosedAt = Date.now();
356
+ },
357
+ });
358
+ graph.handlerRegistry = registry;
359
+
360
+ seedStep(graph, 'open_a');
361
+ graph.openMessageStepByAgent.set('', 'open_a');
362
+
363
+ const successor: t.RunStep = {
364
+ id: 'next_a',
365
+ type: StepTypes.MESSAGE_CREATION,
366
+ index: 0,
367
+ stepDetails: {
368
+ type: StepTypes.MESSAGE_CREATION,
369
+ message_creation: { message_id: 'msg_next_a' },
370
+ },
371
+ usage: null,
372
+ status: 'in_progress',
373
+ };
374
+ await (
375
+ graph as unknown as {
376
+ trackDispatchedRunStep: (step: t.RunStep) => Promise<void>;
377
+ }
378
+ ).trackDispatchedRunStep.bind(graph)(successor);
379
+
380
+ expect(predecessorClosedAt).toBeGreaterThan(0);
381
+ expect(successor.created_at as number).toBeGreaterThanOrEqual(
382
+ predecessorClosedAt
383
+ );
384
+ });
385
+ });
@@ -14,6 +14,8 @@ const CHAIN_OBSERVATION_TYPE = 'chain';
14
14
  const TOOL_OBSERVATION_TYPE = 'tool';
15
15
  const AGENT_TRACE_TAG = 'agent';
16
16
  const TITLE_TRACE_TAG = 'title';
17
+ const ACTIVITY_PHASE_TRACE_TAG = 'activity-phase';
18
+ const ACTIVITY_PHASE_ROOT_NAME = 'summarize-activity-phase';
17
19
  const EPHEMERAL_AGENT_SENDER_SEPARATOR = '___';
18
20
  const EPHEMERAL_AGENT_INDEX_SEPARATOR = '____';
19
21
  const OBSERVATION_METADATA_LANGGRAPH_NODE = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;
@@ -473,14 +475,18 @@ function shapeRootObservationType(span: MutableSpan): void {
473
475
  if (isGenerationSpan(span)) {
474
476
  return;
475
477
  }
476
- if (hasTraceTag(span, AGENT_TRACE_TAG)) {
478
+ if (
479
+ hasTraceTag(span, TITLE_TRACE_TAG) ||
480
+ (span.name === ACTIVITY_PHASE_ROOT_NAME &&
481
+ hasTraceTag(span, ACTIVITY_PHASE_TRACE_TAG))
482
+ ) {
477
483
  span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
478
- ROOT_OBSERVATION_TYPE;
484
+ CHAIN_OBSERVATION_TYPE;
479
485
  return;
480
486
  }
481
- if (hasTraceTag(span, TITLE_TRACE_TAG)) {
487
+ if (hasTraceTag(span, AGENT_TRACE_TAG)) {
482
488
  span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
483
- CHAIN_OBSERVATION_TYPE;
489
+ ROOT_OBSERVATION_TYPE;
484
490
  }
485
491
  }
486
492
 
@@ -497,9 +503,10 @@ function shapeRootObservationType(span: MutableSpan): void {
497
503
  * - Agent nodes become `agent` observations, while tool-dispatch nodes become
498
504
  * stable `chain` observations whose input is scoped to the pending calls.
499
505
  * Individual child calls remain `tool` observations (items 3 & 4).
500
- * - Agent trace roots become `agent` observations and title trace roots become
501
- * `chain` observations. Root and trace input/output are reduced to the user
502
- * question and assistant response when chat messages are available (item 2).
506
+ * - Agent trace roots become `agent` observations, while title and activity
507
+ * summary roots become `chain` observations. Root and trace input/output are
508
+ * reduced to the user question and assistant response when chat messages are
509
+ * available (item 2).
503
510
  */
504
511
  export function shapeLangfuseSpan(span: ReadableSpan): void {
505
512
  const mutable = span as MutableSpan;