@librechat/agents 3.4.4 → 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 (107) 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/hitl/askUserQuestions.cjs +66 -0
  6. package/dist/cjs/hitl/askUserQuestions.cjs.map +1 -0
  7. package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs +46 -0
  8. package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs.map +1 -0
  9. package/dist/cjs/hitl/index.cjs +2 -0
  10. package/dist/cjs/langfuseTraceShaping.cjs +9 -6
  11. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  12. package/dist/cjs/main.cjs +6 -0
  13. package/dist/cjs/run.cjs +147 -14
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/session/AgentSession.cjs +93 -31
  16. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  17. package/dist/cjs/session/handlers.cjs +10 -0
  18. package/dist/cjs/session/handlers.cjs.map +1 -1
  19. package/dist/cjs/stream.cjs +17 -3
  20. package/dist/cjs/stream.cjs.map +1 -1
  21. package/dist/cjs/summarization/node.cjs +1 -0
  22. package/dist/cjs/summarization/node.cjs.map +1 -1
  23. package/dist/cjs/tools/ToolNode.cjs +4 -2
  24. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  25. package/dist/cjs/tools/handlers.cjs +1 -0
  26. package/dist/cjs/tools/handlers.cjs.map +1 -1
  27. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +28 -0
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  29. package/dist/cjs/types/hitl.cjs +13 -0
  30. package/dist/cjs/types/hitl.cjs.map +1 -0
  31. package/dist/cjs/utils/handlers.cjs +3 -0
  32. package/dist/cjs/utils/handlers.cjs.map +1 -1
  33. package/dist/esm/common/enum.mjs +2 -0
  34. package/dist/esm/common/enum.mjs.map +1 -1
  35. package/dist/esm/graphs/Graph.mjs +268 -14
  36. package/dist/esm/graphs/Graph.mjs.map +1 -1
  37. package/dist/esm/hitl/askUserQuestions.mjs +66 -0
  38. package/dist/esm/hitl/askUserQuestions.mjs.map +1 -0
  39. package/dist/esm/hitl/askUserQuestionsInterrupt.mjs +43 -0
  40. package/dist/esm/hitl/askUserQuestionsInterrupt.mjs.map +1 -0
  41. package/dist/esm/hitl/index.mjs +2 -0
  42. package/dist/esm/langfuseTraceShaping.mjs +9 -6
  43. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  44. package/dist/esm/main.mjs +3 -1
  45. package/dist/esm/run.mjs +147 -14
  46. package/dist/esm/run.mjs.map +1 -1
  47. package/dist/esm/session/AgentSession.mjs +93 -31
  48. package/dist/esm/session/AgentSession.mjs.map +1 -1
  49. package/dist/esm/session/handlers.mjs +10 -0
  50. package/dist/esm/session/handlers.mjs.map +1 -1
  51. package/dist/esm/stream.mjs +17 -3
  52. package/dist/esm/stream.mjs.map +1 -1
  53. package/dist/esm/summarization/node.mjs +1 -0
  54. package/dist/esm/summarization/node.mjs.map +1 -1
  55. package/dist/esm/tools/ToolNode.mjs +4 -2
  56. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  57. package/dist/esm/tools/handlers.mjs +1 -0
  58. package/dist/esm/tools/handlers.mjs.map +1 -1
  59. package/dist/esm/tools/subagent/SubagentExecutor.mjs +28 -0
  60. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  61. package/dist/esm/types/hitl.mjs +13 -0
  62. package/dist/esm/types/hitl.mjs.map +1 -0
  63. package/dist/esm/utils/handlers.mjs +3 -0
  64. package/dist/esm/utils/handlers.mjs.map +1 -1
  65. package/dist/types/common/enum.d.ts +2 -0
  66. package/dist/types/graphs/Graph.d.ts +87 -0
  67. package/dist/types/hitl/askUserQuestions.d.ts +24 -0
  68. package/dist/types/hitl/askUserQuestionsInterrupt.d.ts +11 -0
  69. package/dist/types/hitl/index.d.ts +2 -0
  70. package/dist/types/langfuseTraceShaping.d.ts +4 -3
  71. package/dist/types/run.d.ts +9 -1
  72. package/dist/types/session/types.d.ts +1 -1
  73. package/dist/types/summarization/node.d.ts +6 -0
  74. package/dist/types/types/graph.d.ts +3 -3
  75. package/dist/types/types/hitl.d.ts +31 -2
  76. package/dist/types/types/stream.d.ts +57 -0
  77. package/dist/types/utils/handlers.d.ts +1 -0
  78. package/package.json +2 -1
  79. package/src/aggregator.test.ts +127 -0
  80. package/src/common/enum.ts +2 -0
  81. package/src/graphs/Graph.ts +365 -7
  82. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +385 -0
  83. package/src/hitl/askUserQuestions.ts +126 -0
  84. package/src/hitl/askUserQuestionsInterrupt.ts +115 -0
  85. package/src/hitl/index.ts +6 -0
  86. package/src/langfuseTraceShaping.ts +14 -7
  87. package/src/run.ts +219 -41
  88. package/src/session/AgentSession.ts +105 -13
  89. package/src/session/handlers.ts +14 -2
  90. package/src/session/types.ts +1 -0
  91. package/src/specs/activity-label-observability.live.test.ts +262 -0
  92. package/src/specs/activity-label-observability.test.ts +167 -0
  93. package/src/specs/ask-user-questions.live.test.ts +185 -0
  94. package/src/specs/ask-user-questions.test.ts +293 -0
  95. package/src/specs/langfuse-trace-shaping.test.ts +32 -0
  96. package/src/specs/run-step-timestamps.test.ts +414 -0
  97. package/src/stream.ts +29 -6
  98. package/src/summarization/node.ts +11 -0
  99. package/src/tools/ToolNode.ts +2 -0
  100. package/src/tools/__tests__/SubagentExecutor.test.ts +52 -10
  101. package/src/tools/__tests__/handlers.test.ts +2 -0
  102. package/src/tools/handlers.ts +1 -0
  103. package/src/tools/subagent/SubagentExecutor.ts +46 -4
  104. package/src/types/graph.ts +9 -6
  105. package/src/types/hitl.ts +36 -2
  106. package/src/types/stream.ts +65 -12
  107. package/src/utils/handlers.ts +13 -0
package/dist/esm/run.mjs CHANGED
@@ -39,11 +39,14 @@ const defaultOmitOptions = new Set([
39
39
  "maxOutputTokens",
40
40
  "additionalModelRequestFields"
41
41
  ]);
42
+ const ACTIVITY_LABEL_TRACE_NAME = "LibreChat Activity Label";
43
+ const ACTIVITY_PHASE_TRACE_NAME = "LibreChat Activity Phase";
42
44
  const CUSTOM_GRAPH_EVENTS = new Set([
43
45
  "on_agent_update",
44
46
  "on_run_step",
45
47
  "on_run_step_delta",
46
48
  "on_run_step_completed",
49
+ "on_run_step_closed",
47
50
  "on_message_delta",
48
51
  "on_reasoning_delta",
49
52
  "on_tool_execute",
@@ -58,6 +61,7 @@ const CUSTOM_GRAPH_EVENTS = new Set([
58
61
  const DIRECT_DISPATCHED_STEP_EVENTS = new Set([
59
62
  "on_run_step",
60
63
  "on_run_step_delta",
64
+ "on_run_step_closed",
61
65
  "on_message_delta",
62
66
  "on_reasoning_delta"
63
67
  ]);
@@ -66,6 +70,25 @@ function getStepScopedEventId(data) {
66
70
  const candidate = data;
67
71
  return typeof candidate.id === "string" ? candidate.id : void 0;
68
72
  }
73
+ /**
74
+ * Narrows an ON_RUN_STEP_COMPLETED payload (`{ result: ToolCompleteEvent }`)
75
+ * to the ids step closure needs. Returns undefined for malformed payloads and
76
+ * the resume race's empty step id.
77
+ */
78
+ function getToolCompletion(data) {
79
+ if (data == null || typeof data !== "object") return;
80
+ const { result } = data;
81
+ if (result == null || typeof result !== "object") return;
82
+ const candidate = result;
83
+ if (typeof candidate.id !== "string" || candidate.id === "") return;
84
+ const toolCallId = candidate.tool_call?.id;
85
+ const completedAt = candidate.completed_at;
86
+ return {
87
+ stepId: candidate.id,
88
+ ...typeof toolCallId === "string" ? { toolCallId } : {},
89
+ ...typeof completedAt === "number" ? { completedAt } : {}
90
+ };
91
+ }
69
92
  function isLangGraphResumeMapForInterrupt(value, interruptId) {
70
93
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
71
94
  return Object.prototype.hasOwnProperty.call(value, interruptId);
@@ -464,7 +487,32 @@ var Run = class Run {
464
487
  const stepScopedEventId = getStepScopedEventId(data);
465
488
  if (DIRECT_DISPATCHED_STEP_EVENTS.has(eventName) && this.Graph != null && stepScopedEventId != null && this.Graph.hasHandlerDispatchedEvent(eventName, stepScopedEventId)) return;
466
489
  const handler = this.handlerRegistry?.getHandler(eventName);
467
- if (handler && this.Graph) return await handler.handle(eventName, data, metadata, this.Graph);
490
+ /**
491
+ * Tool completions arriving over the custom-event channel are the only
492
+ * signal ToolNode (which holds no graph reference) emits — observe them
493
+ * here to drive step closure. Runs in `finally`, independent of handler
494
+ * registration, so an absent or throwing host handler cannot lose the
495
+ * close; duplicate callback echoes are absorbed by the terminal-status
496
+ * guard in `closeRunStep`.
497
+ */
498
+ try {
499
+ if (handler && this.Graph) return await handler.handle(eventName, data, metadata, this.Graph);
500
+ } finally {
501
+ if (eventName === "on_run_step_completed" && this.Graph != null) {
502
+ const completion = getToolCompletion(data);
503
+ if (completion != null)
504
+ /**
505
+ * The producer stamped `completed_at` before dispatch. Carrying it
506
+ * through keeps the recorded duration the tool's, not the host
507
+ * handler's — this runs after an arbitrarily slow handler resolves.
508
+ */
509
+ await this.Graph.recordStepCompletion(completion.stepId, {
510
+ toolCallId: completion.toolCallId,
511
+ metadata,
512
+ at: completion.completedAt
513
+ });
514
+ }
515
+ }
468
516
  };
469
517
  }
470
518
  shouldClearHookSession(streamThrew) {
@@ -473,6 +521,18 @@ var Run = class Run {
473
521
  isAwaitingResume(streamThrew) {
474
522
  return this._interrupt != null && this._haltedReason == null && !streamThrew;
475
523
  }
524
+ /**
525
+ * Terminal status for steps still open at end-of-run: `cancelled` for
526
+ * intentional stops (caller abort, hook halt), `failed` for unexpected
527
+ * stream errors, `completed` for a natural finish. Reads `_haltedReason`
528
+ * behind a method boundary on purpose — it is assigned inside the
529
+ * `consumeStream` closure, which control-flow narrowing cannot see.
530
+ */
531
+ resolveSweepStatus(streamThrew, streamAborted) {
532
+ if (streamThrew) return streamAborted ? "cancelled" : "failed";
533
+ if (this._haltedReason != null) return "cancelled";
534
+ return "completed";
535
+ }
476
536
  getStreamLangfuseConfig(graph) {
477
537
  const primaryContext = graph.agentContexts.get(graph.defaultAgentId);
478
538
  if (primaryContext != null) return resolveLangfuseConfig(this.langfuse, primaryContext.langfuse);
@@ -644,6 +704,14 @@ var Run = class Run {
644
704
  * preserving session hooks would leak them into the next run.
645
705
  */
646
706
  let streamThrew = false;
707
+ let streamAborted = false;
708
+ /**
709
+ * When the stream itself ended — captured before the post-stream work in
710
+ * the `finally` (Stop/StopFailure hooks, Langfuse disposal, which can
711
+ * force-flush) so a slow hook cannot inflate the terminal stamps that the
712
+ * sweep writes onto steps that were still open.
713
+ */
714
+ let terminalAt;
647
715
  const consumeStream = async () => {
648
716
  /**
649
717
  * `streamEvents` accepts both state inputs and `Command` (resume) at
@@ -700,9 +768,22 @@ var Run = class Run {
700
768
  };
701
769
  }
702
770
  }
771
+ /**
772
+ * Stamped before the handler runs: the close below happens after an
773
+ * arbitrarily slow host handler resolves, and the step's duration
774
+ * should end when the model did, not when the host finished with it.
775
+ */
776
+ const modelEndAt = eventName === "on_chat_model_end" ? Date.now() : void 0;
703
777
  const handler = this.handlerRegistry?.getHandler(eventName);
704
778
  if (handler) await handler.handle(eventName, data, metadata, this.Graph);
705
779
  /**
780
+ * A finished model call ends its lane's open message step. Placed
781
+ * here — not in `ModelEndHandler` — because hosts replace the
782
+ * CHAT_MODEL_END handler with their own instance, which would
783
+ * silently drop the close.
784
+ */
785
+ if (eventName === "on_chat_model_end" && this.Graph != null) await this.Graph.closeOpenMessageStep(metadata, modelEndAt);
786
+ /**
706
787
  * Mid-flight halt: any hook (PreToolUse, PostToolUse,
707
788
  * PostToolBatch, SubagentStart/Stop, PreCompact, PostCompact)
708
789
  * that returned `preventContinuation: true` raises a halt
@@ -729,6 +810,7 @@ var Run = class Run {
729
810
  break;
730
811
  }
731
812
  }
813
+ terminalAt = Date.now();
732
814
  if (this._interrupt != null) await this.resolveInterruptResumeConfig(config);
733
815
  /**
734
816
  * Skip the Stop hook when the run paused on a HITL interrupt
@@ -787,7 +869,16 @@ var Run = class Run {
787
869
  tags: ["librechat", "agent"]
788
870
  }, consumeStream));
789
871
  } catch (err) {
872
+ terminalAt = Date.now();
790
873
  streamThrew = true;
874
+ /**
875
+ * Corroborate cancellation against an actually-aborted signal. A
876
+ * provider SDK or host handler can reject with an `AbortError` while
877
+ * nothing was cancelled — that is an unexpected failure (it also fires
878
+ * `StopFailure`), and naming it `cancelled` would misreport abort
879
+ * forensics.
880
+ */
881
+ streamAborted = config.signal?.aborted === true || this.Graph.signal?.aborted === true;
791
882
  if (this.hookRegistry?.hasHookFor("StopFailure", this.id) === true) {
792
883
  const runMessages = this.Graph.getRunMessages() ?? [];
793
884
  await executeHooks({
@@ -831,6 +922,21 @@ var Run = class Run {
831
922
  this.hookRegistry?.clearHaltSignal(this.id);
832
923
  await disposeLangfuseHandler(langfuseHandler);
833
924
  /**
925
+ * Terminal sweep: close every step that never reached a terminal
926
+ * status — `completed` on a natural end, `cancelled` on caller abort
927
+ * or hook halt, `failed` on an unexpected stream error. Skipped on a
928
+ * HITL pause, where the open steps continue after `resume()`.
929
+ *
930
+ * Runs BEFORE the callback teardown below, so a caller observing
931
+ * lifecycle events only through `RunnableConfig.callbacks` still
932
+ * receives these closures rather than being left with unmatched
933
+ * starts, and before `getContentParts()` so terminal stamps flow into
934
+ * content and session serialization.
935
+ */
936
+ if (!this.isAwaitingResume(streamThrew)) try {
937
+ await this.Graph.closeUnfinishedRunSteps(this.resolveSweepStatus(streamThrew, streamAborted), terminalAt);
938
+ } catch {}
939
+ /**
834
940
  * Break the reference chain that keeps heavy data alive via
835
941
  * LangGraph's internal `__pregel_scratchpad.currentTaskInput` →
836
942
  * `@langchain/core` `RunTree.extra[lc:child_config]` →
@@ -1190,17 +1296,40 @@ var Run = class Run {
1190
1296
  * than the labeled agent's. Skip generation entirely instead. */
1191
1297
  if (agentId != null && requestedContext == null) return {};
1192
1298
  const labelContext = this.Graph == null ? void 0 : requestedContext ?? this.Graph.agentContexts.get(this.Graph.defaultAgentId);
1193
- const traceMetadata = createLangfuseTraceMetadata({
1194
- messageId: "activity-label-" + this.id,
1195
- agentName: labelContext?.name
1196
- });
1197
- const labelRunName = getLangfuseTraceName(traceMetadata, "LibreChat Activity Label");
1198
1299
  /** Shallow-cloned: activity labels run once per tool batch, and writing
1199
1300
  * the Langfuse handler back onto a host-reused `chainOptions` would
1200
1301
  * accumulate duplicate callbacks across batches. */
1201
1302
  const labelChainOptions = { ...chainOptions ?? {} };
1202
1303
  const labelUserId = typeof labelChainOptions.configurable?.user_id === "string" ? labelChainOptions.configurable.user_id : void 0;
1203
1304
  const labelSessionId = typeof labelChainOptions.configurable?.thread_id === "string" ? labelChainOptions.configurable.thread_id : void 0;
1305
+ const labelIndex = labelSeq - 1;
1306
+ const labelParentMessageId = labelChainOptions.configurable?.requestBody?.parentMessageId;
1307
+ /** An omitted `agentId` is attributable only when exactly one context
1308
+ * exists. Multi-agent callers remain unattributed instead of being
1309
+ * incorrectly assigned to the graph's default agent. */
1310
+ const labelAgentId = agentId ?? (this.Graph?.agentContexts.size === 1 ? this.Graph.defaultAgentId : void 0);
1311
+ const labelAgentName = labelAgentId == null ? void 0 : labelContext?.name;
1312
+ const labelMetadata = {
1313
+ sourceRunId: this.id,
1314
+ responseId: this.id,
1315
+ activityIndex: labelIndex,
1316
+ ...typeof labelParentMessageId === "string" ? { parentMessageId: labelParentMessageId } : {},
1317
+ ...labelAgentId == null ? {} : { agentId: labelAgentId },
1318
+ ...labelAgentName == null ? {} : { agentName: labelAgentName }
1319
+ };
1320
+ const traceMetadata = {
1321
+ ...createLangfuseTraceMetadata({
1322
+ messageId: "activity-label-" + this.id,
1323
+ parentMessageId: labelParentMessageId,
1324
+ agentId: labelAgentId,
1325
+ agentName: labelAgentName
1326
+ }),
1327
+ sourceRunId: this.id,
1328
+ responseId: this.id,
1329
+ activityIndex: String(labelIndex)
1330
+ };
1331
+ const labelRunName = labelChainOptions.runName ?? ACTIVITY_LABEL_TRACE_NAME;
1332
+ const labelTags = ["librechat", "activity-label"];
1204
1333
  const labelLangfuseConfig = resolveLangfuseConfig(this.langfuse, labelContext?.langfuse);
1205
1334
  initializeLangfuseTracing(labelLangfuseConfig);
1206
1335
  /** Seed policy, threading two constraints:
@@ -1236,11 +1365,11 @@ var Run = class Run {
1236
1365
  userId: labelUserId,
1237
1366
  sessionId: labelSessionId,
1238
1367
  traceMetadata,
1239
- tags: ["librechat", "activity-label"],
1368
+ tags: labelTags,
1240
1369
  traceIdSeed: labelLangfuseConfig?.deterministicTraceId === true ? labelTraceSeed : void 0,
1241
1370
  runId: labelScopeRunId,
1242
1371
  toolOutputTracing: labelRuntimeScope.toolOutputTracing,
1243
- traceName: labelChainOptions.runName ?? labelRunName
1372
+ traceName: labelRunName
1244
1373
  });
1245
1374
  if (labelLangfuseHandler != null) labelChainOptions.callbacks = appendCallbacks(labelChainOptions.callbacks, [labelLangfuseHandler]);
1246
1375
  /** The label prompt becomes Langfuse generation input, so the resolved
@@ -1293,15 +1422,20 @@ var Run = class Run {
1293
1422
  const invokeConfig = Object.assign({}, labelChainOptions, {
1294
1423
  run_id: labelRunId,
1295
1424
  runId: labelRunId,
1296
- runName: labelChainOptions.runName ?? labelRunName
1425
+ runName: labelRunName,
1426
+ tags: [...new Set([...labelChainOptions.tags ?? [], ...labelTags])],
1427
+ metadata: {
1428
+ ...labelChainOptions.metadata ?? {},
1429
+ ...labelMetadata
1430
+ }
1297
1431
  });
1298
1432
  const invokeLabel = (runtimeConfig) => withLangfuseAttributes({
1299
1433
  langfuse: labelLangfuseConfig,
1300
1434
  userId: labelUserId,
1301
1435
  sessionId: labelSessionId,
1302
- traceName: runtimeConfig.runName ?? labelRunName,
1436
+ traceName: labelRunName,
1303
1437
  traceMetadata,
1304
- tags: ["librechat", "activity-label"]
1438
+ tags: labelTags
1305
1439
  }, () => model.invoke([new SystemMessage(prompt ?? ACTIVITY_LABEL_PROMPT), new HumanMessage(userPrompt)], runtimeConfig));
1306
1440
  const extractLabel = (response) => {
1307
1441
  const content = response?.content;
@@ -1339,7 +1473,7 @@ var Run = class Run {
1339
1473
  }
1340
1474
  /**
1341
1475
  * Generates one parent summary for two or more logical activities. The
1342
- * summary model is traced as a dedicated activity-phase agent root in the
1476
+ * summary model is traced as a dedicated activity-phase chain root in the
1343
1477
  * conversation session, with the model callback recorded as its generation
1344
1478
  * child. No session id means no phase trace, avoiding orphan observations.
1345
1479
  */
@@ -1417,8 +1551,7 @@ var Run = class Run {
1417
1551
  ...contributingAgentIds.length === 0 ? {} : { contributingAgentIds: contributingAgentIds.join(",") },
1418
1552
  ...closingTextPhase == null ? {} : { closingTextPhase }
1419
1553
  };
1420
- const phaseRunName = getLangfuseTraceName(traceMetadata, "LibreChat Activity Phase");
1421
- const phaseTraceName = phaseChainOptions.runName ?? phaseRunName;
1554
+ const phaseTraceName = phaseChainOptions.runName ?? ACTIVITY_PHASE_TRACE_NAME;
1422
1555
  const phaseTags = [
1423
1556
  "librechat",
1424
1557
  "activity-phase",