@librechat/agents 3.6.2 → 3.6.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 (66) hide show
  1. package/dist/cjs/graphs/Graph.cjs +35 -20
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +7 -1
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/index.cjs +1 -1
  6. package/dist/cjs/llm/openai/index.cjs +36 -0
  7. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +4 -1
  9. package/dist/cjs/messages/core.cjs +3 -0
  10. package/dist/cjs/messages/core.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +11 -5
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/tools/SubagentTool.cjs +8 -3
  14. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  15. package/dist/cjs/tools/ToolNode.cjs +1 -1
  16. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +399 -0
  17. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -0
  18. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +168 -60
  19. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  20. package/dist/cjs/tools/subagent/index.cjs +1 -0
  21. package/dist/esm/graphs/Graph.mjs +35 -20
  22. package/dist/esm/graphs/Graph.mjs.map +1 -1
  23. package/dist/esm/hooks/HookRegistry.mjs +7 -1
  24. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  25. package/dist/esm/hooks/index.mjs +1 -1
  26. package/dist/esm/llm/openai/index.mjs +37 -1
  27. package/dist/esm/llm/openai/index.mjs.map +1 -1
  28. package/dist/esm/main.mjs +4 -3
  29. package/dist/esm/messages/core.mjs +3 -1
  30. package/dist/esm/messages/core.mjs.map +1 -1
  31. package/dist/esm/run.mjs +11 -5
  32. package/dist/esm/run.mjs.map +1 -1
  33. package/dist/esm/tools/SubagentTool.mjs +8 -3
  34. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  35. package/dist/esm/tools/ToolNode.mjs +1 -1
  36. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +399 -0
  37. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -0
  38. package/dist/esm/tools/subagent/SubagentExecutor.mjs +168 -60
  39. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  40. package/dist/esm/tools/subagent/index.mjs +1 -0
  41. package/dist/types/graphs/Graph.d.ts +6 -3
  42. package/dist/types/hooks/HookRegistry.d.ts +8 -0
  43. package/dist/types/messages/core.d.ts +2 -0
  44. package/dist/types/run.d.ts +1 -0
  45. package/dist/types/tools/SubagentTool.d.ts +3 -1
  46. package/dist/types/tools/subagent/InMemorySubagentTaskStore.d.ts +45 -0
  47. package/dist/types/tools/subagent/SubagentExecutor.d.ts +26 -3
  48. package/dist/types/tools/subagent/index.d.ts +2 -0
  49. package/dist/types/types/graph.d.ts +11 -4
  50. package/dist/types/types/index.d.ts +1 -0
  51. package/dist/types/types/run.d.ts +6 -0
  52. package/dist/types/types/subagentTasks.d.ts +140 -0
  53. package/package.json +4 -4
  54. package/src/graphs/Graph.ts +84 -36
  55. package/src/hooks/HookRegistry.ts +18 -0
  56. package/src/llm/openai/index.ts +96 -0
  57. package/src/messages/core.ts +5 -0
  58. package/src/run.ts +20 -5
  59. package/src/tools/SubagentTool.ts +20 -2
  60. package/src/tools/subagent/InMemorySubagentTaskStore.ts +624 -0
  61. package/src/tools/subagent/SubagentExecutor.ts +341 -74
  62. package/src/tools/subagent/index.ts +2 -0
  63. package/src/types/graph.ts +11 -4
  64. package/src/types/index.ts +1 -0
  65. package/src/types/run.ts +6 -0
  66. package/src/types/subagentTasks.ts +129 -0
@@ -43,6 +43,7 @@ import type { SeenScalarMetadata } from './streamMetadata';
43
43
  import type { HeaderValue, HeadersLike } from './types';
44
44
  import type { PromptCacheTtl } from '@/messages/cache';
45
45
  import {
46
+ OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY,
46
47
  OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
47
48
  projectOpenAIResponsesToolMessageContent,
48
49
  projectToolStreamContentForProvider,
@@ -606,6 +607,85 @@ function isResponsesReplayOutputItem(item: unknown): boolean {
606
607
  );
607
608
  }
608
609
 
610
+ type ResponsesReasoningSlot = {
611
+ encrypted_content?: string;
612
+ id?: string;
613
+ status?: string;
614
+ };
615
+
616
+ function getResponsesReasoningSlot(
617
+ reasoning: unknown
618
+ ): ResponsesReasoningSlot | undefined {
619
+ return typeof reasoning === 'object' && reasoning != null
620
+ ? (reasoning as ResponsesReasoningSlot)
621
+ : undefined;
622
+ }
623
+
624
+ function isSealedReasoningSlot(
625
+ slot: ResponsesReasoningSlot | undefined
626
+ ): slot is ResponsesReasoningSlot & { encrypted_content: string } {
627
+ return (
628
+ typeof slot?.encrypted_content === 'string' &&
629
+ slot.encrypted_content.length > 0
630
+ );
631
+ }
632
+
633
+ function resolveActiveReasoningItemId(
634
+ incoming: ResponsesReasoningSlot | undefined,
635
+ carried: unknown
636
+ ): string | undefined {
637
+ if (typeof incoming?.id === 'string' && incoming.id.length > 0) {
638
+ return incoming.id;
639
+ }
640
+ return typeof carried === 'string' ? carried : undefined;
641
+ }
642
+
643
+ /**
644
+ * A single `additional_kwargs.reasoning` slot has to stand in for a turn that
645
+ * can emit many reasoning items, and the chunk merge folds it field by field:
646
+ * `encrypted_content` and `status` are strings, so they concatenate, while
647
+ * `id` takes whichever item arrived last. An interrupted turn replays from
648
+ * that slot, handing the provider one item id welded to every item's
649
+ * ciphertext — rejected as "Encrypted content could not be decrypted or
650
+ * parsed". Keep the slot describing whichever item most recently sealed, so
651
+ * the id, its ciphertext, and its status always come from the same item.
652
+ *
653
+ * `activeItemId` tracks the item currently streaming, which is the one a
654
+ * terminal `encrypted_content` belongs to: the slot's own id has already been
655
+ * pinned back to the last sealed item by an earlier merge.
656
+ */
657
+ function resealReasoningItemBoundary(
658
+ combined: AIMessageChunk,
659
+ accumulated: ResponsesReasoningSlot | undefined,
660
+ incoming: ResponsesReasoningSlot | undefined,
661
+ activeItemId: string | undefined
662
+ ): void {
663
+ const merged = getResponsesReasoningSlot(
664
+ combined.additional_kwargs.reasoning
665
+ );
666
+ if (merged == null || incoming == null) {
667
+ return;
668
+ }
669
+ if (isSealedReasoningSlot(incoming)) {
670
+ combined.additional_kwargs.reasoning = {
671
+ ...merged,
672
+ id: activeItemId ?? merged.id,
673
+ encrypted_content: incoming.encrypted_content,
674
+ status: incoming.status,
675
+ };
676
+ return;
677
+ }
678
+ if (!isSealedReasoningSlot(accumulated)) {
679
+ return;
680
+ }
681
+ combined.additional_kwargs.reasoning = {
682
+ ...merged,
683
+ id: accumulated.id,
684
+ encrypted_content: accumulated.encrypted_content,
685
+ status: accumulated.status,
686
+ };
687
+ }
688
+
609
689
  /**
610
690
  * LangChain's Responses converter places the authoritative terminal output in
611
691
  * response_metadata.output. Its chunk merge has no way to delete provisional
@@ -619,10 +699,26 @@ class ResponsesReplayAIMessageChunk extends AIMessageChunk {
619
699
  }
620
700
 
621
701
  override concat(chunk: AIMessageChunk): this {
702
+ const accumulated = getResponsesReasoningSlot(
703
+ this.additional_kwargs.reasoning
704
+ );
705
+ const incoming = getResponsesReasoningSlot(
706
+ chunk.additional_kwargs.reasoning
707
+ );
708
+ const activeItemId = resolveActiveReasoningItemId(
709
+ incoming,
710
+ this.additional_kwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY]
711
+ );
622
712
  const combined = super.concat(chunk);
713
+ resealReasoningItemBoundary(combined, accumulated, incoming, activeItemId);
714
+ if (activeItemId != null) {
715
+ combined.additional_kwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY] =
716
+ activeItemId;
717
+ }
623
718
  if (!Array.isArray(chunk.response_metadata.output)) {
624
719
  return combined;
625
720
  }
721
+ delete combined.additional_kwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY];
626
722
  delete combined.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
627
723
  const toolOutputs = combined.additional_kwargs.tool_outputs;
628
724
  if (!Array.isArray(toolOutputs)) {
@@ -671,6 +671,10 @@ type ResponsesReplayProjection = 'fallback' | 'native';
671
671
  export const OPENAI_RESPONSES_REPLAY_POSITIONS_KEY =
672
672
  '__openai_responses_replay_positions__';
673
673
 
674
+ /** Reasoning item currently streaming, so a terminal ciphertext seals against its own id. */
675
+ export const OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY =
676
+ '__openai_responses_active_reasoning_id__';
677
+
674
678
  export type ResponsesReplayPosition = {
675
679
  contentIndex?: number;
676
680
  itemId: string;
@@ -1813,6 +1817,7 @@ function projectPreemptedOpenAIResponsesMessage(
1813
1817
  }
1814
1818
  delete additionalKwargs.tool_outputs;
1815
1819
  delete additionalKwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
1820
+ delete additionalKwargs[OPENAI_RESPONSES_ACTIVE_REASONING_ID_KEY];
1816
1821
  delete additionalKwargs.__openai_function_call_ids__;
1817
1822
  delete additionalKwargs.__openai_custom_tool_call_ids__;
1818
1823
 
package/src/run.ts CHANGED
@@ -296,6 +296,7 @@ export class Run<_T extends t.BaseGraphState> {
296
296
  private subagentUsageSink?: t.SubagentUsageSink;
297
297
  private preemption?: t.StreamPreemption;
298
298
  private streamLimits?: t.StreamLimits;
299
+ private subagentTasks?: t.SubagentTaskConfig;
299
300
  private indexTokenCountMap?: Record<string, number>;
300
301
  calibrationRatio: number = 1;
301
302
  graphRunnable?: t.CompiledStateWorkflow;
@@ -363,6 +364,7 @@ export class Run<_T extends t.BaseGraphState> {
363
364
  this.interruptingToolNames = config.interruptingToolNames;
364
365
  this.toolExecution = config.toolExecution;
365
366
  this.subagentUsageSink = config.subagentUsageSink;
367
+ this.subagentTasks = config.subagentTasks;
366
368
  this.preemption = config.preemption;
367
369
  this.streamLimits = config.streamLimits;
368
370
 
@@ -455,6 +457,7 @@ export class Run<_T extends t.BaseGraphState> {
455
457
  indexTokenCountMap: this.indexTokenCountMap,
456
458
  calibrationRatio: this.calibrationRatio,
457
459
  subagentUsageSink: this.subagentUsageSink,
460
+ subagentTasks: this.subagentTasks,
458
461
  preemption: this.preemption,
459
462
  streamLimits: this.streamLimits,
460
463
  },
@@ -493,6 +496,7 @@ export class Run<_T extends t.BaseGraphState> {
493
496
  indexTokenCountMap: this.indexTokenCountMap,
494
497
  calibrationRatio: this.calibrationRatio,
495
498
  subagentUsageSink: this.subagentUsageSink,
499
+ subagentTasks: this.subagentTasks,
496
500
  preemption: this.preemption,
497
501
  streamLimits: this.streamLimits,
498
502
  },
@@ -826,12 +830,23 @@ export class Run<_T extends t.BaseGraphState> {
826
830
  * The producer stamped `completed_at` before dispatch. Carrying it
827
831
  * through keeps the recorded duration the tool's, not the host
828
832
  * handler's — this runs after an arbitrarily slow handler resolves.
833
+ *
834
+ * Isolated because this is a `finally`: a throw here would
835
+ * REPLACE an in-flight error from the handler above, so the
836
+ * host's original failure would be reported as whatever went
837
+ * wrong during closure instead. Closing is best-effort at this
838
+ * point either way — the end-of-run sweep still stamps any step
839
+ * this misses.
829
840
  */
830
- await this.Graph.recordStepCompletion(completion.stepId, {
831
- toolCallId: completion.toolCallId,
832
- metadata,
833
- at: completion.completedAt,
834
- });
841
+ try {
842
+ await this.Graph.recordStepCompletion(completion.stepId, {
843
+ toolCallId: completion.toolCallId,
844
+ metadata,
845
+ at: completion.completedAt,
846
+ });
847
+ } catch (_e) {
848
+ /** Never mask the handler's error with a closure failure */
849
+ }
835
850
  }
836
851
  }
837
852
  }
@@ -27,6 +27,9 @@ const DESCRIPTION_PROP_DESCRIPTION =
27
27
  const SUBAGENT_TYPE_PROP_DESCRIPTION =
28
28
  'Which subagent type to delegate to. Must be one of the available types.';
29
29
 
30
+ const RUN_IN_BACKGROUND_PROP_DESCRIPTION =
31
+ 'Set true to start the subagent as a detached process-local task and return a background_task_id immediately. Poll the host background-task tool to collect its result. The task can outlive this turn but does not survive a process restart.';
32
+
30
33
  export const SubagentToolSchema = {
31
34
  type: 'object',
32
35
  properties: {
@@ -54,7 +57,10 @@ export const SubagentToolDefinition: LCTool = {
54
57
  * Used by `Graph.createAgentNode()` when constructing the runtime tool instance.
55
58
  * Extends `SubagentToolSchema` by populating `subagent_type.enum` dynamically.
56
59
  */
57
- export function buildSubagentToolParams(configs: SubagentConfig[]): {
60
+ export function buildSubagentToolParams(
61
+ configs: SubagentConfig[],
62
+ options: { background?: boolean } = {}
63
+ ): {
58
64
  name: string;
59
65
  schema: JsonSchemaType;
60
66
  description: string;
@@ -79,10 +85,22 @@ export function buildSubagentToolParams(configs: SubagentConfig[]): {
79
85
  enum: types,
80
86
  description: `${SUBAGENT_TYPE_PROP_DESCRIPTION} Available: ${types.join(', ')}.`,
81
87
  },
88
+ ...(options.background === true
89
+ ? {
90
+ run_in_background: {
91
+ type: 'boolean',
92
+ description: RUN_IN_BACKGROUND_PROP_DESCRIPTION,
93
+ },
94
+ }
95
+ : {}),
82
96
  },
83
97
  required: ['description', 'subagent_type'],
84
98
  },
85
- description: `${SubagentToolDescription}\n\nAvailable types:\n${typeDescriptions}`,
99
+ description: `${SubagentToolDescription}${
100
+ options.background === true
101
+ ? '\n\nBACKGROUND EXECUTION:\n- Set run_in_background to true when you do not need the result immediately. The call returns a background_task_id; use the host background-task tools to poll, steer, queue, interrupt, or cancel it.'
102
+ : ''
103
+ }\n\nAvailable types:\n${typeDescriptions}`,
86
104
  };
87
105
  }
88
106