@langgraph-js/sdk 4.0.0 → 4.1.0

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.
@@ -1,5 +0,0 @@
1
-
2
- 
3
- > @langgraph-js/sdk@1.6.4 build /Users/konghayao/code/ai/YaoAgent/apps/langgraph-client
4
- > tsc
5
-
@@ -107,6 +107,10 @@ export interface LangGraphEvents {
107
107
  done: {
108
108
  event: "done";
109
109
  };
110
+ /** 中断事件 */
111
+ interruptChange: {
112
+ event: "interruptChange";
113
+ };
110
114
  }
111
115
  /**
112
116
  * @zh LangGraphClient 类是与 LangGraph 后端交互的主要客户端。
@@ -132,7 +136,10 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
132
136
  /** 代理 assistants 属性到内部 client */
133
137
  get assistants(): {
134
138
  search(query?: {
135
- graphId?: string;
139
+ graphId? /**
140
+ * The maximum number of retries that can be made for a single call,
141
+ * with an exponential backoff between each attempt. Defaults to 6.
142
+ */: string;
136
143
  metadata?: import("@langchain/langgraph-sdk").Metadata;
137
144
  limit?: number;
138
145
  offset?: number;
@@ -232,6 +239,8 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
232
239
  }>;
233
240
  messagesMetadata: {};
234
241
  humanInTheLoop: HumanInTheLoopState | null;
242
+ /** 此 interruptData 不包括 humanInTheLoop 的数据 */
243
+ interruptData: any | null;
235
244
  /**
236
245
  * @zh 发送消息到 LangGraph 后端。
237
246
  * @en Sends a message to the LangGraph backend.
@@ -254,6 +263,7 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
254
263
  */
255
264
  private processStreamChunk;
256
265
  private runFETool;
266
+ private getHumanInTheLoopData;
257
267
  private responseHumanInTheLoop;
258
268
  private callFETool;
259
269
  extraParams: Record<string, any>;
@@ -4,6 +4,7 @@ import { createActionRequestID } from "./humanInTheLoop.js";
4
4
  import { MessageProcessor } from "./MessageProcessor.js";
5
5
  import { revertChatTo } from "./time-travel/index.js";
6
6
  import camelcaseKeys from "camelcase-keys";
7
+ import z from "zod";
7
8
  /**
8
9
  * @zh LangGraphClient 类是与 LangGraph 后端交互的主要客户端。
9
10
  * @en The LangGraphClient class is the main client for interacting with the LangGraph backend.
@@ -116,6 +117,8 @@ export class LangGraphClient extends EventEmitter {
116
117
  sortBy: options.sortBy || "updated_at",
117
118
  offset: options.offset || 0,
118
119
  limit: options.limit || 10,
120
+ /** @ts-ignore: 用于删除不需要的字段 */
121
+ without_details: true,
119
122
  });
120
123
  }
121
124
  async deleteThread(threadId) {
@@ -212,6 +215,8 @@ export class LangGraphClient extends EventEmitter {
212
215
  }
213
216
  messagesMetadata = {};
214
217
  humanInTheLoop = null;
218
+ /** 此 interruptData 不包括 humanInTheLoop 的数据 */
219
+ interruptData = null;
215
220
  /**
216
221
  * @zh 发送消息到 LangGraph 后端。
217
222
  * @en Sends a message to the LangGraph backend.
@@ -229,6 +234,12 @@ export class LangGraphClient extends EventEmitter {
229
234
  },
230
235
  });
231
236
  }
237
+ if (this.interruptData) {
238
+ this.interruptData = null;
239
+ this.emit("interruptChange", {
240
+ event: "interruptChange",
241
+ });
242
+ }
232
243
  const messagesToSend = Array.isArray(input)
233
244
  ? input
234
245
  : [
@@ -335,30 +346,16 @@ export class LangGraphClient extends EventEmitter {
335
346
  const data = chunk.data;
336
347
  if (data?.__interrupt__) {
337
348
  this._status = "interrupted";
338
- const humanInTheLoop = data?.__interrupt__.map((i) => {
339
- // 修复 python 版本是以下划线命名的,而 js 版本是以驼峰命名的
340
- if (i && "review_configs" in i.value) {
341
- return {
342
- id: i.id,
343
- value: {
344
- /** @ts-ignore */
345
- actionRequests: i.value.action_requests,
346
- reviewConfigs: camelcaseKeys(i.value.review_configs, { deep: true }),
347
- },
348
- };
349
- }
350
- return i;
351
- });
352
- humanInTheLoop.forEach((i) => {
353
- i.value.actionRequests.forEach((j) => {
354
- j.id = j.id || createActionRequestID(j);
355
- });
356
- return i;
349
+ const humanInTheLoopData = this.getHumanInTheLoopData(data?.__interrupt__);
350
+ if (humanInTheLoopData) {
351
+ this.humanInTheLoop = humanInTheLoopData;
352
+ }
353
+ else {
354
+ this.interruptData = data.__interrupt__;
355
+ }
356
+ this.emit("interruptChange", {
357
+ event: "interruptChange",
357
358
  });
358
- this.humanInTheLoop = {
359
- interruptData: humanInTheLoop,
360
- result: {},
361
- };
362
359
  }
363
360
  else if (data?.messages) {
364
361
  const isResume = !!command?.resume;
@@ -401,6 +398,45 @@ export class LangGraphClient extends EventEmitter {
401
398
  return Promise.all(result);
402
399
  }
403
400
  }
401
+ getHumanInTheLoopData(interruptData) {
402
+ const humanInTheLoopData = z
403
+ .array(z.object({
404
+ id: z.string(),
405
+ value: z.union([
406
+ z.object({
407
+ review_configs: z.array(z.any()),
408
+ }),
409
+ z.object({ reviewConfigs: z.array(z.any()) }),
410
+ ]),
411
+ }))
412
+ .safeParse(interruptData);
413
+ if (humanInTheLoopData.success) {
414
+ const humanInTheLoop = interruptData.map((i) => {
415
+ // 修复 python 版本是以下划线命名的,而 js 版本是以驼峰命名的
416
+ if (i && "review_configs" in i.value) {
417
+ return {
418
+ id: i.id,
419
+ value: {
420
+ /** @ts-ignore */
421
+ actionRequests: i.value.action_requests,
422
+ reviewConfigs: camelcaseKeys(i.value.review_configs, { deep: true }),
423
+ },
424
+ };
425
+ }
426
+ return i;
427
+ });
428
+ humanInTheLoop.forEach((i) => {
429
+ i.value.actionRequests.forEach((j) => {
430
+ j.id = j.id || createActionRequestID(j);
431
+ });
432
+ return i;
433
+ });
434
+ return {
435
+ interruptData: humanInTheLoop,
436
+ result: {},
437
+ };
438
+ }
439
+ }
404
440
  async responseHumanInTheLoop() {
405
441
  if (this.humanInTheLoop) {
406
442
  const humanInTheLoop = this.humanInTheLoop;
@@ -1,4 +1,21 @@
1
1
  import { Client } from "@langchain/langgraph-sdk";
2
2
  export const createLangGraphServerClient = async (config) => {
3
- return new Client(config);
3
+ const client = new Client(config);
4
+ client.threads.search = function (query) {
5
+ return this.fetch("/threads/search", {
6
+ method: "POST",
7
+ json: {
8
+ metadata: (query == null ? void 0 : query.metadata) ?? void 0,
9
+ ids: (query == null ? void 0 : query.ids) ?? void 0,
10
+ limit: (query == null ? void 0 : query.limit) ?? 10,
11
+ offset: (query == null ? void 0 : query.offset) ?? 0,
12
+ status: query == null ? void 0 : query.status,
13
+ sort_by: query == null ? void 0 : query.sortBy,
14
+ sort_order: query == null ? void 0 : query.sortOrder,
15
+ select: (query == null ? void 0 : query.select) ?? void 0,
16
+ without_details: (query == null ? void 0 : query.without_details) ?? false,
17
+ },
18
+ });
19
+ }.bind(client.threads);
20
+ return client;
4
21
  };
@@ -16,6 +16,8 @@ export declare const useChat: () => UnionStore<{
16
16
  currentAgent: import("nanostores").PreinitializedWritableAtom<string> & object;
17
17
  currentChatId: import("nanostores").PreinitializedWritableAtom<string | null> & object;
18
18
  currentNodeName: import("nanostores").PreinitializedWritableAtom<string> & object;
19
+ interruptData: import("nanostores").PreinitializedWritableAtom<import("../humanInTheLoop.js").InterruptData | null> & object;
20
+ isInterrupted: import("nanostores").PreinitializedWritableAtom<boolean> & object;
19
21
  tools: import("nanostores").PreinitializedWritableAtom<import("../index.js").UnionTool<any, Object, any>[]> & object;
20
22
  collapsedTools: import("nanostores").PreinitializedWritableAtom<string[]> & object;
21
23
  showGraph: import("nanostores").PreinitializedWritableAtom<boolean> & object;
@@ -35,7 +37,7 @@ export declare const useChat: () => UnionStore<{
35
37
  createNewSession: () => Promise<void>;
36
38
  refreshSessionList: () => Promise<void>;
37
39
  refreshHistoryList: () => Promise<void>;
38
- sendMessage: (message?: import("@langchain/langgraph-sdk").Message[], extraData?: import("../LangGraphClient.js").SendMessageOptions, withoutCheck?: boolean) => Promise<void>;
40
+ sendMessage: (message?: import("@langchain/langgraph-sdk").Message[], extraData?: import("../LangGraphClient.js").SendMessageOptions, withoutCheck?: boolean, isResume?: boolean) => Promise<void>;
39
41
  stopGeneration: () => void;
40
42
  setUserInput: (input: string) => void;
41
43
  revertChatTo(messageId: string, resend?: boolean, sendOptions?: import("../LangGraphClient.js").SendMessageOptions & import("../time-travel/index.js").RevertChatToOptions): Promise<void>;
@@ -48,6 +50,7 @@ export declare const useChat: () => UnionStore<{
48
50
  toggleGraphVisible(): void;
49
51
  refreshGraph: () => Promise<void>;
50
52
  setCurrentAgent(agent: string): Promise<import("../History.js").History>;
53
+ resumeFromInterrupt(data: any): Promise<void>;
51
54
  addToHistory: (thread: import("@langchain/langgraph-sdk").Thread<{
52
55
  messages: import("@langchain/langgraph-sdk").Message[];
53
56
  }>) => void;
@@ -45,7 +45,7 @@ export declare class ToolRenderData<I, D> {
45
45
  } | null;
46
46
  /** 发送恢复状态的数据 */
47
47
  sendResumeData(response: HumanInTheLoopDecision): void;
48
- get state(): "idle" | "done" | "loading";
48
+ get state(): "idle" | "interrupted" | "done" | "loading";
49
49
  get input(): I | null;
50
50
  get output(): string;
51
51
  getJSONOutput(): D;
@@ -54,7 +54,7 @@ export class ToolRenderData {
54
54
  return "done";
55
55
  }
56
56
  if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
57
- return "done";
57
+ return "interrupted";
58
58
  }
59
59
  if (this.message.tool_input) {
60
60
  return "loading";
@@ -3,6 +3,7 @@ import { AssistantGraph, Message, Thread } from "@langchain/langgraph-sdk";
3
3
  import { UnionTool } from "../tool/createTool.js";
4
4
  import { RevertChatToOptions } from "../time-travel/index.js";
5
5
  import { History, SessionInfo } from "../History.js";
6
+ import { InterruptData } from "../humanInTheLoop.js";
6
7
  export declare const formatTime: (date: Date) => string;
7
8
  export declare const formatTokens: (tokens: number) => string;
8
9
  export declare const getMessageContent: (content: any) => string;
@@ -30,6 +31,8 @@ export declare const createChatStore: (initClientName: string, config: Partial<L
30
31
  currentAgent: import("nanostores").PreinitializedWritableAtom<string> & object;
31
32
  currentChatId: import("nanostores").PreinitializedWritableAtom<string | null> & object;
32
33
  currentNodeName: import("nanostores").PreinitializedWritableAtom<string> & object;
34
+ interruptData: import("nanostores").PreinitializedWritableAtom<InterruptData | null> & object;
35
+ isInterrupted: import("nanostores").PreinitializedWritableAtom<boolean> & object;
33
36
  tools: import("nanostores").PreinitializedWritableAtom<UnionTool<any, Object, any>[]> & object;
34
37
  collapsedTools: import("nanostores").PreinitializedWritableAtom<string[]> & object;
35
38
  showGraph: import("nanostores").PreinitializedWritableAtom<boolean> & object;
@@ -49,7 +52,7 @@ export declare const createChatStore: (initClientName: string, config: Partial<L
49
52
  createNewSession: () => Promise<void>;
50
53
  refreshSessionList: () => Promise<void>;
51
54
  refreshHistoryList: () => Promise<void>;
52
- sendMessage: (message?: Message[], extraData?: SendMessageOptions, withoutCheck?: boolean) => Promise<void>;
55
+ sendMessage: (message?: Message[], extraData?: SendMessageOptions, withoutCheck?: boolean, isResume?: boolean) => Promise<void>;
53
56
  stopGeneration: () => void;
54
57
  setUserInput: (input: string) => void;
55
58
  revertChatTo(messageId: string, resend?: boolean, sendOptions?: SendMessageOptions & RevertChatToOptions): Promise<void>;
@@ -62,6 +65,7 @@ export declare const createChatStore: (initClientName: string, config: Partial<L
62
65
  toggleGraphVisible(): void;
63
66
  refreshGraph: () => Promise<void>;
64
67
  setCurrentAgent(agent: string): Promise<History>;
68
+ resumeFromInterrupt(data: any): Promise<void>;
65
69
  addToHistory: (thread: Thread<{
66
70
  messages: Message[];
67
71
  }>) => void;
@@ -48,6 +48,9 @@ export const createChatStore = (initClientName, config, context = {}) => {
48
48
  const currentAgent = atom(initClientName);
49
49
  const currentChatId = atom(null);
50
50
  const currentNodeName = atom("__start__");
51
+ // Interrupt 状态
52
+ const interruptData = atom(null);
53
+ const isInterrupted = atom(false);
51
54
  // 工具和图表
52
55
  const tools = atom([]);
53
56
  const collapsedTools = atom([]);
@@ -62,7 +65,8 @@ export const createChatStore = (initClientName, config, context = {}) => {
62
65
  const updateLoadingFromClientStatus = () => {
63
66
  const c = client.get();
64
67
  if (c) {
65
- loading.set(c.status === "busy");
68
+ // interrupted 状态也应该被视为 loading,因为用户需要处理中断
69
+ loading.set(c.status === "busy" || c.status === "interrupted");
66
70
  }
67
71
  };
68
72
  // ============ UI 更新逻辑 ============
@@ -182,12 +186,27 @@ export const createChatStore = (initClientName, config, context = {}) => {
182
186
  updateUI(newClient);
183
187
  }
184
188
  };
189
+ const onInterrupted = (event) => {
190
+ if (!isActiveClient())
191
+ return;
192
+ const interruptInfo = newClient.interruptData;
193
+ if (interruptInfo) {
194
+ interruptData.set(interruptInfo);
195
+ isInterrupted.set(true);
196
+ updateLoadingFromClientStatus();
197
+ }
198
+ else {
199
+ interruptData.set(null);
200
+ isInterrupted.set(false);
201
+ }
202
+ };
185
203
  newClient.on("start", onStart);
186
204
  newClient.on("thread", onThread);
187
205
  newClient.on("done", onDone);
188
206
  newClient.on("error", onError);
189
207
  newClient.on("message", onMessage);
190
208
  newClient.on("value", onValue);
209
+ newClient.on("interruptChange", onInterrupted);
191
210
  return () => {
192
211
  newClient.off("start", onStart);
193
212
  newClient.off("thread", onThread);
@@ -195,6 +214,7 @@ export const createChatStore = (initClientName, config, context = {}) => {
195
214
  newClient.off("error", onError);
196
215
  newClient.off("message", onMessage);
197
216
  newClient.off("value", onValue);
217
+ newClient.off("interruptChange", onInterrupted);
198
218
  };
199
219
  }
200
220
  // ============ 会话激活逻辑 ============
@@ -208,6 +228,8 @@ export const createChatStore = (initClientName, config, context = {}) => {
208
228
  cleanupCurrentClient = null;
209
229
  }
210
230
  inChatError.set(null);
231
+ interruptData.set(null);
232
+ isInterrupted.set(false);
211
233
  const session = await historyManager.activateSession(sessionId);
212
234
  const activeClient = session.client;
213
235
  if (activeClient) {
@@ -227,6 +249,14 @@ export const createChatStore = (initClientName, config, context = {}) => {
227
249
  if (currentThread && (currentThread.status === "running" || currentThread.status === "pending")) {
228
250
  await activeClient.resetStream();
229
251
  }
252
+ // 检查是否处于中断状态
253
+ if (currentThread?.status === "interrupted") {
254
+ const interruptInfo = activeClient.interruptData;
255
+ if (interruptInfo) {
256
+ interruptData.set(interruptInfo);
257
+ isInterrupted.set(true);
258
+ }
259
+ }
230
260
  }
231
261
  }
232
262
  catch (error) {
@@ -235,7 +265,7 @@ export const createChatStore = (initClientName, config, context = {}) => {
235
265
  }
236
266
  }
237
267
  // ============ 消息和交互逻辑 ============
238
- async function sendMessage(message, extraData, withoutCheck = false) {
268
+ async function sendMessage(message, extraData, withoutCheck = false, isResume = false) {
239
269
  const c = client.get();
240
270
  if ((!withoutCheck && !userInput.get().trim() && !message?.length) || !c)
241
271
  return;
@@ -301,6 +331,9 @@ export const createChatStore = (initClientName, config, context = {}) => {
301
331
  currentAgent,
302
332
  currentChatId,
303
333
  currentNodeName,
334
+ // Interrupt 状态
335
+ interruptData,
336
+ isInterrupted,
304
337
  // 工具和图表
305
338
  tools,
306
339
  collapsedTools,
@@ -356,6 +389,13 @@ export const createChatStore = (initClientName, config, context = {}) => {
356
389
  currentAgent.set(agent);
357
390
  return initClient();
358
391
  },
392
+ resumeFromInterrupt(data) {
393
+ return sendMessage([], {
394
+ command: {
395
+ resume: data,
396
+ },
397
+ }, true);
398
+ },
359
399
  // 历史记录(兼容旧 API)
360
400
  addToHistory,
361
401
  createNewChat: createNewSession,
@@ -59,6 +59,8 @@ export declare const useChatProvider: (props: ChatProviderProps) => {
59
59
  currentAgent: PreinitializedWritableAtom<string> & object;
60
60
  currentChatId: PreinitializedWritableAtom<string | null> & object;
61
61
  currentNodeName: PreinitializedWritableAtom<string> & object;
62
+ interruptData: PreinitializedWritableAtom<import("../humanInTheLoop.js").InterruptData | null> & object;
63
+ isInterrupted: PreinitializedWritableAtom<boolean> & object;
62
64
  tools: PreinitializedWritableAtom<import("../index.js").UnionTool<any, Object, any>[]> & object;
63
65
  collapsedTools: PreinitializedWritableAtom<string[]> & object;
64
66
  showGraph: PreinitializedWritableAtom<boolean> & object;
@@ -78,7 +80,7 @@ export declare const useChatProvider: (props: ChatProviderProps) => {
78
80
  createNewSession: () => Promise<void>;
79
81
  refreshSessionList: () => Promise<void>;
80
82
  refreshHistoryList: () => Promise<void>;
81
- sendMessage: (message?: import("@langchain/langgraph-sdk").Message[], extraData?: import("../LangGraphClient.js").SendMessageOptions, withoutCheck?: boolean) => Promise<void>;
83
+ sendMessage: (message?: import("@langchain/langgraph-sdk").Message[], extraData?: import("../LangGraphClient.js").SendMessageOptions, withoutCheck?: boolean, isResume?: boolean) => Promise<void>;
82
84
  stopGeneration: () => void;
83
85
  setUserInput: (input: string) => void;
84
86
  revertChatTo(messageId: string, resend?: boolean, sendOptions?: import("../LangGraphClient.js").SendMessageOptions & import("../time-travel/index.js").RevertChatToOptions): Promise<void>;
@@ -91,6 +93,7 @@ export declare const useChatProvider: (props: ChatProviderProps) => {
91
93
  toggleGraphVisible(): void;
92
94
  refreshGraph: () => Promise<void>;
93
95
  setCurrentAgent(agent: string): Promise<import("../History.js").History>;
96
+ resumeFromInterrupt(data: any): Promise<void>;
94
97
  addToHistory: (thread: import("@langchain/langgraph-sdk").Thread<{
95
98
  messages: import("@langchain/langgraph-sdk").Message[];
96
99
  }>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langgraph-js/sdk",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "The UI SDK for LangGraph - seamlessly integrate your AI agents with frontend interfaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ import { type ILangGraphClient } from "@langgraph-js/pure-graph/dist/types.js";
7
7
  import { MessageProcessor } from "./MessageProcessor.js";
8
8
  import { revertChatTo, RevertChatToOptions } from "./time-travel/index.js";
9
9
  import camelcaseKeys from "camelcase-keys";
10
+ import z from "zod";
10
11
  export type RenderMessage = Message & {
11
12
  /** 对于 AIMessage 来说是节点名称,对于工具节点来说是工具名称 */
12
13
  name?: string;
@@ -91,6 +92,8 @@ export interface LangGraphEvents {
91
92
  thread: { event: "thread/create"; data: { thread: Thread } };
92
93
  /** 流完成事件 */
93
94
  done: { event: "done" };
95
+ /** 中断事件 */
96
+ interruptChange: { event: "interruptChange" };
94
97
  }
95
98
 
96
99
  /**
@@ -219,6 +222,8 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
219
222
  sortBy: options.sortBy || "updated_at",
220
223
  offset: options.offset || 0,
221
224
  limit: options.limit || 10,
225
+ /** @ts-ignore: 用于删除不需要的字段 */
226
+ without_details: true,
222
227
  });
223
228
  }
224
229
  async deleteThread(threadId: string) {
@@ -326,6 +331,8 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
326
331
  }
327
332
  public messagesMetadata = {};
328
333
  public humanInTheLoop: HumanInTheLoopState | null = null;
334
+ /** 此 interruptData 不包括 humanInTheLoop 的数据 */
335
+ public interruptData: any | null = null;
329
336
  /**
330
337
  * @zh 发送消息到 LangGraph 后端。
331
338
  * @en Sends a message to the LangGraph backend.
@@ -343,7 +350,12 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
343
350
  },
344
351
  });
345
352
  }
346
-
353
+ if (this.interruptData) {
354
+ this.interruptData = null;
355
+ this.emit("interruptChange", {
356
+ event: "interruptChange",
357
+ });
358
+ }
347
359
  const messagesToSend = Array.isArray(input)
348
360
  ? input
349
361
  : [
@@ -451,33 +463,17 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
451
463
  messages: Message[];
452
464
  }
453
465
  | undefined;
454
-
455
466
  if (data?.__interrupt__) {
456
467
  this._status = "interrupted";
457
- const humanInTheLoop = data?.__interrupt__.map((i) => {
458
- // 修复 python 版本是以下划线命名的,而 js 版本是以驼峰命名的
459
- if (i && "review_configs" in i.value) {
460
- return {
461
- id: i.id,
462
- value: {
463
- /** @ts-ignore */
464
- actionRequests: i.value.action_requests,
465
- reviewConfigs: camelcaseKeys(i.value.review_configs as any[], { deep: true }),
466
- },
467
- } as InterruptData[number];
468
- }
469
- return i;
470
- });
471
- humanInTheLoop.forEach((i) => {
472
- i.value.actionRequests.forEach((j) => {
473
- j.id = j.id || createActionRequestID(j);
474
- });
475
- return i;
468
+ const humanInTheLoopData = this.getHumanInTheLoopData(data?.__interrupt__);
469
+ if (humanInTheLoopData) {
470
+ this.humanInTheLoop = humanInTheLoopData;
471
+ } else {
472
+ this.interruptData = data.__interrupt__;
473
+ }
474
+ this.emit("interruptChange", {
475
+ event: "interruptChange",
476
476
  });
477
- this.humanInTheLoop = {
478
- interruptData: humanInTheLoop,
479
- result: {},
480
- };
481
477
  } else if (data?.messages) {
482
478
  const isResume = !!command?.resume;
483
479
  const isLongerThanLocal = data.messages.length >= this.messageProcessor.getGraphMessages().length;
@@ -518,6 +514,47 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
518
514
  return Promise.all(result);
519
515
  }
520
516
  }
517
+ private getHumanInTheLoopData(interruptData: any) {
518
+ const humanInTheLoopData = z
519
+ .array(
520
+ z.object({
521
+ id: z.string(),
522
+ value: z.union([
523
+ z.object({
524
+ review_configs: z.array(z.any()),
525
+ }),
526
+ z.object({ reviewConfigs: z.array(z.any()) }),
527
+ ]),
528
+ })
529
+ )
530
+ .safeParse(interruptData);
531
+ if (humanInTheLoopData.success) {
532
+ const humanInTheLoop: HumanInTheLoopState["interruptData"] = interruptData.map((i: any) => {
533
+ // 修复 python 版本是以下划线命名的,而 js 版本是以驼峰命名的
534
+ if (i && "review_configs" in i.value) {
535
+ return {
536
+ id: i.id,
537
+ value: {
538
+ /** @ts-ignore */
539
+ actionRequests: i.value.action_requests,
540
+ reviewConfigs: camelcaseKeys(i.value.review_configs as any[], { deep: true }),
541
+ },
542
+ } as InterruptData[number];
543
+ }
544
+ return i;
545
+ });
546
+ humanInTheLoop.forEach((i) => {
547
+ i.value.actionRequests.forEach((j) => {
548
+ j.id = j.id || createActionRequestID(j);
549
+ });
550
+ return i;
551
+ });
552
+ return {
553
+ interruptData: humanInTheLoop,
554
+ result: {},
555
+ };
556
+ }
557
+ }
521
558
  private async responseHumanInTheLoop() {
522
559
  if (this.humanInTheLoop) {
523
560
  const humanInTheLoop = this.humanInTheLoop;
@@ -2,5 +2,22 @@ import { LangGraphClientConfig } from "../LangGraphClient.js";
2
2
  import { type ILangGraphClient } from "@langgraph-js/pure-graph/dist/types.js";
3
3
  import { Client } from "@langchain/langgraph-sdk";
4
4
  export const createLangGraphServerClient = async (config: LangGraphClientConfig): Promise<ILangGraphClient> => {
5
- return new Client(config) as ILangGraphClient;
5
+ const client = new Client(config) as ILangGraphClient;
6
+ client.threads.search = function (this: any, query: any) {
7
+ return this.fetch("/threads/search", {
8
+ method: "POST",
9
+ json: {
10
+ metadata: (query == null ? void 0 : query.metadata) ?? void 0,
11
+ ids: (query == null ? void 0 : query.ids) ?? void 0,
12
+ limit: (query == null ? void 0 : query.limit) ?? 10,
13
+ offset: (query == null ? void 0 : query.offset) ?? 0,
14
+ status: query == null ? void 0 : query.status,
15
+ sort_by: query == null ? void 0 : query.sortBy,
16
+ sort_order: query == null ? void 0 : query.sortOrder,
17
+ select: (query == null ? void 0 : query.select) ?? void 0,
18
+ without_details: (query == null ? void 0 : query.without_details) ?? false,
19
+ },
20
+ });
21
+ }.bind(client.threads);
22
+ return client;
6
23
  };
@@ -59,7 +59,7 @@ export class ToolRenderData<I, D> {
59
59
  return "done";
60
60
  }
61
61
  if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
62
- return "done";
62
+ return "interrupted";
63
63
  }
64
64
  if (this.message.tool_input) {
65
65
  return "loading";
@@ -8,6 +8,7 @@ import { createLangGraphServerClient } from "../client/LanggraphServer.js";
8
8
  import { useArtifacts } from "../artifacts/index.js";
9
9
  import { RevertChatToOptions } from "../time-travel/index.js";
10
10
  import { History, SessionInfo } from "../History.js";
11
+ import { InterruptData, InterruptResponse } from "../humanInTheLoop.js";
11
12
 
12
13
  // ============ 工具函数 ============
13
14
 
@@ -68,6 +69,10 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
68
69
  const currentChatId = atom<string | null>(null);
69
70
  const currentNodeName = atom<string>("__start__");
70
71
 
72
+ // Interrupt 状态
73
+ const interruptData = atom<InterruptData | null>(null);
74
+ const isInterrupted = atom<boolean>(false);
75
+
71
76
  // 工具和图表
72
77
  const tools = atom<UnionTool<any>[]>([]);
73
78
  const collapsedTools = atom<string[]>([]);
@@ -87,7 +92,8 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
87
92
  const updateLoadingFromClientStatus = () => {
88
93
  const c = client.get();
89
94
  if (c) {
90
- loading.set(c.status === "busy");
95
+ // interrupted 状态也应该被视为 loading,因为用户需要处理中断
96
+ loading.set(c.status === "busy" || c.status === "interrupted");
91
97
  }
92
98
  };
93
99
 
@@ -225,12 +231,27 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
225
231
  }
226
232
  };
227
233
 
234
+ const onInterrupted = (event: any) => {
235
+ if (!isActiveClient()) return;
236
+
237
+ const interruptInfo = newClient.interruptData;
238
+ if (interruptInfo) {
239
+ interruptData.set(interruptInfo);
240
+ isInterrupted.set(true);
241
+ updateLoadingFromClientStatus();
242
+ } else {
243
+ interruptData.set(null);
244
+ isInterrupted.set(false);
245
+ }
246
+ };
247
+
228
248
  newClient.on("start", onStart);
229
249
  newClient.on("thread", onThread);
230
250
  newClient.on("done", onDone);
231
251
  newClient.on("error", onError);
232
252
  newClient.on("message", onMessage);
233
253
  newClient.on("value", onValue);
254
+ newClient.on("interruptChange", onInterrupted);
234
255
 
235
256
  return () => {
236
257
  newClient.off("start", onStart);
@@ -239,6 +260,7 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
239
260
  newClient.off("error", onError);
240
261
  newClient.off("message", onMessage);
241
262
  newClient.off("value", onValue);
263
+ newClient.off("interruptChange", onInterrupted);
242
264
  };
243
265
  }
244
266
 
@@ -255,6 +277,8 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
255
277
  }
256
278
 
257
279
  inChatError.set(null);
280
+ interruptData.set(null);
281
+ isInterrupted.set(false);
258
282
 
259
283
  const session = await historyManager.activateSession(sessionId);
260
284
  const activeClient = session.client;
@@ -280,6 +304,15 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
280
304
  if (currentThread && (currentThread.status === "running" || currentThread.status === "pending")) {
281
305
  await activeClient.resetStream();
282
306
  }
307
+
308
+ // 检查是否处于中断状态
309
+ if (currentThread?.status === "interrupted") {
310
+ const interruptInfo = activeClient.interruptData;
311
+ if (interruptInfo) {
312
+ interruptData.set(interruptInfo);
313
+ isInterrupted.set(true);
314
+ }
315
+ }
283
316
  }
284
317
  } catch (error) {
285
318
  console.error("Failed to activate session:", error);
@@ -289,7 +322,7 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
289
322
 
290
323
  // ============ 消息和交互逻辑 ============
291
324
 
292
- async function sendMessage(message?: Message[], extraData?: SendMessageOptions, withoutCheck = false) {
325
+ async function sendMessage(message?: Message[], extraData?: SendMessageOptions, withoutCheck = false, isResume = false) {
293
326
  const c = client.get();
294
327
  if ((!withoutCheck && !userInput.get().trim() && !message?.length) || !c) return;
295
328
 
@@ -361,6 +394,10 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
361
394
  currentChatId,
362
395
  currentNodeName,
363
396
 
397
+ // Interrupt 状态
398
+ interruptData,
399
+ isInterrupted,
400
+
364
401
  // 工具和图表
365
402
  tools,
366
403
  collapsedTools,
@@ -421,7 +458,17 @@ export const createChatStore = (initClientName: string, config: Partial<LangGrap
421
458
  currentAgent.set(agent);
422
459
  return initClient();
423
460
  },
424
-
461
+ resumeFromInterrupt(data: any) {
462
+ return sendMessage(
463
+ [],
464
+ {
465
+ command: {
466
+ resume: data,
467
+ },
468
+ },
469
+ true
470
+ );
471
+ },
425
472
  // 历史记录(兼容旧 API)
426
473
  addToHistory,
427
474
  createNewChat: createNewSession,