@librechat/agents 3.8.4 → 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,25 +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 &&
105
- resumeMap != null &&
106
- !Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
107
- 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';
108
150
  }
109
- if (isChildReplay && resumeMap != null) {
110
- 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';
111
158
  }
112
- const scratchpad: { resume?: { length: number }; nullResume?: unknown } | undefined =
113
- config.configurable?.__pregel_scratchpad;
114
- return scratchpad == null ||
115
- (scratchpad.resume?.length ?? 0) > 0 ||
116
- 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';
117
175
  }
118
176
 
119
177
  export function getToolBatchReplayOwner(
@@ -126,9 +184,39 @@ export function getToolBatchReplayOwner(
126
184
  ? (config.configurable?.checkpoint_ns ?? '')
127
185
  : '',
128
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
+ : '',
129
193
  ]);
130
194
  }
131
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
+
132
220
  export function getToolBatchReplayScope(
133
221
  config: RunnableConfig
134
222
  ): string | undefined {
@@ -147,7 +235,9 @@ export function isChildToolReplayOwner(
147
235
  const manifest = requireValidSubagentResumeManifest(config.configurable);
148
236
  if (manifest == null || trustedParentCallIds.size === 0) return false;
149
237
  const identity: unknown = JSON.parse(owner);
150
- 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;
151
241
  const pending = manifest.executions.filter((execution) => trustedParentCallIds.has(execution.parentToolCallId));
152
242
  while (pending.length > 0) {
153
243
  const execution = pending.pop()!;
@@ -161,14 +251,19 @@ export function isChildToolReplayOwner(
161
251
  export function rebindToolBatchReplayScope(
162
252
  configurable: Record<string, unknown>,
163
253
  sourceScope: string,
164
- destinationScope: string
254
+ destinationScope: string,
255
+ destinationThreadId?: string
165
256
  ): void {
166
257
  const rebind = (key: string): string => {
167
258
  const parts: unknown = JSON.parse(key);
168
259
  if (!Array.isArray(parts) || parts[0] !== sourceScope) {
169
260
  return key;
170
261
  }
171
- 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);
172
267
  };
173
268
  const config = { configurable };
174
269
  const review = getToolApprovalReviewEvidence(config);
@@ -197,7 +292,8 @@ export function rebindToolBatchReplayScope(
197
292
  export function rebindToolBatchReplayPayload(
198
293
  payload: unknown,
199
294
  sourceScope: string,
200
- destinationScope: string
295
+ destinationScope: string,
296
+ destinationThreadId?: string
201
297
  ): unknown {
202
298
  const state = getToolBatchReplayState(payload);
203
299
  const value = stripRunStepResumeState(payload);
@@ -205,7 +301,12 @@ export function rebindToolBatchReplayPayload(
205
301
  return payload;
206
302
  }
207
303
  const configurable = { [TOOL_BATCH_REPLAY_KEY]: state };
208
- rebindToolBatchReplayScope(configurable, sourceScope, destinationScope);
304
+ rebindToolBatchReplayScope(
305
+ configurable,
306
+ sourceScope,
307
+ destinationScope,
308
+ destinationThreadId
309
+ );
209
310
  const rebound = {
210
311
  ...value,
211
312
  [TOOL_BATCH_REPLAY_KEY]: configurable[TOOL_BATCH_REPLAY_KEY],