@langgraph-js/sdk 3.8.3 → 4.0.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.
@@ -2,6 +2,7 @@ import type { Thread, Message, Assistant, Command } from "@langchain/langgraph-s
2
2
  import { EventEmitter } from "eventemitter3";
3
3
  import { ToolManager } from "./ToolManager.js";
4
4
  import { CallToolResult } from "./tool/createTool.js";
5
+ import { HumanInTheLoopDecision, HumanInTheLoopState } from "./humanInTheLoop.js";
5
6
  import { type ILangGraphClient } from "@langgraph-js/pure-graph/dist/types.js";
6
7
  import { RevertChatToOptions } from "./time-travel/index.js";
7
8
  export type RenderMessage = Message & {
@@ -44,20 +45,6 @@ export type SendMessageOptions = {
44
45
  command?: Command;
45
46
  joinRunId?: string;
46
47
  };
47
- export type InterruptData = {
48
- id: string;
49
- value: {
50
- actionRequests: {
51
- name: string;
52
- description: string;
53
- args: any;
54
- }[];
55
- reviewConfigs: {
56
- actionName: string;
57
- allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
58
- }[];
59
- };
60
- }[];
61
48
  export interface LangGraphClientConfig {
62
49
  apiUrl?: string;
63
50
  apiKey?: string;
@@ -244,7 +231,7 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
244
231
  messages: Message[];
245
232
  }>;
246
233
  messagesMetadata: {};
247
- humanInTheLoop: InterruptData | null;
234
+ humanInTheLoop: HumanInTheLoopState | null;
248
235
  /**
249
236
  * @zh 发送消息到 LangGraph 后端。
250
237
  * @en Sends a message to the LangGraph backend.
@@ -267,6 +254,7 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
267
254
  */
268
255
  private processStreamChunk;
269
256
  private runFETool;
257
+ private responseHumanInTheLoop;
270
258
  private callFETool;
271
259
  extraParams: Record<string, any>;
272
260
  /**
@@ -277,8 +265,14 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
277
265
  /**
278
266
  * @zh 标记前端工具等待已完成。
279
267
  * @en Marks the frontend tool waiting as completed.
268
+ * @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
280
269
  */
281
270
  doneFEToolWaiting(id: string, result: CallToolResult): void;
271
+ /**
272
+ * @zh 标记人机交互等待已完成。
273
+ * @en Marks the human in the loop waiting as completed.
274
+ */
275
+ doneHumanInTheLoopWaiting(tool_id: string, action_request_id: string, result: HumanInTheLoopDecision): void;
282
276
  /**
283
277
  * @zh 获取当前的 Thread。
284
278
  * @en Gets the current Thread.
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "eventemitter3";
2
2
  import { ToolManager } from "./ToolManager.js";
3
+ import { createActionRequestID } from "./humanInTheLoop.js";
3
4
  import { MessageProcessor } from "./MessageProcessor.js";
4
5
  import { revertChatTo } from "./time-travel/index.js";
5
6
  import camelcaseKeys from "camelcase-keys";
@@ -281,9 +282,9 @@ export class LangGraphClient extends EventEmitter {
281
282
  }
282
283
  }
283
284
  const data = await this.runFETool();
285
+ await this.responseHumanInTheLoop();
284
286
  if (data)
285
287
  streamRecord.push(...data);
286
- this.humanInTheLoop = null;
287
288
  this._status = "idle";
288
289
  this.emit("done", {
289
290
  event: "done",
@@ -332,13 +333,34 @@ export class LangGraphClient extends EventEmitter {
332
333
  }
333
334
  else if (chunk.event === "values") {
334
335
  const data = chunk.data;
335
- if (data.__interrupt__) {
336
+ if (data?.__interrupt__) {
336
337
  this._status = "interrupted";
337
- this.humanInTheLoop = camelcaseKeys(data.__interrupt__, {
338
- deep: true,
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;
339
357
  });
358
+ this.humanInTheLoop = {
359
+ interruptData: humanInTheLoop,
360
+ result: {},
361
+ };
340
362
  }
341
- else if (data.messages) {
363
+ else if (data?.messages) {
342
364
  const isResume = !!command?.resume;
343
365
  const isLongerThanLocal = data.messages.length >= this.messageProcessor.getGraphMessages().length;
344
366
  // resume 情况下,长度低于前端 message 的统统不接受
@@ -375,16 +397,27 @@ export class LangGraphClient extends EventEmitter {
375
397
  });
376
398
  this._status = "interrupted";
377
399
  this.currentThread.status = "interrupted"; // 修复某些机制下,状态不为 interrupted 与后端有差异
400
+ console.log("batch call tools", result.length);
378
401
  return Promise.all(result);
379
402
  }
380
403
  }
404
+ async responseHumanInTheLoop() {
405
+ if (this.humanInTheLoop) {
406
+ const humanInTheLoop = this.humanInTheLoop;
407
+ this.humanInTheLoop = null;
408
+ return this.resume({
409
+ decisions: humanInTheLoop.interruptData[0].value.actionRequests.map((i) => {
410
+ return humanInTheLoop.result[i.id];
411
+ }),
412
+ });
413
+ }
414
+ }
381
415
  async callFETool(message, args) {
382
416
  const that = this; // 防止 this 被错误解析
383
417
  const result = await this.tools.callTool(message.name, args, { client: that, message });
384
418
  if (!result) {
385
419
  return;
386
420
  }
387
- return this.resume(result);
388
421
  }
389
422
  extraParams = {};
390
423
  /**
@@ -401,6 +434,7 @@ export class LangGraphClient extends EventEmitter {
401
434
  /**
402
435
  * @zh 标记前端工具等待已完成。
403
436
  * @en Marks the frontend tool waiting as completed.
437
+ * @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
404
438
  */
405
439
  doneFEToolWaiting(id, result) {
406
440
  const done = this.tools.doneWaiting(id, result);
@@ -408,6 +442,17 @@ export class LangGraphClient extends EventEmitter {
408
442
  this.resume(result);
409
443
  }
410
444
  }
445
+ /**
446
+ * @zh 标记人机交互等待已完成。
447
+ * @en Marks the human in the loop waiting as completed.
448
+ */
449
+ doneHumanInTheLoopWaiting(tool_id, action_request_id, result) {
450
+ // 移除等待状态
451
+ this.tools.doneWaiting(tool_id, result);
452
+ if (this.humanInTheLoop) {
453
+ this.humanInTheLoop.result[action_request_id] = result;
454
+ }
455
+ }
411
456
  /**
412
457
  * @zh 获取当前的 Thread。
413
458
  * @en Gets the current Thread.
@@ -62,11 +62,12 @@ export declare class ToolManager {
62
62
  * @en Marks the tool waiting with the specified ID as completed and passes the result.
63
63
  */
64
64
  doneWaiting(id: string, value: CallToolResult): boolean;
65
+ isAllToolCompleted(): number;
65
66
  /**
66
67
  * @zh 等待指定 ID 的工具完成。
67
68
  * @en Waits for the tool with the specified ID to complete.
68
69
  */
69
- waitForDone(id: string): Promise<unknown> | ((value: CallToolResult) => void) | undefined;
70
+ waitForDone(id: string): Promise<unknown>;
70
71
  /**
71
72
  * @zh 一个静态方法,用于在前端等待用户界面操作完成。
72
73
  * @en A static method used in the frontend to wait for user interface operations to complete.
@@ -74,5 +75,5 @@ export declare class ToolManager {
74
75
  static waitForUIDone<T>(_: T, context: {
75
76
  client: LangGraphClient;
76
77
  message: ToolMessage;
77
- }): Promise<unknown> | ((value: CallToolResult) => void) | undefined;
78
+ }): Promise<unknown>;
78
79
  }
@@ -92,14 +92,17 @@ export class ToolManager {
92
92
  return false;
93
93
  }
94
94
  }
95
+ isAllToolCompleted() {
96
+ return this.waitingMap.size;
97
+ }
95
98
  /**
96
99
  * @zh 等待指定 ID 的工具完成。
97
100
  * @en Waits for the tool with the specified ID to complete.
98
101
  */
99
102
  waitForDone(id) {
100
- if (this.waitingMap.has(id)) {
101
- return this.waitingMap.get(id);
102
- }
103
+ // if (this.waitingMap.has(id)) {
104
+ // return this.waitingMap.get(id);
105
+ // }
103
106
  const promise = new Promise((resolve, reject) => {
104
107
  this.waitingMap.set(id, resolve);
105
108
  });
@@ -0,0 +1,38 @@
1
+ export type HumanInTheLoopDecision = {
2
+ type: "approve" | "edit" | "reject" | "respond";
3
+ edited_action?: {
4
+ name: string;
5
+ args: Record<string, any>;
6
+ };
7
+ message?: string;
8
+ };
9
+ /**
10
+ * HumanInTheLoop 的标准回复格式
11
+ */
12
+ export type InterruptResponse = {
13
+ decisions: HumanInTheLoopDecision[];
14
+ };
15
+ /** 由于 langchain human in the loop 没有设计调用 id,所以我们需要给一个 id */
16
+ export declare const createActionRequestID: (j: {
17
+ name: string;
18
+ args: any;
19
+ }) => string;
20
+ export type HumanInTheLoopState = {
21
+ interruptData: InterruptData;
22
+ result: Record<string, HumanInTheLoopDecision>;
23
+ };
24
+ export type InterruptData = {
25
+ id: string;
26
+ value: {
27
+ actionRequests: {
28
+ id?: string;
29
+ name: string;
30
+ description: string;
31
+ args: any;
32
+ }[];
33
+ reviewConfigs: {
34
+ actionName: string;
35
+ allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
36
+ }[];
37
+ };
38
+ }[];
@@ -0,0 +1,4 @@
1
+ /** 由于 langchain human in the loop 没有设计调用 id,所以我们需要给一个 id */
2
+ export const createActionRequestID = (j) => {
3
+ return j.name + JSON.stringify(j.args);
4
+ };
@@ -1,6 +1,6 @@
1
1
  import { RenderMessage } from "../LangGraphClient.js";
2
2
  import { LangGraphClient } from "../LangGraphClient.js";
3
- import { InterruptResponse } from "./createTool.js";
3
+ import { HumanInTheLoopDecision } from "../humanInTheLoop.js";
4
4
  export type DeepPartial<T> = {
5
5
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
6
6
  };
@@ -8,16 +8,19 @@ export declare class ToolRenderData<I, D> {
8
8
  message: RenderMessage;
9
9
  client: LangGraphClient;
10
10
  constructor(message: RenderMessage, client: LangGraphClient);
11
+ private getToolActionRequestID;
11
12
  /**
12
13
  * 获取人机交互数据
13
14
  * 直接使用 reviewConfig 获取可以显示的按钮
14
15
  * actionRequest 获取当前工具的入参
15
16
  */
16
17
  getHumanInTheLoopData(): {
18
+ actionRequestIndex: number;
17
19
  config: {
18
20
  id: string;
19
21
  value: {
20
22
  actionRequests: {
23
+ id?: string;
21
24
  name: string;
22
25
  description: string;
23
26
  args: any;
@@ -33,14 +36,16 @@ export declare class ToolRenderData<I, D> {
33
36
  allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
34
37
  };
35
38
  actionRequest: {
39
+ id?: string;
36
40
  name: string;
37
41
  description: string;
38
42
  args: any;
39
43
  };
44
+ result: HumanInTheLoopDecision;
40
45
  } | null;
41
46
  /** 发送恢复状态的数据 */
42
- sendResumeData(response: InterruptResponse["decisions"][number]): void;
43
- get state(): "idle" | "interrupted" | "done" | "loading";
47
+ sendResumeData(response: HumanInTheLoopDecision): void;
48
+ get state(): "idle" | "done" | "loading";
44
49
  get input(): I | null;
45
50
  get output(): string;
46
51
  getJSONOutput(): D;
@@ -1,5 +1,6 @@
1
1
  import { getMessageContent } from "../ui-store/createChatStore.js";
2
2
  import { jsonrepair } from "jsonrepair";
3
+ import { createActionRequestID } from "../humanInTheLoop.js";
3
4
  export class ToolRenderData {
4
5
  message;
5
6
  client;
@@ -7,19 +8,33 @@ export class ToolRenderData {
7
8
  this.message = message;
8
9
  this.client = client;
9
10
  }
11
+ getToolActionRequestID() {
12
+ return createActionRequestID({
13
+ name: this.message.name,
14
+ args: this.getInputRepaired(),
15
+ });
16
+ }
10
17
  /**
11
18
  * 获取人机交互数据
12
19
  * 直接使用 reviewConfig 获取可以显示的按钮
13
20
  * actionRequest 获取当前工具的入参
14
21
  */
15
22
  getHumanInTheLoopData() {
16
- const configOfHumanInTheLoop = this.client.humanInTheLoop?.find((i) => i.value.reviewConfigs?.some((j) => j.actionName === this.message.name));
23
+ const toolActionRequestID = this.getToolActionRequestID();
24
+ if (!this.client.humanInTheLoop)
25
+ return null;
26
+ const configOfHumanInTheLoop = this.client.humanInTheLoop.interruptData.find((i) => i.value.actionRequests.some((j) => j.id === toolActionRequestID));
17
27
  if (!configOfHumanInTheLoop)
18
28
  return null;
29
+ const actionRequestIndex = configOfHumanInTheLoop.value.actionRequests.findIndex((j) => j.id === toolActionRequestID);
30
+ if (actionRequestIndex === -1)
31
+ return null;
19
32
  return {
33
+ actionRequestIndex: actionRequestIndex,
20
34
  config: configOfHumanInTheLoop,
21
- reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName === this.message.name),
22
- actionRequest: configOfHumanInTheLoop.value.actionRequests.find((j) => j.name === this.message.name),
35
+ reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName === configOfHumanInTheLoop.value.actionRequests[actionRequestIndex].name),
36
+ actionRequest: configOfHumanInTheLoop.value.actionRequests[actionRequestIndex],
37
+ result: this.client.humanInTheLoop.result[toolActionRequestID],
23
38
  };
24
39
  }
25
40
  /** 发送恢复状态的数据 */
@@ -28,14 +43,18 @@ export class ToolRenderData {
28
43
  /**@ts-ignore 修复 sb 的 langchain 官方的命名不统一,我们一致采用下划线版本,而非驼峰版本 */
29
44
  response.editedAction = response.edited_action;
30
45
  }
31
- return this.client.doneFEToolWaiting(this.message.id, { decisions: [response] });
46
+ return this.client.doneHumanInTheLoopWaiting(this.message.id, this.getToolActionRequestID(), response);
32
47
  }
33
48
  get state() {
34
49
  if (this.message.type === "tool" && this.message?.additional_kwargs?.done) {
35
50
  return "done";
36
51
  }
37
- if (this.client.status === "interrupted" && this.client.humanInTheLoop?.some((i) => i.value.reviewConfigs?.some((j) => j.actionName === this.message.name))) {
38
- return "interrupted";
52
+ const humanInTheLoopData = this.getHumanInTheLoopData();
53
+ if (humanInTheLoopData?.result) {
54
+ return "done";
55
+ }
56
+ if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
57
+ return "done";
39
58
  }
40
59
  if (this.message.tool_input) {
41
60
  return "loading";
@@ -2,6 +2,7 @@ import { z, ZodRawShape } from "zod";
2
2
  import { Action, Parameter } from "./copilotkit-actions.js";
3
3
  import { Message } from "@langchain/langgraph-sdk";
4
4
  import { ToolRenderData } from "./ToolUI.js";
5
+ import { HumanInTheLoopDecision, InterruptResponse as HumanInTheLoopResponse } from "../humanInTheLoop.js";
5
6
  export interface UnionTool<Args extends ZodRawShape, Child extends Object = Object, ResponseType = any> {
6
7
  name: string;
7
8
  description: string;
@@ -22,27 +23,10 @@ export interface UnionTool<Args extends ZodRawShape, Child extends Object = Obje
22
23
  isPureParams?: boolean;
23
24
  }
24
25
  export type ToolCallback<Args extends ZodRawShape> = (args: z.infer<z.ZodObject<Args>>, context?: any) => CallToolResult | Promise<CallToolResult>;
25
- /**
26
- * HumanInTheLoop 的标准回复格式
27
- */
28
- export type InterruptResponse = {
29
- decisions: ({
30
- type: "approve";
31
- } | {
32
- type: "edit";
33
- edited_action: {
34
- name: string;
35
- args: Record<string, any>;
36
- };
37
- } | {
38
- type: "reject";
39
- message?: string;
40
- })[];
41
- };
42
26
  export type CallToolResult = string | {
43
27
  type: "text";
44
28
  text: string;
45
- }[] | InterruptResponse;
29
+ }[] | HumanInTheLoopDecision | HumanInTheLoopResponse;
46
30
  /** 用于格式校验 */
47
31
  export declare const createTool: <Args extends ZodRawShape>(tool: UnionTool<Args>) => UnionTool<Args, Object, any>;
48
32
  /**
@@ -173,7 +157,7 @@ export declare const createMCPTool: <Args extends ZodRawShape>(tool: UnionTool<A
173
157
  }[];
174
158
  isError?: undefined;
175
159
  } | {
176
- content: InterruptResponse | {
160
+ content: HumanInTheLoopDecision | HumanInTheLoopResponse | {
177
161
  type: "text";
178
162
  text: string;
179
163
  }[] | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langgraph-js/sdk",
3
- "version": "3.8.3",
3
+ "version": "4.0.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",
@@ -2,6 +2,7 @@ import type { Thread, Message, Assistant, HumanMessage, AIMessage, ToolMessage,
2
2
  import { EventEmitter } from "eventemitter3";
3
3
  import { ToolManager } from "./ToolManager.js";
4
4
  import { CallToolResult } from "./tool/createTool.js";
5
+ import { createActionRequestID, HumanInTheLoopDecision, HumanInTheLoopState, InterruptData } from "./humanInTheLoop.js";
5
6
  import { type ILangGraphClient } from "@langgraph-js/pure-graph/dist/types.js";
6
7
  import { MessageProcessor } from "./MessageProcessor.js";
7
8
  import { revertChatTo, RevertChatToOptions } from "./time-travel/index.js";
@@ -45,20 +46,7 @@ export type SendMessageOptions = {
45
46
  command?: Command;
46
47
  joinRunId?: string;
47
48
  };
48
- export type InterruptData = {
49
- id: string;
50
- value: {
51
- actionRequests: {
52
- name: string;
53
- description: string;
54
- args: any;
55
- }[];
56
- reviewConfigs: {
57
- actionName: string;
58
- allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
59
- }[];
60
- };
61
- }[];
49
+
62
50
  export interface LangGraphClientConfig {
63
51
  apiUrl?: string;
64
52
  apiKey?: string;
@@ -337,7 +325,7 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
337
325
  return state;
338
326
  }
339
327
  public messagesMetadata = {};
340
- public humanInTheLoop: InterruptData | null = null;
328
+ public humanInTheLoop: HumanInTheLoopState | null = null;
341
329
  /**
342
330
  * @zh 发送消息到 LangGraph 后端。
343
331
  * @en Sends a message to the LangGraph backend.
@@ -411,8 +399,8 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
411
399
  }
412
400
  }
413
401
  const data = await this.runFETool();
402
+ await this.responseHumanInTheLoop();
414
403
  if (data) streamRecord.push(...data);
415
- this.humanInTheLoop = null;
416
404
  this._status = "idle";
417
405
  this.emit("done", {
418
406
  event: "done",
@@ -457,17 +445,40 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
457
445
  this.emit("message", chunk);
458
446
  return true;
459
447
  } else if (chunk.event === "values") {
460
- const data = chunk.data as {
461
- __interrupt__?: InterruptData;
462
- messages: Message[];
463
- };
448
+ const data = chunk.data as
449
+ | {
450
+ __interrupt__?: InterruptData;
451
+ messages: Message[];
452
+ }
453
+ | undefined;
464
454
 
465
- if (data.__interrupt__) {
455
+ if (data?.__interrupt__) {
466
456
  this._status = "interrupted";
467
- this.humanInTheLoop = camelcaseKeys(data.__interrupt__, {
468
- deep: true,
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;
469
470
  });
470
- } else if (data.messages) {
471
+ humanInTheLoop.forEach((i) => {
472
+ i.value.actionRequests.forEach((j) => {
473
+ j.id = j.id || createActionRequestID(j);
474
+ });
475
+ return i;
476
+ });
477
+ this.humanInTheLoop = {
478
+ interruptData: humanInTheLoop,
479
+ result: {},
480
+ };
481
+ } else if (data?.messages) {
471
482
  const isResume = !!command?.resume;
472
483
  const isLongerThanLocal = data.messages.length >= this.messageProcessor.getGraphMessages().length;
473
484
  // resume 情况下,长度低于前端 message 的统统不接受
@@ -503,16 +514,27 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
503
514
  });
504
515
  this._status = "interrupted";
505
516
  this.currentThread!.status = "interrupted"; // 修复某些机制下,状态不为 interrupted 与后端有差异
517
+ console.log("batch call tools", result.length);
506
518
  return Promise.all(result);
507
519
  }
508
520
  }
521
+ private async responseHumanInTheLoop() {
522
+ if (this.humanInTheLoop) {
523
+ const humanInTheLoop = this.humanInTheLoop;
524
+ this.humanInTheLoop = null;
525
+ return this.resume({
526
+ decisions: humanInTheLoop.interruptData[0].value.actionRequests.map((i) => {
527
+ return humanInTheLoop.result[i.id!] as HumanInTheLoopDecision;
528
+ }),
529
+ });
530
+ }
531
+ }
509
532
  private async callFETool(message: ToolMessage, args: any) {
510
533
  const that = this; // 防止 this 被错误解析
511
534
  const result = await this.tools.callTool(message.name!, args, { client: that, message });
512
535
  if (!result) {
513
536
  return;
514
537
  }
515
- return this.resume(result);
516
538
  }
517
539
  extraParams: Record<string, any> = {};
518
540
 
@@ -530,6 +552,7 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
530
552
  /**
531
553
  * @zh 标记前端工具等待已完成。
532
554
  * @en Marks the frontend tool waiting as completed.
555
+ * @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
533
556
  */
534
557
  doneFEToolWaiting(id: string, result: CallToolResult) {
535
558
  const done = this.tools.doneWaiting(id, result);
@@ -538,6 +561,18 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
538
561
  }
539
562
  }
540
563
 
564
+ /**
565
+ * @zh 标记人机交互等待已完成。
566
+ * @en Marks the human in the loop waiting as completed.
567
+ */
568
+ doneHumanInTheLoopWaiting(tool_id: string, action_request_id: string, result: HumanInTheLoopDecision) {
569
+ // 移除等待状态
570
+ this.tools.doneWaiting(tool_id, result);
571
+ if (this.humanInTheLoop) {
572
+ this.humanInTheLoop.result[action_request_id] = result;
573
+ }
574
+ }
575
+
541
576
  /**
542
577
  * @zh 获取当前的 Thread。
543
578
  * @en Gets the current Thread.
@@ -102,15 +102,18 @@ export class ToolManager {
102
102
  return false;
103
103
  }
104
104
  }
105
+ isAllToolCompleted() {
106
+ return this.waitingMap.size;
107
+ }
105
108
 
106
109
  /**
107
110
  * @zh 等待指定 ID 的工具完成。
108
111
  * @en Waits for the tool with the specified ID to complete.
109
112
  */
110
113
  waitForDone(id: string) {
111
- if (this.waitingMap.has(id)) {
112
- return this.waitingMap.get(id);
113
- }
114
+ // if (this.waitingMap.has(id)) {
115
+ // return this.waitingMap.get(id);
116
+ // }
114
117
  const promise = new Promise((resolve, reject) => {
115
118
  this.waitingMap.set(id, resolve);
116
119
  });
@@ -0,0 +1,40 @@
1
+ export type HumanInTheLoopDecision = {
2
+ type: "approve" | "edit" | "reject" | "respond";
3
+ edited_action?: {
4
+ name: string;
5
+ args: Record<string, any>;
6
+ };
7
+ message?: string;
8
+ };
9
+
10
+ /**
11
+ * HumanInTheLoop 的标准回复格式
12
+ */
13
+ export type InterruptResponse = {
14
+ decisions: HumanInTheLoopDecision[];
15
+ };
16
+ /** 由于 langchain human in the loop 没有设计调用 id,所以我们需要给一个 id */
17
+ export const createActionRequestID = (j: { name: string; args: any }) => {
18
+ return j.name + JSON.stringify(j.args);
19
+ };
20
+
21
+ export type HumanInTheLoopState = {
22
+ interruptData: InterruptData;
23
+ result: Record<string, HumanInTheLoopDecision>;
24
+ };
25
+
26
+ export type InterruptData = {
27
+ id: string;
28
+ value: {
29
+ actionRequests: {
30
+ id?: string;
31
+ name: string;
32
+ description: string;
33
+ args: any;
34
+ }[];
35
+ reviewConfigs: {
36
+ actionName: string;
37
+ allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
38
+ }[];
39
+ };
40
+ }[];
@@ -3,7 +3,7 @@ import { RenderMessage } from "../LangGraphClient.js";
3
3
  import { LangGraphClient } from "../LangGraphClient.js";
4
4
  import { getMessageContent } from "../ui-store/createChatStore.js";
5
5
  import { jsonrepair } from "jsonrepair";
6
- import { InterruptResponse } from "./createTool.js";
6
+ import { createActionRequestID, HumanInTheLoopDecision, InterruptResponse } from "../humanInTheLoop.js";
7
7
 
8
8
  export type DeepPartial<T> = {
9
9
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
@@ -13,34 +13,53 @@ export class ToolRenderData<I, D> {
13
13
  public message: RenderMessage,
14
14
  public client: LangGraphClient
15
15
  ) {}
16
+ private getToolActionRequestID() {
17
+ return createActionRequestID({
18
+ name: this.message.name!,
19
+ args: this.getInputRepaired(),
20
+ });
21
+ }
16
22
  /**
17
23
  * 获取人机交互数据
18
24
  * 直接使用 reviewConfig 获取可以显示的按钮
19
25
  * actionRequest 获取当前工具的入参
20
26
  */
21
27
  getHumanInTheLoopData() {
22
- const configOfHumanInTheLoop = this.client.humanInTheLoop?.find((i) => i.value.reviewConfigs?.some((j) => j.actionName === this.message.name));
28
+ const toolActionRequestID = this.getToolActionRequestID();
29
+ if (!this.client.humanInTheLoop) return null;
30
+ const configOfHumanInTheLoop = this.client.humanInTheLoop.interruptData.find((i) => i.value.actionRequests.some((j) => j.id === toolActionRequestID));
23
31
  if (!configOfHumanInTheLoop) return null;
32
+
33
+ const actionRequestIndex = configOfHumanInTheLoop.value.actionRequests.findIndex((j) => j.id === toolActionRequestID);
34
+ if (actionRequestIndex === -1) return null;
35
+
24
36
  return {
37
+ actionRequestIndex: actionRequestIndex,
25
38
  config: configOfHumanInTheLoop,
26
- reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName === this.message.name)!,
27
- actionRequest: configOfHumanInTheLoop.value.actionRequests.find((j) => j.name === this.message.name)!,
39
+ reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName === configOfHumanInTheLoop.value.actionRequests[actionRequestIndex].name)!,
40
+ actionRequest: configOfHumanInTheLoop.value.actionRequests[actionRequestIndex],
41
+ result: this.client.humanInTheLoop.result[toolActionRequestID],
28
42
  };
29
43
  }
30
44
  /** 发送恢复状态的数据 */
31
- sendResumeData(response: InterruptResponse["decisions"][number]) {
45
+ sendResumeData(response: HumanInTheLoopDecision) {
32
46
  if (response.type === "edit") {
33
47
  /**@ts-ignore 修复 sb 的 langchain 官方的命名不统一,我们一致采用下划线版本,而非驼峰版本 */
34
48
  response.editedAction = response.edited_action;
35
49
  }
36
- return this.client.doneFEToolWaiting(this.message.id!, { decisions: [response] });
50
+
51
+ return this.client.doneHumanInTheLoopWaiting(this.message.id!, this.getToolActionRequestID(), response);
37
52
  }
38
53
  get state() {
39
54
  if (this.message.type === "tool" && this.message?.additional_kwargs?.done) {
40
55
  return "done";
41
56
  }
42
- if (this.client.status === "interrupted" && this.client.humanInTheLoop?.some((i) => i.value.reviewConfigs?.some((j) => j.actionName === this.message.name))) {
43
- return "interrupted";
57
+ const humanInTheLoopData = this.getHumanInTheLoopData();
58
+ if (humanInTheLoopData?.result) {
59
+ return "done";
60
+ }
61
+ if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
62
+ return "done";
44
63
  }
45
64
  if (this.message.tool_input) {
46
65
  return "loading";
@@ -1,9 +1,10 @@
1
1
  import { actionParametersToJsonSchema, convertJsonSchemaToZodRawShape } from "./utils.js";
2
- import { z, ZodRawShape } from "zod";
2
+ import { uuidv4, z, ZodRawShape } from "zod";
3
3
  import { Action, Parameter } from "./copilotkit-actions.js";
4
4
  import { zodToJsonSchema } from "zod-to-json-schema";
5
5
  import { Message } from "@langchain/langgraph-sdk";
6
6
  import { ToolRenderData } from "./ToolUI.js";
7
+ import { HumanInTheLoopDecision, InterruptResponse as HumanInTheLoopResponse } from "../humanInTheLoop.js";
7
8
 
8
9
  export interface UnionTool<Args extends ZodRawShape, Child extends Object = Object, ResponseType = any> {
9
10
  name: string;
@@ -25,23 +26,8 @@ export interface UnionTool<Args extends ZodRawShape, Child extends Object = Obje
25
26
  isPureParams?: boolean;
26
27
  }
27
28
  export type ToolCallback<Args extends ZodRawShape> = (args: z.infer<z.ZodObject<Args>>, context?: any) => CallToolResult | Promise<CallToolResult>;
28
- /**
29
- * HumanInTheLoop 的标准回复格式
30
- */
31
- export type InterruptResponse = {
32
- decisions: (
33
- | { type: "approve" }
34
- | {
35
- type: "edit";
36
- edited_action: {
37
- name: string;
38
- args: Record<string, any>;
39
- };
40
- }
41
- | { type: "reject"; message?: string }
42
- )[];
43
- };
44
- export type CallToolResult = string | { type: "text"; text: string }[] | InterruptResponse;
29
+
30
+ export type CallToolResult = string | { type: "text"; text: string }[] | HumanInTheLoopDecision | HumanInTheLoopResponse;
45
31
 
46
32
  /** 用于格式校验 */
47
33
  export const createTool = <Args extends ZodRawShape>(tool: UnionTool<Args>) => {