@librechat/agents 3.4.5 → 3.4.7

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 (87) 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/llm/google/utils/common.cjs +10 -5
  8. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  9. package/dist/cjs/run.cjs +147 -14
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/session/AgentSession.cjs +93 -31
  12. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  13. package/dist/cjs/session/handlers.cjs +10 -0
  14. package/dist/cjs/session/handlers.cjs.map +1 -1
  15. package/dist/cjs/stream.cjs +17 -3
  16. package/dist/cjs/stream.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +1 -0
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +4 -2
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/handlers.cjs +1 -0
  22. package/dist/cjs/tools/handlers.cjs.map +1 -1
  23. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +28 -0
  24. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  25. package/dist/cjs/utils/handlers.cjs +3 -0
  26. package/dist/cjs/utils/handlers.cjs.map +1 -1
  27. package/dist/esm/common/enum.mjs +2 -0
  28. package/dist/esm/common/enum.mjs.map +1 -1
  29. package/dist/esm/graphs/Graph.mjs +268 -14
  30. package/dist/esm/graphs/Graph.mjs.map +1 -1
  31. package/dist/esm/langfuseTraceShaping.mjs +9 -6
  32. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  33. package/dist/esm/llm/google/utils/common.mjs +10 -5
  34. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  35. package/dist/esm/run.mjs +147 -14
  36. package/dist/esm/run.mjs.map +1 -1
  37. package/dist/esm/session/AgentSession.mjs +93 -31
  38. package/dist/esm/session/AgentSession.mjs.map +1 -1
  39. package/dist/esm/session/handlers.mjs +10 -0
  40. package/dist/esm/session/handlers.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +17 -3
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/summarization/node.mjs +1 -0
  44. package/dist/esm/summarization/node.mjs.map +1 -1
  45. package/dist/esm/tools/ToolNode.mjs +4 -2
  46. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  47. package/dist/esm/tools/handlers.mjs +1 -0
  48. package/dist/esm/tools/handlers.mjs.map +1 -1
  49. package/dist/esm/tools/subagent/SubagentExecutor.mjs +28 -0
  50. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  51. package/dist/esm/utils/handlers.mjs +3 -0
  52. package/dist/esm/utils/handlers.mjs.map +1 -1
  53. package/dist/types/common/enum.d.ts +2 -0
  54. package/dist/types/graphs/Graph.d.ts +87 -0
  55. package/dist/types/langfuseTraceShaping.d.ts +4 -3
  56. package/dist/types/run.d.ts +9 -1
  57. package/dist/types/session/types.d.ts +1 -1
  58. package/dist/types/summarization/node.d.ts +6 -0
  59. package/dist/types/types/graph.d.ts +3 -3
  60. package/dist/types/types/stream.d.ts +57 -0
  61. package/dist/types/utils/handlers.d.ts +1 -0
  62. package/package.json +1 -1
  63. package/src/aggregator.test.ts +127 -0
  64. package/src/common/enum.ts +2 -0
  65. package/src/graphs/Graph.ts +365 -7
  66. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +385 -0
  67. package/src/langfuseTraceShaping.ts +14 -7
  68. package/src/llm/google/utils/common.test.ts +11 -0
  69. package/src/llm/google/utils/common.ts +6 -4
  70. package/src/run.ts +219 -41
  71. package/src/session/AgentSession.ts +105 -13
  72. package/src/session/handlers.ts +14 -2
  73. package/src/session/types.ts +1 -0
  74. package/src/specs/activity-label-observability.live.test.ts +262 -0
  75. package/src/specs/activity-label-observability.test.ts +167 -0
  76. package/src/specs/langfuse-trace-shaping.test.ts +32 -0
  77. package/src/specs/run-step-timestamps.test.ts +414 -0
  78. package/src/stream.ts +29 -6
  79. package/src/summarization/node.ts +11 -0
  80. package/src/tools/ToolNode.ts +2 -0
  81. package/src/tools/__tests__/SubagentExecutor.test.ts +52 -10
  82. package/src/tools/__tests__/handlers.test.ts +2 -0
  83. package/src/tools/handlers.ts +1 -0
  84. package/src/tools/subagent/SubagentExecutor.ts +46 -4
  85. package/src/types/graph.ts +9 -6
  86. package/src/types/stream.ts +65 -12
  87. package/src/utils/handlers.ts +13 -0
@@ -36,6 +36,9 @@ function createHandlers(callbacks) {
36
36
  });
37
37
  callbacks?.onRunStepDelta?.(event, data);
38
38
  } },
39
+ ["on_run_step_closed"]: { handle: (event, data) => {
40
+ callbacks?.onRunStepClosed?.(event, data);
41
+ } },
39
42
  ["on_message_delta"]: { handle: (event, data) => {
40
43
  aggregateContent({
41
44
  event,
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.mjs","names":[],"sources":["../../../src/utils/handlers.ts"],"sourcesContent":["/**\n * Multi-Agent Handler Utilities\n *\n * Provides a simple helper to create handlers with content aggregation for multi-agent scripts.\n *\n * Usage:\n * ```typescript\n * const { contentParts, aggregateContent, handlers } = createHandlers();\n *\n * // With callbacks\n * const { contentParts, aggregateContent, handlers } = createHandlers({\n * onRunStep: (event, data) => console.log('Step:', data),\n * onRunStepCompleted: (event, data) => console.log('Completed:', data)\n * });\n * ```\n */\n\nimport type * as t from '@/types';\nimport { ChatModelStreamHandler, createContentAggregator } from '@/stream';\nimport { ToolEndHandler, ModelEndHandler } from '@/events';\nimport { GraphEvents } from '@/common';\n\ninterface HandlerCallbacks {\n onRunStep?: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData) => void;\n onRunStepCompleted?: (\n event: GraphEvents.ON_RUN_STEP_COMPLETED,\n data: t.StreamEventData\n ) => void;\n onRunStepDelta?: (\n event: GraphEvents.ON_RUN_STEP_DELTA,\n data: t.StreamEventData\n ) => void;\n onMessageDelta?: (\n event: GraphEvents.ON_MESSAGE_DELTA,\n data: t.StreamEventData\n ) => void;\n}\n\n/**\n * Creates handlers with content aggregation for multi-agent scripts\n */\nexport function createHandlers(callbacks?: HandlerCallbacks): {\n contentParts: Array<t.MessageContentComplex | undefined>;\n aggregateContent: ReturnType<\n typeof createContentAggregator\n >['aggregateContent'];\n handlers: Record<string, t.EventHandler>;\n} {\n // Set up content aggregator\n const { contentParts, aggregateContent } = createContentAggregator();\n\n // Create the handlers object\n const handlers = {\n [GraphEvents.TOOL_END]: new ToolEndHandler(),\n [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),\n [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),\n\n [GraphEvents.ON_RUN_STEP]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP,\n data: t.StreamEventData\n ): void => {\n aggregateContent({ event, data: data as t.RunStep });\n callbacks?.onRunStep?.(event, data);\n },\n },\n\n [GraphEvents.ON_RUN_STEP_COMPLETED]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP_COMPLETED,\n data: t.StreamEventData\n ): void => {\n aggregateContent({\n event,\n data: data as unknown as { result: t.ToolEndEvent },\n });\n callbacks?.onRunStepCompleted?.(event, data);\n },\n },\n\n [GraphEvents.ON_RUN_STEP_DELTA]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP_DELTA,\n data: t.StreamEventData\n ): void => {\n aggregateContent({ event, data: data as t.RunStepDeltaEvent });\n callbacks?.onRunStepDelta?.(event, data);\n },\n },\n\n [GraphEvents.ON_MESSAGE_DELTA]: {\n handle: (\n event: GraphEvents.ON_MESSAGE_DELTA,\n data: t.StreamEventData\n ): void => {\n aggregateContent({ event, data: data as t.MessageDeltaEvent });\n callbacks?.onMessageDelta?.(event, data);\n },\n },\n\n [GraphEvents.ON_SUMMARIZE_DELTA]: {\n handle: (event: string, data: t.StreamEventData): void => {\n aggregateContent({\n event: event as GraphEvents,\n data: data as t.SummarizeDeltaData,\n });\n },\n },\n\n [GraphEvents.ON_SUMMARIZE_COMPLETE]: {\n handle: (event: string, data: t.StreamEventData): void => {\n aggregateContent({\n event: event as GraphEvents,\n data: data as t.SummarizeCompleteEvent,\n });\n },\n },\n };\n\n return {\n contentParts,\n aggregateContent,\n handlers,\n };\n}\n"],"mappings":";;;;;;;;AAyCA,SAAgB,eAAe,WAM7B;CAEA,MAAM,EAAE,cAAc,qBAAqB,wBAAwB;CAsEnE,OAAO;EACL;EACA;EACA,UAAA;oBArEwB,IAAI,eAAe;0BACb,IAAI,gBAAgB;6BACjB,IAAI,uBAAuB;oBAEjC,EACzB,SACE,OACA,SACS;IACT,iBAAiB;KAAE;KAAa;IAAkB,CAAC;IACnD,WAAW,YAAY,OAAO,IAAI;GACpC,EACF;8BAEqC,EACnC,SACE,OACA,SACS;IACT,iBAAiB;KACf;KACM;IACR,CAAC;IACD,WAAW,qBAAqB,OAAO,IAAI;GAC7C,EACF;0BAEiC,EAC/B,SACE,OACA,SACS;IACT,iBAAiB;KAAE;KAAa;IAA4B,CAAC;IAC7D,WAAW,iBAAiB,OAAO,IAAI;GACzC,EACF;yBAEgC,EAC9B,SACE,OACA,SACS;IACT,iBAAiB;KAAE;KAAa;IAA4B,CAAC;IAC7D,WAAW,iBAAiB,OAAO,IAAI;GACzC,EACF;2BAEkC,EAChC,SAAS,OAAe,SAAkC;IACxD,iBAAiB;KACR;KACD;IACR,CAAC;GACH,EACF;8BAEqC,EACnC,SAAS,OAAe,SAAkC;IACxD,iBAAiB;KACR;KACD;IACR,CAAC;GACH,EACF;EAMO;CACT;AACF"}
1
+ {"version":3,"file":"handlers.mjs","names":[],"sources":["../../../src/utils/handlers.ts"],"sourcesContent":["/**\n * Multi-Agent Handler Utilities\n *\n * Provides a simple helper to create handlers with content aggregation for multi-agent scripts.\n *\n * Usage:\n * ```typescript\n * const { contentParts, aggregateContent, handlers } = createHandlers();\n *\n * // With callbacks\n * const { contentParts, aggregateContent, handlers } = createHandlers({\n * onRunStep: (event, data) => console.log('Step:', data),\n * onRunStepCompleted: (event, data) => console.log('Completed:', data)\n * });\n * ```\n */\n\nimport type * as t from '@/types';\nimport { ChatModelStreamHandler, createContentAggregator } from '@/stream';\nimport { ToolEndHandler, ModelEndHandler } from '@/events';\nimport { GraphEvents } from '@/common';\n\ninterface HandlerCallbacks {\n onRunStep?: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData) => void;\n onRunStepCompleted?: (\n event: GraphEvents.ON_RUN_STEP_COMPLETED,\n data: t.StreamEventData\n ) => void;\n onRunStepDelta?: (\n event: GraphEvents.ON_RUN_STEP_DELTA,\n data: t.StreamEventData\n ) => void;\n onRunStepClosed?: (\n event: GraphEvents.ON_RUN_STEP_CLOSED,\n data: t.RunStepClosedEvent\n ) => void;\n onMessageDelta?: (\n event: GraphEvents.ON_MESSAGE_DELTA,\n data: t.StreamEventData\n ) => void;\n}\n\n/**\n * Creates handlers with content aggregation for multi-agent scripts\n */\nexport function createHandlers(callbacks?: HandlerCallbacks): {\n contentParts: Array<t.MessageContentComplex | undefined>;\n aggregateContent: ReturnType<\n typeof createContentAggregator\n >['aggregateContent'];\n handlers: Record<string, t.EventHandler>;\n} {\n // Set up content aggregator\n const { contentParts, aggregateContent } = createContentAggregator();\n\n // Create the handlers object\n const handlers = {\n [GraphEvents.TOOL_END]: new ToolEndHandler(),\n [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),\n [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),\n\n [GraphEvents.ON_RUN_STEP]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP,\n data: t.StreamEventData\n ): void => {\n aggregateContent({ event, data: data as t.RunStep });\n callbacks?.onRunStep?.(event, data);\n },\n },\n\n [GraphEvents.ON_RUN_STEP_COMPLETED]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP_COMPLETED,\n data: t.StreamEventData\n ): void => {\n aggregateContent({\n event,\n data: data as unknown as { result: t.ToolEndEvent },\n });\n callbacks?.onRunStepCompleted?.(event, data);\n },\n },\n\n [GraphEvents.ON_RUN_STEP_DELTA]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP_DELTA,\n data: t.StreamEventData\n ): void => {\n aggregateContent({ event, data: data as t.RunStepDeltaEvent });\n callbacks?.onRunStepDelta?.(event, data);\n },\n },\n\n [GraphEvents.ON_RUN_STEP_CLOSED]: {\n handle: (\n event: GraphEvents.ON_RUN_STEP_CLOSED,\n data: t.StreamEventData\n ): void => {\n callbacks?.onRunStepClosed?.(event, data as t.RunStepClosedEvent);\n },\n },\n\n [GraphEvents.ON_MESSAGE_DELTA]: {\n handle: (\n event: GraphEvents.ON_MESSAGE_DELTA,\n data: t.StreamEventData\n ): void => {\n aggregateContent({ event, data: data as t.MessageDeltaEvent });\n callbacks?.onMessageDelta?.(event, data);\n },\n },\n\n [GraphEvents.ON_SUMMARIZE_DELTA]: {\n handle: (event: string, data: t.StreamEventData): void => {\n aggregateContent({\n event: event as GraphEvents,\n data: data as t.SummarizeDeltaData,\n });\n },\n },\n\n [GraphEvents.ON_SUMMARIZE_COMPLETE]: {\n handle: (event: string, data: t.StreamEventData): void => {\n aggregateContent({\n event: event as GraphEvents,\n data: data as t.SummarizeCompleteEvent,\n });\n },\n },\n };\n\n return {\n contentParts,\n aggregateContent,\n handlers,\n };\n}\n"],"mappings":";;;;;;;;AA6CA,SAAgB,eAAe,WAM7B;CAEA,MAAM,EAAE,cAAc,qBAAqB,wBAAwB;CA+EnE,OAAO;EACL;EACA;EACA,UAAA;oBA9EwB,IAAI,eAAe;0BACb,IAAI,gBAAgB;6BACjB,IAAI,uBAAuB;oBAEjC,EACzB,SACE,OACA,SACS;IACT,iBAAiB;KAAE;KAAa;IAAkB,CAAC;IACnD,WAAW,YAAY,OAAO,IAAI;GACpC,EACF;8BAEqC,EACnC,SACE,OACA,SACS;IACT,iBAAiB;KACf;KACM;IACR,CAAC;IACD,WAAW,qBAAqB,OAAO,IAAI;GAC7C,EACF;0BAEiC,EAC/B,SACE,OACA,SACS;IACT,iBAAiB;KAAE;KAAa;IAA4B,CAAC;IAC7D,WAAW,iBAAiB,OAAO,IAAI;GACzC,EACF;2BAEkC,EAChC,SACE,OACA,SACS;IACT,WAAW,kBAAkB,OAAO,IAA4B;GAClE,EACF;yBAEgC,EAC9B,SACE,OACA,SACS;IACT,iBAAiB;KAAE;KAAa;IAA4B,CAAC;IAC7D,WAAW,iBAAiB,OAAO,IAAI;GACzC,EACF;2BAEkC,EAChC,SAAS,OAAe,SAAkC;IACxD,iBAAiB;KACR;KACD;IACR,CAAC;GACH,EACF;8BAEqC,EACnC,SAAS,OAAe,SAAkC;IACxD,iBAAiB;KACR;KACD;IACR,CAAC;GACH,EACF;EAMO;CACT;AACF"}
@@ -13,6 +13,8 @@ export declare enum GraphEvents {
13
13
  ON_RUN_STEP_DELTA = "on_run_step_delta",
14
14
  /** [Custom] Completed event for run steps (tool calls) */
15
15
  ON_RUN_STEP_COMPLETED = "on_run_step_completed",
16
+ /** [Custom] Terminal signal for a run step: closed with status + timestamps */
17
+ ON_RUN_STEP_CLOSED = "on_run_step_closed",
16
18
  /** [Custom] Delta events for messages */
17
19
  ON_MESSAGE_DELTA = "on_message_delta",
18
20
  /** [Custom] Reasoning Delta events for messages */
@@ -45,6 +45,17 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
45
45
  stepKeyIds: Map<string, string[]>;
46
46
  contentIndexMap: Map<string, number>;
47
47
  toolCallStepIds: Map<string, string>;
48
+ /** Step ID -> tool call IDs whose completions have not yet arrived. */
49
+ pendingToolCallsByStep: Map<string, Set<string>>;
50
+ /**
51
+ * Step ID -> latest producer completion time seen for that step. Parallel
52
+ * calls sharing a step can settle out of producer order when their host
53
+ * handlers differ in latency, so the call that happens to drain the set is
54
+ * not necessarily the one that finished last.
55
+ */
56
+ latestCompletionByStep: Map<string, number>;
57
+ /** Agent key ('' for single-agent) -> currently open MESSAGE_CREATION step ID. */
58
+ openMessageStepByAgent: Map<string, string>;
48
59
  /**
49
60
  * Step IDs dispatched through the handler registry during this run.
50
61
  * Event echo suppression is tracked separately so repeated deltas for
@@ -153,6 +164,16 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
153
164
  clearHeavyState(): void;
154
165
  getEagerEventToolUsageCount(agentId?: string): Map<string, number>;
155
166
  protected clearEagerEventToolUsageCounts(): void;
167
+ /**
168
+ * Tracks a tool call whose completion must arrive before its step can be
169
+ * considered finished. Registered wherever `toolCallStepIds` gains entries,
170
+ * except cross-process subagent resume restoration, where pending state
171
+ * cannot be faithfully rebuilt and closes fall back to completions/sweep.
172
+ */
173
+ registerPendingToolCall(toolCallId: string, stepId: string): void;
174
+ /** Lazily creates a step's pending-completions set; callers registering a
175
+ * batch hoist this lookup out of their per-call loop. */
176
+ protected getPendingToolCallSet(stepId: string): Set<string>;
156
177
  markHandlerDispatchedEvent(eventName: string, stepId: string): () => void;
157
178
  hasHandlerDispatchedEvent(eventName: string, stepId: string): boolean;
158
179
  /**
@@ -415,6 +436,72 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
415
436
  releasePreemptSeal(): void;
416
437
  getPreemptStats(): t.PreemptStats;
417
438
  getRunStep(stepId: string): t.RunStep | undefined;
439
+ /**
440
+ * Derives the same lane key `dispatchRunStep` stamps as `runStep.agentId`.
441
+ * The multi-agent check gates the lookup because `getAgentContext` signals
442
+ * a miss by throwing: single-agent graphs key every step under `''`, so
443
+ * resolving the context could only ever produce a thrown-and-discarded
444
+ * Error on a per-model-call path.
445
+ */
446
+ protected getStepAgentKey(metadata?: Record<string, unknown>): string;
447
+ /**
448
+ * O(1) reverse lookup: both dispatch funnels key the open-message map by
449
+ * `runStep.agentId ?? ''`, so the entry is addressable without scanning.
450
+ */
451
+ private untrackRunStep;
452
+ /**
453
+ * Shared step accounting for both dispatch funnels: a successor step in
454
+ * the same agent lane marks the previous message step as finished — its
455
+ * CLOSED event must precede the successor's ON_RUN_STEP so hosts observe
456
+ * a consistent open-step timeline — then the step is registered in the
457
+ * content maps and tracked as the lane's open message step when
458
+ * applicable.
459
+ */
460
+ protected trackDispatchedRunStep(runStep: t.RunStep, metadata?: Record<string, unknown>,
461
+ /**
462
+ * Summarization steps are typed MESSAGE_CREATION but own an explicit
463
+ * completion, and their model call emits `CHAT_MODEL_END` well before the
464
+ * summary is assembled. Tracking one as the lane's open step would let
465
+ * model-end publish an authoritative `completed` closure early — the
466
+ * measured duration would exclude the remaining work and a later
467
+ * post-model failure could no longer change the status. They close
468
+ * through `recordStepCompletion` instead.
469
+ */
470
+ trackAsOpenMessageStep?: boolean): Promise<void>;
471
+ /**
472
+ * Closes a run step: stamps its terminal status + timestamp on the stored
473
+ * `RunStep` and emits `ON_RUN_STEP_CLOSED`. First close wins — later calls
474
+ * are no-ops — except a `restamp` close, which lets a `completed`
475
+ * TOOL_CALLS step refresh `completed_at` when a late-registered parallel
476
+ * tool call finishes after the step already closed (the eager-execution
477
+ * race). `cancelled`/`failed` are immutable once stamped.
478
+ */
479
+ closeRunStep(stepId: string, status: Exclude<t.RunStepStatus, 'in_progress'>, options?: t.RunStepCloseOptions): Promise<boolean>;
480
+ /**
481
+ * Observes one `ON_RUN_STEP_COMPLETED` for a step and closes the step when
482
+ * no registered tool calls remain pending. Steps without pending tracking
483
+ * (summaries, cross-process resume) close on their first completion; the
484
+ * terminal-status guard in `closeRunStep` absorbs duplicate echoes.
485
+ */
486
+ recordStepCompletion(stepId: string, options?: t.RecordStepCompletionOptions): Promise<void>;
487
+ /**
488
+ * Closes the tracked open MESSAGE_CREATION step for the event's agent lane.
489
+ * Fires on every model end, so the empty-map check short-circuits ahead of
490
+ * resolving the lane key — a turn whose message step already closed through
491
+ * successor-close does no work here.
492
+ */
493
+ closeOpenMessageStep(metadata?: Record<string, unknown>,
494
+ /** Model-end time captured before host handlers ran, so a slow usage sink
495
+ * cannot inflate the step's measured duration. */
496
+ at?: number): Promise<void>;
497
+ /**
498
+ * End-of-run sweep: closes every step that never reached a terminal
499
+ * status. Dual-dispatches like any other close — the custom-event channel
500
+ * is usually already torn down here and `safeDispatchCustomEvent` reports
501
+ * that quietly, but callback-only subscribers still receive the terminal
502
+ * signal whenever it is alive.
503
+ */
504
+ closeUnfinishedRunSteps(status: Exclude<t.RunStepStatus, 'in_progress'>, at?: number): Promise<void>;
418
505
  getAgentContext(metadata: Record<string, unknown> | undefined): AgentContext;
419
506
  getStepBaseKey(metadata: Record<string, unknown> | undefined): string;
420
507
  getStepKey(metadata: Record<string, unknown> | undefined): string;
@@ -20,8 +20,9 @@ export declare function shouldDropLangfuseSpan(spanName: string): boolean;
20
20
  * - Agent nodes become `agent` observations, while tool-dispatch nodes become
21
21
  * stable `chain` observations whose input is scoped to the pending calls.
22
22
  * Individual child calls remain `tool` observations (items 3 & 4).
23
- * - Agent trace roots become `agent` observations and title trace roots become
24
- * `chain` observations. Root and trace input/output are reduced to the user
25
- * question and assistant response when chat messages are available (item 2).
23
+ * - Agent trace roots become `agent` observations, while title and activity
24
+ * summary roots become `chain` observations. Root and trace input/output are
25
+ * reduced to the user question and assistant response when chat messages are
26
+ * available (item 2).
26
27
  */
27
28
  export declare function shapeLangfuseSpan(span: ReadableSpan): void;
@@ -138,6 +138,14 @@ export declare class Run<_T extends t.BaseGraphState> {
138
138
  private createCustomEventCallback;
139
139
  private shouldClearHookSession;
140
140
  private isAwaitingResume;
141
+ /**
142
+ * Terminal status for steps still open at end-of-run: `cancelled` for
143
+ * intentional stops (caller abort, hook halt), `failed` for unexpected
144
+ * stream errors, `completed` for a natural finish. Reads `_haltedReason`
145
+ * behind a method boundary on purpose — it is assigned inside the
146
+ * `consumeStream` closure, which control-flow narrowing cannot see.
147
+ */
148
+ private resolveSweepStatus;
141
149
  private getStreamLangfuseConfig;
142
150
  private getStreamToolOutputTracingLangfuseConfig;
143
151
  processStream(inputs: t.IState | Command, callerConfig: t.RunStreamConfig, streamOptions?: t.EventStreamOptions): Promise<MessageContentComplex[] | undefined>;
@@ -247,7 +255,7 @@ export declare class Run<_T extends t.BaseGraphState> {
247
255
  }>;
248
256
  /**
249
257
  * Generates one parent summary for two or more logical activities. The
250
- * summary model is traced as a dedicated activity-phase agent root in the
258
+ * summary model is traced as a dedicated activity-phase chain root in the
251
259
  * conversation session, with the model callback recorded as its generation
252
260
  * child. No session id means no phase trace, avoiding orphan observations.
253
261
  */
@@ -150,7 +150,7 @@ export interface AgentSessionRunOptions {
150
150
  streamOptions?: t.EventStreamOptions;
151
151
  }
152
152
  export interface AgentSessionStreamEvent {
153
- type: 'run.started' | 'message.delta' | 'reasoning.delta' | 'tool.started' | 'tool.delta' | 'tool.completed' | 'usage.updated' | 'run.completed' | 'run.failed' | 'run.interrupted' | 'run.halted';
153
+ type: 'run.started' | 'message.delta' | 'reasoning.delta' | 'tool.started' | 'tool.delta' | 'tool.completed' | 'step.finished' | 'usage.updated' | 'run.completed' | 'run.failed' | 'run.interrupted' | 'run.halted';
154
154
  sequence: number;
155
155
  runId: string;
156
156
  threadId: string;
@@ -21,6 +21,12 @@ interface CreateSummarizeNodeParams {
21
21
  hookRegistry?: HookRegistry;
22
22
  dispatchRunStep: (runStep: t.RunStep, config?: RunnableConfig) => Promise<void>;
23
23
  dispatchRunStepCompleted: (stepId: string, result: t.StepCompleted, config?: RunnableConfig) => Promise<void>;
24
+ /**
25
+ * Terminal close for a summary step that ends without a completion —
26
+ * an errored or empty summary would otherwise stay `in_progress` until
27
+ * the run-end sweep reported it as `completed`.
28
+ */
29
+ closeRunStep?: (stepId: string, status: Exclude<t.RunStepStatus, 'in_progress'>, config?: RunnableConfig) => Promise<void>;
24
30
  /** The run's shared breaker signal, composed into every summarization
25
31
  * model attempt so a sibling branch tripping a stream limit also
26
32
  * cancels in-flight summaries. */
@@ -5,8 +5,8 @@ import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
5
5
  import type { ChatGenerationChunk } from '@langchain/core/outputs';
6
6
  import type { GoogleAIToolType } from '@langchain/google-common';
7
7
  import type { SummarizationNodeInput, SummarizeCompleteEvent, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
8
+ import type { RunStep, RunStepDeltaEvent, RunStepClosedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
8
9
  import type { ToolMap, ToolSessionMap, ToolEndEvent, GenericTool, LCTool, ToolExecuteBatchRequest } from '@/types/tools';
9
- import type { RunStep, RunStepDeltaEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
10
10
  import type { TokenCounter, StreamLimits, StreamPreemption, TokenBudgetBreakdown } from '@/types/run';
11
11
  import type { Providers, Callback, GraphNodeKeys } from '@/common';
12
12
  import type { StandardGraph, MultiAgentGraph } from '@/graphs';
@@ -73,7 +73,7 @@ export interface ContextUsageEvent {
73
73
  calibrationRatio?: number;
74
74
  }
75
75
  export interface EventHandler {
76
- handle(event: string, data: StreamEventData | ModelEndData | RunStep | RunStepDeltaEvent | MessageDeltaEvent | ReasoningDeltaEvent | SummarizeStartEvent | SummarizeDeltaEvent | SummarizeCompleteEvent | SubagentUpdateEvent | AgentLogEvent | ContextUsageEvent | ToolExecuteBatchRequest | {
76
+ handle(event: string, data: StreamEventData | ModelEndData | RunStep | RunStepDeltaEvent | RunStepClosedEvent | MessageDeltaEvent | ReasoningDeltaEvent | SummarizeStartEvent | SummarizeDeltaEvent | SummarizeCompleteEvent | SubagentUpdateEvent | AgentLogEvent | ContextUsageEvent | ToolExecuteBatchRequest | {
77
77
  result: ToolEndEvent;
78
78
  }, metadata?: Record<string, unknown>, graph?: StandardGraph | MultiAgentGraph): void | Promise<void>;
79
79
  }
@@ -442,7 +442,7 @@ export type ExecutableSubagentConfig = ResolvedSubagentConfig | LazySingleAgentS
442
442
  /** Graph-aware config accepted by the executor. Graph configs stay eager. */
443
443
  export type ExecutableSubagentConfigEntry = ExecutableSubagentConfig | GraphSubagentConfig;
444
444
  /** Lifecycle phase carried on {@link SubagentUpdateEvent}. */
445
- export type SubagentUpdatePhase = 'start' | 'run_step' | 'run_step_delta' | 'run_step_completed' | 'message_delta' | 'reasoning_delta' | 'stop' | 'error';
445
+ export type SubagentUpdatePhase = 'start' | 'run_step' | 'run_step_delta' | 'run_step_completed' | 'run_step_closed' | 'message_delta' | 'reasoning_delta' | 'stop' | 'error';
446
446
  export interface SubagentAncestryEntry {
447
447
  readonly subagentRunId: string;
448
448
  readonly subagentType: string;
@@ -2,6 +2,7 @@ import type { MessageContentImageUrl, MessageContentText, ToolMessage, BaseMessa
2
2
  import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
3
3
  import type { LLMResult, Generation } from '@langchain/core/outputs';
4
4
  import type { Command } from '@langchain/langgraph';
5
+ import type Anthropic from '@anthropic-ai/sdk';
5
6
  import type { AnthropicContentBlock } from '@/llm/anthropic/types';
6
7
  import type { AssistantTextPhase } from '@/types/assistantPhase';
7
8
  import type { SummarizeCompleteEvent } from '@/types/summarize';
@@ -34,8 +35,25 @@ start, stream and end are associated with slightly different data payload.
34
35
 
35
36
  Please see the documentation for EventData for more details. */
36
37
  export type EventName = string;
38
+ export type RunStepStatus = 'in_progress' | 'completed' | 'cancelled' | 'failed';
37
39
  export type RunStep = {
38
40
  type: StepTypes;
41
+ /** Epoch ms when the step was dispatched. */
42
+ created_at?: number;
43
+ /**
44
+ * Lifecycle status; terminal values are stamped when the step closes.
45
+ * Invariant (enforced by `closeRunStep`, not the type, to stay wire-compatible
46
+ * with the OpenAI Assistants shape): a terminal status sets exactly one
47
+ * matching `*_at` field; first close wins and `cancelled`/`failed` are
48
+ * immutable once stamped.
49
+ */
50
+ status?: RunStepStatus;
51
+ /** Epoch ms when the step closed with status `completed`. */
52
+ completed_at?: number;
53
+ /** Epoch ms when the step closed with status `cancelled` (abort/halt). */
54
+ cancelled_at?: number;
55
+ /** Epoch ms when the step closed with status `failed`. */
56
+ failed_at?: number;
39
57
  id: string;
40
58
  runId?: string;
41
59
  agentId?: string;
@@ -71,6 +89,41 @@ export interface RunStepDeltaEvent {
71
89
  */
72
90
  delta: ToolCallDelta;
73
91
  }
92
+ /**
93
+ * Terminal signal for a run step, emitted exactly once per step when it
94
+ * finishes (`completed`), is aborted/halted (`cancelled`), or the run errors
95
+ * (`failed`). The `id` is top-level so callback echoes dedupe like other
96
+ * step-scoped events.
97
+ */
98
+ export interface RunStepClosedEvent {
99
+ id: string;
100
+ index: number;
101
+ type: StepTypes;
102
+ status: Exclude<RunStepStatus, 'in_progress'>;
103
+ /** Epoch ms when the step was dispatched, when known. */
104
+ created_at?: number;
105
+ /** Epoch ms when the step reached its terminal status. */
106
+ closed_at: number;
107
+ runId?: string;
108
+ agentId?: string;
109
+ groupId?: number;
110
+ stepIndex?: number;
111
+ }
112
+ export type RecordStepCompletionOptions = {
113
+ /** The completing tool call, when the step tracks pending completions. */
114
+ toolCallId?: string;
115
+ metadata?: Record<string, unknown>;
116
+ /**
117
+ * Producer-stamped completion time (epoch ms). Carried through so a slow
118
+ * host completion handler cannot inflate the recorded step duration.
119
+ */
120
+ at?: number;
121
+ };
122
+ export type RunStepCloseOptions = {
123
+ /** Epoch ms for the terminal stamp; defaults to `Date.now()` at close time. */
124
+ at?: number;
125
+ metadata?: Record<string, unknown>;
126
+ };
74
127
  export type StepDetails = MessageCreationDetails | ToolCallsDetails;
75
128
  export type SummaryCompleted = {
76
129
  type: 'summary';
@@ -132,6 +185,8 @@ export type ToolCompleteEvent = ToolCallCompleted & {
132
185
  /** The content index of the tool call */
133
186
  index: number;
134
187
  type: 'tool_call';
188
+ /** Epoch ms when this tool call's completion was dispatched. */
189
+ completed_at?: number;
135
190
  };
136
191
  export type ToolCallsDetails = {
137
192
  type: StepTypes.TOOL_CALLS;
@@ -224,6 +279,8 @@ export type MessageDeltaUpdate = {
224
279
  type: ContentTypes.TEXT;
225
280
  text: string;
226
281
  tool_call_ids?: string[];
282
+ /** Provider-supplied source citations, accumulated across deltas. */
283
+ citations?: Anthropic.TextCitation[];
227
284
  };
228
285
  export type ReasoningDeltaUpdate = {
229
286
  type: ContentTypes.THINK;
@@ -21,6 +21,7 @@ interface HandlerCallbacks {
21
21
  onRunStep?: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData) => void;
22
22
  onRunStepCompleted?: (event: GraphEvents.ON_RUN_STEP_COMPLETED, data: t.StreamEventData) => void;
23
23
  onRunStepDelta?: (event: GraphEvents.ON_RUN_STEP_DELTA, data: t.StreamEventData) => void;
24
+ onRunStepClosed?: (event: GraphEvents.ON_RUN_STEP_CLOSED, data: t.RunStepClosedEvent) => void;
24
25
  onMessageDelta?: (event: GraphEvents.ON_MESSAGE_DELTA, data: t.StreamEventData) => void;
25
26
  }
26
27
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.4.5",
3
+ "version": "3.4.7",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -1085,3 +1085,130 @@ describe('ContentAggregator multi-entry deltas', () => {
1085
1085
  });
1086
1086
  });
1087
1087
  });
1088
+
1089
+ describe('ContentAggregator citation deltas', () => {
1090
+ const citation = (url: string, cited_text: string) => ({
1091
+ type: 'web_search_result_location',
1092
+ url,
1093
+ title: 'Example',
1094
+ cited_text,
1095
+ encrypted_index: 'enc',
1096
+ });
1097
+
1098
+ /**
1099
+ * Anthropic emits a search turn's citations as their own `citations_delta`,
1100
+ * which `_makeMessageChunkFromAnthropicEvent` normalizes into a `text` part
1101
+ * carrying `citations` and **no** `text` key.
1102
+ */
1103
+ const citationDelta = (id: string, ...citations: unknown[]) => ({
1104
+ event: GraphEvents.ON_MESSAGE_DELTA,
1105
+ data: {
1106
+ id,
1107
+ delta: { content: [{ type: ContentTypes.TEXT, citations }] },
1108
+ } as t.MessageDeltaEvent,
1109
+ });
1110
+
1111
+ const textDelta = (id: string, text: string) => ({
1112
+ event: GraphEvents.ON_MESSAGE_DELTA,
1113
+ data: {
1114
+ id,
1115
+ delta: { content: [{ type: ContentTypes.TEXT, text }] },
1116
+ } as t.MessageDeltaEvent,
1117
+ });
1118
+
1119
+ it('keeps citations that arrive without accompanying text', () => {
1120
+ const { contentParts, aggregateContent } = createContentAggregator();
1121
+ aggregateContent({
1122
+ event: GraphEvents.ON_RUN_STEP,
1123
+ data: createRunStep('step_citations'),
1124
+ });
1125
+
1126
+ aggregateContent(textDelta('step_citations', 'Answer.'));
1127
+ aggregateContent(
1128
+ citationDelta('step_citations', citation('https://a.example', 'quoted a'))
1129
+ );
1130
+
1131
+ expect(contentParts[0]).toEqual({
1132
+ type: ContentTypes.TEXT,
1133
+ text: 'Answer.',
1134
+ citations: [citation('https://a.example', 'quoted a')],
1135
+ });
1136
+ });
1137
+
1138
+ it('accumulates citations across successive deltas', () => {
1139
+ const { contentParts, aggregateContent } = createContentAggregator();
1140
+ aggregateContent({
1141
+ event: GraphEvents.ON_RUN_STEP,
1142
+ data: createRunStep('step_multi'),
1143
+ });
1144
+
1145
+ aggregateContent(textDelta('step_multi', 'Part one. '));
1146
+ aggregateContent(
1147
+ citationDelta('step_multi', citation('https://a.example', 'quoted a'))
1148
+ );
1149
+ aggregateContent(textDelta('step_multi', 'Part two.'));
1150
+ aggregateContent(
1151
+ citationDelta('step_multi', citation('https://b.example', 'quoted b'))
1152
+ );
1153
+
1154
+ expect(contentParts[0]).toEqual({
1155
+ type: ContentTypes.TEXT,
1156
+ text: 'Part one. Part two.',
1157
+ citations: [
1158
+ citation('https://a.example', 'quoted a'),
1159
+ citation('https://b.example', 'quoted b'),
1160
+ ],
1161
+ });
1162
+ });
1163
+
1164
+ it('leaves parts without citations untouched', () => {
1165
+ const { contentParts, aggregateContent } = createContentAggregator();
1166
+ aggregateContent({
1167
+ event: GraphEvents.ON_RUN_STEP,
1168
+ data: createRunStep('step_plain'),
1169
+ });
1170
+
1171
+ aggregateContent(textDelta('step_plain', 'Hello '));
1172
+ aggregateContent(textDelta('step_plain', 'world.'));
1173
+
1174
+ expect(contentParts[0]).toEqual({
1175
+ type: ContentTypes.TEXT,
1176
+ text: 'Hello world.',
1177
+ });
1178
+ expect(contentParts[0]).not.toHaveProperty('citations');
1179
+ });
1180
+
1181
+ it('preserves tool_call_ids when a citations-only delta follows', () => {
1182
+ const { contentParts, aggregateContent } = createContentAggregator();
1183
+ aggregateContent({
1184
+ event: GraphEvents.ON_RUN_STEP,
1185
+ data: createRunStep('step_tool_ids'),
1186
+ });
1187
+
1188
+ aggregateContent({
1189
+ event: GraphEvents.ON_MESSAGE_DELTA,
1190
+ data: {
1191
+ id: 'step_tool_ids',
1192
+ delta: {
1193
+ content: [
1194
+ {
1195
+ type: ContentTypes.TEXT,
1196
+ text: 'Answer.',
1197
+ tool_call_ids: ['call_1'],
1198
+ },
1199
+ ],
1200
+ },
1201
+ } as t.MessageDeltaEvent,
1202
+ });
1203
+ aggregateContent(
1204
+ citationDelta('step_tool_ids', citation('https://a.example', 'quoted a'))
1205
+ );
1206
+
1207
+ expect(contentParts[0]).toEqual({
1208
+ type: ContentTypes.TEXT,
1209
+ text: 'Answer.',
1210
+ tool_call_ids: ['call_1'],
1211
+ citations: [citation('https://a.example', 'quoted a')],
1212
+ });
1213
+ });
1214
+ });
@@ -15,6 +15,8 @@ export enum GraphEvents {
15
15
  ON_RUN_STEP_DELTA = 'on_run_step_delta',
16
16
  /** [Custom] Completed event for run steps (tool calls) */
17
17
  ON_RUN_STEP_COMPLETED = 'on_run_step_completed',
18
+ /** [Custom] Terminal signal for a run step: closed with status + timestamps */
19
+ ON_RUN_STEP_CLOSED = 'on_run_step_closed',
18
20
  /** [Custom] Delta events for messages */
19
21
  ON_MESSAGE_DELTA = 'on_message_delta',
20
22
  /** [Custom] Reasoning Delta events for messages */