File size: 3,079 Bytes
f59fbe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { randomUUID } from "node:crypto";

import type {
  ApprovalRequest,
  ApprovalResponse as CoreApprovalResponse,
  QuestionRequest,
  QuestionResult,
} from "@moonshot-ai/kimi-code-sdk";

import type { ApprovalResponse, QuestionRequest as LegacyQuestionRequest } from "../../shared/legacy-sdk";
import { describeToolDisplay, toLegacyDisplay } from "./tool-display";

export type ReverseRpcEvent =
  | { type: "ApprovalRequest"; payload: ReturnType<typeof approvalPayload> }
  | { type: "QuestionRequest"; payload: LegacyQuestionRequest };

export class ReverseRpcController {
  private readonly approvals = new Map<string, (response: CoreApprovalResponse) => void>();
  private readonly questions = new Map<string, (result: QuestionResult) => void>();

  constructor(private readonly emit: (event: ReverseRpcEvent) => void) {}

  requestApproval(request: ApprovalRequest): Promise<CoreApprovalResponse> {
    const id = randomUUID();
    return new Promise((resolve) => {
      this.approvals.set(id, resolve);
      this.emit({ type: "ApprovalRequest", payload: approvalPayload(id, request) });
    });
  }

  requestQuestion(request: QuestionRequest): Promise<QuestionResult> {
    const id = randomUUID();
    return new Promise((resolve) => {
      this.questions.set(id, resolve);
      this.emit({
        type: "QuestionRequest",
        payload: {
          id,
          tool_call_id: request.toolCallId ?? "",
          questions: request.questions.map((question) => ({
            question: question.question,
            header: question.header,
            options: question.options.map((option) => ({
              label: option.label,
              description: option.description,
            })),
            multi_select: question.multiSelect,
          })),
        },
      });
    });
  }

  respondApproval(id: string, response: ApprovalResponse): boolean {
    const resolve = this.approvals.get(id);
    if (!resolve) return false;
    this.approvals.delete(id);
    if (response === "approve_for_session") {
      resolve({ decision: "approved", scope: "session" });
    } else if (response === "approve") {
      resolve({ decision: "approved" });
    } else {
      resolve({ decision: "rejected" });
    }
    return true;
  }

  respondQuestion(id: string, answers: Record<string, string>): boolean {
    const resolve = this.questions.get(id);
    if (!resolve) return false;
    this.questions.delete(id);
    resolve({ answers });
    return true;
  }

  cancelAll(reason: string): void {
    for (const resolve of this.approvals.values()) {
      resolve({ decision: "cancelled", feedback: reason });
    }
    for (const resolve of this.questions.values()) {
      resolve(null);
    }
    this.approvals.clear();
    this.questions.clear();
  }
}

function approvalPayload(id: string, request: ApprovalRequest) {
  return {
    id,
    tool_call_id: request.toolCallId,
    sender: request.toolName,
    action: request.action,
    description: describeToolDisplay(request.display),
    display: toLegacyDisplay(request.display),
  };
}