@muha-sdk/kimi-adapter 0.0.0-stage → 0.1.13

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.
@@ -0,0 +1,1355 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile, realpath } from "node:fs/promises";
4
+ import { isAbsolute } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ const defaultTimeoutMs = 60_000;
7
+ const wsBearerPrefix = "kimi-code.bearer.";
8
+ export class KimiProcess {
9
+ options;
10
+ context;
11
+ kind = "kimi";
12
+ route = "native";
13
+ #listeners = new Map();
14
+ #pendingAcks = new Map();
15
+ #workspaceIds = new Map();
16
+ #subscriptions = new Set();
17
+ #cursors = new Map();
18
+ #sessions = new Map();
19
+ #child;
20
+ #processGroupId;
21
+ #lines;
22
+ #origin;
23
+ #token;
24
+ #socket;
25
+ #socketReady = false;
26
+ #closePromise;
27
+ #reconnectPromise;
28
+ #incoming = Promise.resolve();
29
+ #closing = false;
30
+ #fatalLoss = false;
31
+ constructor(options, context) {
32
+ this.options = options;
33
+ this.context = context;
34
+ }
35
+ async initialize() {
36
+ const environment = { ...process.env };
37
+ for (const [name, value] of Object.entries(this.options.env ?? {})) {
38
+ if (value === undefined)
39
+ delete environment[name];
40
+ else
41
+ environment[name] = value;
42
+ }
43
+ let child;
44
+ try {
45
+ child = spawn("kimi", ["web", "--no-open", "--host", "127.0.0.1", "--port", "0"], {
46
+ detached: true,
47
+ env: environment,
48
+ shell: false,
49
+ stdio: ["pipe", "pipe", "pipe"],
50
+ });
51
+ }
52
+ catch (error) {
53
+ throw failure("initialize", "spawn", error);
54
+ }
55
+ this.#child = child;
56
+ if (Number.isSafeInteger(child.pid) && child.pid !== undefined && child.pid > 1) {
57
+ this.#processGroupId = child.pid;
58
+ }
59
+ child.stdin.end();
60
+ child.stderr.resume();
61
+ this.#lines = createInterface({ input: child.stdout });
62
+ const ready = new Promise((resolve, reject) => {
63
+ let observedReady = false;
64
+ this.#lines?.on("line", (line) => {
65
+ if (observedReady)
66
+ return;
67
+ const parsed = parseReadyLine(line);
68
+ if (!parsed)
69
+ return;
70
+ observedReady = true;
71
+ this.#origin = parsed.origin;
72
+ this.#token = parsed.token;
73
+ void this.#checkReadiness().then(resolve, reject);
74
+ });
75
+ child.once("error", (error) => reject(failure("initialize", "spawn", error)));
76
+ child.once("exit", (exitCode, signal) => {
77
+ const error = {
78
+ code: "HARNESS_ERROR",
79
+ message: observedReady ? "Kimi server exited" : "Kimi server exited before readiness",
80
+ harness: "kimi",
81
+ operation: observedReady ? "closeHarness" : "initialize",
82
+ command: "kimi",
83
+ stage: "ready",
84
+ exitCode,
85
+ signal,
86
+ };
87
+ if (observedReady && !this.#closePromise)
88
+ this.context.reportFatalError(error);
89
+ else
90
+ reject(error);
91
+ });
92
+ });
93
+ try {
94
+ await withTimeout(ready, this.options.startupTimeoutMs ?? defaultTimeoutMs, () => failure("initialize", "ready", new Error("startup timeout")));
95
+ }
96
+ catch (error) {
97
+ await this.#forceReclaim();
98
+ throw normalizeHarnessFailure(error, "initialize", "handshake");
99
+ }
100
+ }
101
+ async createSession(options) {
102
+ await this.#ensureSocketReady();
103
+ const model = options.model === undefined
104
+ ? undefined
105
+ : await this.validateModel(options.model, "createSession");
106
+ const workspaceId = await this.#ensureWorkspace(options.workspacePath);
107
+ const payload = await this.requestData("POST", "/api/v1/sessions", "createSession", {
108
+ workspace_id: workspaceId,
109
+ ...(model === undefined ? {} : { agent_config: { model } }),
110
+ });
111
+ const session = await validateSession(payload, options.workspacePath);
112
+ const nativeSessionId = session.id;
113
+ if (options.effort !== undefined) {
114
+ await this.validateEffort(options.effort, model, "createSession");
115
+ }
116
+ const permissionMode = await this.#applyApprovalPolicy(nativeSessionId, options.approvalPolicy, "createSession");
117
+ await this.#subscribe(nativeSessionId);
118
+ const result = new KimiSession(this, nativeSessionId, options.workspacePath, permissionMode, model, options.effort);
119
+ this.#registerSession(result);
120
+ return result;
121
+ }
122
+ async resumeSession(options) {
123
+ await this.#ensureSocketReady();
124
+ let payload;
125
+ try {
126
+ payload = await this.requestData("GET", `/api/v1/sessions/${encodeURIComponent(options.nativeSessionId)}`, "resumeSession");
127
+ }
128
+ catch (error) {
129
+ throw normalizeSessionLookupFailure(error);
130
+ }
131
+ const session = await validateSession(payload, options.workspacePath);
132
+ if (session.id !== options.nativeSessionId) {
133
+ throw protocolFailure("Kimi resumed a different Session");
134
+ }
135
+ const model = options.model === undefined
136
+ ? await this.#readSessionModel(options.nativeSessionId)
137
+ : await this.validateModel(options.model, "resumeSession");
138
+ if (options.effort !== undefined) {
139
+ await this.validateEffort(options.effort, model, "resumeSession");
140
+ }
141
+ const permissionMode = await this.#applyApprovalPolicy(session.id, options.approvalPolicy, "resumeSession");
142
+ await this.#subscribe(session.id);
143
+ const result = new KimiSession(this, session.id, options.workspacePath, permissionMode, model, options.effort);
144
+ this.#registerSession(result);
145
+ return result;
146
+ }
147
+ async listSessions(workspacePath) {
148
+ const workspaceId = await this.#ensureWorkspace(workspacePath);
149
+ const sessions = [];
150
+ const cursors = new Set();
151
+ let beforeId;
152
+ do {
153
+ const query = new URLSearchParams({ workspace_id: workspaceId, page_size: "100" });
154
+ if (beforeId !== undefined)
155
+ query.set("before_id", beforeId);
156
+ const page = asObjectProtocol(await this.requestData("GET", `/api/v1/sessions?${query}`, "listSessions"), "Kimi Session page");
157
+ if (!Array.isArray(page.items))
158
+ throw protocolFailure("Kimi Session page items must be an array");
159
+ for (const value of page.items) {
160
+ const info = await validateSession(value, workspacePath);
161
+ sessions.push({
162
+ nativeSessionId: info.id,
163
+ workspacePath,
164
+ ...(info.title === undefined ? {} : { title: info.title }),
165
+ ...(info.createdAt === undefined ? {} : { createdAt: info.createdAt }),
166
+ ...(info.updatedAt === undefined ? {} : { updatedAt: info.updatedAt }),
167
+ });
168
+ }
169
+ if (page.has_more !== true) {
170
+ if (page.has_more !== false)
171
+ throw protocolFailure("Kimi Session page has_more must be boolean");
172
+ break;
173
+ }
174
+ const last = page.items.at(-1);
175
+ if (last === undefined)
176
+ throw protocolFailure("Kimi Session page cannot advance its cursor");
177
+ beforeId = requireProtocolString(asObjectProtocol(last, "Kimi listed Session").id, "Kimi listed Session id");
178
+ if (cursors.has(beforeId))
179
+ throw protocolFailure("Kimi repeated a Session page cursor");
180
+ cursors.add(beforeId);
181
+ } while (true);
182
+ return sessions;
183
+ }
184
+ async startTurn(session, input) {
185
+ await this.#ensureSocketReady();
186
+ const content = await Promise.all(input.map(mapTurnInput));
187
+ const capture = new KimiTurnCapture(this, session);
188
+ const unsubscribe = this.#listen(session.nativeSessionId, (event) => capture.receive(event));
189
+ capture.attach(unsubscribe);
190
+ try {
191
+ const payload = asObjectProtocol(await this.requestData("POST", `/api/v1/sessions/${encodeURIComponent(session.nativeSessionId)}/prompts`, "startTurn", {
192
+ content,
193
+ permission_mode: session.permissionMode,
194
+ ...(session.model === undefined ? {} : { model: session.model }),
195
+ ...(session.effort === undefined ? {} : { thinking: session.effort }),
196
+ }), "Kimi Prompt result");
197
+ capture.setNativeTurnId(requireProtocolString(payload.prompt_id, "Kimi Prompt id"));
198
+ return capture;
199
+ }
200
+ catch (error) {
201
+ capture.dispose();
202
+ throw error;
203
+ }
204
+ }
205
+ async abortTurn(nativeSessionId, nativeTurnId) {
206
+ await this.requestData("POST", `/api/v1/sessions/${encodeURIComponent(nativeSessionId)}/prompts/${encodeURIComponent(nativeTurnId)}:abort`, "interruptTurn");
207
+ }
208
+ async #applyApprovalPolicy(nativeSessionId, policy, operation) {
209
+ const mode = policy === "autoApprove" ? "auto" : "manual";
210
+ await this.requestData("POST", `/api/v1/sessions/${encodeURIComponent(nativeSessionId)}/profile`, operation, { agent_config: { permission_mode: mode } });
211
+ return mode;
212
+ }
213
+ async validateModel(model, operation) {
214
+ const data = asObjectProtocol(await this.requestData("GET", "/api/v1/models", operation), "Kimi Model catalog");
215
+ if (!Array.isArray(data.items))
216
+ throw protocolFailure("Kimi Model catalog items must be an array");
217
+ for (const value of data.items) {
218
+ const item = asObjectProtocol(value, "Kimi Model catalog item");
219
+ if (requireProtocolString(item.model, "Kimi Model identifier") === model)
220
+ return model;
221
+ }
222
+ throw harnessCommandFailure(operation, `Kimi does not expose model: ${model}`, "model_not_found");
223
+ }
224
+ async validateEffort(effort, model, operation) {
225
+ if (model === undefined) {
226
+ throw harnessCommandFailure(operation, "Kimi cannot validate Effort without a resolved Model", "model_unresolved");
227
+ }
228
+ const data = asObjectProtocol(await this.requestData("GET", "/api/v1/models", operation), "Kimi Model catalog");
229
+ if (!Array.isArray(data.items))
230
+ throw protocolFailure("Kimi Model catalog items must be an array");
231
+ for (const value of data.items) {
232
+ const item = asObjectProtocol(value, "Kimi Model catalog item");
233
+ if (requireProtocolString(item.model, "Kimi Model identifier") !== model)
234
+ continue;
235
+ if (Array.isArray(item.support_efforts) &&
236
+ item.support_efforts.some((candidate) => candidate === effort))
237
+ return;
238
+ throw harnessCommandFailure(operation, `Kimi does not support Thinking Effort for model: ${model}`, "effort_not_supported");
239
+ }
240
+ throw harnessCommandFailure(operation, `Kimi does not expose model: ${model}`, "model_not_found");
241
+ }
242
+ async requestData(method, path, operation, body, acceptedCodes = [0]) {
243
+ const origin = this.#origin;
244
+ const token = this.#token;
245
+ if (!origin || !token)
246
+ throw harnessCommandFailure(operation, "Kimi server is not ready");
247
+ let response;
248
+ try {
249
+ response = await fetch(`${origin}${path}`, {
250
+ method,
251
+ headers: {
252
+ authorization: `Bearer ${token}`,
253
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
254
+ },
255
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
256
+ });
257
+ }
258
+ catch {
259
+ throw harnessCommandFailure(operation, `Kimi ${operation} request failed`);
260
+ }
261
+ let payload;
262
+ try {
263
+ payload = JSON.parse(await response.text());
264
+ }
265
+ catch {
266
+ throw protocolFailure(`Kimi ${operation} response is not valid JSON`);
267
+ }
268
+ await this.context.recordNativeEvent("kimi", payload);
269
+ const envelope = asObjectProtocol(payload, `Kimi ${operation} response`);
270
+ if (!response.ok || typeof envelope.code !== "number" || !acceptedCodes.includes(envelope.code)) {
271
+ throw harnessCommandFailure(operation, `Kimi rejected ${operation}`, `http_${response.status}`);
272
+ }
273
+ return envelope.data;
274
+ }
275
+ async respondToApproval(nativeSessionId, nativeRequestId, decision) {
276
+ await this.#ensureSocketReady();
277
+ const data = asObjectProtocol(await this.requestData("POST", `/api/v1/sessions/${encodeURIComponent(nativeSessionId)}/approvals/${encodeURIComponent(nativeRequestId)}`, "respondToApproval", { decision: decision === "allowOnce" ? "approved" : "rejected" }), "Kimi Approval response");
278
+ if (data.resolved !== true)
279
+ throw protocolFailure("Kimi did not resolve the Approval Request");
280
+ }
281
+ async respondToQuestion(nativeSessionId, nativeRequestId, response) {
282
+ await this.#ensureSocketReady();
283
+ const path = `/api/v1/sessions/${encodeURIComponent(nativeSessionId)}/questions/${encodeURIComponent(nativeRequestId)}`;
284
+ const data = asObjectProtocol(await this.requestData("POST", response.action === "dismiss" ? `${path}:dismiss` : path, "respondToQuestion", response.action === "answer" ? { answers: response.answers } : undefined, response.action === "dismiss" ? [0, 40909] : [0]), "Kimi Question response");
285
+ if (response.action === "dismiss") {
286
+ if (data.dismissed !== true)
287
+ throw protocolFailure("Kimi did not dismiss the Question Request");
288
+ }
289
+ else if (data.resolved !== true) {
290
+ throw protocolFailure("Kimi did not resolve the Question Request");
291
+ }
292
+ }
293
+ async #checkReadiness() {
294
+ const health = asObjectProtocol(await this.requestData("GET", "/api/v1/healthz", "initialize"), "Kimi health data");
295
+ if (health.ok !== true)
296
+ throw failure("initialize", "handshake", new Error("unhealthy server"));
297
+ await this.#connectSocket();
298
+ }
299
+ async #connectSocket() {
300
+ const origin = this.#origin;
301
+ const token = this.#token;
302
+ if (!origin || !token)
303
+ throw failure("initialize", "handshake", new Error("missing server credential"));
304
+ const url = new URL("/api/v1/ws", origin);
305
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
306
+ let socket;
307
+ try {
308
+ socket = new WebSocket(url, [`${wsBearerPrefix}${token}`]);
309
+ }
310
+ catch {
311
+ throw failure("initialize", "handshake", new Error("WebSocket construction failed"));
312
+ }
313
+ this.#socket = socket;
314
+ let resolveHello;
315
+ let rejectHello;
316
+ const hello = new Promise((resolve, reject) => {
317
+ resolveHello = resolve;
318
+ rejectHello = reject;
319
+ });
320
+ socket.addEventListener("message", (message) => {
321
+ this.#incoming = this.#incoming.then(async () => {
322
+ const frame = asObjectProtocol(JSON.parse(messageDataToString(message.data)), "Kimi WebSocket frame");
323
+ await this.context.recordNativeEvent("kimi", frame);
324
+ if (frame.type === "server_hello") {
325
+ const payload = asObjectProtocol(frame.payload, "Kimi server hello");
326
+ if (payload.protocol_version !== 2)
327
+ throw new Error("Kimi WebSocket protocol is not v2");
328
+ socket.send(JSON.stringify({
329
+ type: "client_hello",
330
+ id: "muha-ready",
331
+ payload: { client_id: `muha-sdk-${randomUUID()}` },
332
+ }));
333
+ return;
334
+ }
335
+ if (frame.type === "ack" && frame.id === "muha-ready") {
336
+ if (frame.code !== 0)
337
+ throw new Error("Kimi WebSocket client hello was rejected");
338
+ this.#socketReady = true;
339
+ resolveHello();
340
+ return;
341
+ }
342
+ if (frame.type === "ack" && typeof frame.id === "string") {
343
+ const pending = this.#pendingAcks.get(frame.id);
344
+ if (!pending)
345
+ return;
346
+ this.#pendingAcks.delete(frame.id);
347
+ if (frame.code === 0)
348
+ pending.resolve(frame);
349
+ else
350
+ pending.reject(protocolFailure("Kimi WebSocket control frame was rejected"));
351
+ return;
352
+ }
353
+ if (frame.type === "ping") {
354
+ const payload = asObjectProtocol(frame.payload, "Kimi ping");
355
+ socket.send(JSON.stringify({
356
+ type: "pong",
357
+ payload: { nonce: requireProtocolString(payload.nonce, "Kimi ping nonce") },
358
+ }));
359
+ return;
360
+ }
361
+ if (frame.type === "resync_required") {
362
+ this.#receiveResyncRequired(frame);
363
+ return;
364
+ }
365
+ if (frame.type === "error") {
366
+ const payload = asObjectProtocol(frame.payload, "Kimi WebSocket error");
367
+ if (payload.fatal === true) {
368
+ this.#declarePermanentLoss("Kimi WebSocket reported a fatal error");
369
+ }
370
+ return;
371
+ }
372
+ if (typeof frame.session_id === "string" && typeof frame.type === "string") {
373
+ this.#routeSessionEvent(frame);
374
+ }
375
+ }).catch((error) => {
376
+ rejectHello(error);
377
+ this.#declarePermanentLoss(isErrorWithMessage(error) ? error.message : "Kimi WebSocket frame failed");
378
+ });
379
+ });
380
+ socket.addEventListener("error", () => rejectHello(new Error("Kimi WebSocket connection failed")));
381
+ socket.addEventListener("close", () => {
382
+ if (socket !== this.#socket)
383
+ return;
384
+ if (!this.#socketReady)
385
+ rejectHello(new Error("Kimi WebSocket closed before readiness"));
386
+ this.#rejectPendingAcks(protocolFailure("Kimi WebSocket connection closed"));
387
+ this.#socketReady = false;
388
+ this.#socket = undefined;
389
+ if (!this.#closing && !this.#fatalLoss)
390
+ void this.#recoverSocket().catch(() => undefined);
391
+ });
392
+ await hello;
393
+ }
394
+ async #ensureWorkspace(workspacePath) {
395
+ const existing = this.#workspaceIds.get(workspacePath);
396
+ if (existing)
397
+ return existing;
398
+ const payload = asObjectProtocol(await this.requestData("POST", "/api/v1/workspaces", "createSession", { root: workspacePath }), "Kimi Workspace");
399
+ const id = requireProtocolString(payload.id, "Kimi Workspace id");
400
+ this.#workspaceIds.set(workspacePath, id);
401
+ return id;
402
+ }
403
+ async #readSessionModel(nativeSessionId) {
404
+ const status = asObjectProtocol(await this.requestData("GET", `/api/v1/sessions/${encodeURIComponent(nativeSessionId)}/status`, "resumeSession"), "Kimi Session status");
405
+ if (status.model === undefined)
406
+ return undefined;
407
+ return requireProtocolString(status.model, "Kimi Session model");
408
+ }
409
+ async #subscribe(nativeSessionId, recover = true) {
410
+ if (!this.#socketReady || !this.#socket) {
411
+ if (recover)
412
+ await this.#ensureSocketReady();
413
+ if (!this.#socketReady || !this.#socket)
414
+ throw protocolFailure("Kimi WebSocket is not ready");
415
+ }
416
+ const id = `subscribe-${randomUUID()}`;
417
+ const ack = new Promise((resolve, reject) => this.#pendingAcks.set(id, { resolve, reject }));
418
+ const cursor = this.#cursors.get(nativeSessionId);
419
+ let frame;
420
+ try {
421
+ this.#socket.send(JSON.stringify({
422
+ type: "subscribe",
423
+ id,
424
+ payload: {
425
+ session_ids: [nativeSessionId],
426
+ ...(cursor === undefined ? {} : { cursors: { [nativeSessionId]: cursor } }),
427
+ },
428
+ }));
429
+ frame = await ack;
430
+ }
431
+ finally {
432
+ this.#pendingAcks.delete(id);
433
+ }
434
+ const payload = asObjectProtocol(frame.payload, "Kimi subscribe acknowledgement");
435
+ if (!Array.isArray(payload.accepted) || !payload.accepted.includes(nativeSessionId)) {
436
+ throw protocolFailure("Kimi did not accept the Session subscription");
437
+ }
438
+ if (!Array.isArray(payload.resync_required)) {
439
+ throw protocolFailure("Kimi subscribe resync_required must be an array");
440
+ }
441
+ const serverCursor = readServerCursor(payload.cursors, nativeSessionId);
442
+ if (payload.resync_required.includes(nativeSessionId)) {
443
+ this.#handleResync(nativeSessionId, serverCursor, "Kimi subscription requires resync");
444
+ }
445
+ else if (cursor === undefined && serverCursor !== undefined) {
446
+ this.#cursors.set(nativeSessionId, serverCursor);
447
+ }
448
+ this.#subscriptions.add(nativeSessionId);
449
+ }
450
+ async #ensureSocketReady() {
451
+ if (this.#fatalLoss)
452
+ throw protocolFailure("Kimi WebSocket control channel was permanently lost");
453
+ if (this.#socketReady)
454
+ return;
455
+ await this.#recoverSocket();
456
+ if (!this.#socketReady)
457
+ throw protocolFailure("Kimi WebSocket is not ready");
458
+ }
459
+ #recoverSocket() {
460
+ if (this.#reconnectPromise)
461
+ return this.#reconnectPromise;
462
+ const recovery = withTimeout((async () => {
463
+ await this.#connectSocket();
464
+ for (const nativeSessionId of this.#subscriptions)
465
+ await this.#subscribe(nativeSessionId, false);
466
+ })(), this.options.startupTimeoutMs ?? defaultTimeoutMs, () => protocolFailure("Kimi WebSocket recovery timed out")).catch((error) => {
467
+ this.#declarePermanentLoss(isErrorWithMessage(error) ? error.message : "Kimi WebSocket recovery failed");
468
+ throw error;
469
+ }).finally(() => {
470
+ if (this.#reconnectPromise === recovery)
471
+ this.#reconnectPromise = undefined;
472
+ });
473
+ this.#reconnectPromise = recovery;
474
+ return recovery;
475
+ }
476
+ #routeSessionEvent(frame) {
477
+ const nativeSessionId = requireProtocolString(frame.session_id, "Kimi event Session id");
478
+ const seq = requireSequence(frame.seq, "Kimi event seq");
479
+ const epoch = optionalProtocolString(frame.epoch, "Kimi event epoch");
480
+ if (frame.volatile === true) {
481
+ for (const listener of [...(this.#listeners.get(nativeSessionId) ?? [])])
482
+ listener(frame);
483
+ return;
484
+ }
485
+ if (frame.volatile !== undefined && frame.volatile !== false) {
486
+ throw new Error("Kimi event volatile flag must be boolean");
487
+ }
488
+ const cursor = this.#cursors.get(nativeSessionId);
489
+ if (cursor !== undefined) {
490
+ if (cursor.epoch !== undefined && epoch !== undefined && cursor.epoch !== epoch) {
491
+ this.#handleResync(nativeSessionId, { seq, ...(epoch === undefined ? {} : { epoch }) }, "Kimi event epoch changed");
492
+ return;
493
+ }
494
+ if (seq <= cursor.seq)
495
+ return;
496
+ if (seq !== cursor.seq + 1) {
497
+ this.#handleResync(nativeSessionId, { seq, ...(epoch === undefined ? {} : { epoch }) }, "Kimi event sequence has a gap");
498
+ return;
499
+ }
500
+ }
501
+ this.#cursors.set(nativeSessionId, { seq, ...(epoch === undefined ? {} : { epoch }) });
502
+ for (const listener of [...(this.#listeners.get(nativeSessionId) ?? [])])
503
+ listener(frame);
504
+ }
505
+ #receiveResyncRequired(frame) {
506
+ const payload = asObjectProtocol(frame.payload, "Kimi resync_required payload");
507
+ const nativeSessionId = requireProtocolString(payload.session_id, "Kimi resync Session id");
508
+ const seq = requireSequence(payload.current_seq, "Kimi resync current seq");
509
+ const epoch = optionalProtocolString(payload.epoch, "Kimi resync epoch");
510
+ this.#handleResync(nativeSessionId, { seq, ...(epoch === undefined ? {} : { epoch }) }, `Kimi Session requires resync: ${requireProtocolString(payload.reason, "Kimi resync reason")}`);
511
+ }
512
+ #handleResync(nativeSessionId, cursor, message) {
513
+ if ((this.#listeners.get(nativeSessionId)?.size ?? 0) > 0) {
514
+ this.#failSession(nativeSessionId, message);
515
+ }
516
+ if (cursor !== undefined)
517
+ this.#cursors.set(nativeSessionId, cursor);
518
+ }
519
+ #registerSession(session) {
520
+ let sessions = this.#sessions.get(session.nativeSessionId);
521
+ if (!sessions) {
522
+ sessions = new Set();
523
+ this.#sessions.set(session.nativeSessionId, sessions);
524
+ }
525
+ sessions.add(session);
526
+ }
527
+ releaseSession(session) {
528
+ const sessions = this.#sessions.get(session.nativeSessionId);
529
+ sessions?.delete(session);
530
+ if (sessions?.size === 0)
531
+ this.#sessions.delete(session.nativeSessionId);
532
+ }
533
+ #failSession(nativeSessionId, message) {
534
+ for (const session of this.#sessions.get(nativeSessionId) ?? [])
535
+ session.closeAfterStreamFailure();
536
+ for (const listener of [...(this.#listeners.get(nativeSessionId) ?? [])]) {
537
+ listener({ type: "adapter.protocolError", message });
538
+ }
539
+ }
540
+ #declarePermanentLoss(message) {
541
+ if (this.#fatalLoss || this.#closing)
542
+ return;
543
+ this.#fatalLoss = true;
544
+ this.context.reportFatalError({
545
+ code: "HARNESS_ERROR",
546
+ message,
547
+ harness: "kimi",
548
+ operation: "closeHarness",
549
+ command: "kimi",
550
+ stage: "ready",
551
+ });
552
+ this.#socket?.close();
553
+ }
554
+ #listen(nativeSessionId, listener) {
555
+ let listeners = this.#listeners.get(nativeSessionId);
556
+ if (!listeners) {
557
+ listeners = new Set();
558
+ this.#listeners.set(nativeSessionId, listeners);
559
+ }
560
+ listeners.add(listener);
561
+ return () => {
562
+ listeners?.delete(listener);
563
+ if (listeners?.size === 0)
564
+ this.#listeners.delete(nativeSessionId);
565
+ };
566
+ }
567
+ #rejectPendingAcks(error) {
568
+ for (const pending of this.#pendingAcks.values())
569
+ pending.reject(error);
570
+ this.#pendingAcks.clear();
571
+ }
572
+ close() {
573
+ this.#closePromise ??= this.#performClose();
574
+ return this.#closePromise;
575
+ }
576
+ async #performClose() {
577
+ this.#closing = true;
578
+ this.#listeners.clear();
579
+ this.#rejectPendingAcks(protocolFailure("Kimi Adapter is closing"));
580
+ this.#socket?.close();
581
+ this.#socket = undefined;
582
+ this.#socketReady = false;
583
+ const child = this.#child;
584
+ if (!child)
585
+ return;
586
+ const pid = this.#processGroupId;
587
+ if (hasExited(child)) {
588
+ if (pid !== undefined)
589
+ signalProcessGroup(pid, "SIGKILL");
590
+ return;
591
+ }
592
+ if (pid === undefined)
593
+ throw failure("closeHarness", "shutdown", new Error("invalid child process id"));
594
+ signalProcessGroup(pid, "SIGTERM");
595
+ if (await exitsWithin(child, this.options.shutdownTimeoutMs ?? defaultTimeoutMs)) {
596
+ signalProcessGroup(pid, "SIGKILL");
597
+ return;
598
+ }
599
+ await this.#forceReclaim();
600
+ }
601
+ async #forceReclaim() {
602
+ const child = this.#child;
603
+ if (!child)
604
+ return;
605
+ const pid = this.#processGroupId;
606
+ if (pid !== undefined)
607
+ signalProcessGroup(pid, "SIGKILL");
608
+ if (hasExited(child))
609
+ return;
610
+ if (pid === undefined)
611
+ throw failure("closeHarness", "shutdown", new Error("invalid child process id"));
612
+ if (!(await exitsWithin(child, 5_000))) {
613
+ throw failure("closeHarness", "shutdown", new Error("forced process reclamation timed out"));
614
+ }
615
+ }
616
+ }
617
+ class KimiSession {
618
+ process;
619
+ nativeSessionId;
620
+ workspacePath;
621
+ permissionMode;
622
+ #closed = false;
623
+ #model;
624
+ #effort;
625
+ constructor(process, nativeSessionId, workspacePath, permissionMode, model, effort = undefined) {
626
+ this.process = process;
627
+ this.nativeSessionId = nativeSessionId;
628
+ this.workspacePath = workspacePath;
629
+ this.permissionMode = permissionMode;
630
+ this.#model = model;
631
+ this.#effort = effort;
632
+ }
633
+ get model() { return this.#model; }
634
+ get effort() { return this.#effort; }
635
+ get closed() { return this.#closed; }
636
+ startTurn(input) {
637
+ return this.process.startTurn(this, input);
638
+ }
639
+ async setModel(model) {
640
+ this.#model = await this.process.validateModel(model, "setModel");
641
+ this.#effort = undefined;
642
+ }
643
+ async setEffort(effort) {
644
+ await this.process.validateEffort(effort, this.#model, "setEffort");
645
+ this.#effort = effort;
646
+ }
647
+ close() {
648
+ this.process.releaseSession(this);
649
+ this.#closed = true;
650
+ return Promise.resolve();
651
+ }
652
+ closeAfterStreamFailure() {
653
+ this.#closed = true;
654
+ }
655
+ }
656
+ class KimiTurnCapture {
657
+ process;
658
+ session;
659
+ #nativeTurnId = randomUUID();
660
+ #queue = new AsyncQueue();
661
+ #unsubscribe;
662
+ #started = false;
663
+ #assistantStarted = false;
664
+ #assistantText = "";
665
+ #reasoningLength = 0;
666
+ #stepAssistantLength = 0;
667
+ #stepReasoningLength = 0;
668
+ #tools = new Map();
669
+ #questions = new Map();
670
+ #locallyResolvedApprovals = new Set();
671
+ #locallyResolvedQuestions = new Set();
672
+ #usage = { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0 };
673
+ #disposed = false;
674
+ constructor(process, session) {
675
+ this.process = process;
676
+ this.session = session;
677
+ }
678
+ get nativeTurnId() { return this.#nativeTurnId; }
679
+ setNativeTurnId(value) { this.#nativeTurnId = value; }
680
+ attach(unsubscribe) { this.#unsubscribe = unsubscribe; }
681
+ receive(envelope) {
682
+ if (this.#disposed)
683
+ return;
684
+ try {
685
+ if (envelope.type === "adapter.protocolError") {
686
+ this.session.closeAfterStreamFailure();
687
+ void this.process.abortTurn(this.session.nativeSessionId, this.#nativeTurnId).catch(() => undefined);
688
+ this.#push({
689
+ type: "adapter.protocolError",
690
+ message: requireProtocolString(envelope.message, "Kimi protocol error message"),
691
+ });
692
+ this.dispose();
693
+ return;
694
+ }
695
+ const payload = asObjectProtocol(envelope.payload, "Kimi event payload");
696
+ if (payload.agentId !== undefined) {
697
+ if (typeof payload.agentId !== "string") {
698
+ throw new Error("Kimi event agent id must be a string");
699
+ }
700
+ if (payload.agentId !== "main")
701
+ return;
702
+ }
703
+ if (envelope.type === "turn.started") {
704
+ if (this.#started)
705
+ throw new Error("Kimi started the Turn more than once");
706
+ this.#started = true;
707
+ this.#push({ type: "turn.started" });
708
+ this.#startAssistant(payload);
709
+ return;
710
+ }
711
+ if (envelope.type === "turn.step.started") {
712
+ if (!this.#started)
713
+ throw new Error("Kimi step started before the Turn");
714
+ this.#stepAssistantLength = 0;
715
+ this.#stepReasoningLength = 0;
716
+ return;
717
+ }
718
+ if (envelope.type === "assistant.delta") {
719
+ if (!this.#started)
720
+ throw new Error("Kimi Assistant delta preceded the Turn");
721
+ this.#startAssistant(payload);
722
+ const delta = requireProtocolString(payload.delta, "Kimi Assistant delta", true);
723
+ const suffix = this.#alignStepDelta(envelope, delta, this.#stepAssistantLength);
724
+ if (suffix.length === 0)
725
+ return;
726
+ this.#assistantText += suffix;
727
+ this.#stepAssistantLength += suffix.length;
728
+ this.#push({
729
+ type: "assistant.message.delta",
730
+ nativeMessageId: this.#messageId(payload),
731
+ delta: suffix,
732
+ });
733
+ return;
734
+ }
735
+ if (envelope.type === "thinking.delta") {
736
+ if (!this.#started)
737
+ throw new Error("Kimi reasoning delta preceded the Turn");
738
+ this.#startAssistant(payload);
739
+ const delta = requireProtocolString(payload.delta, "Kimi reasoning delta", true);
740
+ const suffix = this.#alignStepDelta(envelope, delta, this.#stepReasoningLength);
741
+ if (suffix.length === 0)
742
+ return;
743
+ this.#reasoningLength += suffix.length;
744
+ this.#stepReasoningLength += suffix.length;
745
+ this.#push({
746
+ type: "assistant.reasoning.delta",
747
+ nativeMessageId: this.#messageId(payload),
748
+ delta: suffix,
749
+ });
750
+ return;
751
+ }
752
+ if (envelope.type === "permission.approval.requested") {
753
+ if (!this.#started)
754
+ throw new Error("Kimi Tool approval preceded the Turn");
755
+ const nativeToolCallId = requireProtocolString(payload.toolCallId, "Kimi Tool Call id");
756
+ const toolName = requireProtocolString(payload.toolName, "Kimi Tool name");
757
+ if (!this.#tools.has(nativeToolCallId)) {
758
+ this.#tools.set(nativeToolCallId, { completed: false, provisional: true });
759
+ this.#push({
760
+ type: "tool.started",
761
+ nativeToolCallId,
762
+ toolName,
763
+ input: payload.toolInput ?? null,
764
+ });
765
+ }
766
+ return;
767
+ }
768
+ if (envelope.type === "tool.call.started") {
769
+ if (!this.#started)
770
+ throw new Error("Kimi Tool Call preceded the Turn");
771
+ const nativeToolCallId = requireProtocolString(payload.toolCallId, "Kimi Tool Call id");
772
+ const existingTool = this.#tools.get(nativeToolCallId);
773
+ if (existingTool) {
774
+ if (!existingTool.provisional)
775
+ throw new Error("Kimi started a Tool Call more than once");
776
+ existingTool.provisional = false;
777
+ return;
778
+ }
779
+ this.#tools.set(nativeToolCallId, { completed: false, provisional: false });
780
+ this.#push({
781
+ type: "tool.started",
782
+ nativeToolCallId,
783
+ toolName: requireProtocolString(payload.name, "Kimi Tool name"),
784
+ input: payload.args ?? null,
785
+ });
786
+ return;
787
+ }
788
+ if (envelope.type === "tool.progress") {
789
+ const nativeToolCallId = requireProtocolString(payload.toolCallId, "Kimi Tool Call id");
790
+ const tool = this.#tools.get(nativeToolCallId);
791
+ if (!tool || tool.completed)
792
+ throw new Error("Kimi updated an inactive Tool Call");
793
+ this.#push({ type: "tool.updated", nativeToolCallId, update: payload.update ?? null });
794
+ return;
795
+ }
796
+ if (envelope.type === "tool.result") {
797
+ const nativeToolCallId = requireProtocolString(payload.toolCallId, "Kimi Tool Call id");
798
+ const tool = this.#tools.get(nativeToolCallId);
799
+ if (!tool || tool.completed)
800
+ throw new Error("Kimi completed an inactive Tool Call");
801
+ if (payload.isError !== undefined && typeof payload.isError !== "boolean") {
802
+ throw new Error("Kimi Tool result isError must be boolean");
803
+ }
804
+ tool.completed = true;
805
+ this.#push({
806
+ type: "tool.completed",
807
+ nativeToolCallId,
808
+ output: payload.output ?? null,
809
+ isError: payload.isError === true,
810
+ });
811
+ return;
812
+ }
813
+ if (envelope.type === "event.approval.requested") {
814
+ const nativeRequestId = requireProtocolString(payload.approval_id, "Kimi Approval id");
815
+ const toolName = requireProtocolString(payload.tool_name, "Kimi Approval Tool name");
816
+ const action = requireProtocolString(payload.action, "Kimi Approval action");
817
+ const nativeToolCallId = payload.tool_call_id === undefined
818
+ ? undefined
819
+ : requireProtocolString(payload.tool_call_id, "Kimi Approval Tool Call id");
820
+ this.#push({
821
+ type: "approval.requested",
822
+ nativeRequestId,
823
+ title: `Kimi requests ${toolName} approval`,
824
+ description: action,
825
+ ...(nativeToolCallId === undefined ? {} : { nativeToolCallId }),
826
+ details: {
827
+ action,
828
+ toolInput: payload.tool_input_display ?? null,
829
+ ...(payload.expires_at === undefined ? {} : { expiresAt: payload.expires_at }),
830
+ },
831
+ });
832
+ return;
833
+ }
834
+ if (envelope.type === "event.approval.resolved") {
835
+ const nativeRequestId = requireProtocolString(payload.approval_id, "Kimi Approval id");
836
+ if (!this.#locallyResolvedApprovals.has(nativeRequestId)) {
837
+ this.#push({ type: "approval.invalidated", nativeRequestId });
838
+ }
839
+ return;
840
+ }
841
+ if (envelope.type === "event.question.requested") {
842
+ const nativeRequestId = requireProtocolString(payload.question_id, "Kimi Question id");
843
+ if (!Array.isArray(payload.questions) || payload.questions.length === 0) {
844
+ throw new Error("Kimi Question Request has no items");
845
+ }
846
+ const questions = payload.questions.map(mapKimiQuestion);
847
+ if (new Set(questions.map(({ id }) => id)).size !== questions.length) {
848
+ throw new Error("Kimi Question item ids must be unique");
849
+ }
850
+ this.#questions.set(nativeRequestId, questions);
851
+ const nativeToolCallId = payload.tool_call_id === undefined
852
+ ? undefined
853
+ : requireProtocolString(payload.tool_call_id, "Kimi Question Tool Call id");
854
+ this.#push({
855
+ type: "question.requested",
856
+ nativeRequestId,
857
+ questions: questions.map(({ item }) => item),
858
+ ...(nativeToolCallId === undefined ? {} : { nativeToolCallId }),
859
+ });
860
+ return;
861
+ }
862
+ if (envelope.type === "event.question.answered" ||
863
+ envelope.type === "event.question.dismissed") {
864
+ const nativeRequestId = requireProtocolString(payload.question_id, "Kimi Question id");
865
+ if (!this.#locallyResolvedQuestions.has(nativeRequestId)) {
866
+ this.#push({
867
+ type: envelope.type === "event.question.dismissed" ? "question.dismissed" : "question.invalidated",
868
+ nativeRequestId,
869
+ });
870
+ }
871
+ return;
872
+ }
873
+ if (envelope.type === "turn.step.completed" && payload.usage !== undefined) {
874
+ const usage = asObjectProtocol(payload.usage, "Kimi Turn usage");
875
+ this.#usage = {
876
+ inputTokens: this.#usage.inputTokens + tokenCount(usage.inputOther, "input") +
877
+ tokenCount(usage.inputCacheCreation, "cache creation"),
878
+ outputTokens: this.#usage.outputTokens + tokenCount(usage.output, "output"),
879
+ cachedInputTokens: this.#usage.cachedInputTokens + tokenCount(usage.inputCacheRead, "cached input"),
880
+ };
881
+ this.#push({
882
+ type: "usage.updated",
883
+ usage: this.#usage,
884
+ });
885
+ return;
886
+ }
887
+ if (envelope.type === "turn.ended") {
888
+ const reason = requireProtocolString(payload.reason, "Kimi Turn end reason");
889
+ if (reason === "completed") {
890
+ this.#startAssistant(payload);
891
+ this.#push({
892
+ type: "assistant.message.completed",
893
+ nativeMessageId: this.#messageId(payload),
894
+ text: this.#assistantText,
895
+ });
896
+ this.#push({ type: "turn.completed" });
897
+ }
898
+ else if (reason === "cancelled") {
899
+ this.#push({ type: "turn.interrupted" });
900
+ }
901
+ else {
902
+ this.#push({
903
+ type: "turn.failed",
904
+ error: harnessCommandFailure("startTurn", "Kimi Turn failed", `turn_${reason}`),
905
+ });
906
+ }
907
+ this.dispose();
908
+ }
909
+ }
910
+ catch (error) {
911
+ this.#push({
912
+ type: "adapter.protocolError",
913
+ message: error instanceof Error ? error.message : "Kimi event is invalid",
914
+ });
915
+ this.dispose();
916
+ }
917
+ }
918
+ [Symbol.asyncIterator]() {
919
+ return this.#queue[Symbol.asyncIterator]();
920
+ }
921
+ async interrupt() {
922
+ await this.process.abortTurn(this.session.nativeSessionId, this.#nativeTurnId);
923
+ }
924
+ async respondToApproval(nativeRequestId, decision) {
925
+ this.#locallyResolvedApprovals.add(nativeRequestId);
926
+ try {
927
+ await this.process.respondToApproval(this.session.nativeSessionId, nativeRequestId, decision);
928
+ }
929
+ catch (error) {
930
+ this.#locallyResolvedApprovals.delete(nativeRequestId);
931
+ throw error;
932
+ }
933
+ }
934
+ async respondToQuestion(nativeRequestId, response) {
935
+ const questions = this.#questions.get(nativeRequestId);
936
+ if (!questions)
937
+ throw protocolFailure("Kimi Question response targets an unknown request");
938
+ const nativeResponse = response.action === "dismiss"
939
+ ? response
940
+ : { action: "answer", answers: mapQuestionResponseToKimi(response.answers, questions) };
941
+ this.#locallyResolvedQuestions.add(nativeRequestId);
942
+ try {
943
+ await this.process.respondToQuestion(this.session.nativeSessionId, nativeRequestId, nativeResponse);
944
+ }
945
+ catch (error) {
946
+ this.#locallyResolvedQuestions.delete(nativeRequestId);
947
+ throw error;
948
+ }
949
+ }
950
+ dispose() {
951
+ if (this.#disposed)
952
+ return;
953
+ this.#disposed = true;
954
+ this.#unsubscribe?.();
955
+ this.#queue.end();
956
+ }
957
+ #startAssistant(payload) {
958
+ if (this.#assistantStarted)
959
+ return;
960
+ this.#assistantStarted = true;
961
+ this.#push({
962
+ type: "assistant.message.started",
963
+ nativeMessageId: this.#messageId(payload),
964
+ });
965
+ }
966
+ #messageId(payload) {
967
+ const turnId = payload.turnId;
968
+ if (typeof turnId !== "number" || !Number.isSafeInteger(turnId)) {
969
+ throw new Error("Kimi Turn id must be an integer");
970
+ }
971
+ return `${this.session.nativeSessionId}:turn-${turnId}:assistant`;
972
+ }
973
+ #alignStepDelta(envelope, delta, localLength) {
974
+ if (envelope.offset === undefined)
975
+ return delta;
976
+ const offset = requireSequence(envelope.offset, "Kimi volatile delta offset");
977
+ if (offset > localLength)
978
+ throw new Error("Kimi volatile delta has a gap");
979
+ const covered = localLength - offset;
980
+ if (covered >= delta.length)
981
+ return "";
982
+ return delta.slice(covered);
983
+ }
984
+ #push(event) { this.#queue.push(event); }
985
+ }
986
+ class AsyncQueue {
987
+ #values = [];
988
+ #waiters = [];
989
+ #ended = false;
990
+ push(value) {
991
+ const waiter = this.#waiters.shift();
992
+ if (waiter)
993
+ waiter({ value, done: false });
994
+ else
995
+ this.#values.push(value);
996
+ }
997
+ end() {
998
+ this.#ended = true;
999
+ for (const waiter of this.#waiters.splice(0))
1000
+ waiter({ value: undefined, done: true });
1001
+ }
1002
+ [Symbol.asyncIterator]() {
1003
+ return {
1004
+ next: () => {
1005
+ const value = this.#values.shift();
1006
+ if (value !== undefined)
1007
+ return Promise.resolve({ value, done: false });
1008
+ if (this.#ended)
1009
+ return Promise.resolve({ value: undefined, done: true });
1010
+ return new Promise((resolve) => this.#waiters.push(resolve));
1011
+ },
1012
+ };
1013
+ }
1014
+ }
1015
+ function mapKimiQuestion(value) {
1016
+ const question = asObjectProtocol(value, "Kimi Question item");
1017
+ const id = requireProtocolString(question.id, "Kimi Question item id");
1018
+ const text = requireProtocolString(question.question, "Kimi Question text");
1019
+ if (!Array.isArray(question.options))
1020
+ throw new Error("Kimi Question options must be an array");
1021
+ const optionIds = new Set();
1022
+ const options = question.options.map((value) => {
1023
+ const option = asObjectProtocol(value, "Kimi Question option");
1024
+ const optionId = requireProtocolString(option.id, "Kimi Question option id");
1025
+ if (optionIds.has(optionId))
1026
+ throw new Error("Kimi Question option ids must be unique");
1027
+ optionIds.add(optionId);
1028
+ const description = optionalProtocolString(option.description, "Kimi Question option description");
1029
+ return {
1030
+ label: requireProtocolString(option.label, "Kimi Question option label"),
1031
+ ...(description === undefined ? {} : { description }),
1032
+ };
1033
+ });
1034
+ const header = optionalProtocolString(question.header, "Kimi Question header");
1035
+ const description = optionalProtocolString(question.body, "Kimi Question body");
1036
+ if (question.multi_select !== undefined && typeof question.multi_select !== "boolean") {
1037
+ throw new Error("Kimi Question multi_select must be boolean");
1038
+ }
1039
+ if (question.allow_other !== undefined && typeof question.allow_other !== "boolean") {
1040
+ throw new Error("Kimi Question allow_other must be boolean");
1041
+ }
1042
+ return {
1043
+ id,
1044
+ optionIds: [...optionIds],
1045
+ item: {
1046
+ ...(header === undefined ? {} : { header }),
1047
+ question: text,
1048
+ ...(description === undefined ? {} : { description }),
1049
+ options,
1050
+ multiple: question.multi_select === true,
1051
+ allowCustom: question.allow_other === true,
1052
+ },
1053
+ };
1054
+ }
1055
+ function mapQuestionResponseToKimi(answers, questions) {
1056
+ const result = {};
1057
+ for (const answer of answers) {
1058
+ const question = questions[answer.questionIndex];
1059
+ if (!question)
1060
+ throw protocolFailure("Kimi Question answer index is invalid");
1061
+ if (answer.kind === "skipped") {
1062
+ result[question.id] = { kind: "skipped" };
1063
+ continue;
1064
+ }
1065
+ if (answer.kind === "custom") {
1066
+ result[question.id] = { kind: "other", text: answer.text };
1067
+ continue;
1068
+ }
1069
+ if (answer.kind !== "options" && answer.kind !== "optionsWithCustom") {
1070
+ throw protocolFailure("Kimi Question answer kind is unsupported");
1071
+ }
1072
+ const optionIds = answer.optionIndexes.map((index) => {
1073
+ const optionId = question.optionIds[index];
1074
+ if (!optionId)
1075
+ throw protocolFailure("Kimi Question option index is invalid");
1076
+ return optionId;
1077
+ });
1078
+ if (answer.kind === "optionsWithCustom") {
1079
+ result[question.id] = {
1080
+ kind: "multi_with_other",
1081
+ option_ids: optionIds,
1082
+ other_text: answer.text,
1083
+ };
1084
+ }
1085
+ else if (question.item.multiple) {
1086
+ result[question.id] = { kind: "multi", option_ids: optionIds };
1087
+ }
1088
+ else {
1089
+ if (optionIds.length !== 1)
1090
+ throw protocolFailure("Kimi single-select Question requires one option");
1091
+ result[question.id] = { kind: "single", option_id: optionIds[0] };
1092
+ }
1093
+ }
1094
+ return result;
1095
+ }
1096
+ async function validateSession(value, workspacePath) {
1097
+ const session = asObjectProtocol(value, "Kimi Session");
1098
+ const id = requireProtocolString(session.id, "Kimi Session id");
1099
+ const metadata = asObjectProtocol(session.metadata, "Kimi Session metadata");
1100
+ const cwd = requireProtocolString(metadata.cwd, "Kimi Session cwd");
1101
+ if (!isAbsolute(cwd))
1102
+ throw protocolFailure("Kimi Session cwd must be an absolute path");
1103
+ let nativeWorkspacePath;
1104
+ try {
1105
+ nativeWorkspacePath = await realpath(cwd);
1106
+ }
1107
+ catch {
1108
+ throw protocolFailure("Kimi Session Workspace cannot be canonicalized");
1109
+ }
1110
+ if (nativeWorkspacePath !== workspacePath) {
1111
+ throw protocolFailure("Kimi Session belongs to a different Workspace");
1112
+ }
1113
+ if (session.title !== undefined && typeof session.title !== "string") {
1114
+ throw protocolFailure("Kimi Session title must be a string");
1115
+ }
1116
+ const title = typeof session.title === "string" && session.title.length > 0
1117
+ ? session.title
1118
+ : undefined;
1119
+ const createdAt = session.created_at === undefined
1120
+ ? undefined
1121
+ : rfc3339(session.created_at, "created_at");
1122
+ const updatedAt = session.updated_at === undefined
1123
+ ? undefined
1124
+ : rfc3339(session.updated_at, "updated_at");
1125
+ return {
1126
+ id,
1127
+ ...(title === undefined ? {} : { title }),
1128
+ ...(createdAt === undefined ? {} : { createdAt }),
1129
+ ...(updatedAt === undefined ? {} : { updatedAt }),
1130
+ };
1131
+ }
1132
+ async function mapTurnInput(input) {
1133
+ if (input.type === "text")
1134
+ return { type: "text", text: input.text };
1135
+ if (input.source.type === "base64") {
1136
+ return {
1137
+ type: "image",
1138
+ source: {
1139
+ kind: "base64",
1140
+ media_type: input.source.mediaType,
1141
+ data: input.source.data,
1142
+ },
1143
+ };
1144
+ }
1145
+ let bytes;
1146
+ try {
1147
+ bytes = await readFile(input.source.path);
1148
+ }
1149
+ catch {
1150
+ throw harnessCommandFailure("startTurn", "Kimi image file became unreadable", "image_unreadable");
1151
+ }
1152
+ const mediaType = detectImageMediaType(bytes);
1153
+ if (mediaType === undefined) {
1154
+ throw harnessCommandFailure("startTurn", "Kimi image file changed after validation", "image_invalid");
1155
+ }
1156
+ return {
1157
+ type: "image",
1158
+ source: { kind: "base64", media_type: mediaType, data: bytes.toString("base64") },
1159
+ };
1160
+ }
1161
+ function detectImageMediaType(bytes) {
1162
+ if (bytes.length >= 8 && Buffer.from(bytes.subarray(0, 8)).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
1163
+ return "image/png";
1164
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
1165
+ return "image/jpeg";
1166
+ }
1167
+ if (bytes.length >= 12 &&
1168
+ Buffer.from(bytes.subarray(0, 4)).toString("ascii") === "RIFF" &&
1169
+ Buffer.from(bytes.subarray(8, 12)).toString("ascii") === "WEBP")
1170
+ return "image/webp";
1171
+ if (bytes.length >= 6) {
1172
+ const signature = Buffer.from(bytes.subarray(0, 6)).toString("ascii");
1173
+ if (signature === "GIF87a" || signature === "GIF89a")
1174
+ return "image/gif";
1175
+ }
1176
+ return undefined;
1177
+ }
1178
+ function rfc3339(value, description) {
1179
+ if (typeof value !== "string" ||
1180
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) ||
1181
+ Number.isNaN(Date.parse(value))) {
1182
+ throw protocolFailure(`Kimi Session ${description} must be an RFC 3339 timestamp`);
1183
+ }
1184
+ return new Date(value).toISOString();
1185
+ }
1186
+ function parseReadyLine(line) {
1187
+ const trimmed = line.trim();
1188
+ const legacyPrefix = "Kimi server: ";
1189
+ const current = /^Local:\s+(\S+)$/.exec(trimmed);
1190
+ const serializedUrl = trimmed.startsWith(legacyPrefix)
1191
+ ? trimmed.slice(legacyPrefix.length)
1192
+ : current?.[1];
1193
+ if (serializedUrl === undefined)
1194
+ return undefined;
1195
+ try {
1196
+ const readyUrl = new URL(serializedUrl);
1197
+ const token = new URLSearchParams(readyUrl.hash.slice(1)).get("token");
1198
+ if (readyUrl.protocol !== "http:" ||
1199
+ readyUrl.hostname !== "127.0.0.1" ||
1200
+ readyUrl.port.length === 0 ||
1201
+ readyUrl.username.length > 0 ||
1202
+ readyUrl.password.length > 0 ||
1203
+ readyUrl.search.length > 0 ||
1204
+ readyUrl.pathname !== "/" ||
1205
+ typeof token !== "string" ||
1206
+ token.length === 0)
1207
+ return undefined;
1208
+ return { origin: readyUrl.origin, token };
1209
+ }
1210
+ catch {
1211
+ return undefined;
1212
+ }
1213
+ }
1214
+ function messageDataToString(value) {
1215
+ if (typeof value === "string")
1216
+ return value;
1217
+ if (value instanceof ArrayBuffer)
1218
+ return Buffer.from(value).toString("utf8");
1219
+ throw new Error("Kimi WebSocket binary frame is unsupported");
1220
+ }
1221
+ function asObjectProtocol(value, description) {
1222
+ if (!isObject(value))
1223
+ throw protocolFailure(`${description} must be an object`);
1224
+ return value;
1225
+ }
1226
+ function requireProtocolString(value, description, allowEmpty = false) {
1227
+ if (typeof value !== "string" || (!allowEmpty && value.length === 0)) {
1228
+ throw protocolFailure(`${description} must be ${allowEmpty ? "a" : "a non-empty"} string`);
1229
+ }
1230
+ return value;
1231
+ }
1232
+ function optionalProtocolString(value, description) {
1233
+ if (value === undefined || value === null || value === "")
1234
+ return undefined;
1235
+ return requireProtocolString(value, description);
1236
+ }
1237
+ function requireSequence(value, description) {
1238
+ if (!Number.isSafeInteger(value) || value < 0) {
1239
+ throw protocolFailure(`${description} must be a non-negative integer`);
1240
+ }
1241
+ return value;
1242
+ }
1243
+ function readServerCursor(value, nativeSessionId) {
1244
+ if (value === undefined)
1245
+ return undefined;
1246
+ const cursors = asObjectProtocol(value, "Kimi subscription cursors");
1247
+ if (cursors[nativeSessionId] === undefined)
1248
+ return undefined;
1249
+ const cursor = asObjectProtocol(cursors[nativeSessionId], "Kimi subscription cursor");
1250
+ const epoch = optionalProtocolString(cursor.epoch, "Kimi subscription cursor epoch");
1251
+ return {
1252
+ seq: requireSequence(cursor.seq, "Kimi subscription cursor seq"),
1253
+ ...(epoch === undefined ? {} : { epoch }),
1254
+ };
1255
+ }
1256
+ function isErrorWithMessage(value) {
1257
+ return isObject(value) && typeof value.message === "string";
1258
+ }
1259
+ function tokenCount(value, description) {
1260
+ if (!Number.isSafeInteger(value) || value < 0) {
1261
+ throw new Error(`Kimi ${description} token count is invalid`);
1262
+ }
1263
+ return value;
1264
+ }
1265
+ function isObject(value) {
1266
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1267
+ }
1268
+ function protocolFailure(message) {
1269
+ return { code: "ADAPTER_PROTOCOL_ERROR", message, harness: "kimi" };
1270
+ }
1271
+ function harnessCommandFailure(operation, message, nativeCode) {
1272
+ return {
1273
+ code: "HARNESS_ERROR",
1274
+ message,
1275
+ harness: "kimi",
1276
+ operation,
1277
+ command: "kimi",
1278
+ ...(nativeCode === undefined ? {} : { nativeCode }),
1279
+ };
1280
+ }
1281
+ function normalizeSessionLookupFailure(error) {
1282
+ if (typeof error === "object" &&
1283
+ error !== null &&
1284
+ "code" in error &&
1285
+ error.code === "HARNESS_ERROR" &&
1286
+ "nativeCode" in error &&
1287
+ error.nativeCode === "http_404") {
1288
+ return { ...error, nativeCode: "session_not_found" };
1289
+ }
1290
+ return error;
1291
+ }
1292
+ function failure(operation, stage, _error) {
1293
+ return {
1294
+ code: "HARNESS_ERROR",
1295
+ message: `Kimi ${stage} failed`,
1296
+ harness: "kimi",
1297
+ operation,
1298
+ command: "kimi",
1299
+ stage,
1300
+ };
1301
+ }
1302
+ function normalizeHarnessFailure(error, operation, stage) {
1303
+ if (isObject(error) && error.code === "HARNESS_ERROR")
1304
+ return error;
1305
+ return failure(operation, stage, error);
1306
+ }
1307
+ function waitForExit(child) {
1308
+ if (hasExited(child))
1309
+ return Promise.resolve();
1310
+ return new Promise((resolve) => child.once("exit", () => resolve()));
1311
+ }
1312
+ function hasExited(child) {
1313
+ return child.exitCode !== null || child.signalCode !== null;
1314
+ }
1315
+ async function withTimeout(operation, milliseconds, createError) {
1316
+ let timer;
1317
+ try {
1318
+ return await Promise.race([
1319
+ operation,
1320
+ new Promise((_resolve, reject) => {
1321
+ timer = setTimeout(() => reject(createError()), milliseconds);
1322
+ }),
1323
+ ]);
1324
+ }
1325
+ finally {
1326
+ if (timer)
1327
+ clearTimeout(timer);
1328
+ }
1329
+ }
1330
+ async function exitsWithin(child, milliseconds) {
1331
+ let timer;
1332
+ try {
1333
+ return await Promise.race([
1334
+ waitForExit(child).then(() => true),
1335
+ new Promise((resolve) => {
1336
+ timer = setTimeout(() => resolve(false), milliseconds);
1337
+ }),
1338
+ ]);
1339
+ }
1340
+ finally {
1341
+ if (timer)
1342
+ clearTimeout(timer);
1343
+ }
1344
+ }
1345
+ function signalProcessGroup(pid, signal) {
1346
+ try {
1347
+ process.kill(-pid, signal);
1348
+ }
1349
+ catch (error) {
1350
+ if (!isObject(error) || error.code !== "ESRCH") {
1351
+ throw failure("closeHarness", "shutdown", error);
1352
+ }
1353
+ }
1354
+ }
1355
+ //# sourceMappingURL=kimi-process.js.map