@langgraph-js/sdk 3.8.4 → 4.0.1

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",
@@ -334,9 +335,30 @@ export class LangGraphClient extends EventEmitter {
334
335
  const data = chunk.data;
335
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
363
  else if (data?.messages) {
342
364
  const isResume = !!command?.resume;
@@ -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,13 +36,15 @@ 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;
47
+ sendResumeData(response: HumanInTheLoopDecision): void;
43
48
  get state(): "idle" | "interrupted" | "done" | "loading";
44
49
  get input(): I | null;
45
50
  get output(): string;
@@ -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,13 +43,17 @@ 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))) {
52
+ const humanInTheLoopData = this.getHumanInTheLoopData();
53
+ if (humanInTheLoopData?.result) {
54
+ return "done";
55
+ }
56
+ if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
38
57
  return "interrupted";
39
58
  }
40
59
  if (this.message.tool_input) {
@@ -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.4",
3
+ "version": "4.0.1",
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",
@@ -466,9 +454,30 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
466
454
 
467
455
  if (data?.__interrupt__) {
468
456
  this._status = "interrupted";
469
- this.humanInTheLoop = camelcaseKeys(data.__interrupt__, {
470
- 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;
470
+ });
471
+ humanInTheLoop.forEach((i) => {
472
+ i.value.actionRequests.forEach((j) => {
473
+ j.id = j.id || createActionRequestID(j);
474
+ });
475
+ return i;
471
476
  });
477
+ this.humanInTheLoop = {
478
+ interruptData: humanInTheLoop,
479
+ result: {},
480
+ };
472
481
  } else if (data?.messages) {
473
482
  const isResume = !!command?.resume;
474
483
  const isLongerThanLocal = data.messages.length >= this.messageProcessor.getGraphMessages().length;
@@ -505,16 +514,27 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
505
514
  });
506
515
  this._status = "interrupted";
507
516
  this.currentThread!.status = "interrupted"; // 修复某些机制下,状态不为 interrupted 与后端有差异
517
+ console.log("batch call tools", result.length);
508
518
  return Promise.all(result);
509
519
  }
510
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
+ }
511
532
  private async callFETool(message: ToolMessage, args: any) {
512
533
  const that = this; // 防止 this 被错误解析
513
534
  const result = await this.tools.callTool(message.name!, args, { client: that, message });
514
535
  if (!result) {
515
536
  return;
516
537
  }
517
- return this.resume(result);
518
538
  }
519
539
  extraParams: Record<string, any> = {};
520
540
 
@@ -532,6 +552,7 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
532
552
  /**
533
553
  * @zh 标记前端工具等待已完成。
534
554
  * @en Marks the frontend tool waiting as completed.
555
+ * @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
535
556
  */
536
557
  doneFEToolWaiting(id: string, result: CallToolResult) {
537
558
  const done = this.tools.doneWaiting(id, result);
@@ -540,6 +561,18 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
540
561
  }
541
562
  }
542
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
+
543
576
  /**
544
577
  * @zh 获取当前的 Thread。
545
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,33 +13,52 @@ 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))) {
57
+ const humanInTheLoopData = this.getHumanInTheLoopData();
58
+ if (humanInTheLoopData?.result) {
59
+ return "done";
60
+ }
61
+ if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
43
62
  return "interrupted";
44
63
  }
45
64
  if (this.message.tool_input) {
@@ -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>) => {