@librechat/agents 3.8.3 → 3.8.5

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.
@@ -46,6 +46,8 @@ export interface ToolBatchReplayRecord {
46
46
  referenceState?: ToolOutputReferenceState;
47
47
  }
48
48
 
49
+ export type ToolReplayResumeStatus = 'active' | 'stale' | 'unverifiable';
50
+
49
51
  interface ToolBatchReplayState {
50
52
  version: 1;
51
53
  approvalOwner?: string;
@@ -62,6 +64,44 @@ function isRecord(value: unknown): value is Record<string, unknown> {
62
64
  return value != null && typeof value === 'object' && !Array.isArray(value);
63
65
  }
64
66
 
67
+ function bindLegacyReplayOwner(
68
+ owner: string,
69
+ configurable: Record<string, unknown>
70
+ ): string {
71
+ let identity: unknown;
72
+ try {
73
+ identity = JSON.parse(owner);
74
+ } catch {
75
+ return owner;
76
+ }
77
+ if (!Array.isArray(identity) || identity.length !== 3) {
78
+ return owner;
79
+ }
80
+ return JSON.stringify([
81
+ ...identity,
82
+ typeof configurable.user_id === 'string' ? configurable.user_id : '',
83
+ typeof configurable.thread_id === 'string' ? configurable.thread_id : '',
84
+ ]);
85
+ }
86
+
87
+ function bindLegacyReplayState(
88
+ state: ToolBatchReplayState | undefined,
89
+ configurable: Record<string, unknown>
90
+ ): ToolBatchReplayState | undefined {
91
+ if (state == null) return undefined;
92
+ return {
93
+ ...state,
94
+ approvalOwner:
95
+ state.approvalOwner == null
96
+ ? undefined
97
+ : bindLegacyReplayOwner(state.approvalOwner, configurable),
98
+ records: state.records.map((record) => ({
99
+ ...record,
100
+ owner: bindLegacyReplayOwner(record.owner, configurable),
101
+ })),
102
+ };
103
+ }
104
+
65
105
  /** The only host-to-runtime entry point for checkpoint-owned tool replay state. */
66
106
  export function restoreToolReplayConfig(
67
107
  configurable: Record<string, unknown>,
@@ -70,7 +110,10 @@ export function restoreToolReplayConfig(
70
110
  ): void {
71
111
  delete configurable[TOOL_BATCH_REPLAY_KEY];
72
112
  delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
73
- const state = getToolBatchReplayState(payload);
113
+ const state = bindLegacyReplayState(
114
+ getToolBatchReplayState(payload),
115
+ configurable
116
+ );
74
117
  const publicPayload = getPublicToolInterruptPayload(payload);
75
118
  const review = createToolApprovalReviewEvidence(
76
119
  interruptId,
@@ -95,23 +138,40 @@ export function restoreToolReplayConfig(
95
138
  * steps, so the presence of replay evidence alone does not imply a resume.
96
139
  * A parent replaying a checkpointed child has no local resume value.
97
140
  */
98
- export function isToolReplayResume(
141
+ export function getToolReplayResumeStatus(
99
142
  config: RunnableConfig,
100
143
  interruptId: string | undefined,
101
144
  isChildReplay: boolean
102
- ): boolean {
103
- const resumeMap: object | undefined = config.configurable?.__pregel_resume_map;
104
- if (interruptId != null && resumeMap != null &&
105
- !Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
106
- return false;
145
+ ): ToolReplayResumeStatus {
146
+ const resumeMap: unknown = config.configurable?.__pregel_resume_map;
147
+ const scratchpad: unknown = config.configurable?.__pregel_scratchpad;
148
+ if (!isRecord(scratchpad) || !Array.isArray(scratchpad.resume)) {
149
+ return 'unverifiable';
107
150
  }
108
- if (isChildReplay && resumeMap != null) {
109
- return true;
151
+ /**
152
+ * LangGraph supports both an interrupt-id resume map and a task-local
153
+ * `nullResume` value. The latter is already scoped to the checkpoint task,
154
+ * so it remains a supported, verifiable resume without a map entry.
155
+ */
156
+ if (scratchpad.nullResume !== undefined) {
157
+ return 'active';
110
158
  }
111
- const scratchpad: { resume?: { length: number }; nullResume?: unknown } | undefined =
112
- config.configurable?.__pregel_scratchpad;
113
- return scratchpad == null ||
114
- (scratchpad.resume?.length ?? 0) > 0 || scratchpad.nullResume !== undefined;
159
+ const hasResume = scratchpad.resume.length > 0;
160
+ if (!isRecord(resumeMap)) {
161
+ return hasResume ? 'unverifiable' : 'stale';
162
+ }
163
+ if (interruptId == null) {
164
+ /** Legacy direct graph callers do not restore the ID into replay state. */
165
+ if (!hasResume) return 'stale';
166
+ return Object.keys(resumeMap).length === 1 ? 'active' : 'unverifiable';
167
+ }
168
+ if (!Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
169
+ return hasResume ? 'unverifiable' : 'stale';
170
+ }
171
+ if (isChildReplay) {
172
+ return 'active';
173
+ }
174
+ return hasResume ? 'active' : 'stale';
115
175
  }
116
176
 
117
177
  export function getToolBatchReplayOwner(
@@ -124,9 +184,39 @@ export function getToolBatchReplayOwner(
124
184
  ? (config.configurable?.checkpoint_ns ?? '')
125
185
  : '',
126
186
  agentId,
187
+ typeof config.configurable?.user_id === 'string'
188
+ ? config.configurable.user_id
189
+ : '',
190
+ typeof config.configurable?.thread_id === 'string'
191
+ ? config.configurable.thread_id
192
+ : '',
127
193
  ]);
128
194
  }
129
195
 
196
+ /** Remove stale authorization while retaining only settled results for this batch. */
197
+ export function clearStaleToolApprovalConfig(
198
+ configurable: Record<string, unknown>,
199
+ owner: string,
200
+ batch: string | undefined
201
+ ): void {
202
+ delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
203
+ const state = getToolBatchReplayState(configurable);
204
+ const records = batch == null
205
+ ? []
206
+ : (state?.records.filter(
207
+ (record) => record.owner === owner && record.batch === batch
208
+ ) ?? []);
209
+ if (state == null || records.length === 0) {
210
+ delete configurable[TOOL_BATCH_REPLAY_KEY];
211
+ return;
212
+ }
213
+ configurable[TOOL_BATCH_REPLAY_KEY] = {
214
+ version: 1,
215
+ records,
216
+ wrappedPayload: state.wrappedPayload,
217
+ } satisfies ToolBatchReplayState;
218
+ }
219
+
130
220
  export function getToolBatchReplayScope(
131
221
  config: RunnableConfig
132
222
  ): string | undefined {
@@ -145,7 +235,9 @@ export function isChildToolReplayOwner(
145
235
  const manifest = requireValidSubagentResumeManifest(config.configurable);
146
236
  if (manifest == null || trustedParentCallIds.size === 0) return false;
147
237
  const identity: unknown = JSON.parse(owner);
148
- if (!Array.isArray(identity) || identity.length !== 3 || identity[1] !== '') return false;
238
+ if (!Array.isArray(identity) ||
239
+ (identity.length !== 3 && identity.length !== 5) ||
240
+ identity[1] !== '') return false;
149
241
  const pending = manifest.executions.filter((execution) => trustedParentCallIds.has(execution.parentToolCallId));
150
242
  while (pending.length > 0) {
151
243
  const execution = pending.pop()!;
@@ -159,14 +251,19 @@ export function isChildToolReplayOwner(
159
251
  export function rebindToolBatchReplayScope(
160
252
  configurable: Record<string, unknown>,
161
253
  sourceScope: string,
162
- destinationScope: string
254
+ destinationScope: string,
255
+ destinationThreadId?: string
163
256
  ): void {
164
257
  const rebind = (key: string): string => {
165
258
  const parts: unknown = JSON.parse(key);
166
259
  if (!Array.isArray(parts) || parts[0] !== sourceScope) {
167
260
  return key;
168
261
  }
169
- return JSON.stringify([destinationScope, ...parts.slice(1)]);
262
+ const rebound = [destinationScope, ...parts.slice(1)];
263
+ if (rebound.length === 5 && destinationThreadId != null) {
264
+ rebound[4] = destinationThreadId;
265
+ }
266
+ return JSON.stringify(rebound);
170
267
  };
171
268
  const config = { configurable };
172
269
  const review = getToolApprovalReviewEvidence(config);
@@ -195,7 +292,8 @@ export function rebindToolBatchReplayScope(
195
292
  export function rebindToolBatchReplayPayload(
196
293
  payload: unknown,
197
294
  sourceScope: string,
198
- destinationScope: string
295
+ destinationScope: string,
296
+ destinationThreadId?: string
199
297
  ): unknown {
200
298
  const state = getToolBatchReplayState(payload);
201
299
  const value = stripRunStepResumeState(payload);
@@ -203,7 +301,12 @@ export function rebindToolBatchReplayPayload(
203
301
  return payload;
204
302
  }
205
303
  const configurable = { [TOOL_BATCH_REPLAY_KEY]: state };
206
- rebindToolBatchReplayScope(configurable, sourceScope, destinationScope);
304
+ rebindToolBatchReplayScope(
305
+ configurable,
306
+ sourceScope,
307
+ destinationScope,
308
+ destinationThreadId
309
+ );
207
310
  const rebound = {
208
311
  ...value,
209
312
  [TOOL_BATCH_REPLAY_KEY]: configurable[TOOL_BATCH_REPLAY_KEY],