@khorsheed/dsh-local-agent-kimi 0.1.0-rc.6
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.
- package/README.en.md +132 -0
- package/README.i18n.yaml +6 -0
- package/README.md +132 -0
- package/cordis.patch.yml +34 -0
- package/lib/client.js +1066 -0
- package/lib/index.js +4716 -0
- package/lib/invariant.js +31 -0
- package/lib/types/client/SettingsCard.d.ts +77 -0
- package/lib/types/client/SettingsCard.js +197 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +89 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/credential-guard.d.ts +29 -0
- package/lib/types/credential-guard.js +107 -0
- package/lib/types/index.d.ts +69 -0
- package/lib/types/index.js +270 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/kimi-cli-provider.d.ts +215 -0
- package/lib/types/kimi-cli-provider.js +855 -0
- package/lib/types/live-driver.d.ts +245 -0
- package/lib/types/live-driver.js +1312 -0
- package/lib/types/live-switch.d.ts +78 -0
- package/lib/types/live-switch.js +112 -0
- package/lib/types/member-bridge-config.d.ts +43 -0
- package/lib/types/member-bridge-config.js +80 -0
- package/lib/types/model-broker.d.ts +97 -0
- package/lib/types/model-broker.js +205 -0
- package/lib/types/model-catalog.d.ts +26 -0
- package/lib/types/model-catalog.js +86 -0
- package/lib/types/model-configuration.d.ts +11 -0
- package/lib/types/model-configuration.js +55 -0
- package/lib/types/preset-tools.d.ts +20 -0
- package/lib/types/preset-tools.js +36 -0
- package/lib/types/provision.d.ts +131 -0
- package/lib/types/provision.js +363 -0
- package/lib/types/records.d.ts +35 -0
- package/lib/types/records.js +85 -0
- package/lib/types/session-configuration.d.ts +5 -0
- package/lib/types/session-configuration.js +26 -0
- package/lib/types/session-mirror.d.ts +180 -0
- package/lib/types/session-mirror.js +446 -0
- package/lib/types/session-view.d.ts +121 -0
- package/lib/types/session-view.js +423 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +107 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,4716 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { LIVE_FLUSH_INTERVAL_MS, LiveFlush, LiveStreamPublisher, ModelDirectoryCache, assertResumeCwdUnchanged, assertResumeScopeUnchanged, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, modelDirectoryContextKey, persistChildSession, probeCliVersion, resolveChildCwd, resolveRoundModel, settingsFace, subagentDelegationLabel, vol } from "@khorsheed/dsh-local-agent";
|
|
3
|
+
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV, endpointHost } from "@khorsheed/dsh-local-agent/types";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
6
|
+
import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { copyFile, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { StringDecoder } from "node:string_decoder";
|
|
10
|
+
import { createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
|
|
15
|
+
/*!
|
|
16
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
17
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
18
|
+
*
|
|
19
|
+
* Redistribution and use in source and binary forms, with or without
|
|
20
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21
|
+
*
|
|
22
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
23
|
+
* list of conditions and the following disclaimer.
|
|
24
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
25
|
+
* this list of conditions and the following disclaimer in the
|
|
26
|
+
* documentation and/or other materials provided with the distribution.
|
|
27
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
28
|
+
* may be used to endorse or promote products derived from this software without
|
|
29
|
+
* specific prior written permission.
|
|
30
|
+
*
|
|
31
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
32
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
33
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
34
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
35
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
36
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
37
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
38
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
39
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
40
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
41
|
+
*/
|
|
42
|
+
let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
|
|
43
|
+
var TomlDate = class TomlDate extends Date {
|
|
44
|
+
#hasDate = false;
|
|
45
|
+
#hasTime = false;
|
|
46
|
+
#offset = null;
|
|
47
|
+
constructor(date) {
|
|
48
|
+
let hasDate = true;
|
|
49
|
+
let hasTime = true;
|
|
50
|
+
let offset = "Z";
|
|
51
|
+
if (typeof date === "string") {
|
|
52
|
+
let match = date.match(DATE_TIME_RE);
|
|
53
|
+
if (match) {
|
|
54
|
+
if (!match[1]) {
|
|
55
|
+
hasDate = false;
|
|
56
|
+
date = `0000-01-01T${date}`;
|
|
57
|
+
}
|
|
58
|
+
hasTime = !!match[2];
|
|
59
|
+
hasTime && date[10] === " " && (date = date.replace(" ", "T"));
|
|
60
|
+
if (match[2] && +match[2] > 23) date = "";
|
|
61
|
+
else {
|
|
62
|
+
offset = match[3] || null;
|
|
63
|
+
date = date.toUpperCase();
|
|
64
|
+
if (!offset && hasTime) date += "Z";
|
|
65
|
+
}
|
|
66
|
+
} else date = "";
|
|
67
|
+
}
|
|
68
|
+
super(date);
|
|
69
|
+
if (!isNaN(this.getTime())) {
|
|
70
|
+
this.#hasDate = hasDate;
|
|
71
|
+
this.#hasTime = hasTime;
|
|
72
|
+
this.#offset = offset;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
isDateTime() {
|
|
76
|
+
return this.#hasDate && this.#hasTime;
|
|
77
|
+
}
|
|
78
|
+
isLocal() {
|
|
79
|
+
return !this.#hasDate || !this.#hasTime || !this.#offset;
|
|
80
|
+
}
|
|
81
|
+
isDate() {
|
|
82
|
+
return this.#hasDate && !this.#hasTime;
|
|
83
|
+
}
|
|
84
|
+
isTime() {
|
|
85
|
+
return this.#hasTime && !this.#hasDate;
|
|
86
|
+
}
|
|
87
|
+
isValid() {
|
|
88
|
+
return this.#hasDate || this.#hasTime;
|
|
89
|
+
}
|
|
90
|
+
toISOString() {
|
|
91
|
+
let iso = super.toISOString();
|
|
92
|
+
if (this.isDate()) return iso.slice(0, 10);
|
|
93
|
+
if (this.isTime()) return iso.slice(11, 23);
|
|
94
|
+
if (this.#offset === null) return iso.slice(0, -1);
|
|
95
|
+
if (this.#offset === "Z") return iso;
|
|
96
|
+
let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
|
|
97
|
+
offset = this.#offset[0] === "-" ? offset : -offset;
|
|
98
|
+
return (/* @__PURE__ */ new Date(this.getTime() - offset * 6e4)).toISOString().slice(0, -1) + this.#offset;
|
|
99
|
+
}
|
|
100
|
+
static wrapAsOffsetDateTime(jsDate, offset = "Z") {
|
|
101
|
+
let date = new TomlDate(jsDate);
|
|
102
|
+
date.#offset = offset;
|
|
103
|
+
return date;
|
|
104
|
+
}
|
|
105
|
+
static wrapAsLocalDateTime(jsDate) {
|
|
106
|
+
let date = new TomlDate(jsDate);
|
|
107
|
+
date.#offset = null;
|
|
108
|
+
return date;
|
|
109
|
+
}
|
|
110
|
+
static wrapAsLocalDate(jsDate) {
|
|
111
|
+
let date = new TomlDate(jsDate);
|
|
112
|
+
date.#hasTime = false;
|
|
113
|
+
date.#offset = null;
|
|
114
|
+
return date;
|
|
115
|
+
}
|
|
116
|
+
static wrapAsLocalTime(jsDate) {
|
|
117
|
+
let date = new TomlDate(jsDate);
|
|
118
|
+
date.#hasDate = false;
|
|
119
|
+
date.#offset = null;
|
|
120
|
+
return date;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/error.js
|
|
125
|
+
/*!
|
|
126
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
127
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
128
|
+
*
|
|
129
|
+
* Redistribution and use in source and binary forms, with or without
|
|
130
|
+
* modification, are permitted provided that the following conditions are met:
|
|
131
|
+
*
|
|
132
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
133
|
+
* list of conditions and the following disclaimer.
|
|
134
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
135
|
+
* this list of conditions and the following disclaimer in the
|
|
136
|
+
* documentation and/or other materials provided with the distribution.
|
|
137
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
138
|
+
* may be used to endorse or promote products derived from this software without
|
|
139
|
+
* specific prior written permission.
|
|
140
|
+
*
|
|
141
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
142
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
143
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
144
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
145
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
146
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
147
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
148
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
149
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
150
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
151
|
+
*/
|
|
152
|
+
function getLineColFromPtr(string, ptr) {
|
|
153
|
+
let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
|
|
154
|
+
return [lines.length, lines.pop().length + 1];
|
|
155
|
+
}
|
|
156
|
+
function makeCodeBlock(string, line, column) {
|
|
157
|
+
let lines = string.split(/\r\n|\n|\r/g);
|
|
158
|
+
let codeblock = "";
|
|
159
|
+
let numberLen = (Math.log10(line + 1) | 0) + 1;
|
|
160
|
+
for (let i = line - 1; i <= line + 1; i++) {
|
|
161
|
+
let l = lines[i - 1];
|
|
162
|
+
if (!l) continue;
|
|
163
|
+
codeblock += i.toString().padEnd(numberLen, " ");
|
|
164
|
+
codeblock += ": ";
|
|
165
|
+
codeblock += l;
|
|
166
|
+
codeblock += "\n";
|
|
167
|
+
if (i === line) {
|
|
168
|
+
codeblock += " ".repeat(numberLen + column + 2);
|
|
169
|
+
codeblock += "^\n";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return codeblock;
|
|
173
|
+
}
|
|
174
|
+
var TomlError = class extends Error {
|
|
175
|
+
line;
|
|
176
|
+
column;
|
|
177
|
+
codeblock;
|
|
178
|
+
constructor(message, options) {
|
|
179
|
+
const [line, column] = getLineColFromPtr(options.toml, options.ptr);
|
|
180
|
+
const codeblock = makeCodeBlock(options.toml, line, column);
|
|
181
|
+
super(`Invalid TOML document: ${message}\n\n${codeblock}`, options);
|
|
182
|
+
this.line = line;
|
|
183
|
+
this.column = column;
|
|
184
|
+
this.codeblock = codeblock;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/util.js
|
|
189
|
+
/*!
|
|
190
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
191
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
192
|
+
*
|
|
193
|
+
* Redistribution and use in source and binary forms, with or without
|
|
194
|
+
* modification, are permitted provided that the following conditions are met:
|
|
195
|
+
*
|
|
196
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
197
|
+
* list of conditions and the following disclaimer.
|
|
198
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
199
|
+
* this list of conditions and the following disclaimer in the
|
|
200
|
+
* documentation and/or other materials provided with the distribution.
|
|
201
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
202
|
+
* may be used to endorse or promote products derived from this software without
|
|
203
|
+
* specific prior written permission.
|
|
204
|
+
*
|
|
205
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
206
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
207
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
208
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
209
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
210
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
211
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
212
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
213
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
214
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
215
|
+
*/
|
|
216
|
+
/** @internal */
|
|
217
|
+
function indexOfNewline(str, start = 0) {
|
|
218
|
+
let idx = str.indexOf("\n", start);
|
|
219
|
+
if (str.charCodeAt(idx - 1) === 13) idx--;
|
|
220
|
+
return idx;
|
|
221
|
+
}
|
|
222
|
+
/** @internal */
|
|
223
|
+
function skipComment(ctx) {
|
|
224
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
225
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
226
|
+
if (c === 10) break;
|
|
227
|
+
if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
|
|
228
|
+
ctx.p++;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (c < 32 && c !== 9 || c === 127) throw new TomlError("control characters are not allowed in comments", {
|
|
232
|
+
toml: ctx.s,
|
|
233
|
+
ptr: ctx.p
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** @internal */
|
|
238
|
+
function skipVoid(ctx, banNewLines, banComments) {
|
|
239
|
+
let c;
|
|
240
|
+
while (1) {
|
|
241
|
+
while ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) ctx.p++;
|
|
242
|
+
if (banComments || c !== 35) break;
|
|
243
|
+
skipComment(ctx);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** @internal */
|
|
247
|
+
function skipUntil(ctx, sep, end) {
|
|
248
|
+
let ptr = ctx.p;
|
|
249
|
+
if (!end) {
|
|
250
|
+
ptr = indexOfNewline(ctx.s, ptr);
|
|
251
|
+
ctx.p = ptr < 0 ? ctx.s.length : ptr;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
255
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
256
|
+
if (c === 35) skipComment(ctx);
|
|
257
|
+
else if (c === end || c === sep) return;
|
|
258
|
+
}
|
|
259
|
+
throw new TomlError("cannot find end of structure", {
|
|
260
|
+
toml: ctx.s,
|
|
261
|
+
ptr
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/primitive.js
|
|
266
|
+
/*!
|
|
267
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
268
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
269
|
+
*
|
|
270
|
+
* Redistribution and use in source and binary forms, with or without
|
|
271
|
+
* modification, are permitted provided that the following conditions are met:
|
|
272
|
+
*
|
|
273
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
274
|
+
* list of conditions and the following disclaimer.
|
|
275
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
276
|
+
* this list of conditions and the following disclaimer in the
|
|
277
|
+
* documentation and/or other materials provided with the distribution.
|
|
278
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
279
|
+
* may be used to endorse or promote products derived from this software without
|
|
280
|
+
* specific prior written permission.
|
|
281
|
+
*
|
|
282
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
283
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
284
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
285
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
286
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
287
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
288
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
289
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
290
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
291
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
292
|
+
*/
|
|
293
|
+
let INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
|
|
294
|
+
let FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
|
|
295
|
+
let LEADING_ZERO = /^[+-]?0[0-9_]/;
|
|
296
|
+
/** @internal */
|
|
297
|
+
function parseString(ctx) {
|
|
298
|
+
let start = ctx.p;
|
|
299
|
+
let c = ctx.s.charCodeAt(ctx.p++);
|
|
300
|
+
let first = c;
|
|
301
|
+
let isLiteral = c === 39;
|
|
302
|
+
let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
|
|
303
|
+
if (isMultiline) {
|
|
304
|
+
if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10) ctx.p++;
|
|
305
|
+
else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) ctx.p += 2;
|
|
306
|
+
}
|
|
307
|
+
let parsed = "";
|
|
308
|
+
let sliceStart = ctx.p;
|
|
309
|
+
let state = 0;
|
|
310
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
311
|
+
c = ctx.s.charCodeAt(ctx.p);
|
|
312
|
+
if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) state = state && 3;
|
|
313
|
+
else if (c < 32 && c !== 9 || c === 127) throw new TomlError("control characters are not allowed in strings", {
|
|
314
|
+
toml: ctx.s,
|
|
315
|
+
ptr: ctx.p
|
|
316
|
+
});
|
|
317
|
+
else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
|
|
318
|
+
if (isMultiline) {
|
|
319
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
|
|
320
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
|
|
321
|
+
}
|
|
322
|
+
if (!state) parsed += ctx.s.slice(sliceStart, ctx.p);
|
|
323
|
+
ctx.p += isMultiline ? 3 : 1;
|
|
324
|
+
return parsed;
|
|
325
|
+
} else if (!state) {
|
|
326
|
+
if (!isLiteral && c === 92) {
|
|
327
|
+
parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
|
|
328
|
+
state = 1;
|
|
329
|
+
}
|
|
330
|
+
} else if (state === 1) {
|
|
331
|
+
if (c === 120 || c === 117 || c === 85) {
|
|
332
|
+
let value = 0;
|
|
333
|
+
let len = c === 120 ? 2 : c === 117 ? 4 : 8;
|
|
334
|
+
for (let j = 0; j < len; j++, ctx.p++) {
|
|
335
|
+
let hex = ctx.s.charCodeAt(ctx.p + 1);
|
|
336
|
+
let digit = hex >= 48 && hex <= 57 ? hex - 48 : hex >= 65 && hex <= 70 ? hex - 65 + 10 : hex >= 97 && hex <= 102 ? hex - 97 + 10 : -1;
|
|
337
|
+
if (digit < 0) throw new TomlError("invalid non-hex character in unicode escape", {
|
|
338
|
+
toml: ctx.s,
|
|
339
|
+
ptr: ctx.p + 1
|
|
340
|
+
});
|
|
341
|
+
value = value << 4 | digit;
|
|
342
|
+
}
|
|
343
|
+
if (value < 0 || value > 1114111 || value >= 55296 && value <= 57343) throw new TomlError("invalid unicode escape", {
|
|
344
|
+
toml: ctx.s,
|
|
345
|
+
ptr: ctx.p
|
|
346
|
+
});
|
|
347
|
+
parsed += String.fromCodePoint(value);
|
|
348
|
+
sliceStart = ctx.p + 1;
|
|
349
|
+
state = 0;
|
|
350
|
+
} else if (c === 32 || c === 9) state = 2;
|
|
351
|
+
else {
|
|
352
|
+
if (c === 98) parsed += "\b";
|
|
353
|
+
else if (c === 116) parsed += " ";
|
|
354
|
+
else if (c === 110) parsed += "\n";
|
|
355
|
+
else if (c === 102) parsed += "\f";
|
|
356
|
+
else if (c === 114) parsed += "\r";
|
|
357
|
+
else if (c === 101) parsed += "\x1B";
|
|
358
|
+
else if (c === 34) parsed += "\"";
|
|
359
|
+
else if (c === 92) parsed += "\\";
|
|
360
|
+
else throw new TomlError("unrecognized escape sequence", {
|
|
361
|
+
toml: ctx.s,
|
|
362
|
+
ptr: ctx.p
|
|
363
|
+
});
|
|
364
|
+
sliceStart = ctx.p + 1;
|
|
365
|
+
state = 0;
|
|
366
|
+
}
|
|
367
|
+
} else if (c !== 32 && c !== 9) {
|
|
368
|
+
if (state === 2) throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
|
|
369
|
+
toml: ctx.s,
|
|
370
|
+
ptr: sliceStart
|
|
371
|
+
});
|
|
372
|
+
state = !isLiteral && c === 92 ? 1 : 0;
|
|
373
|
+
sliceStart = ctx.p;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
throw new TomlError("unfinished string", {
|
|
377
|
+
toml: ctx.s,
|
|
378
|
+
ptr: start
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function sliceAndTrimEndOf(ctx, start, end) {
|
|
382
|
+
let value = ctx.s.slice(start, end);
|
|
383
|
+
let commentIdx = value.indexOf("#");
|
|
384
|
+
if (commentIdx > 0) {
|
|
385
|
+
skipComment({
|
|
386
|
+
s: value,
|
|
387
|
+
p: commentIdx,
|
|
388
|
+
d: 0
|
|
389
|
+
});
|
|
390
|
+
value = value.slice(0, commentIdx);
|
|
391
|
+
}
|
|
392
|
+
return value.trimEnd();
|
|
393
|
+
}
|
|
394
|
+
/** @internal */
|
|
395
|
+
function parseValue(ctx, integersAsBigInt, end) {
|
|
396
|
+
let ptr = ctx.p;
|
|
397
|
+
let err = {
|
|
398
|
+
toml: ctx.s,
|
|
399
|
+
ptr
|
|
400
|
+
};
|
|
401
|
+
skipUntil(ctx, 44, end);
|
|
402
|
+
let value = sliceAndTrimEndOf(ctx, ptr, ctx.p);
|
|
403
|
+
if (!value) throw new TomlError("incomplete declaration: value expected", err);
|
|
404
|
+
if (value === "-inf") return -Infinity;
|
|
405
|
+
if (value === "inf" || value === "+inf") return Infinity;
|
|
406
|
+
if (value === "nan" || value === "+nan" || value === "-nan") return NaN;
|
|
407
|
+
if (value === "-0") return integersAsBigInt ? 0n : 0;
|
|
408
|
+
let isInt = INT_REGEX.test(value);
|
|
409
|
+
if (isInt || FLOAT_REGEX.test(value)) {
|
|
410
|
+
if (LEADING_ZERO.test(value)) throw new TomlError("leading zeroes are not allowed", err);
|
|
411
|
+
value = value.replace(/_/g, "");
|
|
412
|
+
let numeric = +value;
|
|
413
|
+
if (isNaN(numeric)) throw new TomlError("invalid number", err);
|
|
414
|
+
if (isInt) {
|
|
415
|
+
if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) throw new TomlError("integer value cannot be represented losslessly", err);
|
|
416
|
+
if (isInt || integersAsBigInt === true) numeric = BigInt(value);
|
|
417
|
+
}
|
|
418
|
+
return numeric;
|
|
419
|
+
}
|
|
420
|
+
const date = new TomlDate(value);
|
|
421
|
+
if (!date.isValid()) throw new TomlError("invalid value", err);
|
|
422
|
+
return date;
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/extract.js
|
|
426
|
+
/*!
|
|
427
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
428
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
429
|
+
*
|
|
430
|
+
* Redistribution and use in source and binary forms, with or without
|
|
431
|
+
* modification, are permitted provided that the following conditions are met:
|
|
432
|
+
*
|
|
433
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
434
|
+
* list of conditions and the following disclaimer.
|
|
435
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
436
|
+
* this list of conditions and the following disclaimer in the
|
|
437
|
+
* documentation and/or other materials provided with the distribution.
|
|
438
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
439
|
+
* may be used to endorse or promote products derived from this software without
|
|
440
|
+
* specific prior written permission.
|
|
441
|
+
*
|
|
442
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
443
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
444
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
445
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
446
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
447
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
448
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
449
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
450
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
451
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
452
|
+
*/
|
|
453
|
+
/** @internal */
|
|
454
|
+
function extractValue(ctx, end, integersAsBigInt) {
|
|
455
|
+
let ptr = ctx.p;
|
|
456
|
+
let c = ctx.s.charCodeAt(ptr);
|
|
457
|
+
if (c === 91 || c === 123) {
|
|
458
|
+
if (!ctx.d--) throw new TomlError("document contains excessively nested structures. aborting.", {
|
|
459
|
+
toml: ctx.s,
|
|
460
|
+
ptr
|
|
461
|
+
});
|
|
462
|
+
let value = c === 91 ? parseArray(ctx, integersAsBigInt) : parseInlineTable(ctx, integersAsBigInt);
|
|
463
|
+
ctx.d++;
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
if (c === 34 || c === 39) return parseString(ctx);
|
|
467
|
+
if (c === 116) {
|
|
468
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 114 || ctx.s.charCodeAt(++ctx.p) !== 117 || ctx.s.charCodeAt(++ctx.p) !== 101) throw new TomlError("invalid value", {
|
|
469
|
+
toml: ctx.s,
|
|
470
|
+
ptr
|
|
471
|
+
});
|
|
472
|
+
ctx.p++;
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
if (c === 102) {
|
|
476
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 108 || ctx.s.charCodeAt(++ctx.p) !== 115 || ctx.s.charCodeAt(++ctx.p) !== 101) throw new TomlError("invalid value", {
|
|
477
|
+
toml: ctx.s,
|
|
478
|
+
ptr
|
|
479
|
+
});
|
|
480
|
+
ctx.p++;
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
return parseValue(ctx, integersAsBigInt, end);
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/struct.js
|
|
487
|
+
/*!
|
|
488
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
489
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
490
|
+
*
|
|
491
|
+
* Redistribution and use in source and binary forms, with or without
|
|
492
|
+
* modification, are permitted provided that the following conditions are met:
|
|
493
|
+
*
|
|
494
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
495
|
+
* list of conditions and the following disclaimer.
|
|
496
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
497
|
+
* this list of conditions and the following disclaimer in the
|
|
498
|
+
* documentation and/or other materials provided with the distribution.
|
|
499
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
500
|
+
* may be used to endorse or promote products derived from this software without
|
|
501
|
+
* specific prior written permission.
|
|
502
|
+
*
|
|
503
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
504
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
505
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
506
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
507
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
508
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
509
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
510
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
511
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
512
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
513
|
+
*/
|
|
514
|
+
let KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
|
|
515
|
+
/** @internal */
|
|
516
|
+
function parseKey(ctx, end = "=") {
|
|
517
|
+
let start = ctx.p;
|
|
518
|
+
let dot = start - 1;
|
|
519
|
+
let parsed = [];
|
|
520
|
+
let endPtr = ctx.s.indexOf(end, start);
|
|
521
|
+
if (endPtr < 0) throw new TomlError("incomplete key-value: cannot find end of key", {
|
|
522
|
+
toml: ctx.s,
|
|
523
|
+
ptr: start
|
|
524
|
+
});
|
|
525
|
+
do {
|
|
526
|
+
let c = ctx.s.charCodeAt(ctx.p = ++dot);
|
|
527
|
+
if (c !== 32 && c !== 9) {
|
|
528
|
+
if (c === 34 || c === 39) {
|
|
529
|
+
if (c === ctx.s.charCodeAt(ctx.p + 1) && c === ctx.s.charCodeAt(ctx.p + 2)) throw new TomlError("multiline strings are not allowed in keys", {
|
|
530
|
+
toml: ctx.s,
|
|
531
|
+
ptr: ctx.p
|
|
532
|
+
});
|
|
533
|
+
let part = parseString(ctx);
|
|
534
|
+
dot = ctx.s.indexOf(".", ctx.p);
|
|
535
|
+
let strEnd = ctx.s.slice(ctx.p, dot < 0 || dot > endPtr ? endPtr : dot);
|
|
536
|
+
let newLine = indexOfNewline(strEnd);
|
|
537
|
+
if (newLine > -1) throw new TomlError("newlines are not allowed in keys", {
|
|
538
|
+
toml: ctx.s,
|
|
539
|
+
ptr: newLine
|
|
540
|
+
});
|
|
541
|
+
if (strEnd.trimStart()) throw new TomlError("found extra tokens after the string part", {
|
|
542
|
+
toml: ctx.s,
|
|
543
|
+
ptr: ctx.p
|
|
544
|
+
});
|
|
545
|
+
if (endPtr < ctx.p) {
|
|
546
|
+
endPtr = ctx.s.indexOf(end, ctx.p);
|
|
547
|
+
if (endPtr < 0) throw new TomlError("incomplete key-value: cannot find end of key", {
|
|
548
|
+
toml: ctx.s,
|
|
549
|
+
ptr: start
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
parsed.push(part);
|
|
553
|
+
} else {
|
|
554
|
+
dot = ctx.s.indexOf(".", ctx.p);
|
|
555
|
+
let part = ctx.s.slice(ctx.p, dot < 0 || dot > endPtr ? endPtr : dot);
|
|
556
|
+
if (!KEY_PART_RE.test(part)) throw new TomlError("only letter, numbers, dashes and underscores are allowed in keys", {
|
|
557
|
+
toml: ctx.s,
|
|
558
|
+
ptr: ctx.p
|
|
559
|
+
});
|
|
560
|
+
parsed.push(part.trimEnd());
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} while (dot + 1 && dot < endPtr);
|
|
564
|
+
ctx.p = endPtr + 1;
|
|
565
|
+
skipVoid(ctx, true, true);
|
|
566
|
+
return parsed;
|
|
567
|
+
}
|
|
568
|
+
/** @internal */
|
|
569
|
+
function parseInlineTable(ctx, integersAsBigInt) {
|
|
570
|
+
let res = {};
|
|
571
|
+
let seen = /* @__PURE__ */ new Set();
|
|
572
|
+
let c;
|
|
573
|
+
ctx.p++;
|
|
574
|
+
while (ctx.p < ctx.s.length) {
|
|
575
|
+
skipVoid(ctx);
|
|
576
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 125) {
|
|
577
|
+
ctx.p++;
|
|
578
|
+
return res;
|
|
579
|
+
}
|
|
580
|
+
let k;
|
|
581
|
+
let t = res;
|
|
582
|
+
let hasOwn = false;
|
|
583
|
+
let p = ctx.p;
|
|
584
|
+
let key = parseKey(ctx);
|
|
585
|
+
for (let i = 0; i < key.length; i++) {
|
|
586
|
+
if (i) t = hasOwn ? t[k] : t[k] = {};
|
|
587
|
+
k = key[i];
|
|
588
|
+
if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) throw new TomlError("trying to redefine an already defined value", {
|
|
589
|
+
toml: ctx.s,
|
|
590
|
+
ptr: p
|
|
591
|
+
});
|
|
592
|
+
if (!hasOwn && k === "__proto__") Object.defineProperty(t, k, {
|
|
593
|
+
enumerable: true,
|
|
594
|
+
configurable: true,
|
|
595
|
+
writable: true
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (hasOwn) throw new TomlError("trying to redefine an already defined value", {
|
|
599
|
+
toml: ctx.s,
|
|
600
|
+
ptr: ctx.p
|
|
601
|
+
});
|
|
602
|
+
let value = extractValue(ctx, 125, integersAsBigInt);
|
|
603
|
+
seen.add(t[k] = value);
|
|
604
|
+
skipVoid(ctx);
|
|
605
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 125) return res;
|
|
606
|
+
if (c !== 44) throw new TomlError("expected comma or end of structure", {
|
|
607
|
+
toml: ctx.s,
|
|
608
|
+
ptr: ctx.p - 1
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
throw new TomlError("unfinished table encountered", {
|
|
612
|
+
toml: ctx.s,
|
|
613
|
+
ptr: ctx.p
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/** @internal */
|
|
617
|
+
function parseArray(ctx, integersAsBigInt) {
|
|
618
|
+
let res = [];
|
|
619
|
+
let c;
|
|
620
|
+
ctx.p++;
|
|
621
|
+
while (ctx.p < ctx.s.length) {
|
|
622
|
+
skipVoid(ctx);
|
|
623
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 93) {
|
|
624
|
+
ctx.p++;
|
|
625
|
+
return res;
|
|
626
|
+
}
|
|
627
|
+
res.push(extractValue(ctx, 93, integersAsBigInt));
|
|
628
|
+
skipVoid(ctx);
|
|
629
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 93) return res;
|
|
630
|
+
if (c !== 44) throw new TomlError("expected comma or end of structure", {
|
|
631
|
+
toml: ctx.s,
|
|
632
|
+
ptr: ctx.p - 1
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
throw new TomlError("unfinished array encountered", {
|
|
636
|
+
toml: ctx.s,
|
|
637
|
+
ptr: ctx.p
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/parse.js
|
|
642
|
+
/*!
|
|
643
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
644
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
645
|
+
*
|
|
646
|
+
* Redistribution and use in source and binary forms, with or without
|
|
647
|
+
* modification, are permitted provided that the following conditions are met:
|
|
648
|
+
*
|
|
649
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
650
|
+
* list of conditions and the following disclaimer.
|
|
651
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
652
|
+
* this list of conditions and the following disclaimer in the
|
|
653
|
+
* documentation and/or other materials provided with the distribution.
|
|
654
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
655
|
+
* may be used to endorse or promote products derived from this software without
|
|
656
|
+
* specific prior written permission.
|
|
657
|
+
*
|
|
658
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
659
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
660
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
661
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
662
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
663
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
664
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
665
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
666
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
667
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
668
|
+
*/
|
|
669
|
+
function peekTable(key, table, meta, type) {
|
|
670
|
+
let t = table;
|
|
671
|
+
let m = meta;
|
|
672
|
+
let k;
|
|
673
|
+
let hasOwn = false;
|
|
674
|
+
let state;
|
|
675
|
+
for (let i = 0; i < key.length; i++) {
|
|
676
|
+
if (i) {
|
|
677
|
+
t = hasOwn ? t[k] : t[k] = {};
|
|
678
|
+
m = (state = m[k]).c;
|
|
679
|
+
if (type === 0 && (state.t === 1 || state.t === 2)) return null;
|
|
680
|
+
if (state.t === 2) {
|
|
681
|
+
let l = t.length - 1;
|
|
682
|
+
t = t[l];
|
|
683
|
+
m = m[l].c;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
k = key[i];
|
|
687
|
+
if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) return null;
|
|
688
|
+
if (!hasOwn) {
|
|
689
|
+
if (k === "__proto__") {
|
|
690
|
+
Object.defineProperty(t, k, {
|
|
691
|
+
enumerable: true,
|
|
692
|
+
configurable: true,
|
|
693
|
+
writable: true
|
|
694
|
+
});
|
|
695
|
+
Object.defineProperty(m, k, {
|
|
696
|
+
enumerable: true,
|
|
697
|
+
configurable: true,
|
|
698
|
+
writable: true
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
m[k] = {
|
|
702
|
+
t: i < key.length - 1 && type === 2 ? 3 : type,
|
|
703
|
+
d: false,
|
|
704
|
+
i: 0,
|
|
705
|
+
c: {}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
state = m[k];
|
|
710
|
+
if (state.t !== type && !(type === 1 && state.t === 3)) return null;
|
|
711
|
+
if (type === 2) {
|
|
712
|
+
if (!state.d) {
|
|
713
|
+
state.d = true;
|
|
714
|
+
t[k] = [];
|
|
715
|
+
}
|
|
716
|
+
t[k].push(t = {});
|
|
717
|
+
state.c[state.i++] = state = {
|
|
718
|
+
t: 1,
|
|
719
|
+
d: false,
|
|
720
|
+
i: 0,
|
|
721
|
+
c: {}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (state.d) return null;
|
|
725
|
+
state.d = true;
|
|
726
|
+
if (type === 1) t = hasOwn ? t[k] : t[k] = {};
|
|
727
|
+
else if (type === 0 && hasOwn) return null;
|
|
728
|
+
return [
|
|
729
|
+
k,
|
|
730
|
+
t,
|
|
731
|
+
state.c
|
|
732
|
+
];
|
|
733
|
+
}
|
|
734
|
+
function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
735
|
+
let ctx = {
|
|
736
|
+
s: toml,
|
|
737
|
+
p: 0,
|
|
738
|
+
d: maxDepth
|
|
739
|
+
};
|
|
740
|
+
let res = {};
|
|
741
|
+
let meta = {};
|
|
742
|
+
let tmp;
|
|
743
|
+
let tbl = res;
|
|
744
|
+
let m = meta;
|
|
745
|
+
skipVoid(ctx);
|
|
746
|
+
while (ctx.p < toml.length) {
|
|
747
|
+
if (toml.charCodeAt(ctx.p) === 91) {
|
|
748
|
+
let isTableArray = toml.charCodeAt(++ctx.p) === 91;
|
|
749
|
+
tmp = ctx.p += +isTableArray;
|
|
750
|
+
let k = parseKey(ctx, "]");
|
|
751
|
+
if (isTableArray) {
|
|
752
|
+
if (toml.charCodeAt(ctx.p - 1) !== 93) throw new TomlError("expected end of table declaration", {
|
|
753
|
+
toml,
|
|
754
|
+
ptr: ctx.p - 1
|
|
755
|
+
});
|
|
756
|
+
ctx.p++;
|
|
757
|
+
}
|
|
758
|
+
let p = peekTable(k, res, meta, isTableArray ? 2 : 1);
|
|
759
|
+
if (!p) throw new TomlError("trying to redefine an already defined table or value", {
|
|
760
|
+
toml,
|
|
761
|
+
ptr: tmp
|
|
762
|
+
});
|
|
763
|
+
m = p[2];
|
|
764
|
+
tbl = p[1];
|
|
765
|
+
} else {
|
|
766
|
+
tmp = ctx.p;
|
|
767
|
+
let p = peekTable(parseKey(ctx), tbl, m, 0);
|
|
768
|
+
if (!p) throw new TomlError("trying to redefine an already defined table or value", {
|
|
769
|
+
toml,
|
|
770
|
+
ptr: tmp
|
|
771
|
+
});
|
|
772
|
+
p[1][p[0]] = extractValue(ctx, void 0, integersAsBigInt);
|
|
773
|
+
}
|
|
774
|
+
skipVoid(ctx, true);
|
|
775
|
+
if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && tmp !== 13) throw new TomlError("each key-value declaration must be followed by an end-of-line", {
|
|
776
|
+
toml,
|
|
777
|
+
ptr: ctx.p
|
|
778
|
+
});
|
|
779
|
+
skipVoid(ctx);
|
|
780
|
+
}
|
|
781
|
+
return res;
|
|
782
|
+
}
|
|
783
|
+
//#endregion
|
|
784
|
+
//#region lib/types/model-configuration.js
|
|
785
|
+
const table = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
786
|
+
const string = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
787
|
+
/** Native configuration metadata only; provider credentials never leave this function. */
|
|
788
|
+
async function readKimiModelConfiguration(home, selectedModel) {
|
|
789
|
+
let text;
|
|
790
|
+
try {
|
|
791
|
+
text = await readFile(join(home, "config.toml"), "utf8");
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (error.code === "ENOENT") return { entries: [] };
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
const config = parse(text);
|
|
797
|
+
const models = table(config["models"]);
|
|
798
|
+
const entries = Object.entries(models).map(([value, raw]) => {
|
|
799
|
+
const model = table(raw);
|
|
800
|
+
const efforts = model["support_efforts"];
|
|
801
|
+
const defaultEffort = string(model["default_effort"]);
|
|
802
|
+
return {
|
|
803
|
+
value,
|
|
804
|
+
label: string(model["display_name"]) ?? value,
|
|
805
|
+
source: "configuration",
|
|
806
|
+
...!Array.isArray(efforts) ? {} : { reasoning: {
|
|
807
|
+
options: efforts.flatMap((value) => typeof value === "string" ? [{
|
|
808
|
+
value,
|
|
809
|
+
label: value
|
|
810
|
+
}] : []),
|
|
811
|
+
...defaultEffort === void 0 ? {} : { default: defaultEffort }
|
|
812
|
+
} }
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
const defaultModel = string(config["default_model"]);
|
|
816
|
+
const selected = selectedModel ?? defaultModel;
|
|
817
|
+
const model = table(selected === void 0 ? void 0 : models[selected]);
|
|
818
|
+
const provider = table(table(config["providers"])[string(model["provider"]) ?? ""]);
|
|
819
|
+
const protocol = string(model["protocol"]) ?? string(provider["type"]);
|
|
820
|
+
const thinking = table(config["thinking"]);
|
|
821
|
+
const requestedEffort = string(thinking["effort"]);
|
|
822
|
+
const options = entries.find((entry) => entry.value === selected)?.reasoning?.options;
|
|
823
|
+
const effort = thinking["enabled"] === false ? void 0 : requestedEffort !== void 0 && options?.some((option) => option.value === requestedEffort) ? requestedEffort : string(model["default_effort"]);
|
|
824
|
+
return {
|
|
825
|
+
entries,
|
|
826
|
+
...defaultModel === void 0 ? {} : { defaultModel },
|
|
827
|
+
...effort === void 0 ? {} : { effort },
|
|
828
|
+
...protocol === void 0 ? {} : { protocol }
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/** Only the native Kimi protocol advertises a general exec effort override. */
|
|
832
|
+
async function kimiExecConfiguration(home, model, effort) {
|
|
833
|
+
if (effort === void 0) return {};
|
|
834
|
+
const config = await readKimiModelConfiguration(home, model);
|
|
835
|
+
if (!config.entries.find((entry) => entry.value === (model ?? config.defaultModel))?.reasoning?.options.some((option) => option.value === effort)) throw new Error("Kimi configuration does not advertise this model/effort combination");
|
|
836
|
+
if (config.protocol !== "kimi") throw new Error("This Kimi model protocol has no per-invocation exec effort control; use ACP live mode");
|
|
837
|
+
return { KIMI_MODEL_THINKING_EFFORT: effort };
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region lib/types/model-catalog.js
|
|
841
|
+
const object = (value) => typeof value === "object" && value !== null ? value : void 0;
|
|
842
|
+
function options(value) {
|
|
843
|
+
if (!Array.isArray(value)) return [];
|
|
844
|
+
return value.flatMap((candidate) => {
|
|
845
|
+
const entry = object(candidate);
|
|
846
|
+
if (entry === void 0) return [];
|
|
847
|
+
if (Array.isArray(entry["options"])) return options(entry["options"]);
|
|
848
|
+
if (typeof entry["value"] !== "string") return [];
|
|
849
|
+
return [{
|
|
850
|
+
value: entry["value"],
|
|
851
|
+
label: typeof entry["name"] === "string" ? entry["name"] : entry["value"],
|
|
852
|
+
...typeof entry["description"] === "string" ? { description: entry["description"] } : {}
|
|
853
|
+
}];
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/** ACP v1 configOptions are authoritative; old models metadata is a read-only fallback. */
|
|
857
|
+
function kimiNativeConfiguration(value) {
|
|
858
|
+
const response = object(value);
|
|
859
|
+
if (response === void 0) return void 0;
|
|
860
|
+
const configuration = response["configOptions"];
|
|
861
|
+
if (Array.isArray(configuration)) {
|
|
862
|
+
const choices = configuration.map(object).filter((entry) => entry !== void 0 && entry["type"] === "select");
|
|
863
|
+
const model = choices.find((entry) => entry["category"] === "model");
|
|
864
|
+
const effort = choices.find((entry) => entry["category"] === "thought_level");
|
|
865
|
+
if (model !== void 0) {
|
|
866
|
+
const currentModel = typeof model["currentValue"] === "string" ? model["currentValue"] : void 0;
|
|
867
|
+
return {
|
|
868
|
+
directory: {
|
|
869
|
+
entries: options(model["options"]).map((option) => ({
|
|
870
|
+
...option,
|
|
871
|
+
source: "native",
|
|
872
|
+
...effort === void 0 || option.value !== currentModel ? {} : { reasoning: { options: options(effort["options"]) } }
|
|
873
|
+
})),
|
|
874
|
+
complete: true,
|
|
875
|
+
customInput: false
|
|
876
|
+
},
|
|
877
|
+
...typeof model["id"] === "string" ? { modelConfigId: model["id"] } : {},
|
|
878
|
+
...typeof effort?.["id"] === "string" ? { effortConfigId: effort["id"] } : {},
|
|
879
|
+
...currentModel === void 0 ? {} : { currentModel },
|
|
880
|
+
...typeof effort?.["currentValue"] === "string" ? { currentEffort: effort["currentValue"] } : {}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const legacy = object(response["models"]);
|
|
885
|
+
if (!Array.isArray(legacy?.["availableModels"])) return void 0;
|
|
886
|
+
return {
|
|
887
|
+
directory: {
|
|
888
|
+
entries: legacy["availableModels"].flatMap((candidate) => {
|
|
889
|
+
const model = object(candidate);
|
|
890
|
+
if (typeof model?.["modelId"] !== "string") return [];
|
|
891
|
+
return [{
|
|
892
|
+
value: model["modelId"],
|
|
893
|
+
label: typeof model["name"] === "string" ? model["name"] : model["modelId"],
|
|
894
|
+
source: "native",
|
|
895
|
+
...typeof model["description"] === "string" ? { description: model["description"] } : {}
|
|
896
|
+
}];
|
|
897
|
+
}),
|
|
898
|
+
complete: true,
|
|
899
|
+
customInput: false
|
|
900
|
+
},
|
|
901
|
+
...typeof legacy["currentModelId"] === "string" ? { currentModel: legacy["currentModelId"] } : {}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/** Reads configuration before a session exists; never starts a disposable ACP session. */
|
|
905
|
+
var KimiModelCatalog = class {
|
|
906
|
+
context;
|
|
907
|
+
cache = new ModelDirectoryCache({ load: async (key) => {
|
|
908
|
+
const context = JSON.parse(key);
|
|
909
|
+
if (context.native !== void 0) return context.native;
|
|
910
|
+
const { entries, defaultModel } = await readKimiModelConfiguration(context.homeDir);
|
|
911
|
+
return {
|
|
912
|
+
entries,
|
|
913
|
+
...defaultModel === void 0 ? {} : { defaultModel },
|
|
914
|
+
complete: false,
|
|
915
|
+
customInput: true,
|
|
916
|
+
reason: "Configured candidates; native session model metadata is not available yet"
|
|
917
|
+
};
|
|
918
|
+
} });
|
|
919
|
+
constructor(context) {
|
|
920
|
+
this.context = context;
|
|
921
|
+
}
|
|
922
|
+
key(childSessionId) {
|
|
923
|
+
const context = this.context(childSessionId);
|
|
924
|
+
const stamp = modelDirectoryContextKey({
|
|
925
|
+
provider: "kimi",
|
|
926
|
+
homeDir: context.homeDir,
|
|
927
|
+
cwd: context.cwd ?? context.homeDir,
|
|
928
|
+
cli: ["kimi"],
|
|
929
|
+
files: [join(context.homeDir, "config.toml"), join(context.homeDir, "credentials", "kimi-code.json")]
|
|
930
|
+
});
|
|
931
|
+
return JSON.stringify({
|
|
932
|
+
homeDir: context.homeDir,
|
|
933
|
+
stamp,
|
|
934
|
+
childSessionId,
|
|
935
|
+
...context.native === void 0 ? {} : { native: context.native.directory }
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
read(childSessionId) {
|
|
939
|
+
return this.cache.read(this.key(childSessionId));
|
|
940
|
+
}
|
|
941
|
+
refresh(childSessionId) {
|
|
942
|
+
return this.cache.refresh(this.key(childSessionId));
|
|
943
|
+
}
|
|
944
|
+
follow(childSessionId, signal) {
|
|
945
|
+
return this.cache.follow(() => this.key(childSessionId), signal);
|
|
946
|
+
}
|
|
947
|
+
dispose() {
|
|
948
|
+
this.cache.dispose();
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region lib/types/session-configuration.js
|
|
953
|
+
/** Confirm model first: its returned options determine which efforts are valid. */
|
|
954
|
+
async function configureKimiSession(sessionId, initial, desired, request) {
|
|
955
|
+
let current = initial;
|
|
956
|
+
if (desired.model !== void 0 && current?.currentModel !== desired.model) {
|
|
957
|
+
if (current?.modelConfigId === void 0) throw new Error("This Kimi ACP session exposes no verified model selection control");
|
|
958
|
+
if (!current.directory.entries.some((entry) => entry.value === desired.model)) throw new Error("The running Kimi session does not advertise this model");
|
|
959
|
+
current = kimiNativeConfiguration(await request("session/set_config_option", {
|
|
960
|
+
sessionId,
|
|
961
|
+
configId: current.modelConfigId,
|
|
962
|
+
value: desired.model
|
|
963
|
+
}));
|
|
964
|
+
if (current?.currentModel !== desired.model) throw new Error("Kimi did not confirm the requested model");
|
|
965
|
+
}
|
|
966
|
+
if (desired.effort !== void 0 && current?.currentEffort !== desired.effort) {
|
|
967
|
+
const currentModel = current?.currentModel;
|
|
968
|
+
const entry = current?.directory.entries.find((entry) => entry.value === currentModel);
|
|
969
|
+
if (current?.effortConfigId === void 0 || !entry?.reasoning?.options.some((option) => option.value === desired.effort)) throw new Error("The running Kimi session does not advertise this model/effort combination");
|
|
970
|
+
current = kimiNativeConfiguration(await request("session/set_config_option", {
|
|
971
|
+
sessionId,
|
|
972
|
+
configId: current.effortConfigId,
|
|
973
|
+
value: desired.effort
|
|
974
|
+
}));
|
|
975
|
+
if (current?.currentEffort !== desired.effort || desired.model !== void 0 && current.currentModel !== desired.model) throw new Error("Kimi did not confirm the requested model/effort configuration");
|
|
976
|
+
}
|
|
977
|
+
return current;
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region lib/types/session-view.js
|
|
981
|
+
/**
|
|
982
|
+
* Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
|
|
983
|
+
* and render a readable transcript (user prompts, assistant thinking and
|
|
984
|
+
* replies, tool calls with arguments and results). The kimi ACP mode writes
|
|
985
|
+
* responses to the session file rather than pushing them over the protocol,
|
|
986
|
+
* and the dsh subagent record for a one-shot run carries only the final
|
|
987
|
+
* output — this is the way to see what the kimi subagent actually did.
|
|
988
|
+
*
|
|
989
|
+
* The transcript is harness-comparison grade: every line is tagged with its
|
|
990
|
+
* wire turn number (1-based, matching the dsh child session's `turn/start`
|
|
991
|
+
* numbering), kimi's auto-permission `<system-reminder>` user messages are
|
|
992
|
+
* filtered out, tool calls carry their rendered arguments, tool results are
|
|
993
|
+
* matched to their owning call by `parentUuid`, and every `usage.record`
|
|
994
|
+
* (kimi reports accounting per LLM request, not cumulative) is surfaced with
|
|
995
|
+
* its transcript position so a delta mirror can sum a round's total.
|
|
996
|
+
* @module @khorsheed/dsh-local-agent-kimi/session-view
|
|
997
|
+
*/
|
|
998
|
+
/** The wire event log path for a session directory. */
|
|
999
|
+
function wirePath(sessionDir) {
|
|
1000
|
+
return join(sessionDir, "agents", "main", "wire.jsonl");
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Find a session directory under `<homeDir>/sessions/wd_*` whose id matches.
|
|
1004
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1005
|
+
* @param sessionId - the kimi session id.
|
|
1006
|
+
* @returns the session directory, or undefined when absent.
|
|
1007
|
+
*/
|
|
1008
|
+
async function findKimiSessionDir(homeDir, sessionId) {
|
|
1009
|
+
const sessionsRoot = join(homeDir, "sessions");
|
|
1010
|
+
let workspaces;
|
|
1011
|
+
try {
|
|
1012
|
+
workspaces = await readdir(sessionsRoot);
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
if (error.code === "ENOENT") return void 0;
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
for (const workspace of workspaces) {
|
|
1018
|
+
const sessionDir = join(sessionsRoot, workspace, `session_${sessionId}`);
|
|
1019
|
+
try {
|
|
1020
|
+
await readFile(wirePath(sessionDir));
|
|
1021
|
+
return sessionDir;
|
|
1022
|
+
} catch {}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/** Whether a user message is kimi's auto-permission reminder, never real task text. */
|
|
1026
|
+
function isSystemReminder(text) {
|
|
1027
|
+
return text.startsWith("<system-reminder>");
|
|
1028
|
+
}
|
|
1029
|
+
/** Flatten the text blocks of a message content array. */
|
|
1030
|
+
function textOf(content) {
|
|
1031
|
+
return (content ?? []).filter((block) => typeof block === "object" && block !== null && block.type === "text").map((block) => block.text ?? "").join("");
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Render a file-editing tool call's arguments apply-patch style: the path
|
|
1035
|
+
* plus the old/new content. Without this a delegation's edits collapsed to
|
|
1036
|
+
* the bare path (or nothing) and the transcript hid what the member actually
|
|
1037
|
+
* changed — the kimi twin of codex's fileChange → ApplyPatch fold.
|
|
1038
|
+
* @param name - the tool name the wire recorded.
|
|
1039
|
+
* @param record - the call's argument object.
|
|
1040
|
+
* @returns the patch-style summary, or undefined when the tool is not a
|
|
1041
|
+
* known file-editing shape.
|
|
1042
|
+
*/
|
|
1043
|
+
function editArgsOf(name, record) {
|
|
1044
|
+
const path = typeof record["path"] === "string" ? record["path"] : void 0;
|
|
1045
|
+
if (name === "Edit" || name === "MultiEdit") {
|
|
1046
|
+
if (path === void 0 || path.trim() === "") return void 0;
|
|
1047
|
+
const edits = Array.isArray(record["edits"]) ? record["edits"] : [record];
|
|
1048
|
+
const hunks = [];
|
|
1049
|
+
for (const edit of edits) {
|
|
1050
|
+
const oldText = typeof edit["old_string"] === "string" ? edit["old_string"] : "";
|
|
1051
|
+
const newText = typeof edit["new_string"] === "string" ? edit["new_string"] : "";
|
|
1052
|
+
if (oldText === "" && newText === "") continue;
|
|
1053
|
+
hunks.push("@@", ...oldText.split("\n").map((line) => `-${line}`), ...newText.split("\n").map((line) => `+${line}`));
|
|
1054
|
+
}
|
|
1055
|
+
if (hunks.length === 0) return void 0;
|
|
1056
|
+
return [`update: ${path}`, ...hunks].join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
if (name === "Write") {
|
|
1059
|
+
if (path === void 0 || path.trim() === "") return void 0;
|
|
1060
|
+
const content = typeof record["content"] === "string" ? record["content"] : "";
|
|
1061
|
+
if (content === "") return void 0;
|
|
1062
|
+
return [`add: ${path}`, ...content.split("\n").map((line) => `+${line}`)].join("\n");
|
|
1063
|
+
}
|
|
1064
|
+
const patch = record["patch"] ?? record["diff"];
|
|
1065
|
+
if (typeof patch === "string" && patch.trim() !== "") return patch;
|
|
1066
|
+
}
|
|
1067
|
+
/** Render a tool call's arguments into a compact summary. */
|
|
1068
|
+
function argsOf(name, args) {
|
|
1069
|
+
if (args === void 0) return void 0;
|
|
1070
|
+
if (typeof args === "string") return args.trim() === "" ? void 0 : args;
|
|
1071
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return void 0;
|
|
1072
|
+
const record = args;
|
|
1073
|
+
const edit = editArgsOf(name, record);
|
|
1074
|
+
if (edit !== void 0) return edit;
|
|
1075
|
+
for (const key of [
|
|
1076
|
+
"query",
|
|
1077
|
+
"url",
|
|
1078
|
+
"path",
|
|
1079
|
+
"command"
|
|
1080
|
+
]) {
|
|
1081
|
+
const value = record[key];
|
|
1082
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
1083
|
+
}
|
|
1084
|
+
const pairs = [];
|
|
1085
|
+
for (const [key, value] of Object.entries(record)) if (typeof value === "string" && value.trim() !== "") pairs.push(`${key}=${value.trim()}`);
|
|
1086
|
+
else if (typeof value === "number" || typeof value === "boolean") pairs.push(`${key}=${String(value)}`);
|
|
1087
|
+
if (pairs.length === 0) return void 0;
|
|
1088
|
+
const summary = pairs.join(" ");
|
|
1089
|
+
return summary.length > 200 ? `${summary.slice(0, 200)}…` : summary;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Parse a session's wire log into transcript lines plus every usage record.
|
|
1093
|
+
* @param sessionDir - the session directory.
|
|
1094
|
+
* @returns the transcript.
|
|
1095
|
+
*/
|
|
1096
|
+
async function readKimiTranscript(sessionDir) {
|
|
1097
|
+
const sessionId = sessionDir.split("session_").pop() ?? sessionDir;
|
|
1098
|
+
const lines = [];
|
|
1099
|
+
const usageRecords = [];
|
|
1100
|
+
let model;
|
|
1101
|
+
let modelTurn;
|
|
1102
|
+
let lineModel;
|
|
1103
|
+
const push = (line) => {
|
|
1104
|
+
lines.push({
|
|
1105
|
+
...line,
|
|
1106
|
+
...lineModel === void 0 ? {} : { model: lineModel }
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
let turn = 0;
|
|
1110
|
+
let turnSeen = false;
|
|
1111
|
+
const callsByUuid = /* @__PURE__ */ new Map();
|
|
1112
|
+
const callsByToolCallId = /* @__PURE__ */ new Map();
|
|
1113
|
+
let text;
|
|
1114
|
+
try {
|
|
1115
|
+
text = await readFile(wirePath(sessionDir), "utf8");
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
if (error.code === "ENOENT") return {
|
|
1118
|
+
sessionId,
|
|
1119
|
+
lines,
|
|
1120
|
+
usageRecords
|
|
1121
|
+
};
|
|
1122
|
+
throw error;
|
|
1123
|
+
}
|
|
1124
|
+
for (const raw of text.split("\n")) {
|
|
1125
|
+
const line = raw.trim();
|
|
1126
|
+
if (line === "") continue;
|
|
1127
|
+
let event;
|
|
1128
|
+
try {
|
|
1129
|
+
event = JSON.parse(line);
|
|
1130
|
+
} catch {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (event.type === "usage.record") {
|
|
1134
|
+
if (typeof event.model === "string" && event.model !== "") {
|
|
1135
|
+
model = lineModel = event.model;
|
|
1136
|
+
modelTurn = turn || 1;
|
|
1137
|
+
}
|
|
1138
|
+
const usage = usageFromWire(event.usage);
|
|
1139
|
+
if (usage !== void 0) usageRecords.push({
|
|
1140
|
+
line: lines.length,
|
|
1141
|
+
usage
|
|
1142
|
+
});
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
if (event.type === "llm.request") {
|
|
1146
|
+
lineModel = typeof event.model === "string" && event.model !== "" ? event.model : void 0;
|
|
1147
|
+
if (typeof event.model === "string" && event.model !== "") {
|
|
1148
|
+
model = event.model;
|
|
1149
|
+
modelTurn = turn || 1;
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (event.type === "turn.prompt") {
|
|
1154
|
+
lineModel = void 0;
|
|
1155
|
+
turnSeen = true;
|
|
1156
|
+
turn += 1;
|
|
1157
|
+
const prompt = textOf(event.input ?? []);
|
|
1158
|
+
if (prompt.trim() !== "" && !isSystemReminder(prompt)) push({
|
|
1159
|
+
kind: "user",
|
|
1160
|
+
text: prompt,
|
|
1161
|
+
turn: turn === 0 ? 1 : turn
|
|
1162
|
+
});
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (event.type === "context.append_message") {
|
|
1166
|
+
const message = event.message;
|
|
1167
|
+
const role = message?.role;
|
|
1168
|
+
const textContent = textOf(message?.content ?? []);
|
|
1169
|
+
if (role === "user" && textContent.trim() !== "" && !isSystemReminder(textContent)) {
|
|
1170
|
+
const last = lines[lines.length - 1];
|
|
1171
|
+
if (turnSeen && last?.kind === "user" && last.text === textContent) continue;
|
|
1172
|
+
turnSeen = true;
|
|
1173
|
+
turn += 1;
|
|
1174
|
+
lineModel = void 0;
|
|
1175
|
+
push({
|
|
1176
|
+
kind: "user",
|
|
1177
|
+
text: textContent,
|
|
1178
|
+
turn: turn === 0 ? 1 : turn
|
|
1179
|
+
});
|
|
1180
|
+
} else if (role === "assistant" && textContent.trim() !== "") push({
|
|
1181
|
+
kind: "assistant",
|
|
1182
|
+
text: textContent,
|
|
1183
|
+
turn: turn === 0 ? 1 : turn
|
|
1184
|
+
});
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
if (event.type === "context.append_loop_event") {
|
|
1188
|
+
const loop = event.event;
|
|
1189
|
+
if (loop === void 0) continue;
|
|
1190
|
+
const loopTurn = typeof loop.turnId === "number" ? loop.turnId + 1 : void 0;
|
|
1191
|
+
if (loopTurn !== void 0) {
|
|
1192
|
+
turn = Math.max(turn, loopTurn);
|
|
1193
|
+
turnSeen = true;
|
|
1194
|
+
}
|
|
1195
|
+
const part = loop.part;
|
|
1196
|
+
const lineTurn = turn === 0 ? 1 : turn;
|
|
1197
|
+
if (part?.type === "text" && part.text !== void 0 && part.text.trim() !== "") push({
|
|
1198
|
+
kind: "assistant",
|
|
1199
|
+
text: part.text,
|
|
1200
|
+
turn: lineTurn
|
|
1201
|
+
});
|
|
1202
|
+
else if (part?.type === "think" && part.think !== void 0 && part.think.trim() !== "") push({
|
|
1203
|
+
kind: "think",
|
|
1204
|
+
text: part.think,
|
|
1205
|
+
turn: lineTurn
|
|
1206
|
+
});
|
|
1207
|
+
else if (loop.type === "tool.call") {
|
|
1208
|
+
const call = loop;
|
|
1209
|
+
const name = call.toolCall?.name ?? call.name ?? "tool";
|
|
1210
|
+
const args = argsOf(name, call.toolCall?.args ?? call.args);
|
|
1211
|
+
const lineIndex = lines.length;
|
|
1212
|
+
push({
|
|
1213
|
+
kind: "tool",
|
|
1214
|
+
id: typeof call.toolCallId === "string" ? call.toolCallId : typeof call.uuid === "string" ? call.uuid : `kimi-tool-${lineIndex}`,
|
|
1215
|
+
name,
|
|
1216
|
+
...args === void 0 ? {} : { args },
|
|
1217
|
+
turn: lineTurn
|
|
1218
|
+
});
|
|
1219
|
+
if (typeof call.uuid === "string") callsByUuid.set(call.uuid, lineIndex);
|
|
1220
|
+
if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
|
|
1221
|
+
} else if (loop.type === "tool.result") {
|
|
1222
|
+
const result = loop;
|
|
1223
|
+
const output = result.result?.output;
|
|
1224
|
+
let target = typeof result.parentUuid === "string" ? callsByUuid.get(result.parentUuid) : void 0;
|
|
1225
|
+
if (target === void 0 && typeof result.toolCallId === "string") target = callsByToolCallId.get(result.toolCallId);
|
|
1226
|
+
if (target === void 0) {
|
|
1227
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) if (lines[index]?.kind === "tool") {
|
|
1228
|
+
target = index;
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (output !== void 0 && output.trim() !== "" && target !== void 0) {
|
|
1233
|
+
const current = lines[target];
|
|
1234
|
+
if (current !== void 0 && current.kind === "tool") lines[target] = {
|
|
1235
|
+
...current,
|
|
1236
|
+
result: output
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
} else if (loop.type === "content.part" && part?.type !== void 0 && part.type !== "text" && part.type !== "think") push({
|
|
1240
|
+
kind: "assistant",
|
|
1241
|
+
text: `[未支持的内容类型 ${part.type}]`,
|
|
1242
|
+
turn: lineTurn
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
sessionId,
|
|
1248
|
+
lines,
|
|
1249
|
+
usageRecords,
|
|
1250
|
+
...model === void 0 ? {} : { model },
|
|
1251
|
+
...modelTurn === void 0 ? {} : { modelTurn }
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Map a wire `usage.record` payload onto the shared usage contract. kimi
|
|
1256
|
+
* reports input (uncached), cache read, cache creation, and output; the
|
|
1257
|
+
* creation counter is its cache-write analog. Non-numeric or missing
|
|
1258
|
+
* counters are dropped so the record stays usable when kimi omits a field.
|
|
1259
|
+
* @param raw - the event's `usage` value.
|
|
1260
|
+
* @returns the shared usage record, or undefined when nothing was reported.
|
|
1261
|
+
*/
|
|
1262
|
+
function usageFromWire(raw) {
|
|
1263
|
+
const counters = raw;
|
|
1264
|
+
if (counters === void 0 || typeof counters !== "object") return void 0;
|
|
1265
|
+
const num = (value) => {
|
|
1266
|
+
const n = Number(value);
|
|
1267
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1268
|
+
};
|
|
1269
|
+
const input = num(counters.inputOther);
|
|
1270
|
+
const output = num(counters.output);
|
|
1271
|
+
const cacheRead = num(counters.inputCacheRead);
|
|
1272
|
+
const cacheWrite = num(counters.inputCacheCreation);
|
|
1273
|
+
if (input === void 0 && output === void 0 && cacheRead === void 0 && cacheWrite === void 0) return;
|
|
1274
|
+
const usage = {
|
|
1275
|
+
inputTokens: input ?? 0,
|
|
1276
|
+
outputTokens: output ?? 0
|
|
1277
|
+
};
|
|
1278
|
+
if (cacheRead !== void 0 && cacheRead > 0) usage.cacheReadTokens = cacheRead;
|
|
1279
|
+
if (cacheWrite !== void 0 && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
1280
|
+
return usage;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Sum per-request usage records into one round total. Each `usage.record` is
|
|
1284
|
+
* a single LLM request's accounting, so a round's totals are the sum of the
|
|
1285
|
+
* records inside it.
|
|
1286
|
+
* @param records - the usage records to add.
|
|
1287
|
+
* @returns the summed usage, or undefined when the list is empty.
|
|
1288
|
+
*/
|
|
1289
|
+
function sumUsageRecords(records) {
|
|
1290
|
+
let input = 0;
|
|
1291
|
+
let output = 0;
|
|
1292
|
+
let cacheRead = 0;
|
|
1293
|
+
let cacheWrite = 0;
|
|
1294
|
+
for (const record of records) {
|
|
1295
|
+
input += record.usage.inputTokens ?? 0;
|
|
1296
|
+
output += record.usage.outputTokens ?? 0;
|
|
1297
|
+
cacheRead += record.usage.cacheReadTokens ?? 0;
|
|
1298
|
+
cacheWrite += record.usage.cacheWriteTokens ?? 0;
|
|
1299
|
+
}
|
|
1300
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return void 0;
|
|
1301
|
+
const usage = {
|
|
1302
|
+
inputTokens: input,
|
|
1303
|
+
outputTokens: output
|
|
1304
|
+
};
|
|
1305
|
+
if (cacheRead > 0) usage.cacheReadTokens = cacheRead;
|
|
1306
|
+
if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
1307
|
+
return usage;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Merge two usage windows into one running total — the delegation run's
|
|
1311
|
+
* per-round accumulator: each mirror pass (live poll, settle) returns its
|
|
1312
|
+
* own window's accounting, and the disjoint windows sum to the round's
|
|
1313
|
+
* total. `undefined` operands pass through, so callers accumulate without
|
|
1314
|
+
* absence checks.
|
|
1315
|
+
* @param left - the running total, or undefined before the first window.
|
|
1316
|
+
* @param right - the next window's usage, or undefined when it had none.
|
|
1317
|
+
* @returns the merged total, or undefined when both operands are absent.
|
|
1318
|
+
*/
|
|
1319
|
+
function addTokenUsage(left, right) {
|
|
1320
|
+
if (left === void 0) return right;
|
|
1321
|
+
if (right === void 0) return left;
|
|
1322
|
+
return sumUsageRecords([{
|
|
1323
|
+
line: 0,
|
|
1324
|
+
usage: left
|
|
1325
|
+
}, {
|
|
1326
|
+
line: 0,
|
|
1327
|
+
usage: right
|
|
1328
|
+
}]);
|
|
1329
|
+
}
|
|
1330
|
+
/** Render a transcript as command-reply text. */
|
|
1331
|
+
function renderTranscript(transcript) {
|
|
1332
|
+
if (transcript.lines.length === 0) return `kimi session ${transcript.sessionId}: no readable transcript yet.`;
|
|
1333
|
+
const parts = [`kimi session ${transcript.sessionId}:`];
|
|
1334
|
+
for (const line of transcript.lines) if (line.kind === "user") parts.push(`\n用户: ${line.text}`);
|
|
1335
|
+
else if (line.kind === "assistant") parts.push(`\nkimi: ${line.text}`);
|
|
1336
|
+
else if (line.kind === "think") parts.push(`\n[思考] ${line.text}`);
|
|
1337
|
+
else {
|
|
1338
|
+
const call = `[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`;
|
|
1339
|
+
parts.push(`\n ↳ ${call}${line.result !== void 0 ? ` → ${line.result}` : ""}`);
|
|
1340
|
+
}
|
|
1341
|
+
return parts.join("\n");
|
|
1342
|
+
}
|
|
1343
|
+
//#endregion
|
|
1344
|
+
//#region lib/types/session-mirror.js
|
|
1345
|
+
/**
|
|
1346
|
+
* Mirror a kimi session's transcript into a dsh subagent session, so the
|
|
1347
|
+
* delegation shows up in the standard 子代理 surface and opening the child
|
|
1348
|
+
* session shows the full conversation (user prompts, kimi replies, thinking,
|
|
1349
|
+
* and tool activity with arguments and results) instead of a silent one-shot
|
|
1350
|
+
* call.
|
|
1351
|
+
* @module @khorsheed/dsh-local-agent-kimi/session-mirror
|
|
1352
|
+
*/
|
|
1353
|
+
/** One user-role message event. */
|
|
1354
|
+
function userEvent(text) {
|
|
1355
|
+
return createUserMessage({
|
|
1356
|
+
content: [{
|
|
1357
|
+
type: "text",
|
|
1358
|
+
text
|
|
1359
|
+
}],
|
|
1360
|
+
source: { kind: "user" }
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
/** One assistant-role message event, attributed to the kimi route. */
|
|
1364
|
+
function assistantEvent(blocks, model) {
|
|
1365
|
+
return createAssistantMessage({
|
|
1366
|
+
content: blocks,
|
|
1367
|
+
source: {
|
|
1368
|
+
provider: "kimi-cli",
|
|
1369
|
+
model: model ?? "unobserved"
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* The step the next fold pass would assign in one turn (the max existing
|
|
1375
|
+
* step + 1). The token-granularity live
|
|
1376
|
+
* driver reserves its stream merge key here LAZILY at the first delta, so
|
|
1377
|
+
* tool cards folded before the stream started keep their chronological
|
|
1378
|
+
* place below it instead of the stream squatting on step 1.
|
|
1379
|
+
* @param childSession - the run's child session.
|
|
1380
|
+
* @param turn - the round's turn number.
|
|
1381
|
+
* @returns the next free step in the turn.
|
|
1382
|
+
*/
|
|
1383
|
+
function nextKimiSessionStep(childSession, turn) {
|
|
1384
|
+
let next = 1;
|
|
1385
|
+
for (const event of childSession.snapshotEvents()) {
|
|
1386
|
+
const data = event.data;
|
|
1387
|
+
if (data.turn === turn && typeof data.step === "number") next = Math.max(next, data.step + 1);
|
|
1388
|
+
}
|
|
1389
|
+
return next;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Fold one non-tool transcript line into message blocks. Thinking maps to
|
|
1393
|
+
* the native `reasoning` block so the standard conversation renders it as
|
|
1394
|
+
* thinking rather than a `[思考]` text prefix; reply text stays text (content
|
|
1395
|
+
* is never filtered — kimi's own injections remain visible). Tool lines are
|
|
1396
|
+
* NOT folded here: they become native `tool/call`/`tool/result` event pairs
|
|
1397
|
+
* (see the mirror loop), so the standard conversation renders them as tool
|
|
1398
|
+
* cards instead of `[工具 X]` text.
|
|
1399
|
+
*/
|
|
1400
|
+
function lineBlocks(line) {
|
|
1401
|
+
if (line.kind === "think") return [{
|
|
1402
|
+
type: "reasoning",
|
|
1403
|
+
text: line.text
|
|
1404
|
+
}];
|
|
1405
|
+
return [{
|
|
1406
|
+
type: "text",
|
|
1407
|
+
text: line.text
|
|
1408
|
+
}];
|
|
1409
|
+
}
|
|
1410
|
+
/** The run-progress delta text for one transcript line (any kind). */
|
|
1411
|
+
function kimiLineProgressText(line) {
|
|
1412
|
+
if (line.kind !== "tool") return line.text;
|
|
1413
|
+
return `${`[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`}${line.result !== void 0 ? ` → ${line.result}` : ""}`;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Mirror one kimi session's transcript into the dsh subagent session: each
|
|
1417
|
+
* user prompt becomes a `user/message`, each assistant reply (including
|
|
1418
|
+
* folded thinking and tool activity) an `assistant/message`, then the events
|
|
1419
|
+
* are appended to persistence so the child session is viewable.
|
|
1420
|
+
*
|
|
1421
|
+
* A resumed round (or a live poll) passes the already-mirrored
|
|
1422
|
+
* transcript-line count so only the delta is appended — re-mirroring earlier
|
|
1423
|
+
* lines would duplicate their messages. Turn numbers come from the transcript
|
|
1424
|
+
* lines themselves (the wire's 1-based turn, matching the provider's
|
|
1425
|
+
* `turn/start` numbering); step numbers CONTINUE from the assistant messages
|
|
1426
|
+
* already in the child session, so a round mirrored across several live polls
|
|
1427
|
+
* does not restart step 1 per poll.
|
|
1428
|
+
*
|
|
1429
|
+
* Every folded step is wrapped in the `step/start`–`step/end` boundary pair
|
|
1430
|
+
* the live conversation assembler requires: without a boundary the step never
|
|
1431
|
+
* enters the location index, the assistant message resolves to a turn-level
|
|
1432
|
+
* location, and the real-time view renders nothing (only a full rebuild,
|
|
1433
|
+
* which mints step drafts from the explicit coordinates, recovers it). A
|
|
1434
|
+
* think/assistant line that completes an in-flight stream folds at the
|
|
1435
|
+
* stream's reserved step instead (see {@link KimiMirrorStreams}), and a
|
|
1436
|
+
* sequential fold never takes a step a stream reserved.
|
|
1437
|
+
*
|
|
1438
|
+
* Usage accounting is exactly-once ACROSS passes: each `usage.record` is
|
|
1439
|
+
* tagged with the transcript position it occurred at, and a pass attaches the
|
|
1440
|
+
* records in the half-open range `(fromLines, newTotal]` — the records that
|
|
1441
|
+
* trail THIS pass's lines — summed onto the delta's final assistant message.
|
|
1442
|
+
* (A record flushed only after its line was already mirrored belongs to no
|
|
1443
|
+
* later pass and is dropped; kimi flushes content and usage together per
|
|
1444
|
+
* request, so this window is a flush race, not the norm.)
|
|
1445
|
+
* @param ctx - host context carrying the session persistence service.
|
|
1446
|
+
* @param childSession - the dsh subagent session created for this delegation.
|
|
1447
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1448
|
+
* @param kimiSessionId - the kimi session to mirror; omitted mirrors the most
|
|
1449
|
+
* recent session (a one-shot `kimi -p` run always creates a fresh one).
|
|
1450
|
+
* @param fromLines - transcript lines already mirrored into the child session
|
|
1451
|
+
* (0 for the first round); only lines after this offset are appended.
|
|
1452
|
+
* @returns the new total and the newly mirrored lines' texts.
|
|
1453
|
+
*/
|
|
1454
|
+
async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, fromLines = 0, options) {
|
|
1455
|
+
let workspaces;
|
|
1456
|
+
try {
|
|
1457
|
+
workspaces = await readdir(join(homeDir, "sessions"));
|
|
1458
|
+
} catch {
|
|
1459
|
+
return {
|
|
1460
|
+
total: 0,
|
|
1461
|
+
texts: []
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
let transcript;
|
|
1465
|
+
if (kimiSessionId !== void 0) {
|
|
1466
|
+
const dirName = kimiSessionId.startsWith("session_") ? kimiSessionId : `session_${kimiSessionId}`;
|
|
1467
|
+
for (const workspace of workspaces) {
|
|
1468
|
+
const dir = join(homeDir, "sessions", workspace, dirName);
|
|
1469
|
+
try {
|
|
1470
|
+
transcript = await readKimiTranscript(dir);
|
|
1471
|
+
} catch {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (transcript.lines.length > 0) break;
|
|
1475
|
+
}
|
|
1476
|
+
} else {
|
|
1477
|
+
let newest;
|
|
1478
|
+
for (const workspace of workspaces) {
|
|
1479
|
+
let sessions;
|
|
1480
|
+
try {
|
|
1481
|
+
sessions = await readdir(join(homeDir, "sessions", workspace));
|
|
1482
|
+
} catch {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
for (const name of sessions) {
|
|
1486
|
+
if (!name.startsWith("session_")) continue;
|
|
1487
|
+
const dir = join(homeDir, "sessions", workspace, name);
|
|
1488
|
+
const meta = await stat(dir);
|
|
1489
|
+
if (newest === void 0 || meta.mtimeMs > newest.time) newest = {
|
|
1490
|
+
dir,
|
|
1491
|
+
time: meta.mtimeMs
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (newest !== void 0) try {
|
|
1496
|
+
transcript = await readKimiTranscript(newest.dir);
|
|
1497
|
+
} catch {
|
|
1498
|
+
transcript = void 0;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (transcript === void 0 || transcript.lines.length === 0) return {
|
|
1502
|
+
total: 0,
|
|
1503
|
+
texts: []
|
|
1504
|
+
};
|
|
1505
|
+
const newTotal = transcript.lines.length;
|
|
1506
|
+
const delta = transcript.lines.slice(fromLines);
|
|
1507
|
+
const observedModel = options?.turn === void 0 || transcript.modelTurn === options.turn ? transcript.model : void 0;
|
|
1508
|
+
const toolCalls = roundToolCalls(transcript.lines, options?.turn);
|
|
1509
|
+
const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= newTotal));
|
|
1510
|
+
let lastAssistant = -1;
|
|
1511
|
+
for (let index = 0; index < delta.length; index += 1) {
|
|
1512
|
+
const line = delta[index];
|
|
1513
|
+
if (line !== void 0 && line.kind !== "user" && line.kind !== "tool") lastAssistant = index;
|
|
1514
|
+
}
|
|
1515
|
+
const steps = /* @__PURE__ */ new Map();
|
|
1516
|
+
const openCalls = /* @__PURE__ */ new Map();
|
|
1517
|
+
const settledCalls = /* @__PURE__ */ new Set();
|
|
1518
|
+
for (const event of childSession.snapshotEvents()) {
|
|
1519
|
+
const data = event.data;
|
|
1520
|
+
if (typeof data.turn === "number" && typeof data.step === "number") steps.set(data.turn, Math.max(steps.get(data.turn) ?? 0, data.step + 1));
|
|
1521
|
+
if (event.type === "tool/call") {
|
|
1522
|
+
const call = event.data;
|
|
1523
|
+
openCalls.set(call.callId, {
|
|
1524
|
+
turn: call.turn,
|
|
1525
|
+
step: call.step,
|
|
1526
|
+
seq: event.seq
|
|
1527
|
+
});
|
|
1528
|
+
} else if (event.type === "tool/result") {
|
|
1529
|
+
const message = event.data.message;
|
|
1530
|
+
const id = message?.toolCallId ?? message?.content?.[0]?.toolCallId;
|
|
1531
|
+
if (id !== void 0) {
|
|
1532
|
+
openCalls.delete(id);
|
|
1533
|
+
settledCalls.add(id);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* The next sequential fold step in one turn: the running counter, shifted
|
|
1539
|
+
* past every step an in-flight stream reserved (a stream's snapshots and
|
|
1540
|
+
* its completion fold live at the reserved coordinate — a sequential fold
|
|
1541
|
+
* landing there would write a second step/start, which the assembler
|
|
1542
|
+
* rejects).
|
|
1543
|
+
*/
|
|
1544
|
+
const nextFoldStep = (turn) => {
|
|
1545
|
+
let step = steps.get(turn) ?? 1;
|
|
1546
|
+
const reserved = options?.streams?.reservedSteps(turn);
|
|
1547
|
+
if (reserved !== void 0 && reserved.length > 0) {
|
|
1548
|
+
const skip = new Set(reserved);
|
|
1549
|
+
while (skip.has(step)) step += 1;
|
|
1550
|
+
}
|
|
1551
|
+
steps.set(turn, step + 1);
|
|
1552
|
+
return step;
|
|
1553
|
+
};
|
|
1554
|
+
/** Whether this pass attached its window's usage to a folded message. */
|
|
1555
|
+
let usageAttached = false;
|
|
1556
|
+
const texts = [];
|
|
1557
|
+
for (let index = 0; index < fromLines && index < transcript.lines.length; index += 1) {
|
|
1558
|
+
const line = transcript.lines[index];
|
|
1559
|
+
if (line === void 0 || line.kind !== "tool" || line.result === void 0) continue;
|
|
1560
|
+
const call = openCalls.get(line.id);
|
|
1561
|
+
if (call === void 0) continue;
|
|
1562
|
+
openCalls.delete(line.id);
|
|
1563
|
+
settledCalls.add(line.id);
|
|
1564
|
+
childSession.append("tool/result", {
|
|
1565
|
+
turn: call.turn,
|
|
1566
|
+
step: call.step,
|
|
1567
|
+
message: createToolResultMessage({
|
|
1568
|
+
callId: line.id,
|
|
1569
|
+
content: [{
|
|
1570
|
+
type: "text",
|
|
1571
|
+
text: line.result
|
|
1572
|
+
}],
|
|
1573
|
+
isError: false
|
|
1574
|
+
})
|
|
1575
|
+
}, {
|
|
1576
|
+
surfaceOp: "append",
|
|
1577
|
+
sourceEventSeqs: [call.seq]
|
|
1578
|
+
});
|
|
1579
|
+
texts.push(kimiLineProgressText(line));
|
|
1580
|
+
}
|
|
1581
|
+
if (delta.length === 0) {
|
|
1582
|
+
if (texts.length > 0) await persistChildSession(ctx, childSession);
|
|
1583
|
+
return {
|
|
1584
|
+
total: newTotal,
|
|
1585
|
+
texts,
|
|
1586
|
+
...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
|
|
1587
|
+
...usageAttached ? { usageAttached: true } : {},
|
|
1588
|
+
...observedModel !== void 0 ? { model: observedModel } : {},
|
|
1589
|
+
...toolCalls !== void 0 ? { toolCalls } : {}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
for (let index = 0; index < delta.length; index += 1) {
|
|
1593
|
+
const line = delta[index];
|
|
1594
|
+
if (line === void 0) continue;
|
|
1595
|
+
const turn = line.turn;
|
|
1596
|
+
if (line.kind === "user") {
|
|
1597
|
+
if (userAlreadyAppended(childSession, turn, line.text)) continue;
|
|
1598
|
+
steps.set(turn, 1);
|
|
1599
|
+
childSession.append("user/message", userEvent(line.text), { surfaceOp: "append" });
|
|
1600
|
+
texts.push(line.text);
|
|
1601
|
+
} else if (line.kind === "tool") {
|
|
1602
|
+
const reserved = options?.streams?.completeTool?.(line);
|
|
1603
|
+
const step = reserved?.step ?? nextFoldStep(turn);
|
|
1604
|
+
steps.set(turn, Math.max(steps.get(turn) ?? 1, step + 1));
|
|
1605
|
+
if (reserved?.opened !== true) childSession.append("step/start", {
|
|
1606
|
+
turn,
|
|
1607
|
+
step
|
|
1608
|
+
});
|
|
1609
|
+
const call = childSession.append("tool/call", {
|
|
1610
|
+
turn,
|
|
1611
|
+
step,
|
|
1612
|
+
callId: line.id,
|
|
1613
|
+
name: line.name,
|
|
1614
|
+
arguments: line.args ?? ""
|
|
1615
|
+
});
|
|
1616
|
+
texts.push(kimiLineProgressText(line));
|
|
1617
|
+
if (line.result !== void 0) childSession.append("tool/result", {
|
|
1618
|
+
turn,
|
|
1619
|
+
step,
|
|
1620
|
+
message: createToolResultMessage({
|
|
1621
|
+
callId: line.id,
|
|
1622
|
+
content: [{
|
|
1623
|
+
type: "text",
|
|
1624
|
+
text: line.result
|
|
1625
|
+
}],
|
|
1626
|
+
isError: false
|
|
1627
|
+
})
|
|
1628
|
+
}, {
|
|
1629
|
+
surfaceOp: "append",
|
|
1630
|
+
sourceEventSeqs: [call.seq]
|
|
1631
|
+
});
|
|
1632
|
+
childSession.append("step/end", {
|
|
1633
|
+
turn,
|
|
1634
|
+
step
|
|
1635
|
+
});
|
|
1636
|
+
} else {
|
|
1637
|
+
const completion = options?.streams?.completeStream(line);
|
|
1638
|
+
if (completion !== void 0) {
|
|
1639
|
+
steps.set(turn, Math.max(steps.get(turn) ?? 1, completion.step + 1));
|
|
1640
|
+
if (!completion.opened) childSession.append("step/start", {
|
|
1641
|
+
turn,
|
|
1642
|
+
step: completion.step
|
|
1643
|
+
});
|
|
1644
|
+
if (index === lastAssistant && deltaUsage !== void 0) usageAttached = true;
|
|
1645
|
+
childSession.append("assistant/message", {
|
|
1646
|
+
turn,
|
|
1647
|
+
step: completion.step,
|
|
1648
|
+
message: assistantEvent(lineBlocks(line), line.model),
|
|
1649
|
+
stream: [],
|
|
1650
|
+
...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
|
|
1651
|
+
}, { surfaceOp: "append" });
|
|
1652
|
+
childSession.append("step/end", {
|
|
1653
|
+
turn,
|
|
1654
|
+
step: completion.step
|
|
1655
|
+
});
|
|
1656
|
+
texts.push(kimiLineProgressText(line));
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
const step = nextFoldStep(turn);
|
|
1660
|
+
childSession.append("step/start", {
|
|
1661
|
+
turn,
|
|
1662
|
+
step
|
|
1663
|
+
});
|
|
1664
|
+
if (index === lastAssistant && deltaUsage !== void 0) usageAttached = true;
|
|
1665
|
+
childSession.append("assistant/message", {
|
|
1666
|
+
turn,
|
|
1667
|
+
step,
|
|
1668
|
+
message: assistantEvent(lineBlocks(line), line.model),
|
|
1669
|
+
stream: [],
|
|
1670
|
+
...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
|
|
1671
|
+
}, { surfaceOp: "append" });
|
|
1672
|
+
childSession.append("step/end", {
|
|
1673
|
+
turn,
|
|
1674
|
+
step
|
|
1675
|
+
});
|
|
1676
|
+
texts.push(kimiLineProgressText(line));
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
await persistChildSession(ctx, childSession);
|
|
1680
|
+
return {
|
|
1681
|
+
total: newTotal,
|
|
1682
|
+
texts,
|
|
1683
|
+
...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
|
|
1684
|
+
...usageAttached ? { usageAttached: true } : {},
|
|
1685
|
+
...observedModel !== void 0 ? { model: observedModel } : {},
|
|
1686
|
+
...toolCalls !== void 0 ? { toolCalls } : {}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Count one round's tool calls out of a kimi transcript, keyed by the name the
|
|
1691
|
+
* wire gave each tool. No turn named, or no `tool.call` line carrying it,
|
|
1692
|
+
* yields undefined — a round reports nothing rather than a zero it did not
|
|
1693
|
+
* observe. Reads only the transcript lines the mirror already folded.
|
|
1694
|
+
* @param lines - the whole session transcript.
|
|
1695
|
+
* @param turn - the round's 1-based turn, when the caller named one.
|
|
1696
|
+
* @returns the round's accounting, or undefined.
|
|
1697
|
+
*/
|
|
1698
|
+
function roundToolCalls(lines, turn) {
|
|
1699
|
+
if (turn === void 0) return void 0;
|
|
1700
|
+
const byName = {};
|
|
1701
|
+
let count = 0;
|
|
1702
|
+
for (const line of lines) {
|
|
1703
|
+
if (line.kind !== "tool" || line.turn !== turn) continue;
|
|
1704
|
+
count += 1;
|
|
1705
|
+
byName[line.name] = (byName[line.name] ?? 0) + 1;
|
|
1706
|
+
}
|
|
1707
|
+
return count === 0 ? void 0 : {
|
|
1708
|
+
count,
|
|
1709
|
+
byName
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Whether the turn already carries a user/message with this exact text (the
|
|
1714
|
+
* live driver's round-start append). Scoped to the events at or after this
|
|
1715
|
+
* turn's turn/start so identical prompts across rounds stay independent.
|
|
1716
|
+
*/
|
|
1717
|
+
function userAlreadyAppended(childSession, turn, text) {
|
|
1718
|
+
let turnStartSeq = -1;
|
|
1719
|
+
for (const event of childSession.snapshotEvents()) if (event.type === "turn/start" && event.data.turn === turn) turnStartSeq = event.seq;
|
|
1720
|
+
if (turnStartSeq < 0) return false;
|
|
1721
|
+
for (const event of childSession.snapshotEvents()) {
|
|
1722
|
+
if (event.seq < turnStartSeq || event.type !== "user/message") continue;
|
|
1723
|
+
if ((event.data.content ?? []).map((block) => block.text ?? "").join("") === text) return true;
|
|
1724
|
+
}
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
//#endregion
|
|
1728
|
+
//#region lib/types/credential-guard.js
|
|
1729
|
+
/**
|
|
1730
|
+
* Kimi credential sentinel. The kimi CLI occasionally wipes
|
|
1731
|
+
* `credentials/kimi-code.json` into an empty shell (both tokens zero-length)
|
|
1732
|
+
* on its own failure paths — observed twice in production, each time killing
|
|
1733
|
+
* every later delegation with "Authentication required" until someone
|
|
1734
|
+
* restored the file by hand. The sentinel:
|
|
1735
|
+
*
|
|
1736
|
+
* - backs the file up whenever a VALID credential is observed (the backup
|
|
1737
|
+
* lives inside `credentials/` so the family's logout — a directory delete —
|
|
1738
|
+
* clears it too);
|
|
1739
|
+
* - restores the backup when the file is an exact empty shell (both tokens
|
|
1740
|
+
* empty strings) — the only corruption signature we trust;
|
|
1741
|
+
* - logs every state transition with a process snapshot, so the next wipe
|
|
1742
|
+
* can be attributed (which process, at which moment).
|
|
1743
|
+
*
|
|
1744
|
+
* Restore is deliberately narrow: a revoked-elsewhere credential never takes
|
|
1745
|
+
* the empty-shell shape, and a stale backup at worst produces one more loud
|
|
1746
|
+
* 401, never silent misbehavior.
|
|
1747
|
+
* @module @khorsheed/dsh-local-agent-kimi/credential-guard
|
|
1748
|
+
*/
|
|
1749
|
+
/** The credential file inside the scoped home, and its sentinel backup. */
|
|
1750
|
+
const CREDENTIAL_REL = join("credentials", "kimi-code.json");
|
|
1751
|
+
const BACKUP_REL = `${CREDENTIAL_REL}.bak`;
|
|
1752
|
+
/** Last observed state signature per home (`${mtimeMs}:${kind}`) — transition logging. */
|
|
1753
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
1754
|
+
/** A one-line process snapshot for wipe attribution (kimi/dsh processes only). */
|
|
1755
|
+
function processSnapshot() {
|
|
1756
|
+
return new Promise((resolve) => {
|
|
1757
|
+
execFile("ps", ["-eo", "pid,ppid,etime,command"], { timeout: 5e3 }, (error, stdout) => {
|
|
1758
|
+
if (error !== null) {
|
|
1759
|
+
resolve(`(ps failed: ${error.message})`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
resolve(stdout.split("\n").filter((line) => /\bkimi\b|dsh/.test(line) && !line.includes("grep")).slice(0, 12).join(" ; "));
|
|
1763
|
+
});
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
/** Classify the credential file's current content. */
|
|
1767
|
+
async function observe(file) {
|
|
1768
|
+
let raw;
|
|
1769
|
+
try {
|
|
1770
|
+
raw = await readFile(file, "utf8");
|
|
1771
|
+
} catch {
|
|
1772
|
+
return {
|
|
1773
|
+
state: "absent",
|
|
1774
|
+
mtimeMs: 0
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
let mtimeMs = 0;
|
|
1778
|
+
try {
|
|
1779
|
+
mtimeMs = (await stat(file)).mtimeMs;
|
|
1780
|
+
} catch {}
|
|
1781
|
+
try {
|
|
1782
|
+
const parsed = JSON.parse(raw);
|
|
1783
|
+
const empty = (v) => typeof v === "string" && v.length === 0;
|
|
1784
|
+
if (empty(parsed.access_token) && empty(parsed.refresh_token)) return {
|
|
1785
|
+
state: "empty-shell",
|
|
1786
|
+
mtimeMs
|
|
1787
|
+
};
|
|
1788
|
+
return {
|
|
1789
|
+
state: "valid",
|
|
1790
|
+
mtimeMs
|
|
1791
|
+
};
|
|
1792
|
+
} catch {
|
|
1793
|
+
return {
|
|
1794
|
+
state: "absent",
|
|
1795
|
+
mtimeMs
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Guard the scoped home's kimi credential: back up a valid file, restore the
|
|
1801
|
+
* backup over an empty shell, log transitions.
|
|
1802
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1803
|
+
* @param log - warn channel (plugin logger when available).
|
|
1804
|
+
* @returns true when a usable credential exists after the guard ran.
|
|
1805
|
+
*/
|
|
1806
|
+
async function guardKimiCredential(homeDir, log) {
|
|
1807
|
+
const file = join(homeDir, CREDENTIAL_REL);
|
|
1808
|
+
const backup = join(homeDir, BACKUP_REL);
|
|
1809
|
+
const { state, mtimeMs } = await observe(file);
|
|
1810
|
+
const signature = `${mtimeMs}:${state}`;
|
|
1811
|
+
const previous = lastSeen.get(homeDir);
|
|
1812
|
+
lastSeen.set(homeDir, signature);
|
|
1813
|
+
if (state === "valid") {
|
|
1814
|
+
if (previous !== void 0 && previous !== signature) log?.(`local-agent-kimi: credential refreshed (mtime ${new Date(mtimeMs).toISOString()}), snapshotting the backup`);
|
|
1815
|
+
await writeFile(backup, await readFile(file), { mode: 384 });
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
if (state === "empty-shell") {
|
|
1819
|
+
const snapshot = await processSnapshot();
|
|
1820
|
+
log?.(`local-agent-kimi: CREDENTIAL EMPTY SHELL detected (mtime ${new Date(mtimeMs).toISOString()}); previous state ${previous ?? "unknown"}; processes: ${snapshot}`);
|
|
1821
|
+
let backupState = "absent";
|
|
1822
|
+
try {
|
|
1823
|
+
backupState = (await observe(backup)).state;
|
|
1824
|
+
} catch {}
|
|
1825
|
+
if (backupState === "valid") {
|
|
1826
|
+
await copyFile(backup, file);
|
|
1827
|
+
log?.("local-agent-kimi: credential restored from the sentinel backup — the next delegation should succeed");
|
|
1828
|
+
return true;
|
|
1829
|
+
}
|
|
1830
|
+
log?.("local-agent-kimi: no usable backup to restore; the harness needs a fresh login");
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
//#endregion
|
|
1836
|
+
//#region lib/types/provision.js
|
|
1837
|
+
/**
|
|
1838
|
+
* Kimi Code scoped-home provisioning: a working `config.toml` (provider and
|
|
1839
|
+
* model definitions) is required for the ACP server to authenticate. The
|
|
1840
|
+
* scoped home is provisioned once — from the user's own config, redacted —
|
|
1841
|
+
* so the scoped setup mirrors the user's models while carrying no secrets.
|
|
1842
|
+
* Sign-out removes the credentials and OAuth cache so a later login can
|
|
1843
|
+
* authorize a different account. The agent preset the delegation tool row
|
|
1844
|
+
* needs is bootstrapped into the user preset root on first boot, so
|
|
1845
|
+
* installing the bundle needs no manual copy step.
|
|
1846
|
+
* @module @khorsheed/dsh-local-agent-kimi/provision
|
|
1847
|
+
*/
|
|
1848
|
+
const REAL_CONFIG = join(homedir(), ".kimi-code", "config.toml");
|
|
1849
|
+
/** Credentials directory inside the scoped home; its presence marks auth. */
|
|
1850
|
+
const CREDENTIALS_DIR$1 = "credentials";
|
|
1851
|
+
/** OAuth token cache directory inside the scoped home. */
|
|
1852
|
+
const OAUTH_DIR = "oauth";
|
|
1853
|
+
/**
|
|
1854
|
+
* The managed provider's endpoint, written into a fresh scoped config and the
|
|
1855
|
+
* baseline the eval snapshot compares against: a scoped config still pointing
|
|
1856
|
+
* here routes through kimi's own service, i.e. no custom endpoint is in force.
|
|
1857
|
+
*/
|
|
1858
|
+
const KIMI_MANAGED_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
1859
|
+
/** Minimal managed config for a fresh home with no user config to mirror. */
|
|
1860
|
+
function minimalConfig(model, thinkingEffort) {
|
|
1861
|
+
return `default_model = "${model}"
|
|
1862
|
+
|
|
1863
|
+
[thinking]
|
|
1864
|
+
enabled = true
|
|
1865
|
+
effort = "${thinkingEffort}"
|
|
1866
|
+
|
|
1867
|
+
[providers."managed:kimi-code"]
|
|
1868
|
+
base_url = "${KIMI_MANAGED_BASE_URL}"
|
|
1869
|
+
type = "kimi"
|
|
1870
|
+
api_key = ""
|
|
1871
|
+
|
|
1872
|
+
[providers."managed:kimi-code".oauth]
|
|
1873
|
+
storage = "file"
|
|
1874
|
+
key = "oauth/kimi-code"
|
|
1875
|
+
|
|
1876
|
+
[models."${model}"]
|
|
1877
|
+
provider = "managed:kimi-code"
|
|
1878
|
+
model = "${model.split("/").pop()}"
|
|
1879
|
+
max_context_size = 1048576
|
|
1880
|
+
capabilities = [ "thinking", "always_thinking", "image_in", "video_in", "tool_use" ]
|
|
1881
|
+
display_name = "${model}"
|
|
1882
|
+
support_efforts = [ "low", "high", "max" ]
|
|
1883
|
+
default_effort = "${thinkingEffort}"
|
|
1884
|
+
`;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Redact every api_key value so the scoped copy carries no secrets. The
|
|
1888
|
+
* kimi-managed provider keeps an empty api_key (its auth routes through
|
|
1889
|
+
* oauth), which is exactly what an empty value means here.
|
|
1890
|
+
* @param config - the user's config text.
|
|
1891
|
+
* @returns the same config with all api_key values blanked.
|
|
1892
|
+
*/
|
|
1893
|
+
function redactApiKeys(config) {
|
|
1894
|
+
return config.replace(/api_key\s*=\s*"[^"]*"/gu, "api_key = \"\"");
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Provision `<homeDir>/config.toml` unless one already exists. A present
|
|
1898
|
+
* config is respected untouched (a person may have edited it); otherwise the
|
|
1899
|
+
* user's own real config is copied redacted so the scoped home mirrors the
|
|
1900
|
+
* user's models; with no real config, a minimal kimi-managed config is
|
|
1901
|
+
* written. `thinkingEffort` feeds the minimal config's `[thinking] effort`
|
|
1902
|
+
* (and the model's `default_effort`) — it applies at provision time only,
|
|
1903
|
+
* never retroactively to an existing config.
|
|
1904
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1905
|
+
* @param model - the kimi-managed model id for a fresh minimal config.
|
|
1906
|
+
* @param thinkingEffort - the reasoning effort a fresh minimal config pins.
|
|
1907
|
+
* @returns true when a config was written, false when one already existed.
|
|
1908
|
+
*/
|
|
1909
|
+
async function provisionKimiConfig(homeDir, model, thinkingEffort) {
|
|
1910
|
+
try {
|
|
1911
|
+
await readFile(join(homeDir, "config.toml"));
|
|
1912
|
+
return false;
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
if (error.code !== "ENOENT") throw error;
|
|
1915
|
+
}
|
|
1916
|
+
let config = minimalConfig(model, thinkingEffort);
|
|
1917
|
+
try {
|
|
1918
|
+
config = redactApiKeys(await readFile(REAL_CONFIG, "utf8"));
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
if (error.code !== "ENOENT") throw error;
|
|
1921
|
+
}
|
|
1922
|
+
await writeFile(join(homeDir, "config.toml"), config, { flag: "wx" });
|
|
1923
|
+
return true;
|
|
1924
|
+
}
|
|
1925
|
+
/** The managed-service provider section provisioned into the scoped config. */
|
|
1926
|
+
const MANAGED_PROVIDER_SECTION = "providers.\"managed:kimi-code\"";
|
|
1927
|
+
/**
|
|
1928
|
+
* Read the scoped config's effective LLM endpoint: the
|
|
1929
|
+
* `[providers."managed:kimi-code"].base_url` key, if present. A missing or
|
|
1930
|
+
* malformed config yields undefined (the CLI falls back to its default
|
|
1931
|
+
* endpoint). Used only for diagnostics — the config itself is authoritative,
|
|
1932
|
+
* and a user-edited value is respected untouched.
|
|
1933
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1934
|
+
* @returns the configured base URL, or undefined when absent/unreadable.
|
|
1935
|
+
*/
|
|
1936
|
+
async function readKimiBaseUrl(homeDir) {
|
|
1937
|
+
let text;
|
|
1938
|
+
try {
|
|
1939
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1940
|
+
} catch {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
let section = "";
|
|
1944
|
+
for (const rawLine of text.split("\n")) {
|
|
1945
|
+
const line = rawLine.trim();
|
|
1946
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
1947
|
+
if (header !== null) {
|
|
1948
|
+
section = header[1];
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (section === MANAGED_PROVIDER_SECTION) {
|
|
1952
|
+
const match = /^base_url\s*=\s*"([^"]*)"$/.exec(line);
|
|
1953
|
+
if (match !== null) return match[1];
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Read the scoped config's configured default model: the top-level
|
|
1959
|
+
* `default_model` key — the selection a plain `kimi -p` round runs with. A
|
|
1960
|
+
* missing or malformed config, or no key, yields undefined — the honest
|
|
1961
|
+
* "unknown", never a guessed default. Read-only: the config is authoritative
|
|
1962
|
+
* and a user-edited value is reported as-is.
|
|
1963
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1964
|
+
* @returns the configured model identifier, or undefined when none is set.
|
|
1965
|
+
*/
|
|
1966
|
+
async function readKimiDefaultModel(homeDir) {
|
|
1967
|
+
let text;
|
|
1968
|
+
try {
|
|
1969
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1970
|
+
} catch {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
let section = "";
|
|
1974
|
+
for (const rawLine of text.split("\n")) {
|
|
1975
|
+
const line = rawLine.trim();
|
|
1976
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
1977
|
+
if (header !== null) {
|
|
1978
|
+
section = header[1];
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
if (section !== "") continue;
|
|
1982
|
+
const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
|
|
1983
|
+
if (key !== null && key[1] === "default_model") return key[2];
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* List the model identifiers the scoped config DEFINES: every
|
|
1988
|
+
* `[models."…"]` table key (bare `[models.x]` keys included). This is the
|
|
1989
|
+
* pickable vocabulary a model surface can offer without hardcoding a catalog —
|
|
1990
|
+
* the same table `-m` resolves against. A missing or malformed config yields
|
|
1991
|
+
* an empty list. Read-only: the config is authoritative.
|
|
1992
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1993
|
+
* @returns the defined model identifiers, in file order, deduplicated.
|
|
1994
|
+
*/
|
|
1995
|
+
async function listKimiConfigModels(homeDir) {
|
|
1996
|
+
let text;
|
|
1997
|
+
try {
|
|
1998
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1999
|
+
} catch {
|
|
2000
|
+
return [];
|
|
2001
|
+
}
|
|
2002
|
+
const models = [];
|
|
2003
|
+
for (const rawLine of text.split("\n")) {
|
|
2004
|
+
const header = /^\[+([^\]]+)\]+$/.exec(rawLine.trim());
|
|
2005
|
+
if (header === null) continue;
|
|
2006
|
+
const table = /^models\.(?:"([^"]+)"|([\w-]+))$/.exec(header[1]);
|
|
2007
|
+
const name = table?.[1] ?? table?.[2];
|
|
2008
|
+
if (name !== void 0 && name !== "" && !models.includes(name)) models.push(name);
|
|
2009
|
+
}
|
|
2010
|
+
return models;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Rewrite the scoped config's top-level `default_model` to `model`, so the
|
|
2014
|
+
* NEXT process started under this home runs that model. This is the resident
|
|
2015
|
+
* path's only lever: `kimi acp` takes no model flag (verified against kimi
|
|
2016
|
+
* 0.39.1), while `kimi -p` takes `-m` — so the one-shot rounds pass the flag
|
|
2017
|
+
* and the live driver writes the file before spawning its runtime.
|
|
2018
|
+
*
|
|
2019
|
+
* The write is surgical: the first top-level `default_model` assignment is
|
|
2020
|
+
* replaced in place and everything else — model tables, providers, thinking
|
|
2021
|
+
* effort, comments, key order — is left byte-identical. A config with no
|
|
2022
|
+
* top-level `default_model` gains one as its first line; an absent or
|
|
2023
|
+
* unreadable config is left alone (provisioning owns creation, and inventing
|
|
2024
|
+
* a config here would hide a broken home).
|
|
2025
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
2026
|
+
* @param model - the model identifier to pin as the default.
|
|
2027
|
+
* @returns true when the file now names `model`, false when there was no
|
|
2028
|
+
* config to write into.
|
|
2029
|
+
*/
|
|
2030
|
+
async function writeKimiDefaultModel(homeDir, model) {
|
|
2031
|
+
const path = join(homeDir, "config.toml");
|
|
2032
|
+
let text;
|
|
2033
|
+
try {
|
|
2034
|
+
text = await readFile(path, "utf8");
|
|
2035
|
+
} catch {
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
const assignment = `default_model = "${model.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
|
|
2039
|
+
const lines = text.split("\n");
|
|
2040
|
+
let section = "";
|
|
2041
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
2042
|
+
const line = rawLine.trim();
|
|
2043
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
2044
|
+
if (header !== null) {
|
|
2045
|
+
section = header[1];
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (section !== "") continue;
|
|
2049
|
+
const key = /^(\w+)\s*=/.exec(line);
|
|
2050
|
+
if (key === null || key[1] !== "default_model") continue;
|
|
2051
|
+
if (line === assignment) return true;
|
|
2052
|
+
lines[index] = assignment;
|
|
2053
|
+
await writeFile(path, lines.join("\n"), "utf8");
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
2056
|
+
await writeFile(path, `${assignment}\n${text}`, "utf8");
|
|
2057
|
+
return true;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Read the scoped config's effective reasoning effort: the `[thinking] effort`
|
|
2061
|
+
* key, falling back to a `[models."…"]` section's `default_effort` when the
|
|
2062
|
+
* thinking table carries none (the CLI resolves effort the same way). A
|
|
2063
|
+
* missing or malformed config, or no effort key anywhere, yields undefined —
|
|
2064
|
+
* the honest "unknown", never a guessed default. Read-only: the config is
|
|
2065
|
+
* authoritative, a user-edited value is reported as-is.
|
|
2066
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
2067
|
+
* @returns the effective effort, or undefined when it cannot be determined.
|
|
2068
|
+
*/
|
|
2069
|
+
async function readKimiReasoningEffort(homeDir) {
|
|
2070
|
+
let text;
|
|
2071
|
+
try {
|
|
2072
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
2073
|
+
} catch {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
let section = "";
|
|
2077
|
+
let thinkingEffort;
|
|
2078
|
+
let modelDefaultEffort;
|
|
2079
|
+
for (const rawLine of text.split("\n")) {
|
|
2080
|
+
const line = rawLine.trim();
|
|
2081
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
2082
|
+
if (header !== null) {
|
|
2083
|
+
section = header[1];
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
|
|
2087
|
+
if (key === null) continue;
|
|
2088
|
+
if (section === "thinking" && key[1] === "effort" && thinkingEffort === void 0) thinkingEffort = key[2];
|
|
2089
|
+
if (section.startsWith("models.") && key[1] === "default_effort" && modelDefaultEffort === void 0) modelDefaultEffort = key[2];
|
|
2090
|
+
}
|
|
2091
|
+
return thinkingEffort ?? modelDefaultEffort;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Whether the scoped config's permission rules auto-approve the subagent's
|
|
2095
|
+
* tool use: the `Bash(*)` allow rule the provisioning gate
|
|
2096
|
+
* ({@link ensureKimiPermissions}) keys on, detected with the exact same
|
|
2097
|
+
* substring so the snapshot and the provisioning can never disagree about
|
|
2098
|
+
* the same file.
|
|
2099
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
2100
|
+
* @returns whether the auto-approve rule is present.
|
|
2101
|
+
*/
|
|
2102
|
+
async function readKimiAutoApprove(homeDir) {
|
|
2103
|
+
let text;
|
|
2104
|
+
try {
|
|
2105
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
2106
|
+
} catch {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
return text.includes("pattern = \"Bash(*)");
|
|
2110
|
+
}
|
|
2111
|
+
/** Permission rule block letting the kimi subagent run shell commands. */
|
|
2112
|
+
const KIMI_PERMISSION_BLOCK = `
|
|
2113
|
+
[[permission.rules]]
|
|
2114
|
+
decision = "allow"
|
|
2115
|
+
pattern = "Bash(*)"
|
|
2116
|
+
reason = "let the kimi subagent run shell commands"
|
|
2117
|
+
`;
|
|
2118
|
+
/**
|
|
2119
|
+
* Ensure the scoped config allows the subagent's tool use. The one-shot
|
|
2120
|
+
* `kimi -p` child uses the config's permission rules, so without a rule it
|
|
2121
|
+
* answers without executing tools. Idempotent; an existing rule is respected.
|
|
2122
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
2123
|
+
*/
|
|
2124
|
+
async function ensureKimiPermissions(homeDir) {
|
|
2125
|
+
const file = join(homeDir, "config.toml");
|
|
2126
|
+
let content;
|
|
2127
|
+
try {
|
|
2128
|
+
content = await readFile(file, "utf8");
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
if (error.code === "ENOENT") return;
|
|
2131
|
+
throw error;
|
|
2132
|
+
}
|
|
2133
|
+
if (content.includes("pattern = \"Bash(*)")) return;
|
|
2134
|
+
await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Sign out of the scoped account: remove the credentials and OAuth token
|
|
2138
|
+
* cache, so `/<name> status` reports not authenticated and the next login
|
|
2139
|
+
* authorizes a fresh account. The kimi CLI has no `logout` command, so the
|
|
2140
|
+
* scoped files are the logout path.
|
|
2141
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
2142
|
+
*/
|
|
2143
|
+
async function kimiLogout(homeDir) {
|
|
2144
|
+
await Promise.all([rm(join(homeDir, CREDENTIALS_DIR$1), {
|
|
2145
|
+
recursive: true,
|
|
2146
|
+
force: true
|
|
2147
|
+
}), rm(join(homeDir, OAUTH_DIR), {
|
|
2148
|
+
recursive: true,
|
|
2149
|
+
force: true
|
|
2150
|
+
})]);
|
|
2151
|
+
}
|
|
2152
|
+
//#endregion
|
|
2153
|
+
//#region lib/types/live-driver.js
|
|
2154
|
+
/**
|
|
2155
|
+
* The kimi provider's live driver: one resident `kimi acp` process per member
|
|
2156
|
+
* (child session), driven over the vendor's Agent Client Protocol (ACP) —
|
|
2157
|
+
* JSON-RPC NDJSON over stdio (protocol-probed against kimi 0.36.1; the
|
|
2158
|
+
* harness `subagent-acp` package is the reference implementation). A
|
|
2159
|
+
* delegation round is a `session/prompt` on the member's resident session
|
|
2160
|
+
* instead of a fresh `kimi -p` process, which buys what the process boundary
|
|
2161
|
+
* could not: runtime-level graceful interrupt (`session/cancel`; the process
|
|
2162
|
+
* survives and the session stays continuable) and push-driven mirroring
|
|
2163
|
+
* (`session/update` notifications trigger throttled file-mirror passes — see
|
|
2164
|
+
* below why the transcript itself stays file-sourced).
|
|
2165
|
+
*
|
|
2166
|
+
* Mirroring contract (the kimi-specific call): kimi's ACP updates are
|
|
2167
|
+
* token-level chunks, NOT the wire.jsonl line fold the exec mirror owns — so
|
|
2168
|
+
* completed items always fold from the file: push events merely TRIGGER
|
|
2169
|
+
* throttled `mirrorKimiDelta` passes and the settle pass stays authoritative
|
|
2170
|
+
* (one fold, one offset, zero divergence between the two drivers). Every
|
|
2171
|
+
* transcript item folds 1:1 into the child session log.
|
|
2172
|
+
* Live output also mirrors the in-flight item: chunk
|
|
2173
|
+
* deltas accumulate per streaming item and append throttled snapshot
|
|
2174
|
+
* assistant/messages at the item's reserved (turn, step) — the host folds
|
|
2175
|
+
* repeated settles at one coordinate into one live-updating chat node — and
|
|
2176
|
+
* the wire line completing the item folds at the same step, finalizing it.
|
|
2177
|
+
*
|
|
2178
|
+
* Lifecycle mirrors the dsh/codex live drivers' discipline: lazy spawn with an
|
|
2179
|
+
* initialize handshake (loadSession capability required — crash recovery is
|
|
2180
|
+
* `session/load` of the recorded session id), one in-flight spawn per member,
|
|
2181
|
+
* idle-timeout reclaim (stdin EOF → grace → SIGTERM ladder), a channel breaker
|
|
2182
|
+
* with cooldown, `disposeAll` on unload, and cancellation honored in every
|
|
2183
|
+
* window. The wire adapter is self-contained (no `@agentclientprotocol/sdk`
|
|
2184
|
+
* dependency): StringDecoder framing, request correlation, and unattended
|
|
2185
|
+
* auto-answers for `session/request_permission` (allow — matching the exec
|
|
2186
|
+
* mode's auto-approve behavior; the proposal's default policy).
|
|
2187
|
+
* @module @khorsheed/dsh-local-agent-kimi/live-driver
|
|
2188
|
+
*/
|
|
2189
|
+
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
2190
|
+
const DEFAULT_LIVE_IDLE_MS = 18e5;
|
|
2191
|
+
/** Default timeout for a wire request acknowledgement (never for session/prompt). */
|
|
2192
|
+
const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 3e4;
|
|
2193
|
+
/** Default timeout for the ACP handshake (a cold boot can be slow). */
|
|
2194
|
+
const DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS = 6e4;
|
|
2195
|
+
/** Bounded wait for an interrupted turn to converge while a run disposes. */
|
|
2196
|
+
const DEFAULT_LIVE_DISPOSE_CONVERGE_MS = 5e3;
|
|
2197
|
+
/** How long a tripped channel breaker stays on the exec fallback. */
|
|
2198
|
+
const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
|
|
2199
|
+
/** Default minimum interval between one streaming item's snapshot messages. */
|
|
2200
|
+
const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
|
|
2201
|
+
/** Grace between stdin EOF and SIGTERM when reclaiming an ACP server. */
|
|
2202
|
+
const RECLAIM_EOF_GRACE_MS = 1e3;
|
|
2203
|
+
/** The ACP protocol version this driver speaks (kimi 0.36.1 probe). */
|
|
2204
|
+
const ACP_PROTOCOL_VERSION = 1;
|
|
2205
|
+
/**
|
|
2206
|
+
* The ACP channel could not come up (spawn failure, handshake
|
|
2207
|
+
* timeout/mismatch, or a missing loadSession capability). The provider
|
|
2208
|
+
* catches exactly this and falls back to the exec one-shot; any other error
|
|
2209
|
+
* is a real round failure.
|
|
2210
|
+
*/
|
|
2211
|
+
var LiveChannelUnavailableError = class extends Error {
|
|
2212
|
+
constructor(message) {
|
|
2213
|
+
super(message);
|
|
2214
|
+
this.name = "LiveChannelUnavailableError";
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
const DEFAULT_TIMEOUTS = {
|
|
2218
|
+
initializeMs: DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS,
|
|
2219
|
+
requestMs: DEFAULT_LIVE_REQUEST_TIMEOUT_MS,
|
|
2220
|
+
convergeMs: DEFAULT_LIVE_DISPOSE_CONVERGE_MS,
|
|
2221
|
+
channelRetryMs: DEFAULT_LIVE_CHANNEL_RETRY_MS,
|
|
2222
|
+
mirrorThrottleMs: 500
|
|
2223
|
+
};
|
|
2224
|
+
/** Settle-mirror quiescence: poll cadence, required consecutive stable reads, and the overall bound. */
|
|
2225
|
+
const SETTLE_MIRROR_POLL_MS = 300;
|
|
2226
|
+
const SETTLE_MIRROR_STABLE_READS = 3;
|
|
2227
|
+
const SETTLE_MIRROR_QUIESCE_MS = 3e3;
|
|
2228
|
+
function thrown$1(value) {
|
|
2229
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Auth-shaped failure signatures in kimi's error output: the endpoint
|
|
2233
|
+
* rejected the credential. Mirrors the exec path's detector
|
|
2234
|
+
* (KIMI_AUTH_FAILURE in kimi-cli-provider.ts); the live path matches against
|
|
2235
|
+
* session/prompt failure messages, which is where a mid-run 401 surfaces
|
|
2236
|
+
* when the process does not exit.
|
|
2237
|
+
*/
|
|
2238
|
+
const KIMI_LIVE_AUTH_FAILURE = /401|unauthorized|invalid api key|not authenticated|authentication required/i;
|
|
2239
|
+
/**
|
|
2240
|
+
* kimi ACP session ids are directory names (`session_<uuid>`); the family
|
|
2241
|
+
* delegation record convention is the bare uuid (the exec path's settle-time
|
|
2242
|
+
* stderr parse). Strip the prefix so live- and exec-written records stay
|
|
2243
|
+
* interchangeable.
|
|
2244
|
+
*/
|
|
2245
|
+
function bareKimiSessionId(acpSessionId) {
|
|
2246
|
+
return acpSessionId.startsWith("session_") ? acpSessionId.slice(8) : acpSessionId;
|
|
2247
|
+
}
|
|
2248
|
+
/** The ACP-native form of a recorded kimi session id (idempotent). */
|
|
2249
|
+
function acpKimiSessionId(recordedId) {
|
|
2250
|
+
return recordedId.startsWith("session_") ? recordedId : `session_${recordedId}`;
|
|
2251
|
+
}
|
|
2252
|
+
function delay(ms) {
|
|
2253
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Map an ACP prompt stop reason onto the shared stop contract (the harness
|
|
2257
|
+
* subagent-acp mapping, verbatim): an unclean stop is never 'completed'.
|
|
2258
|
+
*/
|
|
2259
|
+
function acpStopReasonToHarness(reason) {
|
|
2260
|
+
switch (reason) {
|
|
2261
|
+
case "end_turn": return "completed";
|
|
2262
|
+
case "max_tokens": return "max-tokens";
|
|
2263
|
+
case "refusal": return "refusal";
|
|
2264
|
+
case "cancelled": return "aborted";
|
|
2265
|
+
default: return "error";
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* The ACP wire peer: newline-delimited JSON-RPC over the child's stdio.
|
|
2270
|
+
* Self-contained on purpose (no SDK dependency): StringDecoder framing for
|
|
2271
|
+
* split UTF-8 tails, request correlation, and server→client request answering
|
|
2272
|
+
* (the permission surface).
|
|
2273
|
+
*/
|
|
2274
|
+
var AcpWirePeer = class {
|
|
2275
|
+
output;
|
|
2276
|
+
warn;
|
|
2277
|
+
onServerRequest;
|
|
2278
|
+
onNotification;
|
|
2279
|
+
requestMs;
|
|
2280
|
+
buffer = "";
|
|
2281
|
+
decoder = new StringDecoder("utf8");
|
|
2282
|
+
nextId = 0;
|
|
2283
|
+
pending = /* @__PURE__ */ new Map();
|
|
2284
|
+
closed = false;
|
|
2285
|
+
constructor(input, output, warn, onServerRequest, onNotification, requestMs) {
|
|
2286
|
+
this.output = output;
|
|
2287
|
+
this.warn = warn;
|
|
2288
|
+
this.onServerRequest = onServerRequest;
|
|
2289
|
+
this.onNotification = onNotification;
|
|
2290
|
+
this.requestMs = requestMs;
|
|
2291
|
+
input?.on("data", (chunk) => {
|
|
2292
|
+
this.feed(this.decoder.write(chunk));
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
write(message) {
|
|
2296
|
+
this.output?.write(JSON.stringify(message) + "\n");
|
|
2297
|
+
}
|
|
2298
|
+
/** Send one request; `timeoutMs` 0 means turn-length (no timer — session/prompt). */
|
|
2299
|
+
request(method, params, timeoutMs) {
|
|
2300
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("subagent-kimi live: the wire is closed"));
|
|
2301
|
+
this.nextId += 1;
|
|
2302
|
+
const id = `req_${this.nextId}`;
|
|
2303
|
+
const timeout = timeoutMs ?? this.requestMs;
|
|
2304
|
+
return new Promise((resolve, reject) => {
|
|
2305
|
+
let timer;
|
|
2306
|
+
if (timeout > 0) {
|
|
2307
|
+
timer = setTimeout(() => {
|
|
2308
|
+
this.pending.delete(id);
|
|
2309
|
+
reject(/* @__PURE__ */ new Error(`subagent-kimi live: ${method} timed out`));
|
|
2310
|
+
timer?.unref();
|
|
2311
|
+
}, timeout);
|
|
2312
|
+
timer.unref();
|
|
2313
|
+
}
|
|
2314
|
+
this.pending.set(id, {
|
|
2315
|
+
resolve: (result) => resolve(result),
|
|
2316
|
+
reject,
|
|
2317
|
+
timer
|
|
2318
|
+
});
|
|
2319
|
+
this.write({
|
|
2320
|
+
jsonrpc: "2.0",
|
|
2321
|
+
id,
|
|
2322
|
+
method,
|
|
2323
|
+
params
|
|
2324
|
+
});
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
notify(method, params) {
|
|
2328
|
+
this.write(params === void 0 ? {
|
|
2329
|
+
jsonrpc: "2.0",
|
|
2330
|
+
method
|
|
2331
|
+
} : {
|
|
2332
|
+
jsonrpc: "2.0",
|
|
2333
|
+
method,
|
|
2334
|
+
params
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
/** Detach and reject every outstanding request (a turn-length prompt settles here on teardown). */
|
|
2338
|
+
close() {
|
|
2339
|
+
if (this.closed) return;
|
|
2340
|
+
this.closed = true;
|
|
2341
|
+
for (const request of this.pending.values()) {
|
|
2342
|
+
if (request.timer !== void 0) clearTimeout(request.timer);
|
|
2343
|
+
request.reject(/* @__PURE__ */ new Error("subagent-kimi live: the wire closed"));
|
|
2344
|
+
}
|
|
2345
|
+
this.pending.clear();
|
|
2346
|
+
}
|
|
2347
|
+
feed(text) {
|
|
2348
|
+
this.buffer += text;
|
|
2349
|
+
let index = this.buffer.indexOf("\n");
|
|
2350
|
+
while (index >= 0) {
|
|
2351
|
+
const line = this.buffer.slice(0, index);
|
|
2352
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
2353
|
+
index = this.buffer.indexOf("\n");
|
|
2354
|
+
if (line.trim() !== "") this.dispatchLine(line);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
/** Handle one wire line; a malformed or hostile line never escapes this frame. */
|
|
2358
|
+
dispatchLine(line) {
|
|
2359
|
+
let message;
|
|
2360
|
+
try {
|
|
2361
|
+
message = JSON.parse(line);
|
|
2362
|
+
} catch {
|
|
2363
|
+
this.warn("subagent-kimi live: ignored a malformed wire line");
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
const hasId = typeof message.id === "string" || typeof message.id === "number";
|
|
2368
|
+
if (hasId && typeof message.method === "string") {
|
|
2369
|
+
const id = message.id;
|
|
2370
|
+
try {
|
|
2371
|
+
const result = this.onServerRequest(message.method, message.params ?? {});
|
|
2372
|
+
this.write({
|
|
2373
|
+
jsonrpc: "2.0",
|
|
2374
|
+
id,
|
|
2375
|
+
result
|
|
2376
|
+
});
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
this.write({
|
|
2379
|
+
jsonrpc: "2.0",
|
|
2380
|
+
id,
|
|
2381
|
+
error: {
|
|
2382
|
+
code: -32603,
|
|
2383
|
+
message: thrown$1(error).message
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
if (hasId && ("result" in message || "error" in message)) {
|
|
2390
|
+
const request = this.pending.get(String(message.id));
|
|
2391
|
+
if (request === void 0) return;
|
|
2392
|
+
this.pending.delete(String(message.id));
|
|
2393
|
+
if (request.timer !== void 0) clearTimeout(request.timer);
|
|
2394
|
+
const wireError = message.error;
|
|
2395
|
+
if (wireError !== void 0 && wireError !== null) request.reject(/* @__PURE__ */ new Error(`subagent-kimi live: ${wireError.message ?? "wire error"}`));
|
|
2396
|
+
else request.resolve(message.result);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (typeof message.method === "string") this.onNotification(message.method, message.params ?? {});
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
this.warn(`subagent-kimi live: wire line handler failed: ${thrown$1(error).message}`);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
/**
|
|
2406
|
+
* One resident `kimi acp` process: the wire peer, the loaded session id, the
|
|
2407
|
+
* per-session turn chain (one prompt at a time — a cancelled turn converges
|
|
2408
|
+
* before the next prompt goes out), and the reclaim ladder.
|
|
2409
|
+
*/
|
|
2410
|
+
var KimiLiveRuntime = class {
|
|
2411
|
+
child;
|
|
2412
|
+
dead = false;
|
|
2413
|
+
reclaimed = false;
|
|
2414
|
+
peer;
|
|
2415
|
+
/** The member's ACP session; assigned by the first round's session/new or session/load. */
|
|
2416
|
+
sessionId;
|
|
2417
|
+
/**
|
|
2418
|
+
* The model this process was spawned with (the scoped `default_model` value
|
|
2419
|
+
* rewritten just before spawn; undefined means the config decided). A round
|
|
2420
|
+
* resolving a different model for the member retires this runtime so the
|
|
2421
|
+
* respawn binds the new one.
|
|
2422
|
+
*/
|
|
2423
|
+
configurationKey;
|
|
2424
|
+
boundModel;
|
|
2425
|
+
modelConfiguration;
|
|
2426
|
+
/** Serializes session/prompt per member (converge-before-next-turn). */
|
|
2427
|
+
turnChain = Promise.resolve();
|
|
2428
|
+
/** The active round's notification sink; installed per round, cleared at settle. */
|
|
2429
|
+
onSessionUpdate;
|
|
2430
|
+
onDead;
|
|
2431
|
+
stderrTail = "";
|
|
2432
|
+
stderrDecoder = new StringDecoder("utf8");
|
|
2433
|
+
constructor(child, onServerRequest, warn, requestMs) {
|
|
2434
|
+
this.child = child;
|
|
2435
|
+
this.peer = new AcpWirePeer(child.stdout, child.stdin, warn, onServerRequest, (method, params) => {
|
|
2436
|
+
if (method !== "session/update") return;
|
|
2437
|
+
if (params["sessionId"] !== this.sessionId) return;
|
|
2438
|
+
const update = params["update"] ?? {};
|
|
2439
|
+
if (update["sessionUpdate"] === "config_option_update") this.modelConfiguration = kimiNativeConfiguration(update);
|
|
2440
|
+
this.onSessionUpdate?.(update);
|
|
2441
|
+
}, requestMs);
|
|
2442
|
+
child.stderr?.on("data", (chunk) => {
|
|
2443
|
+
this.stderrTail = (this.stderrTail + this.stderrDecoder.write(chunk)).slice(-4096);
|
|
2444
|
+
});
|
|
2445
|
+
child.done.then(() => {
|
|
2446
|
+
this.markDead();
|
|
2447
|
+
}, () => {
|
|
2448
|
+
this.markDead();
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
get diagnostics() {
|
|
2452
|
+
return this.stderrTail.trim();
|
|
2453
|
+
}
|
|
2454
|
+
markDead() {
|
|
2455
|
+
if (this.dead) return;
|
|
2456
|
+
this.dead = true;
|
|
2457
|
+
this.peer.close();
|
|
2458
|
+
this.onDead?.();
|
|
2459
|
+
}
|
|
2460
|
+
/** Graceful teardown: close the wire, EOF stdin, grace, then the terminate ladder. */
|
|
2461
|
+
async reclaim() {
|
|
2462
|
+
if (this.reclaimed) return;
|
|
2463
|
+
this.reclaimed = true;
|
|
2464
|
+
this.peer.close();
|
|
2465
|
+
if (!this.dead) {
|
|
2466
|
+
this.child.stdin?.end();
|
|
2467
|
+
await Promise.race([this.child.done.catch(() => {}), delay(RECLAIM_EOF_GRACE_MS)]);
|
|
2468
|
+
}
|
|
2469
|
+
if (!this.dead) {
|
|
2470
|
+
this.child.terminate();
|
|
2471
|
+
await this.child.waitForExit();
|
|
2472
|
+
}
|
|
2473
|
+
await this.child.done.catch(() => {});
|
|
2474
|
+
this.markDead();
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
/**
|
|
2478
|
+
* The kimi live driver owns every resident ACP runtime of this provider
|
|
2479
|
+
* generation. Created only when `live: true`; disposed with the plugin.
|
|
2480
|
+
*/
|
|
2481
|
+
var KimiAcpLiveDriver = class {
|
|
2482
|
+
ctx;
|
|
2483
|
+
config;
|
|
2484
|
+
timeouts;
|
|
2485
|
+
runtimes = /* @__PURE__ */ new Map();
|
|
2486
|
+
ensuring = /* @__PURE__ */ new Map();
|
|
2487
|
+
/** The model each in-flight spawn is binding (a stale-model spawn never serves a switched member). */
|
|
2488
|
+
ensuringModel = /* @__PURE__ */ new Map();
|
|
2489
|
+
ensuringConfiguration = /* @__PURE__ */ new Map();
|
|
2490
|
+
idleTimers = /* @__PURE__ */ new Map();
|
|
2491
|
+
/** Per-member round serialization (the resume lock covers resume-vs-resume only). */
|
|
2492
|
+
roundChains = /* @__PURE__ */ new Map();
|
|
2493
|
+
channelBrokenAt;
|
|
2494
|
+
disposed = false;
|
|
2495
|
+
/**
|
|
2496
|
+
* Set by drain() (a settings-driven generation handoff): new rounds are
|
|
2497
|
+
* refused so the provider falls back to exec, while in-flight rounds finish
|
|
2498
|
+
* on their runtime undisturbed. Unlike `disposed`, the driver still serves
|
|
2499
|
+
* what it already accepted.
|
|
2500
|
+
*/
|
|
2501
|
+
draining = false;
|
|
2502
|
+
disposeController = new AbortController();
|
|
2503
|
+
constructor(ctx, config = {}, timeouts = DEFAULT_TIMEOUTS) {
|
|
2504
|
+
this.ctx = ctx;
|
|
2505
|
+
this.config = config;
|
|
2506
|
+
this.timeouts = timeouts;
|
|
2507
|
+
}
|
|
2508
|
+
/** Whether the channel is in its post-failure cooldown (rounds fall back to exec). */
|
|
2509
|
+
get disabled() {
|
|
2510
|
+
return this.channelBrokenAt !== void 0 && Date.now() - this.channelBrokenAt < (this.timeouts.channelRetryMs ?? 3e5);
|
|
2511
|
+
}
|
|
2512
|
+
/** Count of resident runtimes currently registered (zombie accounting in tests). */
|
|
2513
|
+
get liveCount() {
|
|
2514
|
+
return this.runtimes.size;
|
|
2515
|
+
}
|
|
2516
|
+
clearIdleTimer(key) {
|
|
2517
|
+
const timer = this.idleTimers.get(key);
|
|
2518
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2519
|
+
this.idleTimers.delete(key);
|
|
2520
|
+
}
|
|
2521
|
+
armIdleTimer(key) {
|
|
2522
|
+
this.clearIdleTimer(key);
|
|
2523
|
+
const idleMs = this.config.liveIdleMs ?? 18e5;
|
|
2524
|
+
const timer = setTimeout(() => {
|
|
2525
|
+
this.reclaim(key);
|
|
2526
|
+
}, idleMs);
|
|
2527
|
+
timer.unref();
|
|
2528
|
+
this.idleTimers.set(key, timer);
|
|
2529
|
+
}
|
|
2530
|
+
async reclaim(key) {
|
|
2531
|
+
const runtime = this.runtimes.get(key);
|
|
2532
|
+
this.clearIdleTimer(key);
|
|
2533
|
+
if (runtime === void 0) return;
|
|
2534
|
+
this.runtimes.delete(key);
|
|
2535
|
+
await runtime.reclaim();
|
|
2536
|
+
}
|
|
2537
|
+
/** Reclaim every runtime (plugin unload); no process survives the profile. */
|
|
2538
|
+
async disposeAll() {
|
|
2539
|
+
this.disposed = true;
|
|
2540
|
+
this.disposeController.abort();
|
|
2541
|
+
await Promise.all([...this.ensuring.values()].map((pending) => pending.catch(() => void 0)));
|
|
2542
|
+
for (const key of [...this.runtimes.keys()]) await this.reclaim(key);
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* True while the member's runtime exists or is being spawned. The settings
|
|
2546
|
+
* controller gates a new generation on this: a fresh driver must not serve
|
|
2547
|
+
* a member whose retiring generation still hosts the (same) kimi session.
|
|
2548
|
+
*/
|
|
2549
|
+
hasRuntime(key) {
|
|
2550
|
+
return this.runtimes.has(key) || this.ensuring.has(key);
|
|
2551
|
+
}
|
|
2552
|
+
/** @deprecated Compatibility no-op: live output is always incremental. */
|
|
2553
|
+
setLiveMirrorGranularity(_granularity) {}
|
|
2554
|
+
/**
|
|
2555
|
+
* Drain for a settings-driven generation handoff: refuse new rounds (the
|
|
2556
|
+
* provider's catch falls back to exec), let every in-flight round finish on
|
|
2557
|
+
* its runtime, then reclaim. Resolves when no runtime or spawn remains.
|
|
2558
|
+
* Unlike disposeAll, in-flight work is never interrupted.
|
|
2559
|
+
*/
|
|
2560
|
+
async drain() {
|
|
2561
|
+
this.draining = true;
|
|
2562
|
+
const keys = /* @__PURE__ */ new Set([
|
|
2563
|
+
...this.runtimes.keys(),
|
|
2564
|
+
...this.ensuring.keys(),
|
|
2565
|
+
...this.roundChains.keys()
|
|
2566
|
+
]);
|
|
2567
|
+
await Promise.all([...keys].map(async (key) => {
|
|
2568
|
+
await (this.roundChains.get(key) ?? Promise.resolve()).catch(() => void 0);
|
|
2569
|
+
await (this.ensuring.get(key) ?? Promise.resolve()).catch(() => void 0);
|
|
2570
|
+
await this.reclaim(key);
|
|
2571
|
+
}));
|
|
2572
|
+
}
|
|
2573
|
+
/** The model a runtime spawned for this member right now would bind. */
|
|
2574
|
+
resolveBoundModel(key) {
|
|
2575
|
+
const model = this.config.model?.(key)?.trim();
|
|
2576
|
+
return model === void 0 || model === "" ? void 0 : model;
|
|
2577
|
+
}
|
|
2578
|
+
ensureRuntime(spec, signal) {
|
|
2579
|
+
const key = String(spec.childSession.id);
|
|
2580
|
+
const model = spec.configuration === void 0 ? this.resolveBoundModel(key) : spec.configuration.model;
|
|
2581
|
+
const configurationKey = spec.configuration === void 0 ? void 0 : JSON.stringify(spec.configuration);
|
|
2582
|
+
const existing = this.runtimes.get(key);
|
|
2583
|
+
if (existing !== void 0 && !existing.dead && existing.boundModel === model && existing.configurationKey === configurationKey) {
|
|
2584
|
+
this.clearIdleTimer(key);
|
|
2585
|
+
return Promise.resolve(existing);
|
|
2586
|
+
}
|
|
2587
|
+
if (existing !== void 0) {
|
|
2588
|
+
this.runtimes.delete(key);
|
|
2589
|
+
this.clearIdleTimer(key);
|
|
2590
|
+
if (!existing.dead) existing.reclaim();
|
|
2591
|
+
}
|
|
2592
|
+
const pending = this.ensuring.get(key);
|
|
2593
|
+
if (pending !== void 0 && this.ensuringModel.get(key) === model && this.ensuringConfiguration.get(key) === configurationKey) return pending;
|
|
2594
|
+
const spawn = (pending ?? Promise.resolve()).catch(() => void 0).then(async (stale) => {
|
|
2595
|
+
if (stale !== void 0 && !stale.dead) {
|
|
2596
|
+
if (this.runtimes.get(key) === stale) this.runtimes.delete(key);
|
|
2597
|
+
await stale.reclaim();
|
|
2598
|
+
}
|
|
2599
|
+
return this.spawnRuntime(spec, signal, model);
|
|
2600
|
+
}).finally(() => {
|
|
2601
|
+
if (this.ensuring.get(key) === spawn) {
|
|
2602
|
+
this.ensuring.delete(key);
|
|
2603
|
+
this.ensuringModel.delete(key);
|
|
2604
|
+
this.ensuringConfiguration.delete(key);
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
this.ensuring.set(key, spawn);
|
|
2608
|
+
this.ensuringModel.set(key, model);
|
|
2609
|
+
this.ensuringConfiguration.set(key, configurationKey);
|
|
2610
|
+
return spawn;
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Retire one member's resident runtime (a composer-driven model switch):
|
|
2614
|
+
* the process is reclaimed and the member's next round respawns onto the
|
|
2615
|
+
* new model, resuming the same CLI session. A member with no runtime (or
|
|
2616
|
+
* only a dead one) is a no-op. Lazy: nothing spawns here.
|
|
2617
|
+
*/
|
|
2618
|
+
async retireRuntime(childSessionId) {
|
|
2619
|
+
const pending = this.ensuring.get(childSessionId);
|
|
2620
|
+
if (pending !== void 0) await pending.catch(() => void 0);
|
|
2621
|
+
await this.reclaim(childSessionId);
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* The model the member's live runtime is bound to, or undefined when the
|
|
2625
|
+
* member has no live runtime (or it bound none). The broker compares this
|
|
2626
|
+
* against the post-switch effective model to make a same-model set a no-op.
|
|
2627
|
+
*/
|
|
2628
|
+
runtimeModel(childSessionId) {
|
|
2629
|
+
const runtime = this.runtimes.get(childSessionId);
|
|
2630
|
+
return runtime === void 0 || runtime.dead ? void 0 : runtime.boundModel;
|
|
2631
|
+
}
|
|
2632
|
+
runtimeConfiguration(childSessionId) {
|
|
2633
|
+
const runtime = this.runtimes.get(childSessionId);
|
|
2634
|
+
return runtime === void 0 || runtime.dead ? void 0 : runtime.modelConfiguration;
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Spawn the member's resident `kimi acp` and prove the wire: initialize +
|
|
2638
|
+
* the loadSession capability (crash recovery needs it — without it the
|
|
2639
|
+
* channel is not live-capable and the breaker trips). A cancellation
|
|
2640
|
+
* mid-spawn reclaims the half-started process without touching the breaker.
|
|
2641
|
+
*/
|
|
2642
|
+
async spawnRuntime(spec, signal, boundModel) {
|
|
2643
|
+
const key = String(spec.childSession.id);
|
|
2644
|
+
if (this.disposed) throw new LiveChannelUnavailableError("the live driver is disposed");
|
|
2645
|
+
const member = this.registerMember(spec);
|
|
2646
|
+
if (spec.configuration === void 0 && boundModel !== void 0) await writeKimiDefaultModel(spec.homeDir, boundModel).catch((error) => {
|
|
2647
|
+
this.ctx.logger.warn(`local-agent-kimi: pinning default_model for the resident runtime failed: ${thrown$1(error).message}`);
|
|
2648
|
+
return false;
|
|
2649
|
+
});
|
|
2650
|
+
const spawnSpec = {
|
|
2651
|
+
argv: [
|
|
2652
|
+
"kimi",
|
|
2653
|
+
...spec.configuration?.model === void 0 ? [] : ["--model", spec.configuration.model],
|
|
2654
|
+
"acp"
|
|
2655
|
+
],
|
|
2656
|
+
cwd: spec.cwd,
|
|
2657
|
+
stdio: {
|
|
2658
|
+
stdin: "pipe",
|
|
2659
|
+
stdout: "pipe",
|
|
2660
|
+
stderr: "pipe"
|
|
2661
|
+
},
|
|
2662
|
+
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
2663
|
+
env: delegationEnv({ KIMI_CODE_HOME: spec.homeDir })
|
|
2664
|
+
};
|
|
2665
|
+
let child;
|
|
2666
|
+
try {
|
|
2667
|
+
child = this.ctx.subprocess.spawn(spawnSpec);
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
member?.release();
|
|
2670
|
+
this.markChannelBroken();
|
|
2671
|
+
throw new LiveChannelUnavailableError(`kimi acp failed to spawn: ${thrown$1(error).message}`);
|
|
2672
|
+
}
|
|
2673
|
+
const runtime = new KimiLiveRuntime(child, (method, params) => this.answerServerRequest(method, params), (message) => {
|
|
2674
|
+
this.ctx.logger.warn(message);
|
|
2675
|
+
}, this.timeouts.requestMs);
|
|
2676
|
+
runtime.onDead = () => {
|
|
2677
|
+
if (this.runtimes.get(key) === runtime) this.runtimes.delete(key);
|
|
2678
|
+
this.clearIdleTimer(key);
|
|
2679
|
+
member?.release();
|
|
2680
|
+
};
|
|
2681
|
+
const aborted = new Promise((_, reject) => {
|
|
2682
|
+
const cancel = () => {
|
|
2683
|
+
reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
|
|
2684
|
+
};
|
|
2685
|
+
const unload = () => {
|
|
2686
|
+
reject(new LiveChannelUnavailableError("the live driver was disposed during spawn"));
|
|
2687
|
+
};
|
|
2688
|
+
if (signal.aborted) {
|
|
2689
|
+
cancel();
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
if (this.disposeController.signal.aborted) {
|
|
2693
|
+
unload();
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
2697
|
+
this.disposeController.signal.addEventListener("abort", unload, { once: true });
|
|
2698
|
+
});
|
|
2699
|
+
aborted.catch(() => {});
|
|
2700
|
+
try {
|
|
2701
|
+
if ((await Promise.race([runtime.peer.request("initialize", {
|
|
2702
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2703
|
+
clientCapabilities: {}
|
|
2704
|
+
}, this.timeouts.initializeMs), aborted]))?.agentCapabilities?.loadSession !== true) throw new Error("kimi acp does not advertise loadSession (crash recovery requires it)");
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
await runtime.reclaim();
|
|
2707
|
+
if (signal.aborted) throw thrown$1(error);
|
|
2708
|
+
this.markChannelBroken();
|
|
2709
|
+
throw new LiveChannelUnavailableError(`the kimi acp handshake failed: ${thrown$1(error).message}`);
|
|
2710
|
+
}
|
|
2711
|
+
if (this.disposed) {
|
|
2712
|
+
await runtime.reclaim();
|
|
2713
|
+
throw new LiveChannelUnavailableError("the live driver was disposed during spawn");
|
|
2714
|
+
}
|
|
2715
|
+
this.channelBrokenAt = void 0;
|
|
2716
|
+
runtime.boundModel = boundModel;
|
|
2717
|
+
runtime.configurationKey = spec.configuration === void 0 ? void 0 : JSON.stringify(spec.configuration);
|
|
2718
|
+
this.runtimes.set(key, runtime);
|
|
2719
|
+
runtimeMember.set(runtime, member);
|
|
2720
|
+
return runtime;
|
|
2721
|
+
}
|
|
2722
|
+
markChannelBroken() {
|
|
2723
|
+
this.channelBrokenAt = Date.now();
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Report an auth-shaped round failure to the family registry's auth-failure
|
|
2727
|
+
* mark (the exec path's post-exit 401 detection ported to a process that
|
|
2728
|
+
* never exits: the match runs against the settled error instead). Degrades
|
|
2729
|
+
* silently on a core predating reportAuthFailure.
|
|
2730
|
+
*/
|
|
2731
|
+
reportAuthIfShaped(error, homeDir) {
|
|
2732
|
+
if (!KIMI_LIVE_AUTH_FAILURE.test(error.message)) return;
|
|
2733
|
+
if (homeDir !== void 0) guardKimiCredential(homeDir, (message) => {
|
|
2734
|
+
this.ctx.logger.warn(message);
|
|
2735
|
+
});
|
|
2736
|
+
const registry = this.ctx.localAgent;
|
|
2737
|
+
if (typeof registry.reportAuthFailure === "function") registry.reportAuthFailure("kimi", error.message.split("\n").find((line) => line.trim() !== "") ?? error.message);
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Register the member channel for one resident process (token + bridge
|
|
2741
|
+
* coordinates), duck-typed like the exec path's memberRun but WITHOUT the
|
|
2742
|
+
* mcp.json write — the ACP session declares the bridge inline.
|
|
2743
|
+
*/
|
|
2744
|
+
registerMember(spec) {
|
|
2745
|
+
const registry = this.ctx.localAgent;
|
|
2746
|
+
if (typeof registry.registerMemberRun !== "function" || typeof registry.memberBridgeSocketPath !== "function" || typeof registry.memberBridgeCommand !== "function") return void 0;
|
|
2747
|
+
const token = registry.registerMemberRun({
|
|
2748
|
+
childSessionId: String(spec.childSession.id),
|
|
2749
|
+
parentSessionId: spec.parentSessionId,
|
|
2750
|
+
provider: "kimi-cli"
|
|
2751
|
+
});
|
|
2752
|
+
const bridge = registry.memberBridgeCommand();
|
|
2753
|
+
let released = false;
|
|
2754
|
+
return {
|
|
2755
|
+
mcpServers: [{
|
|
2756
|
+
name: "dsh-member",
|
|
2757
|
+
command: bridge.command,
|
|
2758
|
+
args: [...bridge.args],
|
|
2759
|
+
env: [{
|
|
2760
|
+
name: MEMBER_BRIDGE_SOCKET_ENV,
|
|
2761
|
+
value: registry.memberBridgeSocketPath()
|
|
2762
|
+
}, {
|
|
2763
|
+
name: MEMBER_BRIDGE_TOKEN_ENV,
|
|
2764
|
+
value: token
|
|
2765
|
+
}]
|
|
2766
|
+
}],
|
|
2767
|
+
release: () => {
|
|
2768
|
+
if (released) return;
|
|
2769
|
+
released = true;
|
|
2770
|
+
registry.unregisterMemberRun(token);
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Auto-answer the ACP permission surface unattended. `session/request_permission`
|
|
2776
|
+
* selects the first allow option (allow_once/allow_always) — matching the
|
|
2777
|
+
* exec mode's auto-approve behavior for `kimi -p` — and cancels when the
|
|
2778
|
+
* child offered none. Anything else (fs/terminal, which we never
|
|
2779
|
+
* advertised) is an error response.
|
|
2780
|
+
*/
|
|
2781
|
+
answerServerRequest(method, params) {
|
|
2782
|
+
if (method === "session/request_permission") {
|
|
2783
|
+
const allow = (Array.isArray(params["options"]) ? params["options"] : []).find((o) => o.kind === "allow_once" || o.kind === "allow_always");
|
|
2784
|
+
if (allow !== void 0 && typeof allow.optionId === "string") return { outcome: {
|
|
2785
|
+
outcome: "selected",
|
|
2786
|
+
optionId: allow.optionId
|
|
2787
|
+
} };
|
|
2788
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2789
|
+
}
|
|
2790
|
+
throw new Error(`subagent-kimi live: unsupported ACP request ${method}`);
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Drive one delegation round on the member's resident ACP session. Mirrors
|
|
2794
|
+
* the exec run's settlement contract exactly (settleRunResult + turn/end
|
|
2795
|
+
* bookkeeping); cancel is `session/cancel` and the process survives.
|
|
2796
|
+
*/
|
|
2797
|
+
/** Initialize the native session and model controls without sending a prompt. */
|
|
2798
|
+
async prepare(spec, signal) {
|
|
2799
|
+
signal.throwIfAborted();
|
|
2800
|
+
if (this.draining || this.disabled) throw new LiveChannelUnavailableError("the live driver is unavailable for preparation");
|
|
2801
|
+
try {
|
|
2802
|
+
const runtime = await this.ensureRuntime(spec, signal);
|
|
2803
|
+
await this.configureSession(runtime, spec);
|
|
2804
|
+
signal.throwIfAborted();
|
|
2805
|
+
this.armIdleTimer(String(spec.childSession.id));
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
await this.reclaim(String(spec.childSession.id));
|
|
2808
|
+
throw error;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
async configureSession(rt, spec) {
|
|
2812
|
+
const member = runtimeMember.get(rt);
|
|
2813
|
+
if (rt.sessionId === void 0) {
|
|
2814
|
+
if (spec.resume === void 0) {
|
|
2815
|
+
const response = await rt.peer.request("session/new", {
|
|
2816
|
+
cwd: spec.cwd,
|
|
2817
|
+
mcpServers: member?.mcpServers ?? []
|
|
2818
|
+
});
|
|
2819
|
+
if (typeof response?.sessionId !== "string") throw new Error("subagent-kimi live: session/new returned no session id");
|
|
2820
|
+
rt.sessionId = response.sessionId;
|
|
2821
|
+
rt.modelConfiguration = kimiNativeConfiguration(response);
|
|
2822
|
+
} else {
|
|
2823
|
+
rt.sessionId = acpKimiSessionId(spec.resume.cliSessionId);
|
|
2824
|
+
rt.modelConfiguration = kimiNativeConfiguration(await rt.peer.request("session/load", {
|
|
2825
|
+
sessionId: rt.sessionId,
|
|
2826
|
+
cwd: spec.cwd,
|
|
2827
|
+
mcpServers: member?.mcpServers ?? []
|
|
2828
|
+
}));
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
if (spec.configuration !== void 0) rt.modelConfiguration = await configureKimiSession(rt.sessionId, rt.modelConfiguration, spec.configuration, (method, params) => rt.peer.request(method, params));
|
|
2832
|
+
if (spec.resume === void 0 && rt.sessionId !== void 0) spec.onCliSessionId?.(bareKimiSessionId(rt.sessionId));
|
|
2833
|
+
}
|
|
2834
|
+
async startRound(request, spec) {
|
|
2835
|
+
if (this.draining) throw new LiveChannelUnavailableError("the live driver is draining (a settings change retired this generation)");
|
|
2836
|
+
const key = String(spec.childSession.id);
|
|
2837
|
+
const round = (this.roundChains.get(key) ?? Promise.resolve()).catch(() => {}).then(() => this.startRoundLocked(request, spec));
|
|
2838
|
+
this.roundChains.set(key, round.then((handle) => handle.result.catch(() => ({})), () => ({})));
|
|
2839
|
+
return round;
|
|
2840
|
+
}
|
|
2841
|
+
/** The serialized round body. */
|
|
2842
|
+
async startRoundLocked(request, spec) {
|
|
2843
|
+
const task = textTask(request.prompt);
|
|
2844
|
+
if (request.signal.aborted) throw new Error("subagent-kimi: request was aborted before the run started");
|
|
2845
|
+
if (this.draining) throw new LiveChannelUnavailableError("the live driver is draining (a settings change retired this generation)");
|
|
2846
|
+
if (this.disposed) throw new Error("subagent-kimi: the live driver is disposed");
|
|
2847
|
+
const turn = spec.resume?.turn ?? 1;
|
|
2848
|
+
const childSession = spec.childSession;
|
|
2849
|
+
const localAgent = this.ctx.get("localAgent");
|
|
2850
|
+
const runAbort = new AbortController();
|
|
2851
|
+
let roundSettled = false;
|
|
2852
|
+
let runtime;
|
|
2853
|
+
/** True while this round's session/prompt is in flight (cancel targets it). */
|
|
2854
|
+
let promptInFlight = false;
|
|
2855
|
+
let turnOpened = false;
|
|
2856
|
+
/** The round's accumulated assistant text (the run output — chunks are the only source). */
|
|
2857
|
+
let roundText = "";
|
|
2858
|
+
/**
|
|
2859
|
+
* The step ledger: every step a stream reservation consumed this round
|
|
2860
|
+
* (in increasing order — permanently, whether the stream later completes
|
|
2861
|
+
* or stays an orphan snapshot). The file fold skips these steps for its
|
|
2862
|
+
* sequential folds, so a tool card folded mid-stream lands ABOVE the
|
|
2863
|
+
* stream's step and the projection renders the round's chronological
|
|
2864
|
+
* order.
|
|
2865
|
+
*/
|
|
2866
|
+
const reservedSteps = [];
|
|
2867
|
+
/**
|
|
2868
|
+
* The streaming items seen this round . kimi's ACP
|
|
2869
|
+
* chunks carry no item id, so the stream key is synthetic per kind per
|
|
2870
|
+
* round (the codex live driver's fallback shape): thought chunks share
|
|
2871
|
+
* one stream, message chunks another. Deltas accumulate into throttled
|
|
2872
|
+
* snapshot assistant/messages appended at the stream's reserved
|
|
2873
|
+
* (turn, step) — the host folds repeated settles at one coordinate into
|
|
2874
|
+
* one live-updating chat node. The wire line that completes the stream's
|
|
2875
|
+
* kind folds at the same step and finalizes it; an entry whose completion
|
|
2876
|
+
* never folds is force-finalized at settle.
|
|
2877
|
+
*/
|
|
2878
|
+
const streams = /* @__PURE__ */ new Map();
|
|
2879
|
+
/** The stream the latest delta accumulated into (a kind switch force-snapshots it). */
|
|
2880
|
+
let activeStream;
|
|
2881
|
+
/**
|
|
2882
|
+
* The round's usage NOT carried by a folded line, summed across mirror
|
|
2883
|
+
* passes (each pass's window is disjoint). The settle's final snapshot
|
|
2884
|
+
* carries the remainder when no folded message did — host 0.1.5 has no
|
|
2885
|
+
* usage-backfill event.
|
|
2886
|
+
*/
|
|
2887
|
+
let roundUsage;
|
|
2888
|
+
/**
|
|
2889
|
+
* The model identifier the wire named this round (a mirror pass's
|
|
2890
|
+
* `usage.record` / `llm.request` `model`, last one seen), reported as the
|
|
2891
|
+
* round's settled observation — the live drive's half of the exec path's
|
|
2892
|
+
* settle-mirror read-back.
|
|
2893
|
+
*/
|
|
2894
|
+
let roundModel;
|
|
2895
|
+
let lastMirrorAt = 0;
|
|
2896
|
+
let mirrorQueue = Promise.resolve();
|
|
2897
|
+
let persistQueue = Promise.resolve();
|
|
2898
|
+
const persist = () => {
|
|
2899
|
+
persistQueue = persistQueue.then(() => persistChildSession(this.ctx, childSession)).catch((error) => {
|
|
2900
|
+
this.ctx.logger.warn(`subagent-kimi: live persistence failed: ${thrown$1(error).message}`);
|
|
2901
|
+
});
|
|
2902
|
+
};
|
|
2903
|
+
/** A stream's synthetic key: kimi's ACP deltas carry no item id, so kind + turn pairs them. */
|
|
2904
|
+
let generation = 0;
|
|
2905
|
+
const seenToolCalls = /* @__PURE__ */ new Set();
|
|
2906
|
+
const toolSteps = /* @__PURE__ */ new Map();
|
|
2907
|
+
const streamKey = (kind) => `kimi-stream-${kind}-${turn}-${generation}`;
|
|
2908
|
+
/** The plan stream's key: a snapshot stream beside the think/text delta streams. */
|
|
2909
|
+
const planKey = `kimi-stream-plan-${turn}`;
|
|
2910
|
+
/**
|
|
2911
|
+
* Fold one non-text chunk as a placeholder: image/resource_link content
|
|
2912
|
+
* has no text channel, so the transcript keeps a visible marker (and the
|
|
2913
|
+
* run output notes the drop) instead of the chunk vanishing. Flows exactly
|
|
2914
|
+
* like a text delta from here.
|
|
2915
|
+
*/
|
|
2916
|
+
const foldDelta = (text) => {
|
|
2917
|
+
roundText += text;
|
|
2918
|
+
const key = streamKey("text");
|
|
2919
|
+
reserveStream(key, "text");
|
|
2920
|
+
const stream = streams.get(key);
|
|
2921
|
+
if (stream !== void 0) {
|
|
2922
|
+
stream.text += text;
|
|
2923
|
+
appendStreamSnapshot(stream, false, false);
|
|
2924
|
+
}
|
|
2925
|
+
localAgent?.reportRunProgress(childSession.id, {
|
|
2926
|
+
kind: "delta",
|
|
2927
|
+
text
|
|
2928
|
+
});
|
|
2929
|
+
};
|
|
2930
|
+
/**
|
|
2931
|
+
* Render one ACP plan snapshot ({entries: [{content, status}]}) as
|
|
2932
|
+
* think-style lines. Each update carries the WHOLE plan, so the fold
|
|
2933
|
+
* replaces rather than appends.
|
|
2934
|
+
*/
|
|
2935
|
+
const renderPlan = (update) => {
|
|
2936
|
+
return (Array.isArray(update["entries"]) ? update["entries"] : []).map((entry) => {
|
|
2937
|
+
const content = typeof entry["content"] === "string" ? entry["content"] : "";
|
|
2938
|
+
const status = entry["status"];
|
|
2939
|
+
return `${status === "completed" ? "☑" : status === "in_progress" ? "▶" : "☐"} ${content}`;
|
|
2940
|
+
}).filter((line) => line.trim() !== "☐" && line !== "").join("\n");
|
|
2941
|
+
};
|
|
2942
|
+
const foldPlan = (update) => {
|
|
2943
|
+
const plan = renderPlan(update);
|
|
2944
|
+
if (plan === "") return;
|
|
2945
|
+
reserveStream(planKey, "think");
|
|
2946
|
+
const stream = streams.get(planKey);
|
|
2947
|
+
if (stream !== void 0 && stream.text !== plan) {
|
|
2948
|
+
stream.text = plan;
|
|
2949
|
+
appendStreamSnapshot(stream, true, false);
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
/**
|
|
2953
|
+
* Append one snapshot of a streaming item at its reserved (turn, step).
|
|
2954
|
+
* Batched per stream on a bounded deadline unless `force`; the forced
|
|
2955
|
+
* final snapshot carries `interrupted` (a cancelled turn reads 已停止
|
|
2956
|
+
* legitimately) and, when `withUsage`, the round's uncarried usage. An
|
|
2957
|
+
* empty stream leaves no boundary at all.
|
|
2958
|
+
*/
|
|
2959
|
+
const streamPublisher = localAgent?.liveStreams === void 0 ? void 0 : new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, (error) => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
|
|
2960
|
+
const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
2961
|
+
const pendingStreamArrival = /* @__PURE__ */ new Map();
|
|
2962
|
+
const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
|
|
2963
|
+
if (stream.text.trim() === "") return;
|
|
2964
|
+
const now = Date.now();
|
|
2965
|
+
if (!force) {
|
|
2966
|
+
if (!pendingStreamArrival.has(stream.step)) pendingStreamArrival.set(stream.step, now);
|
|
2967
|
+
liveFlush.schedule(stream.step, () => appendStreamSnapshot(stream, true, interrupted, withUsage));
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
liveFlush.cancel(stream.step);
|
|
2971
|
+
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
2972
|
+
pendingStreamArrival.delete(stream.step);
|
|
2973
|
+
if (streamPublisher !== void 0 && !final) {
|
|
2974
|
+
if (!stream.opened) {
|
|
2975
|
+
childSession.append("step/start", {
|
|
2976
|
+
turn,
|
|
2977
|
+
step: stream.step
|
|
2978
|
+
});
|
|
2979
|
+
stream.opened = true;
|
|
2980
|
+
}
|
|
2981
|
+
streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
|
|
2982
|
+
stream.lastSnapshotAt = now;
|
|
2983
|
+
stream.lastSnapshotLen = stream.text.length;
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
if (!stream.opened) {
|
|
2987
|
+
childSession.append("step/start", {
|
|
2988
|
+
turn,
|
|
2989
|
+
step: stream.step
|
|
2990
|
+
});
|
|
2991
|
+
stream.opened = true;
|
|
2992
|
+
}
|
|
2993
|
+
childSession.append("assistant/message", {
|
|
2994
|
+
turn,
|
|
2995
|
+
step: stream.step,
|
|
2996
|
+
message: assistantEvent([stream.kind === "think" ? {
|
|
2997
|
+
type: "reasoning",
|
|
2998
|
+
text: stream.text
|
|
2999
|
+
} : {
|
|
3000
|
+
type: "text",
|
|
3001
|
+
text: stream.text
|
|
3002
|
+
}], runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
3003
|
+
stream: [],
|
|
3004
|
+
...withUsage && roundUsage !== void 0 ? { usage: roundUsage } : {},
|
|
3005
|
+
...interrupted ? { interrupted: true } : {}
|
|
3006
|
+
}, { surfaceOp: "append" });
|
|
3007
|
+
if (withUsage) roundUsage = void 0;
|
|
3008
|
+
stream.lastSnapshotAt = now;
|
|
3009
|
+
stream.lastSnapshotLen = stream.text.length;
|
|
3010
|
+
persist();
|
|
3011
|
+
};
|
|
3012
|
+
/**
|
|
3013
|
+
* Reserve the step one streaming item will occupy — past every folded
|
|
3014
|
+
* line (the session's own event ledger: the file fold writes events
|
|
3015
|
+
* synchronously) and every earlier reservation. A kind switch forces one
|
|
3016
|
+
* freshness snapshot of the current stream (skipped when its text has not
|
|
3017
|
+
* grown since the last one); its completion still folds at its own
|
|
3018
|
+
* reserved step when the wire line lands.
|
|
3019
|
+
*/
|
|
3020
|
+
const reserveStream = (key, kind) => {
|
|
3021
|
+
if (activeStream !== void 0 && activeStream !== key) {
|
|
3022
|
+
const previous = streams.get(activeStream);
|
|
3023
|
+
if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen) appendStreamSnapshot(previous, true, false);
|
|
3024
|
+
activeStream = void 0;
|
|
3025
|
+
}
|
|
3026
|
+
let stream = streams.get(key);
|
|
3027
|
+
if (stream === void 0) {
|
|
3028
|
+
let step = nextKimiSessionStep(childSession, turn);
|
|
3029
|
+
for (const reserved of reservedSteps) step = Math.max(step, reserved + 1);
|
|
3030
|
+
reservedSteps.push(step);
|
|
3031
|
+
stream = {
|
|
3032
|
+
kind,
|
|
3033
|
+
step,
|
|
3034
|
+
text: "",
|
|
3035
|
+
lastSnapshotAt: 0,
|
|
3036
|
+
lastSnapshotLen: 0,
|
|
3037
|
+
opened: false
|
|
3038
|
+
};
|
|
3039
|
+
streams.set(key, stream);
|
|
3040
|
+
}
|
|
3041
|
+
activeStream = key;
|
|
3042
|
+
};
|
|
3043
|
+
/**
|
|
3044
|
+
* The fold-pass coordination surface : the file
|
|
3045
|
+
* fold skips reserved steps for sequential folds and pairs a completed
|
|
3046
|
+
* think/assistant line with its stream — scoped to THIS round's turn, so
|
|
3047
|
+
* an earlier round's leftover lines never consume this round's stream.
|
|
3048
|
+
*/
|
|
3049
|
+
const mirrorStreams = {
|
|
3050
|
+
reservedSteps: (t) => t === turn ? reservedSteps : [],
|
|
3051
|
+
completeTool: (line) => {
|
|
3052
|
+
if (line.turn !== turn) return void 0;
|
|
3053
|
+
const step = toolSteps.get(line.id);
|
|
3054
|
+
if (step === void 0) return void 0;
|
|
3055
|
+
toolSteps.delete(line.id);
|
|
3056
|
+
return {
|
|
3057
|
+
step,
|
|
3058
|
+
opened: true
|
|
3059
|
+
};
|
|
3060
|
+
},
|
|
3061
|
+
completeStream: (line) => {
|
|
3062
|
+
if (line.turn !== turn) return void 0;
|
|
3063
|
+
const kind = line.kind === "think" ? "think" : "text";
|
|
3064
|
+
const entry = [...streams.entries()].find(([key, stream]) => key !== planKey && stream.kind === kind);
|
|
3065
|
+
if (entry === void 0) return void 0;
|
|
3066
|
+
const [key, stream] = entry;
|
|
3067
|
+
liveFlush.cancel(stream.step);
|
|
3068
|
+
streamPublisher?.finish(stream.step);
|
|
3069
|
+
streams.delete(key);
|
|
3070
|
+
if (activeStream === key) activeStream = void 0;
|
|
3071
|
+
return {
|
|
3072
|
+
step: stream.step,
|
|
3073
|
+
opened: stream.opened
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
const mirrorOptions = () => ({ streams: mirrorStreams });
|
|
3078
|
+
/**
|
|
3079
|
+
* Observation accounting across mirror passes: a pass whose window
|
|
3080
|
+
* attached its usage to a folded message is already carried; anything else
|
|
3081
|
+
* sums into the round's uncarried remainder (the final snapshot's payload).
|
|
3082
|
+
* The wire's model rides every pass that saw one — the settle report reads
|
|
3083
|
+
* the latest.
|
|
3084
|
+
*/
|
|
3085
|
+
const noteMirrorUsage = (delta) => {
|
|
3086
|
+
if (delta.usage !== void 0 && delta.usageAttached !== true) roundUsage = addTokenUsage(roundUsage, delta.usage);
|
|
3087
|
+
if (delta.model !== void 0) roundModel = delta.model;
|
|
3088
|
+
};
|
|
3089
|
+
/** Throttled push-triggered mirror pass; the file fold stays the only transcript source. */
|
|
3090
|
+
const triggerMirror = () => {
|
|
3091
|
+
const now = Date.now();
|
|
3092
|
+
if (now - lastMirrorAt < this.timeouts.mirrorThrottleMs) return;
|
|
3093
|
+
lastMirrorAt = now;
|
|
3094
|
+
mirrorQueue = mirrorQueue.then(() => mirrorKimiDelta(this.ctx, childSession, spec.homeDir, runtime?.sessionId === void 0 ? void 0 : bareKimiSessionId(runtime.sessionId), mirrorOptions()).then((delta) => {
|
|
3095
|
+
noteMirrorUsage(delta);
|
|
3096
|
+
}).catch((error) => {
|
|
3097
|
+
this.ctx.logger.warn(`subagent-kimi: live mirror pass failed: ${thrown$1(error).message}`);
|
|
3098
|
+
}));
|
|
3099
|
+
};
|
|
3100
|
+
const requestCancel = () => {
|
|
3101
|
+
if (roundSettled || runAbort.signal.aborted) return;
|
|
3102
|
+
runAbort.abort(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
|
|
3103
|
+
if (promptInFlight && runtime?.sessionId !== void 0 && !runtime.dead) runtime.peer.notify("session/cancel", { sessionId: runtime.sessionId });
|
|
3104
|
+
};
|
|
3105
|
+
const onAbort = () => {
|
|
3106
|
+
requestCancel();
|
|
3107
|
+
};
|
|
3108
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
3109
|
+
const abortBranch = new Promise((_, reject) => {
|
|
3110
|
+
runAbort.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally")), { once: true });
|
|
3111
|
+
});
|
|
3112
|
+
const collectOutput = () => {
|
|
3113
|
+
return roundText.trim() === "" ? [] : [{
|
|
3114
|
+
type: "text",
|
|
3115
|
+
text: roundText
|
|
3116
|
+
}];
|
|
3117
|
+
};
|
|
3118
|
+
/** The round's update sink (installed on the runtime while it runs). */
|
|
3119
|
+
const onSessionUpdate = (update) => {
|
|
3120
|
+
const kind = update["sessionUpdate"];
|
|
3121
|
+
if (kind === "agent_message_chunk") {
|
|
3122
|
+
const content = update["content"];
|
|
3123
|
+
if (content !== void 0 && content.type !== void 0 && content.type !== "text") {
|
|
3124
|
+
foldDelta(`[未支持的内容类型 ${content.type}]`);
|
|
3125
|
+
triggerMirror();
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
const text = content?.text ?? "";
|
|
3129
|
+
if (text !== "") foldDelta(text);
|
|
3130
|
+
} else if (kind === "agent_thought_chunk") {
|
|
3131
|
+
const text = update["content"]?.text ?? "";
|
|
3132
|
+
if (text !== "") {
|
|
3133
|
+
const key = streamKey("think");
|
|
3134
|
+
reserveStream(key, "think");
|
|
3135
|
+
const stream = streams.get(key);
|
|
3136
|
+
if (stream !== void 0) {
|
|
3137
|
+
stream.text += text;
|
|
3138
|
+
appendStreamSnapshot(stream, false, false);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
} else if (kind === "tool_call") {
|
|
3142
|
+
const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0;
|
|
3143
|
+
if (id === void 0 || !seenToolCalls.has(id)) {
|
|
3144
|
+
if (id !== void 0) {
|
|
3145
|
+
seenToolCalls.add(id);
|
|
3146
|
+
let step = nextKimiSessionStep(childSession, turn);
|
|
3147
|
+
for (const reserved of reservedSteps) step = Math.max(step, reserved + 1);
|
|
3148
|
+
reservedSteps.push(step);
|
|
3149
|
+
toolSteps.set(id, step);
|
|
3150
|
+
childSession.append("step/start", {
|
|
3151
|
+
turn,
|
|
3152
|
+
step
|
|
3153
|
+
});
|
|
3154
|
+
persist();
|
|
3155
|
+
}
|
|
3156
|
+
generation++;
|
|
3157
|
+
}
|
|
3158
|
+
} else if (kind === "plan") foldPlan(update);
|
|
3159
|
+
triggerMirror();
|
|
3160
|
+
};
|
|
3161
|
+
const accepted = (async () => {
|
|
3162
|
+
const rt = await this.ensureRuntime(spec, request.signal);
|
|
3163
|
+
runtime = rt;
|
|
3164
|
+
if (runAbort.signal.aborted) {
|
|
3165
|
+
await this.reclaim(String(childSession.id));
|
|
3166
|
+
throw new Error("subagent-kimi: run cancelled locally");
|
|
3167
|
+
}
|
|
3168
|
+
try {
|
|
3169
|
+
await this.configureSession(rt, spec);
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
if (!runAbort.signal.aborted) await this.reclaim(String(childSession.id));
|
|
3172
|
+
throw thrown$1(error);
|
|
3173
|
+
}
|
|
3174
|
+
rt.onSessionUpdate = onSessionUpdate;
|
|
3175
|
+
childSession.append("turn/start", { turn });
|
|
3176
|
+
childSession.append("user/message", createUserMessage({
|
|
3177
|
+
content: [{
|
|
3178
|
+
type: "text",
|
|
3179
|
+
text: task
|
|
3180
|
+
}],
|
|
3181
|
+
source: { kind: "user" }
|
|
3182
|
+
}), { surfaceOp: "append" });
|
|
3183
|
+
turnOpened = true;
|
|
3184
|
+
})();
|
|
3185
|
+
const attempt = accepted.then(async () => {
|
|
3186
|
+
if (runAbort.signal.aborted) throw new Error("subagent-kimi: run cancelled locally");
|
|
3187
|
+
const rt = runtime;
|
|
3188
|
+
const processFailure = rt.child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-kimi: the live runtime exited mid-round (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown$1(error)));
|
|
3189
|
+
processFailure.catch(() => {});
|
|
3190
|
+
const sendPrompt = () => {
|
|
3191
|
+
if (runAbort.signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
|
|
3192
|
+
promptInFlight = true;
|
|
3193
|
+
return rt.peer.request("session/prompt", {
|
|
3194
|
+
sessionId: rt.sessionId,
|
|
3195
|
+
prompt: [{
|
|
3196
|
+
type: "text",
|
|
3197
|
+
text: task
|
|
3198
|
+
}]
|
|
3199
|
+
}, 0);
|
|
3200
|
+
};
|
|
3201
|
+
const promptPromise = rt.turnChain.catch(() => {}).then(sendPrompt);
|
|
3202
|
+
rt.turnChain = promptPromise;
|
|
3203
|
+
const promptResult = await Promise.race([promptPromise, processFailure]);
|
|
3204
|
+
const stopReason = acpStopReasonToHarness(String(promptResult?.stopReason ?? ""));
|
|
3205
|
+
if (stopReason === "completed" && collectOutput().length === 0) throw new Error("subagent-kimi live: the turn completed but produced no answer");
|
|
3206
|
+
return {
|
|
3207
|
+
output: collectOutput(),
|
|
3208
|
+
stopReason
|
|
3209
|
+
};
|
|
3210
|
+
});
|
|
3211
|
+
/**
|
|
3212
|
+
* Settle reconciliation: the kimi wire flushes asynchronously past the
|
|
3213
|
+
* prompt response (prompt lines early, the answer by turn end), so the
|
|
3214
|
+
* final mirror folds until consecutive reads see no growth — bounded, so
|
|
3215
|
+
* a stuck flush cannot pin the round beyond the quiescence window.
|
|
3216
|
+
*/
|
|
3217
|
+
const settleMirror = async () => {
|
|
3218
|
+
await mirrorQueue.catch(() => {});
|
|
3219
|
+
const sessionId = runtime?.sessionId === void 0 ? void 0 : bareKimiSessionId(runtime.sessionId);
|
|
3220
|
+
let lastTotal = -1;
|
|
3221
|
+
let stableReads = 0;
|
|
3222
|
+
const deadline = Date.now() + SETTLE_MIRROR_QUIESCE_MS;
|
|
3223
|
+
for (;;) {
|
|
3224
|
+
const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions());
|
|
3225
|
+
noteMirrorUsage(delta);
|
|
3226
|
+
localAgent?.reportRunProgress(childSession.id, {
|
|
3227
|
+
kind: "mirror",
|
|
3228
|
+
mirroredLines: delta.total
|
|
3229
|
+
});
|
|
3230
|
+
stableReads = delta.total === lastTotal ? stableReads + 1 : 0;
|
|
3231
|
+
lastTotal = delta.total;
|
|
3232
|
+
if (stableReads >= SETTLE_MIRROR_STABLE_READS) break;
|
|
3233
|
+
if (Date.now() >= deadline) break;
|
|
3234
|
+
await delay(SETTLE_MIRROR_POLL_MS);
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
const result = settleRunResult({
|
|
3238
|
+
attempt: () => Promise.race([attempt, abortBranch]),
|
|
3239
|
+
collectOutput,
|
|
3240
|
+
cancelled: () => runAbort.signal.aborted,
|
|
3241
|
+
onError: (error, stopReason) => {
|
|
3242
|
+
const diagnostics = runtime?.diagnostics ?? "";
|
|
3243
|
+
const suffix = diagnostics === "" ? "" : `; ${diagnostics}`;
|
|
3244
|
+
this.ctx.logger.warn(`subagent-kimi: live round failed (${stopReason}): ${error.message}${suffix}`);
|
|
3245
|
+
this.reportAuthIfShaped(error, spec.homeDir);
|
|
3246
|
+
},
|
|
3247
|
+
signal: request.signal,
|
|
3248
|
+
onAbort
|
|
3249
|
+
}).then(async (settled) => {
|
|
3250
|
+
roundSettled = true;
|
|
3251
|
+
liveFlush.dispose();
|
|
3252
|
+
promptInFlight = false;
|
|
3253
|
+
if (turnOpened) {
|
|
3254
|
+
try {
|
|
3255
|
+
await settleMirror();
|
|
3256
|
+
} catch (error) {
|
|
3257
|
+
this.ctx.logger.warn(`subagent-kimi: live settle mirror failed: ${thrown$1(error).message}`);
|
|
3258
|
+
}
|
|
3259
|
+
if (streams.size > 0) {
|
|
3260
|
+
const remaining = [...streams.values()];
|
|
3261
|
+
for (const [position, stream] of remaining.entries()) {
|
|
3262
|
+
appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
|
|
3263
|
+
streamPublisher?.finish(stream.step);
|
|
3264
|
+
if (stream.opened) childSession.append("step/end", {
|
|
3265
|
+
turn,
|
|
3266
|
+
step: stream.step
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
streams.clear();
|
|
3270
|
+
activeStream = void 0;
|
|
3271
|
+
persist();
|
|
3272
|
+
}
|
|
3273
|
+
for (const step of toolSteps.values()) childSession.append("step/end", {
|
|
3274
|
+
turn,
|
|
3275
|
+
step
|
|
3276
|
+
});
|
|
3277
|
+
toolSteps.clear();
|
|
3278
|
+
if (settled.stopReason === "completed") childSession.append("turn/end", {
|
|
3279
|
+
turn,
|
|
3280
|
+
reason: { kind: "completed" }
|
|
3281
|
+
});
|
|
3282
|
+
else if (settled.stopReason === "aborted") childSession.append("turn/end", {
|
|
3283
|
+
turn,
|
|
3284
|
+
reason: {
|
|
3285
|
+
kind: "aborted",
|
|
3286
|
+
reason: { kind: "parent" }
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
else childSession.append("turn/end", {
|
|
3290
|
+
turn,
|
|
3291
|
+
reason: {
|
|
3292
|
+
kind: "error",
|
|
3293
|
+
error: {
|
|
3294
|
+
message: "the live round did not complete",
|
|
3295
|
+
code: "UNKNOWN"
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
streamPublisher?.dispose();
|
|
3301
|
+
if (runtime !== void 0) runtime.onSessionUpdate = void 0;
|
|
3302
|
+
if (turnOpened) {
|
|
3303
|
+
persist();
|
|
3304
|
+
await persistQueue;
|
|
3305
|
+
}
|
|
3306
|
+
return settled;
|
|
3307
|
+
});
|
|
3308
|
+
result.then(() => {
|
|
3309
|
+
if (turnOpened) {
|
|
3310
|
+
const round = { ...roundModel === void 0 ? {} : { observedModel: roundModel } };
|
|
3311
|
+
localAgent?.recordRoundSettled?.(childSession.id, round);
|
|
3312
|
+
}
|
|
3313
|
+
this.armIdleTimer(String(childSession.id));
|
|
3314
|
+
});
|
|
3315
|
+
await Promise.race([accepted, abortBranch.catch(() => void 0)]);
|
|
3316
|
+
return subprocessRunHandle({
|
|
3317
|
+
id: childSession.id,
|
|
3318
|
+
result,
|
|
3319
|
+
signal: request.signal,
|
|
3320
|
+
onAbort,
|
|
3321
|
+
requestCancel,
|
|
3322
|
+
teardown: async () => {
|
|
3323
|
+
if (roundSettled) return;
|
|
3324
|
+
const rt = runtime;
|
|
3325
|
+
if (rt === void 0) return;
|
|
3326
|
+
await Promise.race([rt.turnChain.catch(() => {}), delay(this.timeouts.convergeMs)]);
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
/** The member handle each runtime carries (for its session declarations). */
|
|
3332
|
+
const runtimeMember = /* @__PURE__ */ new WeakMap();
|
|
3333
|
+
//#endregion
|
|
3334
|
+
//#region lib/types/member-bridge-config.js
|
|
3335
|
+
/**
|
|
3336
|
+
* Kimi scoped-home MCP config injection for the member bridge. Kimi declares
|
|
3337
|
+
* stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
|
|
3338
|
+
* is read once per CLI session start, and edits never interrupt an open
|
|
3339
|
+
* session — so the family writes one entry PER RUN (keyed by the run's token
|
|
3340
|
+
* suffix, keeping concurrent delegations of the same scoped home race-free),
|
|
3341
|
+
* prunes it when the run settles, and drops stale family entries (crashed
|
|
3342
|
+
* host) on every write. User-declared servers are preserved verbatim.
|
|
3343
|
+
* @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
|
|
3344
|
+
*/
|
|
3345
|
+
/** Kimi's user-level MCP config filename inside the scoped home. */
|
|
3346
|
+
const MCP_CONFIG_FILENAME = "mcp.json";
|
|
3347
|
+
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
3348
|
+
const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
|
|
3349
|
+
/**
|
|
3350
|
+
* The per-run MCP server key. The run's token suffix keeps concurrent runs
|
|
3351
|
+
* distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
|
|
3352
|
+
* than per-member) naming costs nothing.
|
|
3353
|
+
* @param token - the run's member-channel token.
|
|
3354
|
+
* @returns the mcp.json server key.
|
|
3355
|
+
*/
|
|
3356
|
+
function memberBridgeServerKey(token) {
|
|
3357
|
+
return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
|
|
3358
|
+
}
|
|
3359
|
+
/** Read the scoped mcp.json; a missing or malformed file reads as empty. */
|
|
3360
|
+
function readMcpConfig(file) {
|
|
3361
|
+
let parsed;
|
|
3362
|
+
try {
|
|
3363
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
3364
|
+
} catch {
|
|
3365
|
+
return {};
|
|
3366
|
+
}
|
|
3367
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
3368
|
+
return parsed;
|
|
3369
|
+
}
|
|
3370
|
+
/** Write the config back, preserving every non-family key. */
|
|
3371
|
+
function writeMcpConfig(file, config) {
|
|
3372
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Declare the member bridge for one run: set the per-run server entry and
|
|
3376
|
+
* prune other family entries (settled or crashed runs — an open session's
|
|
3377
|
+
* already-registered server is unaffected, per kimi's mid-session semantics).
|
|
3378
|
+
* Synchronous read-modify-write: single-process callers race-free by event
|
|
3379
|
+
* loop; the file is small and written once per run.
|
|
3380
|
+
* @param homeDir - the kimi scoped home.
|
|
3381
|
+
* @param key - this run's server key ({@link memberBridgeServerKey}).
|
|
3382
|
+
* @param command - the bridge spawn command (`node` + entry path).
|
|
3383
|
+
* @param env - the bridge environment (socket path + per-run token).
|
|
3384
|
+
*/
|
|
3385
|
+
function injectMemberBridge(homeDir, key, command, env) {
|
|
3386
|
+
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
3387
|
+
const config = readMcpConfig(file);
|
|
3388
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
3389
|
+
for (const name of Object.keys(servers)) if (name.startsWith("dsh-member-") && name !== key) delete servers[name];
|
|
3390
|
+
servers[key] = {
|
|
3391
|
+
command: command.command,
|
|
3392
|
+
args: command.args,
|
|
3393
|
+
env
|
|
3394
|
+
};
|
|
3395
|
+
writeMcpConfig(file, {
|
|
3396
|
+
...config,
|
|
3397
|
+
mcpServers: servers
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Remove one run's bridge entry (settle path). Absent entries and a missing
|
|
3402
|
+
* config are no-ops.
|
|
3403
|
+
* @param homeDir - the kimi scoped home.
|
|
3404
|
+
* @param key - the run's server key.
|
|
3405
|
+
*/
|
|
3406
|
+
function removeMemberBridge(homeDir, key) {
|
|
3407
|
+
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
3408
|
+
const config = readMcpConfig(file);
|
|
3409
|
+
if (config.mcpServers === void 0 || !(key in config.mcpServers)) return;
|
|
3410
|
+
const servers = { ...config.mcpServers };
|
|
3411
|
+
delete servers[key];
|
|
3412
|
+
writeMcpConfig(file, {
|
|
3413
|
+
...config,
|
|
3414
|
+
mcpServers: servers
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
//#endregion
|
|
3418
|
+
//#region lib/types/kimi-cli-provider.js
|
|
3419
|
+
/**
|
|
3420
|
+
* One-shot and resumable Kimi CLI subagent lifecycle: spawn `kimi -p "<task>"`
|
|
3421
|
+
* through the subprocess seam, capture its printed response as the run output,
|
|
3422
|
+
* and dispose to whole-tree quiescence. Mirrors the official one-shot Codex
|
|
3423
|
+
* provider, and adds the family's own resume: a later round calls the tool with
|
|
3424
|
+
* the first round's dsh child session id, the family registry maps it back to
|
|
3425
|
+
* the kimi session, and the provider spawns `kimi -S session_<id> -p` so the
|
|
3426
|
+
* SAME kimi conversation continues inside the SAME dsh child session.
|
|
3427
|
+
* @module @khorsheed/dsh-local-agent-kimi/kimi-cli-provider
|
|
3428
|
+
*/
|
|
3429
|
+
/** Default POSIX grace between subprocess termination tiers. */
|
|
3430
|
+
const DEFAULT_DISPOSE_GRACE_MS = 3e3;
|
|
3431
|
+
/**
|
|
3432
|
+
* Auth-shaped failure signatures in kimi's stderr or output: the endpoint
|
|
3433
|
+
* rejected the credential. Narrow on purpose — matched against
|
|
3434
|
+
* provider-controlled error output only.
|
|
3435
|
+
*/
|
|
3436
|
+
const KIMI_AUTH_FAILURE = /401|unauthorized|invalid api key|not authenticated/i;
|
|
3437
|
+
/**
|
|
3438
|
+
* One-shot Kimi CLI subagent provider: every accepted fresh run starts a
|
|
3439
|
+
* fresh `kimi -p` process in the delegating Session's workspace, under the
|
|
3440
|
+
* harness scoped home; a resume round (the family tool's staged resume intent)
|
|
3441
|
+
* continues the SAME kimi session with `kimi -S session_<id> -p` inside the
|
|
3442
|
+
* SAME dsh child session. Mirrors the official one-shot Codex provider for the
|
|
3443
|
+
* fresh path, including `NO_START_CAPABILITIES` — continuation is the family's
|
|
3444
|
+
* own resume mechanism, not the official Agent-type continuable seam.
|
|
3445
|
+
*/
|
|
3446
|
+
/**
|
|
3447
|
+
* Mark the harness credential verified after a completed round, degrading
|
|
3448
|
+
* silently on a core that predates the mark (the family's companion-pair
|
|
3449
|
+
* rule: a provider paired with an older core loses the grade, never the run).
|
|
3450
|
+
* @param ctx - host context carrying the family registry.
|
|
3451
|
+
*/
|
|
3452
|
+
function markCredentialVerified(ctx, scope) {
|
|
3453
|
+
const registry = ctx.get("localAgent");
|
|
3454
|
+
if (registry === void 0 || typeof registry.reportAuthSuccess !== "function") return;
|
|
3455
|
+
registry.reportAuthSuccess("kimi", scope);
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* The version `kimi --version` reports, probed through the shared subprocess
|
|
3459
|
+
* seam inside the SCOPED home (never the user's own installation) and cached
|
|
3460
|
+
* by the family probe against the executable's identity. Kimi's wire log names
|
|
3461
|
+
* no build, so this is the only version channel the harness has — both the
|
|
3462
|
+
* effective-settings snapshot and a settled round's read-back read it.
|
|
3463
|
+
* @param ctx - host context carrying the subprocess seam.
|
|
3464
|
+
* @param homeDir - the harness's scoped home.
|
|
3465
|
+
* @returns the version, or undefined when the CLI cannot be asked.
|
|
3466
|
+
*/
|
|
3467
|
+
function kimiCliVersion(ctx, homeDir) {
|
|
3468
|
+
const subprocess = ctx.get("subprocess");
|
|
3469
|
+
if (subprocess === void 0) return Promise.resolve(void 0);
|
|
3470
|
+
return probeCliVersion({
|
|
3471
|
+
argv: ["kimi", "--version"],
|
|
3472
|
+
cwd: homeDir,
|
|
3473
|
+
spawn: (spec) => subprocess.spawn(spec),
|
|
3474
|
+
env: delegationEnv({ KIMI_CODE_HOME: homeDir })
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
var KimiCliProvider = class {
|
|
3478
|
+
ctx;
|
|
3479
|
+
live;
|
|
3480
|
+
model;
|
|
3481
|
+
memberModels;
|
|
3482
|
+
name = "kimi-cli";
|
|
3483
|
+
capabilities = NO_START_CAPABILITIES;
|
|
3484
|
+
inheritsParentContext = false;
|
|
3485
|
+
/**
|
|
3486
|
+
* @param live - the live driver, or a resolver returning the current
|
|
3487
|
+
* generation's driver per member (the settings toggle swaps generations;
|
|
3488
|
+
* a resolver may return undefined to steer one member's round to exec
|
|
3489
|
+
* while a retiring generation still hosts it).
|
|
3490
|
+
* @param model - resolver for the configured model, read PER ROUND so a
|
|
3491
|
+
* settings-card write takes effect on the next delegation without a
|
|
3492
|
+
* reload. Undefined (the resolver absent, or returning undefined) leaves
|
|
3493
|
+
* the argv exactly as it was before the key rode every round — the scoped
|
|
3494
|
+
* config.toml's `default_model` then decides, as it always did.
|
|
3495
|
+
* @param memberModels - the broker's member-model ledger: the provider
|
|
3496
|
+
* records each delegation's start model here (the live driver's spawn
|
|
3497
|
+
* resolver binds it) and reads the session-level override as the exec
|
|
3498
|
+
* path's top layer. Absent (older wiring), both drop out.
|
|
3499
|
+
*/
|
|
3500
|
+
constructor(ctx, live, model, memberModels) {
|
|
3501
|
+
this.ctx = ctx;
|
|
3502
|
+
this.live = live;
|
|
3503
|
+
this.model = model;
|
|
3504
|
+
this.memberModels = memberModels;
|
|
3505
|
+
}
|
|
3506
|
+
/** Resolve the live driver for one round's member, if live is on for it. */
|
|
3507
|
+
liveDriver(childSessionId) {
|
|
3508
|
+
const live = this.live;
|
|
3509
|
+
if (live === void 0) return void 0;
|
|
3510
|
+
return typeof live === "function" ? live(childSessionId) : live;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Register one run with the member channel and declare the bridge MCP server
|
|
3514
|
+
* in the scoped home's mcp.json (per-run entry, socket + token in its env).
|
|
3515
|
+
* Returns the release handle, or undefined when the mounted core
|
|
3516
|
+
* predates the member channel (declare-and-degrade: the run proceeds
|
|
3517
|
+
* exactly as before). Token-only auth: host 0.1.5 removed the child pid the
|
|
3518
|
+
* parentage cross-check used; the per-run token is the sole credential (see
|
|
3519
|
+
* the package README's threat model). `release` invalidates the token and
|
|
3520
|
+
* prunes the config entry on any settle path.
|
|
3521
|
+
*/
|
|
3522
|
+
memberRun(childSessionId, parentSessionId, homeDir) {
|
|
3523
|
+
const registry = this.ctx.localAgent;
|
|
3524
|
+
if (typeof registry.registerMemberRun !== "function" || typeof registry.memberBridgeSocketPath !== "function" || typeof registry.memberBridgeCommand !== "function") return void 0;
|
|
3525
|
+
const token = registry.registerMemberRun({
|
|
3526
|
+
childSessionId,
|
|
3527
|
+
parentSessionId,
|
|
3528
|
+
provider: this.name
|
|
3529
|
+
});
|
|
3530
|
+
const key = memberBridgeServerKey(token);
|
|
3531
|
+
try {
|
|
3532
|
+
injectMemberBridge(homeDir, key, registry.memberBridgeCommand(), {
|
|
3533
|
+
[MEMBER_BRIDGE_SOCKET_ENV]: registry.memberBridgeSocketPath(),
|
|
3534
|
+
[MEMBER_BRIDGE_TOKEN_ENV]: token
|
|
3535
|
+
});
|
|
3536
|
+
} catch (error) {
|
|
3537
|
+
registry.unregisterMemberRun(token);
|
|
3538
|
+
this.ctx.logger.warn(`subagent-kimi: member bridge config injection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
let released = false;
|
|
3542
|
+
return { release: () => {
|
|
3543
|
+
if (released) return;
|
|
3544
|
+
released = true;
|
|
3545
|
+
registry.unregisterMemberRun(token);
|
|
3546
|
+
try {
|
|
3547
|
+
removeMemberBridge(homeDir, key);
|
|
3548
|
+
} catch (error) {
|
|
3549
|
+
this.ctx.logger.warn(`subagent-kimi: member bridge config cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3550
|
+
}
|
|
3551
|
+
} };
|
|
3552
|
+
}
|
|
3553
|
+
async start(request) {
|
|
3554
|
+
const intent = this.ctx.localAgent.takeDelegationIntent(request.parent.session.id, this.name);
|
|
3555
|
+
const scope = intent?.scope;
|
|
3556
|
+
const homeDir = this.ctx.localAgent.homeDir("kimi", scope);
|
|
3557
|
+
const cwd = resolveChildCwd(request.parent.session.header.cwd, intent?.cwd);
|
|
3558
|
+
if (cwd === void 0) throw new Error("subagent-kimi: the parent session has no working directory to run the CLI in");
|
|
3559
|
+
const exec = intent?.exec;
|
|
3560
|
+
if (exec !== void 0) containerScopedHome(exec, "KIMI_CODE_HOME", "subagent-kimi");
|
|
3561
|
+
if (intent !== void 0 && intent.kind === "resume") {
|
|
3562
|
+
const record = this.ctx.localAgent.getDelegation(intent.childSessionId);
|
|
3563
|
+
assertResumeCwdUnchanged(record, cwd, "subagent-kimi");
|
|
3564
|
+
assertResumeScopeUnchanged(record, scope, "subagent-kimi");
|
|
3565
|
+
if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") return this.startKimiResume(request, intent, cwd, homeDir, exec, scope, record?.model);
|
|
3566
|
+
return this.ctx.localAgent.withMemberConfigurationRound({
|
|
3567
|
+
childSessionId: intent.childSessionId,
|
|
3568
|
+
provider: this.name,
|
|
3569
|
+
parentSessionId: request.parent.session.id,
|
|
3570
|
+
cwd,
|
|
3571
|
+
...scope === void 0 ? {} : { scope },
|
|
3572
|
+
...record?.model === void 0 ? {} : { model: record.model },
|
|
3573
|
+
...record?.effort === void 0 ? {} : { effort: record.effort },
|
|
3574
|
+
...record?.configurationLock === void 0 ? {} : { configurationLock: record.configurationLock }
|
|
3575
|
+
}, (configuration) => this.startKimiResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
|
|
3576
|
+
}
|
|
3577
|
+
const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
|
|
3578
|
+
if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") {
|
|
3579
|
+
if (intent?.effort !== void 0) throw new Error("Kimi effort requires the configuration admission core");
|
|
3580
|
+
return this.startKimiFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId);
|
|
3581
|
+
}
|
|
3582
|
+
return this.ctx.localAgent.withMemberConfigurationRound({
|
|
3583
|
+
childSessionId,
|
|
3584
|
+
provider: this.name,
|
|
3585
|
+
parentSessionId: request.parent.session.id,
|
|
3586
|
+
cwd,
|
|
3587
|
+
...scope === void 0 ? {} : { scope },
|
|
3588
|
+
...intent?.model === void 0 ? {} : { model: intent.model },
|
|
3589
|
+
...intent?.effort === void 0 ? {} : { effort: intent.effort },
|
|
3590
|
+
...intent?.configurationLock === void 0 ? {} : { configurationLock: intent.configurationLock }
|
|
3591
|
+
}, (configuration) => this.startKimiFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId, configuration), request.signal, intent?.onAdmitted);
|
|
3592
|
+
}
|
|
3593
|
+
/** Fresh round: record the child session, spawn `kimi -p`, mirror after settle. */
|
|
3594
|
+
async startKimiFresh(request, cwd, homeDir, exec, scope, requestedModel, runId, configuration) {
|
|
3595
|
+
let childSession;
|
|
3596
|
+
try {
|
|
3597
|
+
const sessions = this.ctx.get("sessions");
|
|
3598
|
+
if (sessions === void 0) throw new Error("the sessions service is not mounted");
|
|
3599
|
+
childSession = sessions.get(runId) ?? sessions.create(runId, { meta: {
|
|
3600
|
+
cwd,
|
|
3601
|
+
parentSession: request.parent.session.id,
|
|
3602
|
+
origin: "subagent",
|
|
3603
|
+
delegationDepth: (request.parent.session.header.delegationDepth ?? 0) + 1
|
|
3604
|
+
} });
|
|
3605
|
+
const harness = this.ctx.localAgent.get("kimi");
|
|
3606
|
+
const label = subagentDelegationLabel(harness?.displayName ?? "kimi", request.descriptor.label);
|
|
3607
|
+
childSession.append("subagent/descriptor", {
|
|
3608
|
+
...request.descriptor,
|
|
3609
|
+
label
|
|
3610
|
+
});
|
|
3611
|
+
establishSubagentCatalogChild(request.parent.session, childSession.header, label);
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
this.ctx.logger.warn(`subagent-kimi: subagent session record failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3614
|
+
}
|
|
3615
|
+
const baseUrl = await readKimiBaseUrl(homeDir).catch(() => void 0);
|
|
3616
|
+
this.ctx.logger.info(`subagent-kimi: delegating via ${baseUrl ?? "kimi default endpoint"}`);
|
|
3617
|
+
const live = exec === void 0 ? this.liveDriver(runId) : void 0;
|
|
3618
|
+
if (live !== void 0 && childSession !== void 0 && !live.disabled) {
|
|
3619
|
+
if (requestedModel !== void 0) this.memberModels?.noteStartModel(runId, requestedModel);
|
|
3620
|
+
try {
|
|
3621
|
+
return await live.startRound(request, {
|
|
3622
|
+
cwd,
|
|
3623
|
+
homeDir,
|
|
3624
|
+
childSession,
|
|
3625
|
+
...configuration === void 0 ? {} : { configuration: configuration.resolved },
|
|
3626
|
+
parentSessionId: request.parent.session.id,
|
|
3627
|
+
onCliSessionId: (cliSessionId) => {
|
|
3628
|
+
this.ctx.localAgent.recordDelegation({
|
|
3629
|
+
childSessionId: runId,
|
|
3630
|
+
provider: this.name,
|
|
3631
|
+
parentSessionId: request.parent.session.id,
|
|
3632
|
+
cliSessionId,
|
|
3633
|
+
cwd,
|
|
3634
|
+
...scope === void 0 ? {} : { scope },
|
|
3635
|
+
...requestedModel === void 0 ? {} : { model: requestedModel }
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
});
|
|
3639
|
+
} catch (error) {
|
|
3640
|
+
if (!(error instanceof LiveChannelUnavailableError) || request.signal.aborted) throw error;
|
|
3641
|
+
this.ctx.logger.warn(`subagent-kimi: live driver unavailable, using the exec one-shot: ${error.message}`);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
const member = exec === void 0 && scope === void 0 ? this.memberRun(runId, request.parent.session.id, homeDir) : void 0;
|
|
3645
|
+
try {
|
|
3646
|
+
const run = await startKimiCliRun(request, {
|
|
3647
|
+
cwd,
|
|
3648
|
+
env: delegationEnv({ KIMI_CODE_HOME: homeDir }),
|
|
3649
|
+
...exec === void 0 ? {} : { exec },
|
|
3650
|
+
endpointLabel: baseUrl,
|
|
3651
|
+
...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(runId) ?? requestedModel, this.model),
|
|
3652
|
+
...configuration === void 0 ? {} : { controlled: true },
|
|
3653
|
+
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
3654
|
+
spawn: (spec) => this.ctx.subprocess.spawn(spec),
|
|
3655
|
+
onError: (error, stopReason) => {
|
|
3656
|
+
this.ctx.logger.warn(`subagent-kimi: child run failed (${stopReason}) via ${baseUrl ?? "kimi default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3657
|
+
},
|
|
3658
|
+
onAuthFailure: (detail) => {
|
|
3659
|
+
guardKimiCredential(homeDir, (message) => {
|
|
3660
|
+
this.ctx.logger.warn(message);
|
|
3661
|
+
});
|
|
3662
|
+
this.ctx.localAgent.reportAuthFailure("kimi", detail, scope);
|
|
3663
|
+
},
|
|
3664
|
+
onAuthSuccess: () => {
|
|
3665
|
+
markCredentialVerified(this.ctx, scope);
|
|
3666
|
+
},
|
|
3667
|
+
cliVersion: () => kimiCliVersion(this.ctx, homeDir),
|
|
3668
|
+
childSession,
|
|
3669
|
+
homeDir,
|
|
3670
|
+
ctx: this.ctx,
|
|
3671
|
+
onCliSessionId: (cliSessionId) => {
|
|
3672
|
+
if (cliSessionId === void 0) return;
|
|
3673
|
+
this.ctx.localAgent.recordDelegation({
|
|
3674
|
+
childSessionId: runId,
|
|
3675
|
+
provider: this.name,
|
|
3676
|
+
parentSessionId: request.parent.session.id,
|
|
3677
|
+
cliSessionId,
|
|
3678
|
+
cwd,
|
|
3679
|
+
...scope === void 0 ? {} : { scope },
|
|
3680
|
+
...requestedModel === void 0 ? {} : { model: requestedModel }
|
|
3681
|
+
});
|
|
3682
|
+
},
|
|
3683
|
+
onRoundSettled: (round) => {
|
|
3684
|
+
this.ctx.localAgent.recordRoundSettled(runId, round);
|
|
3685
|
+
}
|
|
3686
|
+
});
|
|
3687
|
+
if (member !== void 0) run.result.then(member.release, member.release);
|
|
3688
|
+
return run;
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
member?.release();
|
|
3691
|
+
throw error;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
/** Resume round: continue the recorded kimi session inside the existing child session. */
|
|
3695
|
+
async startKimiResume(request, intent, cwd, homeDir, exec, scope, requestedModel, configuration) {
|
|
3696
|
+
if (!this.ctx.localAgent.acquireResumeLock(intent.childSessionId)) throw new Error(`subagent-kimi: 该子会话有进行中的委派,等其完成后再追问 (child session ${intent.childSessionId})`);
|
|
3697
|
+
try {
|
|
3698
|
+
const childSession = this.ctx.get("sessions")?.get(SessionId(intent.childSessionId));
|
|
3699
|
+
if (childSession === void 0) throw new Error(`subagent-kimi: resume target child session ${intent.childSessionId} is not live — start a fresh delegation instead`);
|
|
3700
|
+
const nextTurn = childSession.snapshotEvents().filter((event) => event.type === "turn/start").length + 1;
|
|
3701
|
+
const live = exec === void 0 ? this.liveDriver(intent.childSessionId) : void 0;
|
|
3702
|
+
if (live !== void 0 && !live.disabled) {
|
|
3703
|
+
if (requestedModel !== void 0) this.memberModels?.noteStartModel(intent.childSessionId, requestedModel);
|
|
3704
|
+
try {
|
|
3705
|
+
const liveRun = await live.startRound(request, {
|
|
3706
|
+
cwd,
|
|
3707
|
+
homeDir,
|
|
3708
|
+
childSession,
|
|
3709
|
+
...configuration === void 0 ? {} : { configuration: configuration.resolved },
|
|
3710
|
+
parentSessionId: request.parent.session.id,
|
|
3711
|
+
resume: {
|
|
3712
|
+
cliSessionId: intent.cliSessionId,
|
|
3713
|
+
turn: nextTurn
|
|
3714
|
+
}
|
|
3715
|
+
});
|
|
3716
|
+
liveRun.result.then(() => {
|
|
3717
|
+
this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
|
|
3718
|
+
}, () => {
|
|
3719
|
+
this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
|
|
3720
|
+
});
|
|
3721
|
+
return liveRun;
|
|
3722
|
+
} catch (error) {
|
|
3723
|
+
if (!(error instanceof LiveChannelUnavailableError) || request.signal.aborted) throw error;
|
|
3724
|
+
this.ctx.logger.warn(`subagent-kimi: live driver unavailable, using the exec one-shot: ${error.message}`);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
const member = exec === void 0 && scope === void 0 ? this.memberRun(intent.childSessionId, request.parent.session.id, homeDir) : void 0;
|
|
3728
|
+
const baseUrl = await readKimiBaseUrl(homeDir).catch(() => void 0);
|
|
3729
|
+
this.ctx.logger.info(`subagent-kimi: resuming via ${baseUrl ?? "kimi default endpoint"}`);
|
|
3730
|
+
let run;
|
|
3731
|
+
try {
|
|
3732
|
+
run = await startKimiCliRun(request, {
|
|
3733
|
+
cwd,
|
|
3734
|
+
env: delegationEnv({ KIMI_CODE_HOME: homeDir }),
|
|
3735
|
+
...exec === void 0 ? {} : { exec },
|
|
3736
|
+
endpointLabel: baseUrl,
|
|
3737
|
+
...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(intent.childSessionId) ?? requestedModel, this.model),
|
|
3738
|
+
...configuration === void 0 ? {} : { controlled: true },
|
|
3739
|
+
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
3740
|
+
spawn: (spec) => this.ctx.subprocess.spawn(spec),
|
|
3741
|
+
onError: (error, stopReason) => {
|
|
3742
|
+
this.ctx.logger.warn(`subagent-kimi: child run failed (${stopReason}) via ${baseUrl ?? "kimi default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3743
|
+
},
|
|
3744
|
+
onAuthFailure: (detail) => {
|
|
3745
|
+
guardKimiCredential(homeDir, (message) => {
|
|
3746
|
+
this.ctx.logger.warn(message);
|
|
3747
|
+
});
|
|
3748
|
+
this.ctx.localAgent.reportAuthFailure("kimi", detail, scope);
|
|
3749
|
+
},
|
|
3750
|
+
onAuthSuccess: () => {
|
|
3751
|
+
markCredentialVerified(this.ctx, scope);
|
|
3752
|
+
},
|
|
3753
|
+
cliVersion: () => kimiCliVersion(this.ctx, homeDir),
|
|
3754
|
+
childSession,
|
|
3755
|
+
homeDir,
|
|
3756
|
+
ctx: this.ctx,
|
|
3757
|
+
resume: {
|
|
3758
|
+
cliSessionId: intent.cliSessionId,
|
|
3759
|
+
turn: nextTurn
|
|
3760
|
+
},
|
|
3761
|
+
onRoundSettled: (round) => {
|
|
3762
|
+
this.ctx.localAgent.recordRoundSettled(intent.childSessionId, round);
|
|
3763
|
+
}
|
|
3764
|
+
});
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
member?.release();
|
|
3767
|
+
throw error;
|
|
3768
|
+
}
|
|
3769
|
+
run.result.then(() => {
|
|
3770
|
+
this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
|
|
3771
|
+
member?.release();
|
|
3772
|
+
}, () => {
|
|
3773
|
+
this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
|
|
3774
|
+
member?.release();
|
|
3775
|
+
});
|
|
3776
|
+
return run;
|
|
3777
|
+
} catch (error) {
|
|
3778
|
+
this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
|
|
3779
|
+
throw error;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
};
|
|
3783
|
+
function thrown(value) {
|
|
3784
|
+
/* v8 ignore next -- typed subprocess failures reject with Error. */
|
|
3785
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Extract the kimi session id from the CLI's printed response. `kimi -p`
|
|
3789
|
+
* prints a resume hint (`To resume this session: kimi -r session_<id>`) on
|
|
3790
|
+
* stdout after a run; mirroring that exact session instead of the newest-by-
|
|
3791
|
+
* mtime keeps concurrent delegations from cross-mirroring.
|
|
3792
|
+
* @param output - the collected CLI stdout.
|
|
3793
|
+
* @returns the session id, or undefined when the hint is absent.
|
|
3794
|
+
*/
|
|
3795
|
+
function kimiSessionIdFromOutput(output) {
|
|
3796
|
+
return /kimi -r session_([0-9a-fA-F-]+)/.exec(output)?.[1];
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Validate and join the one-shot task before crossing the process boundary.
|
|
3800
|
+
* @param prompt - task content accepted from the shared subagent service.
|
|
3801
|
+
* @returns the joined non-empty text.
|
|
3802
|
+
*/
|
|
3803
|
+
function textTask(prompt) {
|
|
3804
|
+
if (prompt.length === 0) throw new Error("subagent-kimi: the one-shot task must contain only text blocks");
|
|
3805
|
+
const texts = [];
|
|
3806
|
+
for (const block of prompt) {
|
|
3807
|
+
if (block.type !== "text") throw new Error("subagent-kimi: the one-shot task must contain only text blocks");
|
|
3808
|
+
texts.push(block.text);
|
|
3809
|
+
}
|
|
3810
|
+
if (texts.every((text) => text.trim().length === 0)) throw new Error("subagent-kimi: the one-shot task must not be empty");
|
|
3811
|
+
return texts.join("\n");
|
|
3812
|
+
}
|
|
3813
|
+
/**
|
|
3814
|
+
* Mirror the transcript delta after the recorded offset, advance the offset,
|
|
3815
|
+
* and report one `delta` progress per newly mirrored line. Shared by the live
|
|
3816
|
+
* poll and the settle-time mirror so the two paths cannot drift. The offset
|
|
3817
|
+
* read (`kimiMirroredLines`) and write (`setKimiMirroredLines`) are the same
|
|
3818
|
+
* bookkeeping the settle path used before live mirroring existed.
|
|
3819
|
+
* @param ctx - host context carrying localAgent and session persistence.
|
|
3820
|
+
* @param childSession - the dsh child session being mirrored into.
|
|
3821
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
3822
|
+
* @param kimiSessionId - the kimi session to mirror.
|
|
3823
|
+
* @returns the mirror delta (new total + newly mirrored line texts).
|
|
3824
|
+
*/
|
|
3825
|
+
async function mirrorKimiDelta(ctx, childSession, homeDir, kimiSessionId, options) {
|
|
3826
|
+
const localAgent = ctx.get("localAgent");
|
|
3827
|
+
const delta = await mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, localAgent?.kimiMirroredLines(childSession.id) ?? 0, options);
|
|
3828
|
+
if (delta.texts.length === 0) return delta;
|
|
3829
|
+
localAgent?.setKimiMirroredLines(childSession.id, delta.total);
|
|
3830
|
+
for (const text of delta.texts) localAgent?.reportRunProgress(childSession.id, {
|
|
3831
|
+
kind: "delta",
|
|
3832
|
+
text
|
|
3833
|
+
});
|
|
3834
|
+
return delta;
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* Mirror the kimi session's transcript into the child session AFTER the CLI
|
|
3838
|
+
* process has exited, whatever its stop reason. A cancelled or failed round
|
|
3839
|
+
* still preserves whatever the wire already recorded (partial answer, tool
|
|
3840
|
+
* activity, usage) instead of leaving the child blank — the run result
|
|
3841
|
+
* settles 'aborted'/'error' at the cancel moment, but the CLI may have
|
|
3842
|
+
* produced content before the kill landed, and that content belongs in the
|
|
3843
|
+
* 子代理 surface with its real token accounting. Also records the delegation
|
|
3844
|
+
* (fresh rounds) so a later resume can continue the session.
|
|
3845
|
+
* @param spec - the run spec carrying the child session, home, and context.
|
|
3846
|
+
* @param stderr - the collected CLI stderr (the resume hint lives here).
|
|
3847
|
+
* @returns this pass's observation — the wire's model identifier and the
|
|
3848
|
+
* window's usage — or undefined when nothing was mirrored; the caller sums
|
|
3849
|
+
* the window usage into the round total and reports the round's settled
|
|
3850
|
+
* observation.
|
|
3851
|
+
*/
|
|
3852
|
+
async function mirrorKimiAfterExit(spec, stderr) {
|
|
3853
|
+
if (spec.childSession === void 0 || spec.homeDir === void 0 || spec.ctx === void 0) return void 0;
|
|
3854
|
+
try {
|
|
3855
|
+
const kimiSessionId = spec.resume?.cliSessionId ?? kimiSessionIdFromOutput(stderr);
|
|
3856
|
+
if (spec.resume === void 0) spec.onCliSessionId?.(kimiSessionId);
|
|
3857
|
+
const delta = await mirrorKimiDelta(spec.ctx, spec.childSession, spec.homeDir, kimiSessionId, { turn: spec.resume?.turn ?? 1 });
|
|
3858
|
+
spec.ctx.get("localAgent")?.reportRunProgress(spec.childSession.id, {
|
|
3859
|
+
kind: "mirror",
|
|
3860
|
+
mirroredLines: delta.total
|
|
3861
|
+
});
|
|
3862
|
+
return {
|
|
3863
|
+
...delta.model === void 0 ? {} : { model: delta.model },
|
|
3864
|
+
...delta.usage === void 0 ? {} : { usage: delta.usage },
|
|
3865
|
+
...delta.toolCalls === void 0 ? {} : { toolCalls: delta.toolCalls }
|
|
3866
|
+
};
|
|
3867
|
+
} catch (error) {
|
|
3868
|
+
spec.onError?.(thrown(error), "error");
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Start the real `kimi -p` (fresh) or `kimi -S session_<id> -p` (resume)
|
|
3874
|
+
* child and publish its run. The kimi response is printed to stdout; stderr
|
|
3875
|
+
* is inherited for diagnostics. The run id is the child session id for a
|
|
3876
|
+
* session-backed run, so the tool's `resume="<id>"` self-description names
|
|
3877
|
+
* the same handle the registry records.
|
|
3878
|
+
* @param request - resolved shared subagent request.
|
|
3879
|
+
* @param spec - workspace, environment, process service, and diagnostic policy.
|
|
3880
|
+
* @returns the published run after the child starts.
|
|
3881
|
+
*/
|
|
3882
|
+
async function startKimiCliRun(request, spec) {
|
|
3883
|
+
const task = textTask(request.prompt);
|
|
3884
|
+
if (request.signal.aborted) throw new Error("subagent-kimi: request was aborted before the CLI started");
|
|
3885
|
+
const turn = spec.resume?.turn ?? 1;
|
|
3886
|
+
spec.childSession?.append("turn/start", { turn });
|
|
3887
|
+
const modelArgv = spec.model === void 0 ? [] : ["-m", spec.model];
|
|
3888
|
+
const argv = spec.resume === void 0 ? [
|
|
3889
|
+
"kimi",
|
|
3890
|
+
...modelArgv,
|
|
3891
|
+
"-p",
|
|
3892
|
+
task
|
|
3893
|
+
] : [
|
|
3894
|
+
"kimi",
|
|
3895
|
+
"-S",
|
|
3896
|
+
spec.resume.cliSessionId.startsWith("session_") ? spec.resume.cliSessionId : `session_${spec.resume.cliSessionId}`,
|
|
3897
|
+
...modelArgv,
|
|
3898
|
+
"-p",
|
|
3899
|
+
task
|
|
3900
|
+
];
|
|
3901
|
+
const configurationHome = spec.homeDir ?? spec.env["KIMI_CODE_HOME"];
|
|
3902
|
+
if (spec.controlled && spec.effort !== void 0 && !configurationHome) throw new Error("Kimi effort requires an explicit scoped configuration home");
|
|
3903
|
+
const configEnv = spec.controlled && spec.effort !== void 0 ? await kimiExecConfiguration(configurationHome, spec.model, spec.effort) : {};
|
|
3904
|
+
const env = {
|
|
3905
|
+
...spec.env,
|
|
3906
|
+
...configEnv
|
|
3907
|
+
};
|
|
3908
|
+
const launch = spec.exec === void 0 ? {
|
|
3909
|
+
argv,
|
|
3910
|
+
env
|
|
3911
|
+
} : containerExecSpawn(spec.exec, {
|
|
3912
|
+
argv,
|
|
3913
|
+
env
|
|
3914
|
+
}, "subagent-kimi");
|
|
3915
|
+
const child = spec.spawn({
|
|
3916
|
+
argv: launch.argv,
|
|
3917
|
+
cwd: spec.cwd,
|
|
3918
|
+
stdio: {
|
|
3919
|
+
stdin: "ignore",
|
|
3920
|
+
stdout: "pipe",
|
|
3921
|
+
stderr: "pipe"
|
|
3922
|
+
},
|
|
3923
|
+
graceMs: spec.disposeGraceMs,
|
|
3924
|
+
env: launch.env
|
|
3925
|
+
});
|
|
3926
|
+
let output = "";
|
|
3927
|
+
child.stdout?.on("data", (chunk) => {
|
|
3928
|
+
output += chunk.toString();
|
|
3929
|
+
});
|
|
3930
|
+
let stderr = "";
|
|
3931
|
+
child.stderr?.on("data", (chunk) => {
|
|
3932
|
+
stderr += chunk.toString();
|
|
3933
|
+
});
|
|
3934
|
+
let roundUsage;
|
|
3935
|
+
const addRoundUsage = (usage) => {
|
|
3936
|
+
roundUsage = addTokenUsage(roundUsage, usage);
|
|
3937
|
+
};
|
|
3938
|
+
let mirrorQueue = Promise.resolve();
|
|
3939
|
+
const enqueueMirror = (task) => {
|
|
3940
|
+
mirrorQueue = mirrorQueue.then(task);
|
|
3941
|
+
};
|
|
3942
|
+
if (spec.childSession !== void 0 && spec.homeDir !== void 0 && spec.ctx !== void 0) {
|
|
3943
|
+
const childSession = spec.childSession;
|
|
3944
|
+
const homeDir = spec.homeDir;
|
|
3945
|
+
const mirrorCtx = spec.ctx;
|
|
3946
|
+
const poll = async () => {
|
|
3947
|
+
const kimiSessionId = spec.resume?.cliSessionId ?? kimiSessionIdFromOutput(stderr);
|
|
3948
|
+
if (kimiSessionId === void 0) return;
|
|
3949
|
+
const delta = await mirrorKimiDelta(mirrorCtx, childSession, homeDir, kimiSessionId);
|
|
3950
|
+
addRoundUsage(delta.usage);
|
|
3951
|
+
if (delta.texts.length > 0) mirrorCtx.get("localAgent")?.reportRunProgress(childSession.id, {
|
|
3952
|
+
kind: "mirror",
|
|
3953
|
+
mirroredLines: delta.total
|
|
3954
|
+
});
|
|
3955
|
+
};
|
|
3956
|
+
const liveMirror = setInterval(() => {
|
|
3957
|
+
enqueueMirror(async () => {
|
|
3958
|
+
try {
|
|
3959
|
+
await poll();
|
|
3960
|
+
} catch (error) {
|
|
3961
|
+
mirrorCtx.logger.warn(`subagent-kimi: live mirror poll failed: ${thrown(error).message}`);
|
|
3962
|
+
}
|
|
3963
|
+
});
|
|
3964
|
+
}, spec.liveMirrorIntervalMs ?? 2e3);
|
|
3965
|
+
liveMirror.unref();
|
|
3966
|
+
child.done.then(() => {
|
|
3967
|
+
clearInterval(liveMirror);
|
|
3968
|
+
}, () => {
|
|
3969
|
+
clearInterval(liveMirror);
|
|
3970
|
+
});
|
|
3971
|
+
}
|
|
3972
|
+
const disposeProcess = async () => {
|
|
3973
|
+
child.terminate();
|
|
3974
|
+
await child.waitForExit().catch(() => false);
|
|
3975
|
+
await child.done.catch(() => {});
|
|
3976
|
+
};
|
|
3977
|
+
const runAbort = new AbortController();
|
|
3978
|
+
const requestCancel = () => {
|
|
3979
|
+
if (runAbort.signal.aborted) return;
|
|
3980
|
+
runAbort.abort(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
|
|
3981
|
+
};
|
|
3982
|
+
const onAbort = () => {
|
|
3983
|
+
requestCancel();
|
|
3984
|
+
};
|
|
3985
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
3986
|
+
const abortBranch = new Promise((_, reject) => {
|
|
3987
|
+
runAbort.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally")), { once: true });
|
|
3988
|
+
});
|
|
3989
|
+
const processFailure = child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-kimi: CLI exited before the run settled (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown(error)));
|
|
3990
|
+
processFailure.catch(() => {});
|
|
3991
|
+
const collectOutput = () => {
|
|
3992
|
+
const drained = child.collected.stdout?.readFrom(0).text;
|
|
3993
|
+
const text = (drained !== void 0 && drained !== "" ? drained : output).trim();
|
|
3994
|
+
return text === "" ? [] : [{
|
|
3995
|
+
type: "text",
|
|
3996
|
+
text
|
|
3997
|
+
}];
|
|
3998
|
+
};
|
|
3999
|
+
const result = settleRunResult({
|
|
4000
|
+
attempt: () => Promise.race([
|
|
4001
|
+
child.done.then((outcome) => {
|
|
4002
|
+
if (outcome.exitCode !== 0) {
|
|
4003
|
+
const via = spec.endpointLabel ?? "kimi default endpoint";
|
|
4004
|
+
throw new Error(`subagent-kimi: kimi -p exited with code ${String(outcome.exitCode)} via ${via}`);
|
|
4005
|
+
}
|
|
4006
|
+
const output = collectOutput();
|
|
4007
|
+
if (output.length === 0) throw new Error("subagent-kimi: kimi -p exited 0 but produced no answer");
|
|
4008
|
+
return {
|
|
4009
|
+
output,
|
|
4010
|
+
stopReason: "completed"
|
|
4011
|
+
};
|
|
4012
|
+
}),
|
|
4013
|
+
processFailure,
|
|
4014
|
+
abortBranch
|
|
4015
|
+
]),
|
|
4016
|
+
collectOutput,
|
|
4017
|
+
cancelled: () => runAbort.signal.aborted,
|
|
4018
|
+
onError: spec.onError,
|
|
4019
|
+
signal: request.signal,
|
|
4020
|
+
onAbort
|
|
4021
|
+
}).then((settled) => {
|
|
4022
|
+
if (settled.stopReason === "completed") spec.onAuthSuccess?.();
|
|
4023
|
+
if (spec.childSession !== void 0) {
|
|
4024
|
+
if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
|
|
4025
|
+
turn,
|
|
4026
|
+
reason: { kind: "completed" }
|
|
4027
|
+
});
|
|
4028
|
+
else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
|
|
4029
|
+
turn,
|
|
4030
|
+
reason: {
|
|
4031
|
+
kind: "aborted",
|
|
4032
|
+
reason: { kind: "parent" }
|
|
4033
|
+
}
|
|
4034
|
+
});
|
|
4035
|
+
else spec.childSession.append("turn/end", {
|
|
4036
|
+
turn,
|
|
4037
|
+
reason: {
|
|
4038
|
+
kind: "error",
|
|
4039
|
+
error: {
|
|
4040
|
+
message: "kimi -p exited before the run completed",
|
|
4041
|
+
code: "UNKNOWN"
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
return settled;
|
|
4047
|
+
});
|
|
4048
|
+
result.then(() => child.done).then(() => {
|
|
4049
|
+
if (spec.onAuthFailure !== void 0) {
|
|
4050
|
+
const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
|
|
4051
|
+
if (KIMI_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
|
|
4052
|
+
}
|
|
4053
|
+
enqueueMirror(async () => {
|
|
4054
|
+
const settled = await mirrorKimiAfterExit(spec, stderr);
|
|
4055
|
+
addRoundUsage(settled?.usage);
|
|
4056
|
+
const cliVersion = await spec.cliVersion?.();
|
|
4057
|
+
spec.onRoundSettled?.({
|
|
4058
|
+
...settled?.model === void 0 ? {} : { observedModel: settled.model },
|
|
4059
|
+
...cliVersion === void 0 ? {} : { cliVersion },
|
|
4060
|
+
...roundUsage === void 0 ? {} : { usage: roundUsage },
|
|
4061
|
+
...settled?.toolCalls === void 0 ? {} : { toolCalls: settled.toolCalls }
|
|
4062
|
+
});
|
|
4063
|
+
});
|
|
4064
|
+
}, () => {});
|
|
4065
|
+
return Promise.resolve(subprocessRunHandle({
|
|
4066
|
+
id: spec.childSession?.id ?? SessionId(randomUUID()),
|
|
4067
|
+
result,
|
|
4068
|
+
signal: request.signal,
|
|
4069
|
+
onAbort,
|
|
4070
|
+
requestCancel,
|
|
4071
|
+
teardown: disposeProcess
|
|
4072
|
+
}));
|
|
4073
|
+
}
|
|
4074
|
+
//#endregion
|
|
4075
|
+
//#region lib/types/live-switch.js
|
|
4076
|
+
/**
|
|
4077
|
+
* The settings-driven live-driver switch: owns the active driver generation
|
|
4078
|
+
* and retires old ones on a settings change, without a plugin reload.
|
|
4079
|
+
*
|
|
4080
|
+
* Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
|
|
4081
|
+
* new rounds are refused (the provider's catch falls back to exec), in-flight
|
|
4082
|
+
* rounds finish on their runtime, idle runtimes are reclaimed at once.
|
|
4083
|
+
* Toggling ON builds the next generation lazily (no process until the first
|
|
4084
|
+
* round). Legacy granularity settings are ignored.
|
|
4085
|
+
*
|
|
4086
|
+
* @module @khorsheed/dsh-local-agent-kimi — internal, unit-tested directly.
|
|
4087
|
+
*/
|
|
4088
|
+
var LiveDriverSwitch = class {
|
|
4089
|
+
ctx;
|
|
4090
|
+
liveIdleMs;
|
|
4091
|
+
model;
|
|
4092
|
+
liveOn = false;
|
|
4093
|
+
active;
|
|
4094
|
+
/** Retiring generations still draining their in-flight rounds. */
|
|
4095
|
+
draining = /* @__PURE__ */ new Set();
|
|
4096
|
+
unwatch;
|
|
4097
|
+
constructor(ctx, face, liveIdleMs, model) {
|
|
4098
|
+
this.ctx = ctx;
|
|
4099
|
+
this.liveIdleMs = liveIdleMs;
|
|
4100
|
+
this.model = model;
|
|
4101
|
+
this.apply(face.get());
|
|
4102
|
+
this.unwatch = face.watch((next) => {
|
|
4103
|
+
this.apply(next);
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Resolve the live driver for one round's member (the provider's call
|
|
4108
|
+
* site). A retiring generation still hosting the member gates the new one —
|
|
4109
|
+
* the round falls back to exec rather than double-loading the same kimi
|
|
4110
|
+
* session into two processes.
|
|
4111
|
+
*/
|
|
4112
|
+
resolve = (childSessionId) => {
|
|
4113
|
+
if (this.active === void 0) return void 0;
|
|
4114
|
+
for (const retiring of this.draining) if (retiring.hasRuntime(childSessionId)) return void 0;
|
|
4115
|
+
return this.active;
|
|
4116
|
+
};
|
|
4117
|
+
/** Mirror the resolved settings into the driver generation. */
|
|
4118
|
+
apply(next) {
|
|
4119
|
+
if (next.live === this.liveOn) return;
|
|
4120
|
+
this.liveOn = next.live;
|
|
4121
|
+
const retiring = this.active;
|
|
4122
|
+
this.active = next.live ? new KimiAcpLiveDriver(this.ctx, {
|
|
4123
|
+
...this.liveIdleMs === void 0 ? {} : { liveIdleMs: this.liveIdleMs },
|
|
4124
|
+
...this.model === void 0 ? {} : { model: this.model }
|
|
4125
|
+
}) : void 0;
|
|
4126
|
+
if (retiring !== void 0) {
|
|
4127
|
+
this.draining.add(retiring);
|
|
4128
|
+
retiring.drain().then(() => {
|
|
4129
|
+
this.draining.delete(retiring);
|
|
4130
|
+
}, (error) => {
|
|
4131
|
+
this.draining.delete(retiring);
|
|
4132
|
+
this.ctx.logger.warn(`local-agent-kimi: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
/**
|
|
4137
|
+
* Retire one member's runtime on the ACTIVE generation (a composer-driven
|
|
4138
|
+
* model switch): the next round respawns onto the new model. A member still
|
|
4139
|
+
* hosted by a draining generation is left alone — its rounds already gate
|
|
4140
|
+
* to exec, and the drain reclaims the process.
|
|
4141
|
+
*/
|
|
4142
|
+
async retireMemberRuntime(childSessionId) {
|
|
4143
|
+
await this.active?.retireRuntime(childSessionId);
|
|
4144
|
+
}
|
|
4145
|
+
/**
|
|
4146
|
+
* The model the member's active-generation runtime is bound to, or
|
|
4147
|
+
* undefined when no live runtime serves the member (the broker's
|
|
4148
|
+
* same-model no-op check).
|
|
4149
|
+
*/
|
|
4150
|
+
memberRuntimeModel(childSessionId) {
|
|
4151
|
+
return this.active?.runtimeModel(childSessionId);
|
|
4152
|
+
}
|
|
4153
|
+
/**
|
|
4154
|
+
* Whether the active generation hosts (or is spawning) the member's
|
|
4155
|
+
* runtime — the broker's retire decision needs this apart from the bound
|
|
4156
|
+
* model, since a runtime that bound NO model reports undefined either way.
|
|
4157
|
+
*/
|
|
4158
|
+
memberHasRuntime(childSessionId) {
|
|
4159
|
+
return this.active?.hasRuntime(childSessionId) ?? false;
|
|
4160
|
+
}
|
|
4161
|
+
memberRuntimeConfiguration(childSessionId) {
|
|
4162
|
+
for (const driver of [this.active, ...this.draining]) {
|
|
4163
|
+
const configuration = driver?.runtimeConfiguration(childSessionId);
|
|
4164
|
+
if (configuration !== void 0) return configuration;
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
/** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
|
|
4168
|
+
dispose() {
|
|
4169
|
+
this.unwatch();
|
|
4170
|
+
this.active?.disposeAll();
|
|
4171
|
+
for (const retiring of this.draining) retiring.disposeAll();
|
|
4172
|
+
this.draining.clear();
|
|
4173
|
+
}
|
|
4174
|
+
};
|
|
4175
|
+
//#endregion
|
|
4176
|
+
//#region lib/types/model-broker.js
|
|
4177
|
+
/**
|
|
4178
|
+
* The kimi harness's model broker: the member/harness model surface the
|
|
4179
|
+
* family gateway routes to (`harnessModel` for the settings card,
|
|
4180
|
+
* `memberModel`/`setMemberModel` for the composer picker). Resolution follows
|
|
4181
|
+
* the family's fixed order — the session-level override (the composer's
|
|
4182
|
+
* picker write) first, then the delegation's recorded model, then the
|
|
4183
|
+
* plugin-config `model` key, then the scoped config's own `default_model`,
|
|
4184
|
+
* and last the CLI's built-in default, which names nothing.
|
|
4185
|
+
*
|
|
4186
|
+
* Per-member state is IN MEMORY by design: the session-level override is a
|
|
4187
|
+
* composer gesture that deliberately does not survive a host restart, and the
|
|
4188
|
+
* start-model ledger only mirrors what the delegation records already persist
|
|
4189
|
+
* (the core hands the recorded value back as `delegationModel` on reads).
|
|
4190
|
+
* The spawn side of a switch is lazy: `setMemberModel` retires the member's
|
|
4191
|
+
* resident runtime when its bound model no longer matches, and the member's
|
|
4192
|
+
* next round respawns onto the new model, `session/load`-ing the same CLI
|
|
4193
|
+
* session — the conversation carries over, the process fact changes.
|
|
4194
|
+
* @module @khorsheed/dsh-local-agent-kimi/model-broker
|
|
4195
|
+
*/
|
|
4196
|
+
/** The refusal a mid-round switch gets (in flight = the runtime must not die). */
|
|
4197
|
+
const SWITCH_IN_FLIGHT_REASON = "该成员有进行中的委派轮次,等其完成后再切换模型";
|
|
4198
|
+
/** Dedupe a candidate list, dropping blanks and keeping first-seen order. */
|
|
4199
|
+
function dedupe(candidates) {
|
|
4200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4201
|
+
const out = [];
|
|
4202
|
+
for (const candidate of candidates) {
|
|
4203
|
+
const value = candidate?.trim();
|
|
4204
|
+
if (value === void 0 || value === "" || seen.has(value)) continue;
|
|
4205
|
+
seen.add(value);
|
|
4206
|
+
out.push(value);
|
|
4207
|
+
}
|
|
4208
|
+
return out;
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
|
|
4212
|
+
* member-model ledger: the provider records each delegation's start model
|
|
4213
|
+
* here and reads the session-level override for its exec-path resolution, so
|
|
4214
|
+
* the picker write reaches the very next round on either driver.
|
|
4215
|
+
*/
|
|
4216
|
+
var KimiModelBroker = class {
|
|
4217
|
+
ctx;
|
|
4218
|
+
deps;
|
|
4219
|
+
/** Session-level overrides per member (the composer picker's writes). */
|
|
4220
|
+
overrides = /* @__PURE__ */ new Map();
|
|
4221
|
+
/** Each member's START model — the model its delegation's first round named. */
|
|
4222
|
+
startModels = /* @__PURE__ */ new Map();
|
|
4223
|
+
/**
|
|
4224
|
+
* @param ctx - plugin context carrying the family registry (the in-flight
|
|
4225
|
+
* read behind `switchable`).
|
|
4226
|
+
* @param deps - the harness's live readers: the scoped home, the settings
|
|
4227
|
+
* layer, the card's recent-model memory, the live toggle, and the driver
|
|
4228
|
+
* switch a switch retires runtimes through.
|
|
4229
|
+
*/
|
|
4230
|
+
constructor(ctx, deps) {
|
|
4231
|
+
this.ctx = ctx;
|
|
4232
|
+
this.deps = deps;
|
|
4233
|
+
}
|
|
4234
|
+
configurationAdapter(binding) {
|
|
4235
|
+
const home = this.ctx.localAgent.homeDir("kimi", binding.scope);
|
|
4236
|
+
const resolve = async (selection) => {
|
|
4237
|
+
const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel() ?? await readKimiDefaultModel(home);
|
|
4238
|
+
const configured = await readKimiModelConfiguration(home, model);
|
|
4239
|
+
const entry = this.deps.liveSwitch.memberRuntimeConfiguration(binding.childSessionId)?.directory.entries.find((entry) => entry.value === model) ?? configured.entries.find((entry) => entry.value === model);
|
|
4240
|
+
const effort = (selection.effort.mode === "value" ? selection.effort.value : selection.effort.mode === "inherit" ? binding.effort : void 0) ?? configured.effort;
|
|
4241
|
+
if (effort !== void 0 && entry?.reasoning !== void 0 && !entry.reasoning.options.some((option) => option.value === effort)) throw new Error("Kimi has not advertised this reasoning effort for the selected model");
|
|
4242
|
+
if (!this.deps.isLive() && effort !== void 0 && configured.protocol !== "kimi") throw new Error("This model protocol has no Kimi exec effort override; use ACP live mode");
|
|
4243
|
+
return {
|
|
4244
|
+
...model === void 0 ? {} : { model },
|
|
4245
|
+
...effort === void 0 ? {} : { effort }
|
|
4246
|
+
};
|
|
4247
|
+
};
|
|
4248
|
+
return {
|
|
4249
|
+
validate: async (selection) => {
|
|
4250
|
+
await resolve(selection);
|
|
4251
|
+
},
|
|
4252
|
+
prepare: resolve,
|
|
4253
|
+
apply: async (selection) => {
|
|
4254
|
+
const resolved = await resolve(selection);
|
|
4255
|
+
await this.deps.liveSwitch.retireMemberRuntime(binding.childSessionId);
|
|
4256
|
+
return resolved;
|
|
4257
|
+
},
|
|
4258
|
+
reconcile: async (state) => {
|
|
4259
|
+
if (this.ctx.localAgent.isDelegationActive(binding.childSessionId)) return {
|
|
4260
|
+
active: true,
|
|
4261
|
+
matches: "unknown",
|
|
4262
|
+
resolved: {}
|
|
4263
|
+
};
|
|
4264
|
+
await this.deps.liveSwitch.retireMemberRuntime(binding.childSessionId);
|
|
4265
|
+
return {
|
|
4266
|
+
active: false,
|
|
4267
|
+
matches: "current",
|
|
4268
|
+
resolved: await resolve(state.current.selection)
|
|
4269
|
+
};
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
async modelDirectory(childSessionId, refresh = false) {
|
|
4274
|
+
if (refresh) await this.deps.catalog?.refresh(childSessionId);
|
|
4275
|
+
return (await this.modelInfo(childSessionId)).directory ?? {
|
|
4276
|
+
entries: [],
|
|
4277
|
+
complete: false,
|
|
4278
|
+
customInput: true,
|
|
4279
|
+
status: "unsupported",
|
|
4280
|
+
refreshing: false,
|
|
4281
|
+
revision: 0
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
async *followModelDirectory(childSessionId, signal) {
|
|
4285
|
+
if (this.deps.catalog === void 0) {
|
|
4286
|
+
yield await this.modelDirectory(childSessionId);
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
for await (const _snapshot of this.deps.catalog.follow(childSessionId, signal)) yield await this.modelDirectory(childSessionId);
|
|
4290
|
+
}
|
|
4291
|
+
/**
|
|
4292
|
+
* Record the model a member's delegation STARTED with (the provider's call
|
|
4293
|
+
* site, fresh and resume rounds alike). The live driver's spawn resolver
|
|
4294
|
+
* ranks it between the override and the settings layer.
|
|
4295
|
+
*/
|
|
4296
|
+
noteStartModel(childSessionId, model) {
|
|
4297
|
+
const value = model.trim();
|
|
4298
|
+
if (value !== "") this.startModels.set(childSessionId, value);
|
|
4299
|
+
}
|
|
4300
|
+
/**
|
|
4301
|
+
* The member's session-level override, when the composer picker set one —
|
|
4302
|
+
* the exec path's top layer (it outranks even the delegation's recorded
|
|
4303
|
+
* model, so the provider folds it into the requested slot).
|
|
4304
|
+
*/
|
|
4305
|
+
overrideFor(childSessionId) {
|
|
4306
|
+
return this.overrides.get(childSessionId);
|
|
4307
|
+
}
|
|
4308
|
+
/**
|
|
4309
|
+
* The member's spawn-binding resolver (the live driver config): override,
|
|
4310
|
+
* then the member's start model, then the settings layer. Undefined writes
|
|
4311
|
+
* nothing — the scoped config's own `default_model` decides, as it always
|
|
4312
|
+
* did.
|
|
4313
|
+
*/
|
|
4314
|
+
spawnModel = (childSessionId) => this.overrideFor(childSessionId) ?? this.startModels.get(childSessionId) ?? this.deps.settingsModel();
|
|
4315
|
+
/**
|
|
4316
|
+
* Read the model surface. With no member this is the harness default (the
|
|
4317
|
+
* settings card's "what would a round run with"): override and delegation
|
|
4318
|
+
* stay absent by construction.
|
|
4319
|
+
*/
|
|
4320
|
+
async modelInfo(childSessionId, delegationModel) {
|
|
4321
|
+
const override = childSessionId === void 0 ? void 0 : this.overrideFor(childSessionId);
|
|
4322
|
+
const delegation = childSessionId === void 0 ? void 0 : delegationModel ?? this.startModels.get(childSessionId);
|
|
4323
|
+
const settings = this.deps.settingsModel();
|
|
4324
|
+
const homeDir = this.deps.homeDir(childSessionId);
|
|
4325
|
+
const native = this.deps.catalog?.read(childSessionId);
|
|
4326
|
+
const [cliDefault, discovered] = await Promise.all([readKimiDefaultModel(homeDir).catch(() => void 0), listKimiConfigModels(homeDir).catch(() => [])]);
|
|
4327
|
+
const choices = dedupe([
|
|
4328
|
+
settings,
|
|
4329
|
+
cliDefault,
|
|
4330
|
+
...discovered,
|
|
4331
|
+
...native?.entries.filter((entry) => !entry.hidden).map((entry) => entry.value) ?? [],
|
|
4332
|
+
...this.deps.recentModels()
|
|
4333
|
+
]);
|
|
4334
|
+
const [effective, source] = override !== void 0 ? [override, "override"] : delegation !== void 0 ? [delegation, "delegation"] : settings !== void 0 ? [settings, "settings"] : cliDefault !== void 0 ? [cliDefault, "cli-config"] : [void 0, "cli-builtin"];
|
|
4335
|
+
const inFlight = childSessionId !== void 0 && this.ctx.localAgent.activeDelegations().includes(childSessionId);
|
|
4336
|
+
return {
|
|
4337
|
+
...effective === void 0 ? {} : { effective },
|
|
4338
|
+
source,
|
|
4339
|
+
...override === void 0 ? {} : { override },
|
|
4340
|
+
...delegation === void 0 ? {} : { delegation },
|
|
4341
|
+
...settings === void 0 ? {} : { settings },
|
|
4342
|
+
...cliDefault === void 0 ? {} : { cliDefault },
|
|
4343
|
+
choices,
|
|
4344
|
+
...native === void 0 ? {} : { directory: extendModelDirectory(native, [
|
|
4345
|
+
settings,
|
|
4346
|
+
cliDefault,
|
|
4347
|
+
...discovered
|
|
4348
|
+
], this.deps.recentModels()) },
|
|
4349
|
+
live: this.deps.isLive(),
|
|
4350
|
+
switchable: !inFlight,
|
|
4351
|
+
...inFlight ? { reason: SWITCH_IN_FLIGHT_REASON } : {}
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
/**
|
|
4355
|
+
* Set (or clear) the member's session-level override. Throws while a round
|
|
4356
|
+
* is in flight for the member — the switch retires the resident runtime,
|
|
4357
|
+
* and retiring mid-round would kill the run. A same-value set (or clearing
|
|
4358
|
+
* an unset override) is a no-op; otherwise a live runtime whose bound model
|
|
4359
|
+
* differs from the new effective model is retired so the next round
|
|
4360
|
+
* respawns onto it, resuming the same CLI session.
|
|
4361
|
+
*/
|
|
4362
|
+
async setMemberModel(childSessionId, model) {
|
|
4363
|
+
if (typeof this.ctx.localAgent.setMemberModel === "function") return this.ctx.localAgent.setMemberModel(childSessionId, model);
|
|
4364
|
+
if (this.ctx.localAgent.activeDelegations().includes(childSessionId)) throw new Error(SWITCH_IN_FLIGHT_REASON);
|
|
4365
|
+
const value = model?.trim();
|
|
4366
|
+
const previous = this.overrides.get(childSessionId);
|
|
4367
|
+
if (value === void 0 || value === "") {
|
|
4368
|
+
if (previous === void 0) return;
|
|
4369
|
+
this.overrides.delete(childSessionId);
|
|
4370
|
+
} else {
|
|
4371
|
+
if (previous === value) return;
|
|
4372
|
+
this.overrides.set(childSessionId, value);
|
|
4373
|
+
}
|
|
4374
|
+
const effective = (await this.modelInfo(childSessionId)).effective;
|
|
4375
|
+
if (this.deps.liveSwitch.memberHasRuntime(childSessionId) && this.deps.liveSwitch.memberRuntimeModel(childSessionId) !== effective) await this.deps.liveSwitch.retireMemberRuntime(childSessionId);
|
|
4376
|
+
}
|
|
4377
|
+
};
|
|
4378
|
+
//#endregion
|
|
4379
|
+
//#region lib/types/records.js
|
|
4380
|
+
/**
|
|
4381
|
+
* Kimi Code session records adapter: reads the scoped home's
|
|
4382
|
+
* `session_index.jsonl`, kimi's own append-only index.
|
|
4383
|
+
* @module @khorsheed/dsh-local-agent-kimi/records
|
|
4384
|
+
*/
|
|
4385
|
+
/** Credentials live under this directory inside the scoped home. */
|
|
4386
|
+
const CREDENTIALS_DIR = "credentials";
|
|
4387
|
+
/**
|
|
4388
|
+
* Whether the scoped home holds usable login credentials, guarded by the
|
|
4389
|
+
* credential sentinel: a VALID credential file (both tokens non-empty) reads
|
|
4390
|
+
* true and refreshes the backup; an empty shell (the kimi CLI's failure-path
|
|
4391
|
+
* wipe) restores the backup when one exists and reads false otherwise. The
|
|
4392
|
+
* pre-sentinel "directory non-empty" check could not tell the empty shell
|
|
4393
|
+
* from a real credential at all.
|
|
4394
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
4395
|
+
* @returns true when usable credentials are present (after any restore).
|
|
4396
|
+
*/
|
|
4397
|
+
async function kimiAuthenticated(homeDir) {
|
|
4398
|
+
return guardKimiCredential(homeDir);
|
|
4399
|
+
}
|
|
4400
|
+
/**
|
|
4401
|
+
* The newest credential file's modification stamp (epoch ms), undefined when
|
|
4402
|
+
* no credential exists. A completed device-code login rewrites the
|
|
4403
|
+
* credentials directory, so the stamp distinguishes a fresh login from a
|
|
4404
|
+
* leftover (possibly expired) credential.
|
|
4405
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
4406
|
+
* @returns the newest credential mtime, or undefined when none exists.
|
|
4407
|
+
*/
|
|
4408
|
+
async function kimiCredentialStamp(homeDir) {
|
|
4409
|
+
try {
|
|
4410
|
+
const entries = await readdir(join(homeDir, CREDENTIALS_DIR));
|
|
4411
|
+
let newest;
|
|
4412
|
+
for (const entry of entries) {
|
|
4413
|
+
const { mtimeMs } = await stat(join(homeDir, CREDENTIALS_DIR, entry));
|
|
4414
|
+
if (newest === void 0 || mtimeMs > newest) newest = mtimeMs;
|
|
4415
|
+
}
|
|
4416
|
+
return newest;
|
|
4417
|
+
} catch {
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
/**
|
|
4422
|
+
* Read the scoped home's session index. The index is kimi's own
|
|
4423
|
+
* newline-delimited JSONL; a torn tail line (the file is append-only) is
|
|
4424
|
+
* skipped rather than failing the whole list.
|
|
4425
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
4426
|
+
* @returns the parsed records in file order, empty when no index exists yet.
|
|
4427
|
+
*/
|
|
4428
|
+
async function listKimiSessions(homeDir) {
|
|
4429
|
+
const indexPath = join(homeDir, "session_index.jsonl");
|
|
4430
|
+
let text;
|
|
4431
|
+
try {
|
|
4432
|
+
text = await readFile(indexPath, "utf8");
|
|
4433
|
+
} catch (error) {
|
|
4434
|
+
if (error.code === "ENOENT") return [];
|
|
4435
|
+
throw error;
|
|
4436
|
+
}
|
|
4437
|
+
const records = [];
|
|
4438
|
+
for (const line of text.split("\n")) {
|
|
4439
|
+
const trimmed = line.trim();
|
|
4440
|
+
if (trimmed === "") continue;
|
|
4441
|
+
try {
|
|
4442
|
+
const parsed = JSON.parse(trimmed);
|
|
4443
|
+
records.push({
|
|
4444
|
+
id: parsed.sessionId,
|
|
4445
|
+
workDir: parsed.workDir,
|
|
4446
|
+
...parsed.title !== void 0 && typeof parsed.title === "string" && { title: parsed.title },
|
|
4447
|
+
...parsed.createdAt !== void 0 && typeof parsed.createdAt === "string" && { startedAt: Date.parse(parsed.createdAt) }
|
|
4448
|
+
});
|
|
4449
|
+
} catch {}
|
|
4450
|
+
}
|
|
4451
|
+
return records;
|
|
4452
|
+
}
|
|
4453
|
+
//#endregion
|
|
4454
|
+
//#region lib/types/preset-tools.js
|
|
4455
|
+
/**
|
|
4456
|
+
* Legacy kimi preset-variant cleanup. Earlier versions of this bundle
|
|
4457
|
+
* bootstrapped `standard-kimi`, `code-kimi`, `minimal-kimi`, and `kimi`
|
|
4458
|
+
* preset variants into the user preset root so an official-style preset could
|
|
4459
|
+
* carry the `subagent_kimi` tool row. The tool row now mounts at the profile
|
|
4460
|
+
* root (see the bundle patch), so every preset delegates without variants;
|
|
4461
|
+
* this module removes the leftovers once on startup. The repo is pre-release,
|
|
4462
|
+
* so no compatibility path is kept — variants that still exist are deleted.
|
|
4463
|
+
* @module @khorsheed/dsh-local-agent-kimi/preset-tools
|
|
4464
|
+
*/
|
|
4465
|
+
/** The variant preset ids earlier bundle versions bootstrapped. */
|
|
4466
|
+
const LEGACY_VARIANTS = [
|
|
4467
|
+
"standard-kimi",
|
|
4468
|
+
"code-kimi",
|
|
4469
|
+
"minimal-kimi",
|
|
4470
|
+
"kimi"
|
|
4471
|
+
];
|
|
4472
|
+
/**
|
|
4473
|
+
* Remove the legacy kimi preset variants from the user preset root. Uses the
|
|
4474
|
+
* agent-presets service's remove (which also clears a default that named one
|
|
4475
|
+
* of them), so a session later created under a different default no longer
|
|
4476
|
+
* points at a deleted composition.
|
|
4477
|
+
* @param ctx - host context carrying the agent-presets service.
|
|
4478
|
+
*/
|
|
4479
|
+
async function removeLegacyVariants(ctx) {
|
|
4480
|
+
const presets = ctx.get("agentPresets");
|
|
4481
|
+
if (presets === void 0) return;
|
|
4482
|
+
for (const id of LEGACY_VARIANTS) try {
|
|
4483
|
+
await presets.remove(id);
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
if (error.code === "ENOENT") continue;
|
|
4486
|
+
throw error;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
//#endregion
|
|
4490
|
+
//#region lib/types/index.js
|
|
4491
|
+
/**
|
|
4492
|
+
* Kimi Code harness instantiation of the local-agent family. Registers the
|
|
4493
|
+
* `kimi` harness into the core registry: scoped home under the shared homes
|
|
4494
|
+
* root (`KIMI_CODE_HOME`), device-code login through `kimi login`, and the
|
|
4495
|
+
* `session_index.jsonl` records adapter. Delegation mounts through the
|
|
4496
|
+
* `kimi-cli` provider (one-shot `kimi -p` under the scoped home); the
|
|
4497
|
+
* bundle patch's `tool-subagent-kimi` row puts the `subagent_kimi` tool on
|
|
4498
|
+
* the profile root, so every agent preset can delegate without variants.
|
|
4499
|
+
*
|
|
4500
|
+
* @module @khorsheed/dsh-local-agent-kimi
|
|
4501
|
+
*/
|
|
4502
|
+
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
4503
|
+
const name = "local-agent-kimi";
|
|
4504
|
+
/** Services required before the harness can register. */
|
|
4505
|
+
const inject = [
|
|
4506
|
+
"localAgent",
|
|
4507
|
+
"subagents",
|
|
4508
|
+
"subprocess",
|
|
4509
|
+
"settings"
|
|
4510
|
+
];
|
|
4511
|
+
/**
|
|
4512
|
+
* The settings fields (`live`, `liveMirrorGranularity`, `model`,
|
|
4513
|
+
* `recentModels`) ride the plugin Config, marked `.volatile()` where the
|
|
4514
|
+
* host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
|
|
4515
|
+
* there the SettingsForms writes hot-track the running fiber's Volatile
|
|
4516
|
+
* references without a remount, and the official one-shot import moves an old
|
|
4517
|
+
* settings.yaml section into this row's config. On 0.1.5 the call is absent,
|
|
4518
|
+
* the fields stay plain config keys (a semantic superset — the profile row
|
|
4519
|
+
* can now preset them too), and the legacy settings namespace below carries
|
|
4520
|
+
* the user layer exactly as before.
|
|
4521
|
+
*/
|
|
4522
|
+
const SETTINGS_FIELDS = {
|
|
4523
|
+
live: z.boolean().default(false),
|
|
4524
|
+
liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
|
|
4525
|
+
model: z.string(),
|
|
4526
|
+
recentModels: z.array(z.string()).default([])
|
|
4527
|
+
};
|
|
4528
|
+
/** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
|
|
4529
|
+
function volatilize(field) {
|
|
4530
|
+
return typeof field.volatile === "function" ? field.volatile() : field;
|
|
4531
|
+
}
|
|
4532
|
+
const Config = z.object({
|
|
4533
|
+
model: volatilize(SETTINGS_FIELDS.model),
|
|
4534
|
+
thinkingEffort: z.union([
|
|
4535
|
+
z.const("low"),
|
|
4536
|
+
z.const("high"),
|
|
4537
|
+
z.const("max")
|
|
4538
|
+
]),
|
|
4539
|
+
live: volatilize(SETTINGS_FIELDS.live),
|
|
4540
|
+
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
4541
|
+
liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
|
|
4542
|
+
recentModels: volatilize(SETTINGS_FIELDS.recentModels)
|
|
4543
|
+
});
|
|
4544
|
+
/**
|
|
4545
|
+
* Settings namespace for the settings-page card. The Cordis config feeds the
|
|
4546
|
+
* composition `base` layer, so a field absent from the user layer inherits the
|
|
4547
|
+
* YAML value — the card only ever stores deliberate overrides.
|
|
4548
|
+
*/
|
|
4549
|
+
const KIMI_SETTINGS_NAMESPACE = "local-agent-kimi";
|
|
4550
|
+
/**
|
|
4551
|
+
* The card's schema; field defaults are the innermost layer below `base`.
|
|
4552
|
+
* `model` deliberately carries NO default: an unset key must resolve to
|
|
4553
|
+
* undefined, which is what keeps the pre-key behavior byte-identical.
|
|
4554
|
+
*/
|
|
4555
|
+
const KIMI_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
4556
|
+
/**
|
|
4557
|
+
* Handle the harness's extra subcommands: `session <id>` renders a kimi
|
|
4558
|
+
* session transcript. Returns synchronously with undefined for any input
|
|
4559
|
+
* that is not this harness's own.
|
|
4560
|
+
* @param input - the trimmed subcommand input.
|
|
4561
|
+
* @param invocation - the full command invocation.
|
|
4562
|
+
* @returns the command result promise, or undefined when not this harness's.
|
|
4563
|
+
*/
|
|
4564
|
+
function handleSubcommand(input, invocation) {
|
|
4565
|
+
const [verb] = input.split(/\s+/);
|
|
4566
|
+
if (verb === "session") return handleSessionSubcommand(input, invocation);
|
|
4567
|
+
}
|
|
4568
|
+
/** Run one `session` subcommand: render the transcript of a kimi session. */
|
|
4569
|
+
async function handleSessionSubcommand(input, invocation) {
|
|
4570
|
+
const id = input.split(/\s+/)[1];
|
|
4571
|
+
if (id === void 0) return {
|
|
4572
|
+
kind: "error",
|
|
4573
|
+
text: "Usage: /kimi session <session-id> (see /kimi sessions for ids)"
|
|
4574
|
+
};
|
|
4575
|
+
const sessionDir = await findKimiSessionDir(invocation.agent.ctx.localAgent.homeDir("kimi"), id);
|
|
4576
|
+
if (sessionDir === void 0) return {
|
|
4577
|
+
kind: "error",
|
|
4578
|
+
text: `no kimi session "${id}" in the scoped home`
|
|
4579
|
+
};
|
|
4580
|
+
try {
|
|
4581
|
+
return {
|
|
4582
|
+
kind: "success",
|
|
4583
|
+
text: renderTranscript(await readKimiTranscript(sessionDir))
|
|
4584
|
+
};
|
|
4585
|
+
} catch (error) {
|
|
4586
|
+
return {
|
|
4587
|
+
kind: "error",
|
|
4588
|
+
text: error instanceof Error ? error.message : String(error)
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* Register the Kimi harness into the local-agent registry.
|
|
4594
|
+
* @param ctx - plugin context carrying the registry.
|
|
4595
|
+
*/
|
|
4596
|
+
function apply(ctx, config) {
|
|
4597
|
+
ctx.effect(() => {
|
|
4598
|
+
const homeDir = ctx.localAgent.homeDir("kimi");
|
|
4599
|
+
const thinkingEffort = config.thinkingEffort ?? "high";
|
|
4600
|
+
removeLegacyVariants(ctx).catch((error) => {
|
|
4601
|
+
ctx.logger.warn(`local-agent-kimi: legacy preset cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4602
|
+
});
|
|
4603
|
+
provisionKimiConfig(homeDir, vol(config.model) ?? "kimi-code/k3", thinkingEffort).then(() => ensureKimiPermissions(homeDir)).catch((error) => {
|
|
4604
|
+
ctx.logger.warn(`local-agent-kimi: config provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4605
|
+
});
|
|
4606
|
+
const readSettings = () => {
|
|
4607
|
+
const model = vol(config.model);
|
|
4608
|
+
const recentModels = vol(config.recentModels);
|
|
4609
|
+
return {
|
|
4610
|
+
live: vol(config.live) ?? false,
|
|
4611
|
+
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
|
|
4612
|
+
...model === void 0 ? {} : { model },
|
|
4613
|
+
...recentModels === void 0 ? {} : { recentModels }
|
|
4614
|
+
};
|
|
4615
|
+
};
|
|
4616
|
+
const legacyBase = {};
|
|
4617
|
+
for (const field of [
|
|
4618
|
+
"live",
|
|
4619
|
+
"liveMirrorGranularity",
|
|
4620
|
+
"model"
|
|
4621
|
+
]) {
|
|
4622
|
+
const value = vol(config[field]);
|
|
4623
|
+
if (value !== void 0) legacyBase[field] = value;
|
|
4624
|
+
}
|
|
4625
|
+
const face = settingsFace(ctx, {
|
|
4626
|
+
namespace: KIMI_SETTINGS_NAMESPACE,
|
|
4627
|
+
legacySchema: KIMI_SETTINGS_SCHEMA,
|
|
4628
|
+
legacyBase,
|
|
4629
|
+
read: readSettings
|
|
4630
|
+
});
|
|
4631
|
+
const resolveModel = () => {
|
|
4632
|
+
const model = face.get().model?.trim();
|
|
4633
|
+
return model === void 0 || model === "" ? void 0 : model;
|
|
4634
|
+
};
|
|
4635
|
+
let broker;
|
|
4636
|
+
const liveSwitch = new LiveDriverSwitch(ctx, face, config.liveIdleMs, (child) => broker.spawnModel(child));
|
|
4637
|
+
const modelCatalog = new KimiModelCatalog((childSessionId) => {
|
|
4638
|
+
const record = childSessionId === void 0 ? void 0 : ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId);
|
|
4639
|
+
const native = childSessionId === void 0 ? void 0 : liveSwitch.memberRuntimeConfiguration(childSessionId);
|
|
4640
|
+
return {
|
|
4641
|
+
homeDir: ctx.localAgent.homeDir("kimi", record?.scope),
|
|
4642
|
+
...record?.cwd === void 0 ? {} : { cwd: record.cwd },
|
|
4643
|
+
...native === void 0 ? {} : { native }
|
|
4644
|
+
};
|
|
4645
|
+
});
|
|
4646
|
+
broker = new KimiModelBroker(ctx, {
|
|
4647
|
+
homeDir: (childSessionId) => ctx.localAgent.homeDir("kimi", childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
|
|
4648
|
+
catalog: modelCatalog,
|
|
4649
|
+
settingsModel: resolveModel,
|
|
4650
|
+
recentModels: () => face.get().recentModels ?? [],
|
|
4651
|
+
isLive: () => face.get().live,
|
|
4652
|
+
liveSwitch
|
|
4653
|
+
});
|
|
4654
|
+
const disposeProvider = ctx.subagents.registerProvider(new KimiCliProvider(ctx, liveSwitch.resolve, resolveModel, broker));
|
|
4655
|
+
const disposeHarness = ctx.localAgent.register({
|
|
4656
|
+
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
4657
|
+
const driver = liveSwitch.resolve(binding.childSessionId);
|
|
4658
|
+
if (driver === void 0 || driver.disabled) throw new Error("Enable the native live harness before preparing a coordinator");
|
|
4659
|
+
await driver.prepare({
|
|
4660
|
+
cwd: binding.cwd,
|
|
4661
|
+
homeDir: ctx.localAgent.homeDir("kimi", binding.scope),
|
|
4662
|
+
childSession,
|
|
4663
|
+
parentSessionId: binding.parentSessionId,
|
|
4664
|
+
configuration: configuration.resolved
|
|
4665
|
+
}, signal);
|
|
4666
|
+
},
|
|
4667
|
+
name: "kimi",
|
|
4668
|
+
displayName: "Kimi Code",
|
|
4669
|
+
homeEnvVar: "KIMI_CODE_HOME",
|
|
4670
|
+
delegationProvider: "kimi-cli",
|
|
4671
|
+
login: {
|
|
4672
|
+
command: "kimi",
|
|
4673
|
+
args: ["login"]
|
|
4674
|
+
},
|
|
4675
|
+
records: { listSessions: (homeDir) => listKimiSessions(homeDir) },
|
|
4676
|
+
provision: async (scopedHome) => {
|
|
4677
|
+
await provisionKimiConfig(scopedHome, vol(config.model) ?? "kimi-code/k3", thinkingEffort);
|
|
4678
|
+
await ensureKimiPermissions(scopedHome);
|
|
4679
|
+
},
|
|
4680
|
+
isAuthenticated: kimiAuthenticated,
|
|
4681
|
+
credentialStamp: kimiCredentialStamp,
|
|
4682
|
+
logout: kimiLogout,
|
|
4683
|
+
subcommand: handleSubcommand,
|
|
4684
|
+
modelBroker: broker,
|
|
4685
|
+
effectiveSettings: async (scopedHome) => {
|
|
4686
|
+
const [reasoningEffort, baseUrl, autoApprove, defaultModel, cliVersion] = await Promise.all([
|
|
4687
|
+
readKimiReasoningEffort(scopedHome).catch(() => void 0),
|
|
4688
|
+
readKimiBaseUrl(scopedHome).catch(() => void 0),
|
|
4689
|
+
readKimiAutoApprove(scopedHome).catch(() => false),
|
|
4690
|
+
readKimiDefaultModel(scopedHome).catch(() => void 0),
|
|
4691
|
+
kimiCliVersion(ctx, scopedHome).catch(() => void 0)
|
|
4692
|
+
]);
|
|
4693
|
+
const custom = baseUrl !== void 0 && baseUrl !== "https://api.kimi.com/coding/v1";
|
|
4694
|
+
const baseUrlHost = custom ? endpointHost(baseUrl) : void 0;
|
|
4695
|
+
const model = resolveModel() ?? defaultModel;
|
|
4696
|
+
return {
|
|
4697
|
+
drive: face.get().live ? "live" : "exec",
|
|
4698
|
+
autoApprove,
|
|
4699
|
+
...reasoningEffort !== void 0 ? { reasoningEffort } : {},
|
|
4700
|
+
baseUrlSet: custom,
|
|
4701
|
+
...baseUrlHost !== void 0 ? { baseUrlHost } : {},
|
|
4702
|
+
...model !== void 0 ? { model } : {},
|
|
4703
|
+
...cliVersion !== void 0 ? { cliVersion } : {}
|
|
4704
|
+
};
|
|
4705
|
+
}
|
|
4706
|
+
});
|
|
4707
|
+
return () => {
|
|
4708
|
+
modelCatalog.dispose();
|
|
4709
|
+
disposeProvider();
|
|
4710
|
+
disposeHarness();
|
|
4711
|
+
liveSwitch.dispose();
|
|
4712
|
+
};
|
|
4713
|
+
}, "local-agent-kimi: harness");
|
|
4714
|
+
}
|
|
4715
|
+
//#endregion
|
|
4716
|
+
export { Config, KIMI_SETTINGS_NAMESPACE, apply, inject, name };
|