@khorsheed/dsh-local-agent-kimi 0.2.0-rc.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.en.md +24 -112
- package/README.i18n.yaml +2 -2
- package/README.md +24 -112
- package/lib/client.js +816 -324
- package/lib/index.js +2121 -1311
- package/lib/types/client/SettingsCard.d.ts +75 -24
- package/lib/types/client/SettingsCard.js +110 -137
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.d.ts +6 -25
- package/lib/types/client/index.js +134 -51
- package/lib/types/client/locales.d.ts +34 -24
- package/lib/types/client/locales.js +38 -28
- package/lib/types/endpoint-auth.d.ts +202 -0
- package/lib/types/endpoint-auth.js +287 -0
- package/lib/types/index.d.ts +19 -2
- package/lib/types/index.js +146 -19
- package/lib/types/kimi-cli-provider.d.ts +31 -4
- package/lib/types/kimi-cli-provider.js +110 -60
- package/lib/types/live-driver.d.ts +17 -5
- package/lib/types/live-driver.js +345 -129
- package/lib/types/live-switch.d.ts +16 -1
- package/lib/types/live-switch.js +11 -1
- package/lib/types/model-broker.d.ts +12 -2
- package/lib/types/model-broker.js +51 -6
- package/lib/types/model-catalog.d.ts +44 -1
- package/lib/types/model-catalog.js +110 -17
- package/lib/types/model-configuration.d.ts +26 -0
- package/lib/types/model-configuration.js +43 -0
- package/lib/types/provision.d.ts +102 -31
- package/lib/types/provision.js +257 -45
- package/lib/types/records.d.ts +0 -20
- package/lib/types/records.js +1 -40
- package/lib/types/session-mirror.d.ts +115 -52
- package/lib/types/session-mirror.js +286 -210
- package/lib/types/session-view.d.ts +23 -2
- package/lib/types/session-view.js +37 -7
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +21 -22
- package/lib/invariant.js +0 -31
- package/lib/types/credential-guard.d.ts +0 -29
- package/lib/types/credential-guard.js +0 -107
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -33
package/lib/index.js
CHANGED
|
@@ -1,16 +1,295 @@
|
|
|
1
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";
|
|
2
|
+
import { LIVE_FLUSH_INTERVAL_MS, LiveFlush, LiveStreamPublisher, ModelDirectoryCache, assertResumeCwdUnchanged, assertResumeScopeUnchanged, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, firstModelOf, modelDirectoryContextKey, persistChildSession, probeCliVersion, readModelList, readModelListSync, resolveChildCwd, resolveRoundModel, settingsFace, subagentDelegationLabel, vol, writeModelList } from "@khorsheed/dsh-local-agent";
|
|
3
3
|
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV, endpointHost } from "@khorsheed/dsh-local-agent/types";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
6
|
-
import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
6
|
+
import { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
9
9
|
import { StringDecoder } from "node:string_decoder";
|
|
10
10
|
import { createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
11
|
-
import { execFile } from "node:child_process";
|
|
12
11
|
import { homedir } from "node:os";
|
|
13
12
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
//#region lib/types/endpoint-auth.js
|
|
14
|
+
/**
|
|
15
|
+
* Endpoint-key authentication — the kimi harness's ONLY auth mode. There is
|
|
16
|
+
* no OAuth anywhere in this package: the scoped config's managed provider
|
|
17
|
+
* (`managed:kimi-code`) declares `api_key_env = "DSH_KIMI_ENDPOINT_KEY"`
|
|
18
|
+
* (kimi-cli ≥2.1 reads the key from that environment variable — verified by
|
|
19
|
+
* the CLI's own "declares api_key_env but the environment variable is not
|
|
20
|
+
* set" diagnostic), the key itself lives only here and in the child process's
|
|
21
|
+
* environment, and `config.toml` carries no secret, so it stays a legitimate
|
|
22
|
+
* evaluation-condition input.
|
|
23
|
+
*
|
|
24
|
+
* Two layers, merged per field, file over config:
|
|
25
|
+
*
|
|
26
|
+
* - `<scopedHome>/endpoint-auth.json` (mode 0600) — the per-scope credential.
|
|
27
|
+
* Its name deliberately matches the eval contract hash's credential denylist
|
|
28
|
+
* (`auth`), so the key material never enters an evaluation's condition
|
|
29
|
+
* fingerprint; the file's mtime serves as the credential stamp, which makes
|
|
30
|
+
* "edit the file" the recovery gesture for a `rejected` grade.
|
|
31
|
+
* - The plugin config's `baseUrl` / `apiKey` — the deployment default every
|
|
32
|
+
* scope without a file shares. Read PER ROUND from the apply-time snapshot,
|
|
33
|
+
* so a profile edit applies to the next round (and a host restart clears
|
|
34
|
+
* the in-memory `rejected` mark).
|
|
35
|
+
*
|
|
36
|
+
* `baseUrl` is optional: absent means kimi's own coding service
|
|
37
|
+
* ({@link KIMI_MANAGED_BASE_URL} — an official platform key needs no relay).
|
|
38
|
+
* A present baseUrl is written into the managed provider's `base_url` by the
|
|
39
|
+
* config surgery (provision.ts), which is also what keeps the two layers'
|
|
40
|
+
* merge honest: the file the CLI reads always shows the effective endpoint.
|
|
41
|
+
*
|
|
42
|
+
* The env layer rides the family's existing plumbing: `delegationEnv` on the
|
|
43
|
+
* host, `containerExecSpawn`'s name-only `-e` forwarding into a container —
|
|
44
|
+
* a containerized round authenticates with the same credential as the host,
|
|
45
|
+
* nothing is authorized inside the unit.
|
|
46
|
+
* @module @khorsheed/dsh-local-agent-kimi/endpoint-auth
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* The scoped-home credential file's name. Chosen to match the eval contract
|
|
50
|
+
* hash's credential-shaped denylist (`/token|key|credential|secret|password|auth|\.env/i`),
|
|
51
|
+
* so the file is excluded from condition fingerprints by name.
|
|
52
|
+
*/
|
|
53
|
+
const ENDPOINT_AUTH_FILE = "endpoint-auth.json";
|
|
54
|
+
/**
|
|
55
|
+
* The environment variable the managed provider's `api_key_env` names. The
|
|
56
|
+
* key material only ever lives in endpoint-auth.json and in the child
|
|
57
|
+
* process's environment under this name.
|
|
58
|
+
*/
|
|
59
|
+
const ENDPOINT_ENV_KEY = "DSH_KIMI_ENDPOINT_KEY";
|
|
60
|
+
/** Trim and normalize one credential field; blank reads as absent. */
|
|
61
|
+
function field(value) {
|
|
62
|
+
if (typeof value !== "string") return void 0;
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
65
|
+
}
|
|
66
|
+
/** Whether a resolved endpoint carries a usable credential. */
|
|
67
|
+
function hasEndpointCredential(auth) {
|
|
68
|
+
return auth !== void 0 && field(auth.apiKey) !== void 0;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the scope's endpoint-auth.json. Absent or unreadable yields undefined
|
|
72
|
+
* (the caller falls back to the plugin config); malformed JSON or a non-object
|
|
73
|
+
* payload also yields undefined — a torn file must not fail a status read.
|
|
74
|
+
* Non-string fields are dropped, never carried through.
|
|
75
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
76
|
+
* @returns the file's endpoint credential, or undefined.
|
|
77
|
+
*/
|
|
78
|
+
async function readEndpointAuthFile(homeDir) {
|
|
79
|
+
let text;
|
|
80
|
+
try {
|
|
81
|
+
text = await readFile(join(homeDir, ENDPOINT_AUTH_FILE), "utf8");
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(text);
|
|
87
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
88
|
+
const baseUrl = field(parsed["baseUrl"]);
|
|
89
|
+
const apiKey = field(parsed["apiKey"]);
|
|
90
|
+
return {
|
|
91
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
92
|
+
...apiKey === void 0 ? {} : { apiKey }
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve one scope's effective endpoint credential: the scope's
|
|
100
|
+
* endpoint-auth.json merged FIELD BY FIELD over the plugin config's
|
|
101
|
+
* `baseUrl` / `apiKey` (a file that names only a key rotates the key while
|
|
102
|
+
* keeping the deployment's endpoint). The result may carry no credential at
|
|
103
|
+
* all — {@link hasEndpointCredential} is the judgment, kept separate so the
|
|
104
|
+
* caller can still route/log the resolved baseUrl.
|
|
105
|
+
* @param homeDir - the scope's scoped home.
|
|
106
|
+
* @param config - the plugin config's deployment-level endpoint pair.
|
|
107
|
+
* @returns the merged endpoint auth.
|
|
108
|
+
*/
|
|
109
|
+
async function resolveEndpointAuth(homeDir, config) {
|
|
110
|
+
const file = await readEndpointAuthFile(homeDir);
|
|
111
|
+
const configBaseUrl = field(config.baseUrl);
|
|
112
|
+
const configApiKey = field(config.apiKey);
|
|
113
|
+
return {
|
|
114
|
+
...configBaseUrl === void 0 ? {} : { baseUrl: configBaseUrl },
|
|
115
|
+
...configApiKey === void 0 ? {} : { apiKey: configApiKey },
|
|
116
|
+
...file
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The spawn-env layer for a resolved endpoint: the key under the managed
|
|
121
|
+
* provider's `api_key_env` name, nothing else. A credential-less resolution
|
|
122
|
+
* yields an empty layer — the spread then adds no keys at all, byte-identical
|
|
123
|
+
* to the pre-endpoint env.
|
|
124
|
+
* @param auth - the resolved endpoint auth, or undefined.
|
|
125
|
+
* @returns the explicit env entries for the delegation spawn.
|
|
126
|
+
*/
|
|
127
|
+
function endpointAuthEnv(auth) {
|
|
128
|
+
const apiKey = field(auth?.apiKey);
|
|
129
|
+
return apiKey === void 0 ? {} : { [ENDPOINT_ENV_KEY]: apiKey };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The credential marker's modification stamp (epoch ms): the endpoint file's
|
|
133
|
+
* mtime, or undefined when the scope has no file (a config-sourced credential
|
|
134
|
+
* has no marker — rotating it pairs with the profile edit and instance
|
|
135
|
+
* restart that clear the in-memory grade marks). A fresh file write is a
|
|
136
|
+
* fresh stamp, which is how an edited-in key recovers a `rejected` grade.
|
|
137
|
+
* @param homeDir - the scope's scoped home.
|
|
138
|
+
* @returns the stamp, or undefined.
|
|
139
|
+
*/
|
|
140
|
+
async function endpointAuthStamp(homeDir) {
|
|
141
|
+
try {
|
|
142
|
+
return (await stat(join(homeDir, ENDPOINT_AUTH_FILE))).mtimeMs;
|
|
143
|
+
} catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Whether the scope carries its own endpoint file at all. */
|
|
148
|
+
async function endpointAuthFileExists(homeDir) {
|
|
149
|
+
return await endpointAuthStamp(homeDir) !== void 0;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Write the scope's endpoint-auth.json (mode 0600, mkdir -p first).
|
|
153
|
+
* Whole-file write: the payload is exactly what {@link readEndpointAuthFile}
|
|
154
|
+
* parses back.
|
|
155
|
+
* @param homeDir - the scope's scoped home.
|
|
156
|
+
* @param auth - the endpoint credential to persist.
|
|
157
|
+
*/
|
|
158
|
+
async function writeEndpointAuth(homeDir, auth) {
|
|
159
|
+
await mkdir(homeDir, { recursive: true });
|
|
160
|
+
const baseUrl = field(auth.baseUrl);
|
|
161
|
+
const apiKey = field(auth.apiKey);
|
|
162
|
+
const payload = {
|
|
163
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
164
|
+
...apiKey === void 0 ? {} : { apiKey }
|
|
165
|
+
};
|
|
166
|
+
await writeFile(join(homeDir, ENDPOINT_AUTH_FILE), JSON.stringify(payload, void 0, 2) + "\n", { mode: 384 });
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Clear the scope's OWN endpoint credential (the endpoint-auth.json file).
|
|
170
|
+
* A deployment-level credential from the plugin config is untouched — the
|
|
171
|
+
* core's logout reply then names the profile row as the place to edit.
|
|
172
|
+
* @param homeDir - the scope's scoped home.
|
|
173
|
+
*/
|
|
174
|
+
async function clearEndpointAuth(homeDir) {
|
|
175
|
+
await rm(join(homeDir, ENDPOINT_AUTH_FILE), { force: true });
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The managed provider's endpoint, written into a fresh scoped config and the
|
|
179
|
+
* baseline the eval snapshot compares against: a scoped config still pointing
|
|
180
|
+
* here routes through kimi's own service, i.e. no custom endpoint is in force.
|
|
181
|
+
* It lives in THIS module (not provision.ts) because the endpoint check and
|
|
182
|
+
* the model catalog need it without a cycle — provision.ts already imports
|
|
183
|
+
* {@link ENDPOINT_ENV_KEY} from here.
|
|
184
|
+
*/
|
|
185
|
+
const KIMI_MANAGED_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
186
|
+
/** How many served model ids the check reply lists before truncating. */
|
|
187
|
+
const CHECK_MODEL_LIST_LIMIT = 8;
|
|
188
|
+
/** Bound on the error-body excerpt a failed check reports. */
|
|
189
|
+
const CHECK_ERROR_BODY_LIMIT = 200;
|
|
190
|
+
/**
|
|
191
|
+
* `GET <baseUrl>/v1/models` with `Authorization: Bearer <key>` — the
|
|
192
|
+
* token-free endpoint probe shared by the settings card's verify button
|
|
193
|
+
* (`checkEndpoint`) and the model catalog. Kimi's own coding service answers
|
|
194
|
+
* it with the managed key (verified 2026-10-22 against
|
|
195
|
+
* `https://api.kimi.com/coding/v1`: 200 with Bearer — the sibling x-api-key
|
|
196
|
+
* header also works, but Bearer is the one convention this package sends).
|
|
197
|
+
* The reply carries kimi's own fields — `display_name`,
|
|
198
|
+
* `think_efforts.valid_efforts`/`default_effort` (a sibling
|
|
199
|
+
* `reasoning.effort.valid`/`default` carries the same data; `think_efforts`
|
|
200
|
+
* is the field this reads), `context_length` — while a generic
|
|
201
|
+
* OpenAI-shaped relay answers ids only, so every optional field tolerates
|
|
202
|
+
* absence. Never throws: every failure mode folds into the result.
|
|
203
|
+
* @param endpoint - the resolved endpoint auth (caller checks the credential).
|
|
204
|
+
* @param fetchImpl - the HTTP fetch; default the host's global fetch.
|
|
205
|
+
* @param signal - abort for the whole probe (timeout bundled by the caller).
|
|
206
|
+
* @returns the folded result.
|
|
207
|
+
*/
|
|
208
|
+
async function fetchEndpointModels(endpoint, fetchImpl, signal) {
|
|
209
|
+
const headers = {};
|
|
210
|
+
if (endpoint.apiKey !== void 0) headers["authorization"] = `Bearer ${endpoint.apiKey}`;
|
|
211
|
+
const fetcher = fetchImpl ?? globalThis.fetch;
|
|
212
|
+
const root = endpoint.baseUrl.replace(/\/+$/, "");
|
|
213
|
+
const url = `${root}${root.endsWith("/v1") ? "" : "/v1"}/models`;
|
|
214
|
+
let response;
|
|
215
|
+
try {
|
|
216
|
+
response = await fetcher(url, {
|
|
217
|
+
headers,
|
|
218
|
+
signal: signal ?? AbortSignal.timeout(8e3)
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
kind: "unreachable",
|
|
223
|
+
message: error instanceof Error ? error.message : String(error)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (!response.ok) return {
|
|
227
|
+
kind: "http",
|
|
228
|
+
status: response.status,
|
|
229
|
+
body: (await response.text().catch(() => "")).slice(0, CHECK_ERROR_BODY_LIMIT)
|
|
230
|
+
};
|
|
231
|
+
let data;
|
|
232
|
+
try {
|
|
233
|
+
data = JSON.parse(await response.text()).data;
|
|
234
|
+
} catch {
|
|
235
|
+
return { kind: "malformed" };
|
|
236
|
+
}
|
|
237
|
+
if (!Array.isArray(data)) return { kind: "malformed" };
|
|
238
|
+
const models = [];
|
|
239
|
+
for (const entry of data) {
|
|
240
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
241
|
+
const record = entry;
|
|
242
|
+
if (typeof record.id !== "string" || record.id === "") continue;
|
|
243
|
+
const thinkEfforts = typeof record.think_efforts === "object" && record.think_efforts !== null ? record.think_efforts : void 0;
|
|
244
|
+
const efforts = thinkEfforts?.valid_efforts;
|
|
245
|
+
models.push({
|
|
246
|
+
id: record.id,
|
|
247
|
+
...typeof record.display_name === "string" && record.display_name !== "" ? { displayName: record.display_name } : {},
|
|
248
|
+
...Array.isArray(efforts) && efforts.some((effort) => typeof effort === "string" && effort !== "") ? { efforts: efforts.filter((effort) => typeof effort === "string" && effort !== "") } : {},
|
|
249
|
+
...typeof thinkEfforts?.default_effort === "string" && thinkEfforts.default_effort !== "" ? { defaultEffort: thinkEfforts.default_effort } : {},
|
|
250
|
+
...typeof record.context_length === "number" && Number.isFinite(record.context_length) ? { contextSize: record.context_length } : {}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
kind: "models",
|
|
255
|
+
models
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Probe the resolved endpoint with a cheap, token-free round-trip: `GET
|
|
260
|
+
* <baseUrl>/v1/models` with the resolved credential's Bearer header. A 200
|
|
261
|
+
* proves both reachability and credential validity — the exact evidence a
|
|
262
|
+
* "verify" button needs — and the served model list rides the reply so the
|
|
263
|
+
* user sees what the endpoint offers. One transport-level retry rides along:
|
|
264
|
+
* a local proxy restart (a Surge/clash reload) stalls the first attempt into
|
|
265
|
+
* the timeout and answers the retry immediately. Never throws: every failure
|
|
266
|
+
* mode folds into the reply text, since the reply IS the diagnostic.
|
|
267
|
+
* @param endpoint - the resolved endpoint auth (scope file over settings).
|
|
268
|
+
* @param fetchImpl - the HTTP fetch; default the host's global fetch.
|
|
269
|
+
* @param timeoutMs - one attempt's deadline (two attempts at most).
|
|
270
|
+
* @returns the human-readable verdict for the check reply.
|
|
271
|
+
*/
|
|
272
|
+
async function checkKimiEndpoint(endpoint, fetchImpl, timeoutMs = 2e4) {
|
|
273
|
+
if (!hasEndpointCredential(endpoint)) return "No endpoint credential configured — fill API Key on the settings card (or the profile row) first.";
|
|
274
|
+
const baseUrl = (endpoint.baseUrl ?? "https://api.kimi.com/coding/v1").replace(/\/+$/, "");
|
|
275
|
+
let result = await fetchEndpointModels({
|
|
276
|
+
...endpoint,
|
|
277
|
+
baseUrl
|
|
278
|
+
}, fetchImpl, AbortSignal.timeout(timeoutMs));
|
|
279
|
+
if (result.kind === "unreachable") result = await fetchEndpointModels({
|
|
280
|
+
...endpoint,
|
|
281
|
+
baseUrl
|
|
282
|
+
}, fetchImpl, AbortSignal.timeout(timeoutMs));
|
|
283
|
+
if (result.kind === "unreachable") return `Endpoint unreachable (${baseUrl}): ${result.message}`;
|
|
284
|
+
if (result.kind === "http") return `Endpoint answered HTTP ${String(result.status)} (${baseUrl})${result.body === "" ? "" : `: ${result.body}`}`;
|
|
285
|
+
if (result.kind === "malformed") return `Endpoint reachable (${baseUrl}) but the model list did not parse — the credential worked, the reply was not /v1/models-shaped.`;
|
|
286
|
+
if (result.models.length === 0) return `Endpoint reachable (${baseUrl}); it serves no models.`;
|
|
287
|
+
const ids = result.models.map((model) => model.id);
|
|
288
|
+
const listed = ids.slice(0, CHECK_MODEL_LIST_LIMIT).join(", ");
|
|
289
|
+
const more = ids.length > CHECK_MODEL_LIST_LIMIT ? `, … +${String(ids.length - CHECK_MODEL_LIST_LIMIT)} more` : "";
|
|
290
|
+
return `Endpoint reachable (${baseUrl}): ${String(ids.length)} model(s) served — ${listed}${more}`;
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
14
293
|
//#region ../../node_modules/.pnpm/smol-toml@1.9.0/node_modules/smol-toml/dist/error.js
|
|
15
294
|
/*!
|
|
16
295
|
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
@@ -856,6 +1135,47 @@ function parse(toml, options = {}) {
|
|
|
856
1135
|
//#region lib/types/model-configuration.js
|
|
857
1136
|
const table = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
858
1137
|
const string = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1138
|
+
/**
|
|
1139
|
+
* List the scoped config's `[models.*]` tables (quoted and bare keys alike —
|
|
1140
|
+
* smol-toml folds both to the same object keys). An absent, unreadable, or
|
|
1141
|
+
* malformed config yields an empty list: the catalog's enrichment layer then
|
|
1142
|
+
* simply has nothing to enrich with. Read-only: the config is authoritative.
|
|
1143
|
+
* @param home - the `kimi` harness's scoped home.
|
|
1144
|
+
* @returns the model tables, in file order.
|
|
1145
|
+
*/
|
|
1146
|
+
async function readKimiModelTables(home) {
|
|
1147
|
+
let text;
|
|
1148
|
+
try {
|
|
1149
|
+
text = await readFile(join(home, "config.toml"), "utf8");
|
|
1150
|
+
} catch {
|
|
1151
|
+
return [];
|
|
1152
|
+
}
|
|
1153
|
+
let config;
|
|
1154
|
+
try {
|
|
1155
|
+
config = parse(text);
|
|
1156
|
+
} catch {
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
const models = table(config["models"]);
|
|
1160
|
+
return Object.entries(models).map(([key, raw]) => {
|
|
1161
|
+
const model = table(raw);
|
|
1162
|
+
const provider = string(model["provider"]);
|
|
1163
|
+
const upstream = string(model["model"]);
|
|
1164
|
+
const displayName = string(model["display_name"]);
|
|
1165
|
+
const efforts = model["support_efforts"];
|
|
1166
|
+
const defaultEffort = string(model["default_effort"]);
|
|
1167
|
+
const contextSize = model["max_context_size"];
|
|
1168
|
+
return {
|
|
1169
|
+
key,
|
|
1170
|
+
...provider === void 0 ? {} : { provider },
|
|
1171
|
+
...upstream === void 0 ? {} : { model: upstream },
|
|
1172
|
+
...displayName === void 0 ? {} : { displayName },
|
|
1173
|
+
...!Array.isArray(efforts) ? {} : { efforts: efforts.filter((effort) => typeof effort === "string" && effort !== "") },
|
|
1174
|
+
...defaultEffort === void 0 ? {} : { defaultEffort },
|
|
1175
|
+
...typeof contextSize === "number" && Number.isFinite(contextSize) ? { contextSize } : {}
|
|
1176
|
+
};
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
859
1179
|
/** Native configuration metadata only; provider credentials never leave this function. */
|
|
860
1180
|
async function readKimiModelConfiguration(home, selectedModel) {
|
|
861
1181
|
let text;
|
|
@@ -973,52 +1293,149 @@ function kimiNativeConfiguration(value) {
|
|
|
973
1293
|
...typeof legacy["currentModelId"] === "string" ? { currentModel: legacy["currentModelId"] } : {}
|
|
974
1294
|
};
|
|
975
1295
|
}
|
|
976
|
-
/**
|
|
1296
|
+
/** One directory answer that names why it is empty. kimi never offers custom
|
|
1297
|
+
* input: a free-typed value names no `[models.…]` table, and kimi-cli refuses
|
|
1298
|
+
* `-m` values without one (verified 2.1.1) — unlike claude, only the
|
|
1299
|
+
* listed/configured models are runnable. */
|
|
1300
|
+
function unsupported(reason) {
|
|
1301
|
+
return {
|
|
1302
|
+
entries: [],
|
|
1303
|
+
complete: false,
|
|
1304
|
+
customInput: false,
|
|
1305
|
+
unsupported: true,
|
|
1306
|
+
reason
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* The model directory, sourced from the configured ENDPOINT (`GET
|
|
1311
|
+
* <baseUrl>/v1/models` with the resolved credential's Bearer header — kimi's
|
|
1312
|
+
* own coding service answers it with the managed key, verified 2026-10-22) —
|
|
1313
|
+
* never from the scoped config's model tables alone: what the picker lists is
|
|
1314
|
+
* exactly what the endpoint serves, so a credential that does not work can
|
|
1315
|
+
* never hide behind a locally-known list. The answers are deliberate:
|
|
1316
|
+
*
|
|
1317
|
+
* - no credential resolved → empty with a "save a key first" reason;
|
|
1318
|
+
* - 401/403 → empty with the rejection (definitive — cached as data);
|
|
1319
|
+
* - another HTTP status → empty with it (the relay serves no model route);
|
|
1320
|
+
* - a transport failure THROWS, so the cache keeps the last good list and
|
|
1321
|
+
* retries on schedule instead of blanking a momentarily-down endpoint.
|
|
1322
|
+
*
|
|
1323
|
+
* The config's `[models.…]` tables survive as METADATA ENRICHMENT: a served
|
|
1324
|
+
* model whose upstream id matches a managed table's `model` takes that
|
|
1325
|
+
* table's KEY as its value — the runnable name `-m` resolves against — and
|
|
1326
|
+
* the table's display_name/support_efforts/default_effort fill whatever the
|
|
1327
|
+
* endpoint did not advertise; a served id with no managed table stays the
|
|
1328
|
+
* bare id, which the model list's write face provisions a table for
|
|
1329
|
+
* (provision.ts's `ensureKimiModelTables`). Reads before a session exists
|
|
1330
|
+
* never start a disposable ACP session.
|
|
1331
|
+
*/
|
|
977
1332
|
var KimiModelCatalog = class {
|
|
978
1333
|
context;
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const { entries, defaultModel } = await readKimiModelConfiguration(context.homeDir);
|
|
983
|
-
return {
|
|
984
|
-
entries,
|
|
985
|
-
...defaultModel === void 0 ? {} : { defaultModel },
|
|
986
|
-
complete: false,
|
|
987
|
-
customInput: true,
|
|
988
|
-
reason: "Configured candidates; native session model metadata is not available yet"
|
|
989
|
-
};
|
|
990
|
-
} });
|
|
991
|
-
constructor(context) {
|
|
1334
|
+
deps;
|
|
1335
|
+
cache;
|
|
1336
|
+
constructor(context, deps) {
|
|
992
1337
|
this.context = context;
|
|
1338
|
+
this.deps = deps;
|
|
1339
|
+
this.cache = new ModelDirectoryCache({
|
|
1340
|
+
load: (key, signal) => {
|
|
1341
|
+
const context = JSON.parse(key);
|
|
1342
|
+
if (context.native !== void 0) return Promise.resolve(context.native);
|
|
1343
|
+
return this.probe(context.homeDir, signal);
|
|
1344
|
+
},
|
|
1345
|
+
...deps.ttlMs === void 0 ? {} : { ttlMs: deps.ttlMs },
|
|
1346
|
+
onError: (error) => deps.warn?.(`Kimi model discovery failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
1347
|
+
});
|
|
993
1348
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const stamp = modelDirectoryContextKey({
|
|
1349
|
+
stamp(homeDir, cwd) {
|
|
1350
|
+
return modelDirectoryContextKey({
|
|
997
1351
|
provider: "kimi",
|
|
998
|
-
homeDir
|
|
999
|
-
cwd:
|
|
1352
|
+
homeDir,
|
|
1353
|
+
cwd: cwd ?? homeDir,
|
|
1000
1354
|
cli: ["kimi"],
|
|
1001
|
-
files: [join(
|
|
1355
|
+
files: [join(homeDir, "config.toml"), join(homeDir, "endpoint-auth.json")]
|
|
1002
1356
|
});
|
|
1357
|
+
}
|
|
1358
|
+
key(childSessionId) {
|
|
1359
|
+
const context = this.context(childSessionId);
|
|
1003
1360
|
return JSON.stringify({
|
|
1004
1361
|
homeDir: context.homeDir,
|
|
1005
|
-
stamp,
|
|
1362
|
+
stamp: this.stamp(context.homeDir, context.cwd),
|
|
1006
1363
|
childSessionId,
|
|
1007
1364
|
...context.native === void 0 ? {} : { native: context.native.directory }
|
|
1008
1365
|
});
|
|
1009
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* The cache key for one scoped home directly (no member, no native): the
|
|
1369
|
+
* model-list write face's read — its entries carry the metadata the write
|
|
1370
|
+
* face's table provisioning (provision.ts's `ensureKimiModelTables`)
|
|
1371
|
+
* stamps into the tables it creates.
|
|
1372
|
+
*/
|
|
1373
|
+
homeKey(homeDir) {
|
|
1374
|
+
return JSON.stringify({
|
|
1375
|
+
homeDir,
|
|
1376
|
+
stamp: this.stamp(homeDir)
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1010
1379
|
read(childSessionId) {
|
|
1011
1380
|
return this.cache.read(this.key(childSessionId));
|
|
1012
1381
|
}
|
|
1013
1382
|
refresh(childSessionId) {
|
|
1014
1383
|
return this.cache.refresh(this.key(childSessionId));
|
|
1015
1384
|
}
|
|
1385
|
+
readHome(homeDir) {
|
|
1386
|
+
return this.cache.read(this.homeKey(homeDir));
|
|
1387
|
+
}
|
|
1388
|
+
refreshHome(homeDir) {
|
|
1389
|
+
return this.cache.refresh(this.homeKey(homeDir));
|
|
1390
|
+
}
|
|
1016
1391
|
follow(childSessionId, signal) {
|
|
1017
1392
|
return this.cache.follow(() => this.key(childSessionId), signal);
|
|
1018
1393
|
}
|
|
1019
1394
|
dispose() {
|
|
1020
1395
|
this.cache.dispose();
|
|
1021
1396
|
}
|
|
1397
|
+
async probe(homeDir, signal) {
|
|
1398
|
+
const endpoint = await this.deps.endpoint(homeDir);
|
|
1399
|
+
if (!hasEndpointCredential(endpoint)) return unsupported("No endpoint credential configured — the model list loads from the endpoint once an API Key is saved");
|
|
1400
|
+
const result = await fetchEndpointModels({
|
|
1401
|
+
...endpoint,
|
|
1402
|
+
baseUrl: endpoint.baseUrl ?? "https://api.kimi.com/coding/v1"
|
|
1403
|
+
}, this.deps.fetchImpl, AbortSignal.any([signal, AbortSignal.timeout(this.deps.timeoutMs ?? 2e4)]));
|
|
1404
|
+
if (result.kind === "unreachable") throw new Error(`endpoint unreachable: ${result.message}`);
|
|
1405
|
+
if (result.kind === "malformed") return unsupported("The endpoint answered but the model list did not parse (not a /models-shaped reply)");
|
|
1406
|
+
if (result.kind === "http") {
|
|
1407
|
+
if (result.status === 401 || result.status === 403) return unsupported(`The endpoint rejected the credential (HTTP ${String(result.status)}) — the list loads once a valid key is saved`);
|
|
1408
|
+
return unsupported(`The endpoint does not serve a model list (HTTP ${String(result.status)})`);
|
|
1409
|
+
}
|
|
1410
|
+
if (result.models.length === 0) return unsupported("The endpoint serves no models");
|
|
1411
|
+
const byUpstream = /* @__PURE__ */ new Map();
|
|
1412
|
+
for (const modelTable of await readKimiModelTables(homeDir)) {
|
|
1413
|
+
if (modelTable.provider !== "managed:kimi-code" || modelTable.model === void 0) continue;
|
|
1414
|
+
if (!byUpstream.has(modelTable.model)) byUpstream.set(modelTable.model, modelTable);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
entries: result.models.map((model) => {
|
|
1418
|
+
const modelTable = byUpstream.get(model.id);
|
|
1419
|
+
const value = modelTable?.key ?? model.id;
|
|
1420
|
+
const efforts = model.efforts ?? modelTable?.efforts;
|
|
1421
|
+
const defaultEffort = model.defaultEffort ?? modelTable?.defaultEffort;
|
|
1422
|
+
return {
|
|
1423
|
+
value,
|
|
1424
|
+
label: model.displayName ?? modelTable?.displayName ?? value,
|
|
1425
|
+
source: "native",
|
|
1426
|
+
...efforts === void 0 || efforts.length === 0 ? {} : { reasoning: {
|
|
1427
|
+
options: efforts.map((effort) => ({
|
|
1428
|
+
value: effort,
|
|
1429
|
+
label: effort
|
|
1430
|
+
})),
|
|
1431
|
+
...defaultEffort === void 0 ? {} : { default: defaultEffort }
|
|
1432
|
+
} }
|
|
1433
|
+
};
|
|
1434
|
+
}),
|
|
1435
|
+
complete: true,
|
|
1436
|
+
customInput: false
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1022
1439
|
};
|
|
1023
1440
|
//#endregion
|
|
1024
1441
|
//#region lib/types/session-configuration.js
|
|
@@ -1049,1177 +1466,1307 @@ async function configureKimiSession(sessionId, initial, desired, request) {
|
|
|
1049
1466
|
return current;
|
|
1050
1467
|
}
|
|
1051
1468
|
//#endregion
|
|
1052
|
-
//#region lib/types/
|
|
1469
|
+
//#region lib/types/provision.js
|
|
1053
1470
|
/**
|
|
1054
|
-
* Kimi
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
* its transcript position so a delta mirror can sum a round's total.
|
|
1068
|
-
* @module @khorsheed/dsh-local-agent-kimi/session-view
|
|
1471
|
+
* Kimi Code scoped-home provisioning: a working `config.toml` (provider and
|
|
1472
|
+
* model definitions) is required for the ACP server to run. The scoped home
|
|
1473
|
+
* is provisioned once — from the user's own config, redacted — so the scoped
|
|
1474
|
+
* setup mirrors the user's models while carrying no secrets. Authentication
|
|
1475
|
+
* is endpoint-key only (endpoint-auth.ts): the managed provider declares
|
|
1476
|
+
* `api_key_env = "DSH_KIMI_ENDPOINT_KEY"` and the key rides the child
|
|
1477
|
+
* process's environment, so `config.toml` itself never holds a secret.
|
|
1478
|
+
* Provisioning also sweeps the OAuth era's leftovers (`credentials/`,
|
|
1479
|
+
* `oauth/`) and migrates an existing managed-provider section onto the
|
|
1480
|
+
* env-key shape. The agent preset the delegation tool row needs is
|
|
1481
|
+
* bootstrapped into the user preset root on first boot, so installing the
|
|
1482
|
+
* bundle needs no manual copy step.
|
|
1483
|
+
* @module @khorsheed/dsh-local-agent-kimi/provision
|
|
1069
1484
|
*/
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1485
|
+
const REAL_CONFIG = join(homedir(), ".kimi-code", "config.toml");
|
|
1486
|
+
/** Credentials directory inside the scoped home — the OAuth era's grant. */
|
|
1487
|
+
const CREDENTIALS_DIR = "credentials";
|
|
1488
|
+
/** OAuth token cache directory inside the scoped home. */
|
|
1489
|
+
const OAUTH_DIR = "oauth";
|
|
1074
1490
|
/**
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
* @
|
|
1078
|
-
* @returns the session directory, or undefined when absent.
|
|
1491
|
+
* The capability list every managed model table carries — the minimal
|
|
1492
|
+
* config's own, hoisted so the write face's on-add table provisioning
|
|
1493
|
+
* ({@link ensureKimiModelTables}) stamps the same shape.
|
|
1079
1494
|
*/
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
workspaces = await readdir(sessionsRoot);
|
|
1085
|
-
} catch (error) {
|
|
1086
|
-
if (error.code === "ENOENT") return void 0;
|
|
1087
|
-
throw error;
|
|
1088
|
-
}
|
|
1089
|
-
for (const workspace of workspaces) {
|
|
1090
|
-
const sessionDir = join(sessionsRoot, workspace, `session_${sessionId}`);
|
|
1091
|
-
try {
|
|
1092
|
-
await readFile(wirePath(sessionDir));
|
|
1093
|
-
return sessionDir;
|
|
1094
|
-
} catch {}
|
|
1095
|
-
}
|
|
1495
|
+
const KIMI_MODEL_CAPABILITIES = "capabilities = [ \"thinking\", \"always_thinking\", \"image_in\", \"video_in\", \"tool_use\" ]";
|
|
1496
|
+
/** Quote one TOML basic string (model ids take backslash escapes). */
|
|
1497
|
+
function tomlString(value) {
|
|
1498
|
+
return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
|
|
1096
1499
|
}
|
|
1097
|
-
/**
|
|
1098
|
-
function
|
|
1099
|
-
return
|
|
1500
|
+
/** Minimal managed config for a fresh home with no user config to mirror. */
|
|
1501
|
+
function minimalConfig(model, thinkingEffort) {
|
|
1502
|
+
return `default_model = "${model}"
|
|
1503
|
+
|
|
1504
|
+
[thinking]
|
|
1505
|
+
enabled = true
|
|
1506
|
+
effort = "${thinkingEffort}"
|
|
1507
|
+
|
|
1508
|
+
[providers."managed:kimi-code"]
|
|
1509
|
+
base_url = "${KIMI_MANAGED_BASE_URL}"
|
|
1510
|
+
type = "kimi"
|
|
1511
|
+
api_key_env = "${ENDPOINT_ENV_KEY}"
|
|
1512
|
+
|
|
1513
|
+
[models."${model}"]
|
|
1514
|
+
provider = "managed:kimi-code"
|
|
1515
|
+
model = "${model.split("/").pop()}"
|
|
1516
|
+
max_context_size = 1048576
|
|
1517
|
+
${KIMI_MODEL_CAPABILITIES}
|
|
1518
|
+
display_name = "${model}"
|
|
1519
|
+
support_efforts = [ "low", "high", "max" ]
|
|
1520
|
+
default_effort = "${thinkingEffort}"
|
|
1521
|
+
`;
|
|
1100
1522
|
}
|
|
1101
|
-
/**
|
|
1102
|
-
|
|
1103
|
-
|
|
1523
|
+
/**
|
|
1524
|
+
* Redact every api_key value so the scoped copy carries no secrets. The
|
|
1525
|
+
* kimi-managed provider's key comes from the `api_key_env` indirection (the
|
|
1526
|
+
* endpoint credential), which is exactly what an empty value means here.
|
|
1527
|
+
* @param config - the user's config text.
|
|
1528
|
+
* @returns the same config with all api_key values blanked.
|
|
1529
|
+
*/
|
|
1530
|
+
function redactApiKeys(config) {
|
|
1531
|
+
return config.replace(/api_key\s*=\s*"[^"]*"/gu, "api_key = \"\"");
|
|
1104
1532
|
}
|
|
1533
|
+
/** The managed provider's section header (the colon forces the quoted form). */
|
|
1534
|
+
const MANAGED_PROVIDER_HEADER = "[providers.\"managed:kimi-code\"]";
|
|
1535
|
+
/** The OAuth sub-table the endpoint-key shape drops. */
|
|
1536
|
+
const MANAGED_OAUTH_HEADER = "[providers.\"managed:kimi-code\".oauth]";
|
|
1105
1537
|
/**
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1538
|
+
* Migrate the managed provider section of a config.toml onto the endpoint-key
|
|
1539
|
+
* shape, as a pure text transform:
|
|
1540
|
+
*
|
|
1541
|
+
* - the section's `api_key = "…"` line (any value — a mirrored config is
|
|
1542
|
+
* already redacted, and a live one must not keep a secret in a hashed
|
|
1543
|
+
* file) is replaced by `api_key_env = "<ENDPOINT_ENV_KEY>"`;
|
|
1544
|
+
* - the `[providers."managed:kimi-code".oauth]` sub-table is dropped (the
|
|
1545
|
+
* OAuth flow is gone);
|
|
1546
|
+
* - when `baseUrl` is given, the section's `base_url` is set to it (the
|
|
1547
|
+
* endpoint credential owns the managed provider's endpoint; a hand-edit
|
|
1548
|
+
* is re-pointed at the next surgery).
|
|
1549
|
+
*
|
|
1550
|
+
* Other sections — user-added providers, model tables, permission rules —
|
|
1551
|
+
* are byte-preserved. A config without the managed provider section is
|
|
1552
|
+
* returned unchanged (a hand-built config's own providers are the user's
|
|
1553
|
+
* business). Idempotent: an in-sync input comes back byte-identical, so the
|
|
1554
|
+
* caller writes only on drift.
|
|
1555
|
+
* @param config - the config.toml text.
|
|
1556
|
+
* @param baseUrl - the endpoint to pin, or undefined to keep the file's own.
|
|
1557
|
+
* @returns the migrated config text.
|
|
1114
1558
|
*/
|
|
1115
|
-
function
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1559
|
+
function migrateManagedProviderConfig(config, baseUrl) {
|
|
1560
|
+
const lines = config.split("\n");
|
|
1561
|
+
const start = lines.findIndex((line) => line.trim() === MANAGED_PROVIDER_HEADER);
|
|
1562
|
+
if (start === -1) return config;
|
|
1563
|
+
let bodyEnd = lines.length;
|
|
1564
|
+
for (let index = start + 1; index < lines.length; index += 1) if (/^\s*\[/.test(lines[index])) {
|
|
1565
|
+
bodyEnd = index;
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
let end = bodyEnd;
|
|
1569
|
+
if (lines[bodyEnd]?.trim() === MANAGED_OAUTH_HEADER) {
|
|
1570
|
+
end = lines.length;
|
|
1571
|
+
for (let index = bodyEnd + 1; index < lines.length; index += 1) if (/^\s*\[/.test(lines[index])) {
|
|
1572
|
+
end = index;
|
|
1573
|
+
break;
|
|
1126
1574
|
}
|
|
1127
|
-
if (hunks.length === 0) return void 0;
|
|
1128
|
-
return [`update: ${path}`, ...hunks].join("\n");
|
|
1129
1575
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1576
|
+
const section = lines.slice(start + 1, bodyEnd);
|
|
1577
|
+
const tail = lines.slice(end);
|
|
1578
|
+
const out = [];
|
|
1579
|
+
let sawEnvKey = false;
|
|
1580
|
+
let sawBaseUrl = false;
|
|
1581
|
+
for (const raw of section) {
|
|
1582
|
+
const trimmed = raw.trim();
|
|
1583
|
+
if (/^api_key\s*=/.test(trimmed)) continue;
|
|
1584
|
+
if (/^api_key_env\s*=/.test(trimmed)) {
|
|
1585
|
+
if (sawEnvKey) continue;
|
|
1586
|
+
sawEnvKey = true;
|
|
1587
|
+
out.push(`api_key_env = "${ENDPOINT_ENV_KEY}"`);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (baseUrl !== void 0 && /^base_url\s*=/.test(trimmed)) {
|
|
1591
|
+
if (sawBaseUrl) continue;
|
|
1592
|
+
sawBaseUrl = true;
|
|
1593
|
+
out.push(`base_url = "${baseUrl.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
out.push(raw);
|
|
1135
1597
|
}
|
|
1136
|
-
|
|
1137
|
-
if (
|
|
1598
|
+
if (!sawEnvKey) out.push(`api_key_env = "${ENDPOINT_ENV_KEY}"`);
|
|
1599
|
+
if (baseUrl !== void 0 && !sawBaseUrl) out.unshift(`base_url = "${baseUrl.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
|
|
1600
|
+
return [
|
|
1601
|
+
...lines.slice(0, start + 1),
|
|
1602
|
+
...out,
|
|
1603
|
+
...tail
|
|
1604
|
+
].join("\n");
|
|
1138
1605
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1606
|
+
/**
|
|
1607
|
+
* Ensure the scoped config's managed provider runs on the endpoint-key
|
|
1608
|
+
* shape (see {@link migrateManagedProviderConfig}); writes only on drift.
|
|
1609
|
+
* Called at provision, at boot, on the card's credential save, and before
|
|
1610
|
+
* each spawn — the last makes a hand-edited endpoint-auth.json's baseUrl
|
|
1611
|
+
* land without a restart.
|
|
1612
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1613
|
+
* @param baseUrl - the resolved endpoint's baseUrl, when one is configured.
|
|
1614
|
+
* @returns true when the config was rewritten, false when already in sync
|
|
1615
|
+
* (or absent — provisioning owns creation).
|
|
1616
|
+
*/
|
|
1617
|
+
async function ensureKimiEndpointProvider(homeDir, baseUrl) {
|
|
1618
|
+
const file = join(homeDir, "config.toml");
|
|
1619
|
+
let text;
|
|
1620
|
+
try {
|
|
1621
|
+
text = await readFile(file, "utf8");
|
|
1622
|
+
} catch {
|
|
1623
|
+
return false;
|
|
1155
1624
|
}
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const summary = pairs.join(" ");
|
|
1161
|
-
return summary.length > 200 ? `${summary.slice(0, 200)}…` : summary;
|
|
1625
|
+
const migrated = migrateManagedProviderConfig(text, baseUrl);
|
|
1626
|
+
if (migrated === text) return false;
|
|
1627
|
+
await writeFile(file, migrated, "utf8");
|
|
1628
|
+
return true;
|
|
1162
1629
|
}
|
|
1163
1630
|
/**
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1631
|
+
* Sweep the OAuth era's leftovers (the credentials directory and the oauth
|
|
1632
|
+
* token cache). The harness no longer reads either; a leftover grant would
|
|
1633
|
+
* keep authenticating nothing (the managed provider no longer declares an
|
|
1634
|
+
* oauth section), and removing them keeps the scoped home honest about its
|
|
1635
|
+
* only credential being the endpoint key. Idempotent.
|
|
1636
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1167
1637
|
*/
|
|
1168
|
-
async function
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1638
|
+
async function sweepKimiOauthLeftovers(homeDir) {
|
|
1639
|
+
await Promise.all([rm(join(homeDir, CREDENTIALS_DIR), {
|
|
1640
|
+
recursive: true,
|
|
1641
|
+
force: true
|
|
1642
|
+
}), rm(join(homeDir, OAUTH_DIR), {
|
|
1643
|
+
recursive: true,
|
|
1644
|
+
force: true
|
|
1645
|
+
})]);
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Provision `<homeDir>/config.toml` unless one already exists, and always
|
|
1649
|
+
* sweep the OAuth era's leftovers. A present config is migrated onto the
|
|
1650
|
+
* endpoint-key shape (managed provider onto `api_key_env`, oauth sub-table
|
|
1651
|
+
* dropped) but otherwise respected; otherwise the user's own real config is
|
|
1652
|
+
* copied redacted AND migrated, so the scoped home mirrors the user's models
|
|
1653
|
+
* while carrying no secrets; with no real config, a minimal kimi-managed
|
|
1654
|
+
* config is written. `thinkingEffort` feeds the minimal config's
|
|
1655
|
+
* `[thinking] effort` (and the model's `default_effort`) — it applies at
|
|
1656
|
+
* provision time only, never retroactively to an existing config.
|
|
1657
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1658
|
+
* @param model - the kimi-managed model id for a fresh minimal config.
|
|
1659
|
+
* @param thinkingEffort - the reasoning effort a fresh minimal config pins.
|
|
1660
|
+
* @param baseUrl - the deployment endpoint to pin on the managed provider,
|
|
1661
|
+
* when the plugin config names one (the scope file's own baseUrl lands via
|
|
1662
|
+
* the card save / the per-spawn surgery).
|
|
1663
|
+
* @returns true when a config was written, false when one already existed.
|
|
1664
|
+
*/
|
|
1665
|
+
async function provisionKimiConfig(homeDir, model, thinkingEffort, baseUrl) {
|
|
1666
|
+
await sweepKimiOauthLeftovers(homeDir);
|
|
1667
|
+
let existed = true;
|
|
1186
1668
|
try {
|
|
1187
|
-
|
|
1669
|
+
await readFile(join(homeDir, "config.toml"));
|
|
1188
1670
|
} catch (error) {
|
|
1189
|
-
if (error.code
|
|
1190
|
-
|
|
1191
|
-
lines,
|
|
1192
|
-
usageRecords
|
|
1193
|
-
};
|
|
1194
|
-
throw error;
|
|
1671
|
+
if (error.code !== "ENOENT") throw error;
|
|
1672
|
+
existed = false;
|
|
1195
1673
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1674
|
+
if (existed) {
|
|
1675
|
+
await ensureKimiEndpointProvider(homeDir, baseUrl);
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
let config = minimalConfig(model, thinkingEffort);
|
|
1679
|
+
try {
|
|
1680
|
+
config = migrateManagedProviderConfig(redactApiKeys(await readFile(REAL_CONFIG, "utf8")), baseUrl);
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
if (error.code !== "ENOENT") throw error;
|
|
1683
|
+
}
|
|
1684
|
+
await writeFile(join(homeDir, "config.toml"), config, { flag: "wx" });
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
/** The managed-service provider section provisioned into the scoped config. */
|
|
1688
|
+
const MANAGED_PROVIDER_SECTION = "providers.\"managed:kimi-code\"";
|
|
1689
|
+
/**
|
|
1690
|
+
* Read the scoped config's effective LLM endpoint: the
|
|
1691
|
+
* `[providers."managed:kimi-code"].base_url` key, if present. A missing or
|
|
1692
|
+
* malformed config yields undefined (the CLI falls back to its default
|
|
1693
|
+
* endpoint). Used only for diagnostics — the config itself is authoritative,
|
|
1694
|
+
* and a user-edited value is respected untouched.
|
|
1695
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1696
|
+
* @returns the configured base URL, or undefined when absent/unreadable.
|
|
1697
|
+
*/
|
|
1698
|
+
async function readKimiBaseUrl(homeDir) {
|
|
1699
|
+
let text;
|
|
1700
|
+
try {
|
|
1701
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1702
|
+
} catch {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
let section = "";
|
|
1706
|
+
for (const rawLine of text.split("\n")) {
|
|
1707
|
+
const line = rawLine.trim();
|
|
1708
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
1709
|
+
if (header !== null) {
|
|
1710
|
+
section = header[1];
|
|
1203
1711
|
continue;
|
|
1204
1712
|
}
|
|
1205
|
-
if (
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
modelTurn = turn || 1;
|
|
1209
|
-
}
|
|
1210
|
-
const usage = usageFromWire(event.usage);
|
|
1211
|
-
if (usage !== void 0) usageRecords.push({
|
|
1212
|
-
line: lines.length,
|
|
1213
|
-
usage
|
|
1214
|
-
});
|
|
1215
|
-
continue;
|
|
1713
|
+
if (section === MANAGED_PROVIDER_SECTION) {
|
|
1714
|
+
const match = /^base_url\s*=\s*"([^"]*)"$/.exec(line);
|
|
1715
|
+
if (match !== null) return match[1];
|
|
1216
1716
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Read the scoped config's configured default model: the top-level
|
|
1721
|
+
* `default_model` key — the selection a plain `kimi -p` round runs with. A
|
|
1722
|
+
* missing or malformed config, or no key, yields undefined — the honest
|
|
1723
|
+
* "unknown", never a guessed default. Read-only: the config is authoritative
|
|
1724
|
+
* and a user-edited value is reported as-is.
|
|
1725
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1726
|
+
* @returns the configured model identifier, or undefined when none is set.
|
|
1727
|
+
*/
|
|
1728
|
+
async function readKimiDefaultModel(homeDir) {
|
|
1729
|
+
let text;
|
|
1730
|
+
try {
|
|
1731
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1732
|
+
} catch {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
let section = "";
|
|
1736
|
+
for (const rawLine of text.split("\n")) {
|
|
1737
|
+
const line = rawLine.trim();
|
|
1738
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
1739
|
+
if (header !== null) {
|
|
1740
|
+
section = header[1];
|
|
1223
1741
|
continue;
|
|
1224
1742
|
}
|
|
1225
|
-
if (
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1743
|
+
if (section !== "") continue;
|
|
1744
|
+
const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
|
|
1745
|
+
if (key !== null && key[1] === "default_model") return key[2];
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* List the model identifiers the scoped config DEFINES: every
|
|
1750
|
+
* `[models."…"]` table key (bare `[models.x]` keys included). This is the
|
|
1751
|
+
* pickable vocabulary a model surface can offer without hardcoding a catalog —
|
|
1752
|
+
* the same table `-m` resolves against. A missing or malformed config yields
|
|
1753
|
+
* an empty list. Read-only: the config is authoritative.
|
|
1754
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1755
|
+
* @returns the defined model identifiers, in file order, deduplicated.
|
|
1756
|
+
*/
|
|
1757
|
+
async function listKimiConfigModels(homeDir) {
|
|
1758
|
+
let text;
|
|
1759
|
+
try {
|
|
1760
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1761
|
+
} catch {
|
|
1762
|
+
return [];
|
|
1763
|
+
}
|
|
1764
|
+
const models = [];
|
|
1765
|
+
for (const rawLine of text.split("\n")) {
|
|
1766
|
+
const header = /^\[+([^\]]+)\]+$/.exec(rawLine.trim());
|
|
1767
|
+
if (header === null) continue;
|
|
1768
|
+
const table = /^models\.(?:"([^"]+)"|([\w-]+))$/.exec(header[1]);
|
|
1769
|
+
const name = table?.[1] ?? table?.[2];
|
|
1770
|
+
if (name !== void 0 && name !== "" && !models.includes(name)) models.push(name);
|
|
1771
|
+
}
|
|
1772
|
+
return models;
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* The header scan's one table key: a quoted `models."…"` key (unescaped — the
|
|
1776
|
+
* writer escapes, so the reader must fold back) or a bare `models.x` one.
|
|
1777
|
+
*/
|
|
1778
|
+
function modelTableKey(header) {
|
|
1779
|
+
const table = /^models\.(?:"((?:[^"\\]|\\.)*)"|([\w-]+))$/.exec(header);
|
|
1780
|
+
const raw = table?.[1];
|
|
1781
|
+
if (raw !== void 0) return raw.replace(/\\(["\\])/gu, "$1");
|
|
1782
|
+
return table?.[2];
|
|
1783
|
+
}
|
|
1784
|
+
/** Whether the config text already keys a `[models.…]` table for `value`. */
|
|
1785
|
+
function hasModelTable(text, value) {
|
|
1786
|
+
for (const rawLine of text.split("\n")) {
|
|
1787
|
+
const header = /^\[+([^\]]+)\]+$/.exec(rawLine.trim());
|
|
1788
|
+
if (header !== null && modelTableKey(header[1]) === value) return true;
|
|
1789
|
+
}
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Give every listed model value a `[models."<value>"]` table in the scoped
|
|
1794
|
+
* config — the runnable name `-m` resolves against. kimi-cli ≥2.1 REQUIRES
|
|
1795
|
+
* the table: `kimi -m k3` fails with `Model "k3" is not configured in
|
|
1796
|
+
* config.toml` when only `[models."kimi-code/k3"]` (carrying `model = "k3"`)
|
|
1797
|
+
* exists (verified against kimi-cli 2.1.1), so an endpoint-served id the
|
|
1798
|
+
* catalog lists is not directly runnable until its table exists. The model
|
|
1799
|
+
* list's write face calls this after each save so an added endpoint id is
|
|
1800
|
+
* runnable from that round on.
|
|
1801
|
+
*
|
|
1802
|
+
* Only MISSING values get a table: an existing key (quoted or bare) is the
|
|
1803
|
+
* user's own definition and stays untouched. The appended table mirrors the
|
|
1804
|
+
* minimal config's managed shape — `provider = "managed:kimi-code"`, `model`
|
|
1805
|
+
* = the value, `max_context_size` (the metadata's endpoint-reported size,
|
|
1806
|
+
* else 1048576 — the minimal template's value), the shared capabilities
|
|
1807
|
+
* list, `display_name` (the metadata's label, else the value), and
|
|
1808
|
+
* `support_efforts`/`default_effort` when the metadata carries them. An
|
|
1809
|
+
* absent or unreadable config yields [] (provisioning owns creation;
|
|
1810
|
+
* inventing a whole config here would hide a broken home). Idempotent: all
|
|
1811
|
+
* values already keyed → no write, [].
|
|
1812
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1813
|
+
* @param models - the model values that must be runnable (already normalized).
|
|
1814
|
+
* @param metadata - per-value lookup of the catalog's endpoint/table facts.
|
|
1815
|
+
* @returns the table keys this call created, in the order listed.
|
|
1816
|
+
*/
|
|
1817
|
+
async function ensureKimiModelTables(homeDir, models, metadata) {
|
|
1818
|
+
const path = join(homeDir, "config.toml");
|
|
1819
|
+
let text;
|
|
1820
|
+
try {
|
|
1821
|
+
text = await readFile(path, "utf8");
|
|
1822
|
+
} catch {
|
|
1823
|
+
return [];
|
|
1824
|
+
}
|
|
1825
|
+
const created = [];
|
|
1826
|
+
let fragment = "";
|
|
1827
|
+
for (const value of models) {
|
|
1828
|
+
if (value === "" || hasModelTable(text, value) || created.includes(value)) continue;
|
|
1829
|
+
const facts = metadata?.(value);
|
|
1830
|
+
fragment += `\n[models.${tomlString(value)}]\nprovider = "managed:kimi-code"
|
|
1831
|
+
model = ${tomlString(value)}\nmax_context_size = ${String(facts?.contextSize ?? 1048576)}\n${KIMI_MODEL_CAPABILITIES}\ndisplay_name = ${tomlString(facts?.label ?? value)}\n` + (facts?.efforts === void 0 || facts.efforts.length === 0 ? "" : `support_efforts = [ ${facts.efforts.map(tomlString).join(", ")} ]\n`) + (facts?.defaultEffort === void 0 ? "" : `default_effort = ${tomlString(facts.defaultEffort)}\n`);
|
|
1832
|
+
created.push(value);
|
|
1833
|
+
}
|
|
1834
|
+
if (created.length === 0) return [];
|
|
1835
|
+
await writeFile(path, `${text.trimEnd()}\n${fragment}`, "utf8");
|
|
1836
|
+
return created;
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Rewrite the scoped config's top-level `default_model` to `model`, so the
|
|
1840
|
+
* NEXT process started under this home runs that model. This is the resident
|
|
1841
|
+
* path's only lever: `kimi acp` takes no model flag (verified against kimi
|
|
1842
|
+
* 0.39.1), while `kimi -p` takes `-m` — so the one-shot rounds pass the flag
|
|
1843
|
+
* and the live driver writes the file before spawning its runtime.
|
|
1844
|
+
*
|
|
1845
|
+
* The write is surgical: the first top-level `default_model` assignment is
|
|
1846
|
+
* replaced in place and everything else — model tables, providers, thinking
|
|
1847
|
+
* effort, comments, key order — is left byte-identical. A config with no
|
|
1848
|
+
* top-level `default_model` gains one as its first line; an absent or
|
|
1849
|
+
* unreadable config is left alone (provisioning owns creation, and inventing
|
|
1850
|
+
* a config here would hide a broken home).
|
|
1851
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1852
|
+
* @param model - the model identifier to pin as the default.
|
|
1853
|
+
* @returns true when the file now names `model`, false when there was no
|
|
1854
|
+
* config to write into.
|
|
1855
|
+
*/
|
|
1856
|
+
async function writeKimiDefaultModel(homeDir, model) {
|
|
1857
|
+
const path = join(homeDir, "config.toml");
|
|
1858
|
+
let text;
|
|
1859
|
+
try {
|
|
1860
|
+
text = await readFile(path, "utf8");
|
|
1861
|
+
} catch {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
const assignment = `default_model = "${model.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
|
|
1865
|
+
const lines = text.split("\n");
|
|
1866
|
+
let section = "";
|
|
1867
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
1868
|
+
const line = rawLine.trim();
|
|
1869
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
1870
|
+
if (header !== null) {
|
|
1871
|
+
section = header[1];
|
|
1235
1872
|
continue;
|
|
1236
1873
|
}
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1874
|
+
if (section !== "") continue;
|
|
1875
|
+
const key = /^(\w+)\s*=/.exec(line);
|
|
1876
|
+
if (key === null || key[1] !== "default_model") continue;
|
|
1877
|
+
if (line === assignment) return true;
|
|
1878
|
+
lines[index] = assignment;
|
|
1879
|
+
await writeFile(path, lines.join("\n"), "utf8");
|
|
1880
|
+
return true;
|
|
1881
|
+
}
|
|
1882
|
+
await writeFile(path, `${assignment}\n${text}`, "utf8");
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Read the scoped config's effective reasoning effort: the `[thinking] effort`
|
|
1887
|
+
* key, falling back to a `[models."…"]` section's `default_effort` when the
|
|
1888
|
+
* thinking table carries none (the CLI resolves effort the same way). A
|
|
1889
|
+
* missing or malformed config, or no effort key anywhere, yields undefined —
|
|
1890
|
+
* the honest "unknown", never a guessed default. Read-only: the config is
|
|
1891
|
+
* authoritative, a user-edited value is reported as-is.
|
|
1892
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1893
|
+
* @returns the effective effort, or undefined when it cannot be determined.
|
|
1894
|
+
*/
|
|
1895
|
+
async function readKimiReasoningEffort(homeDir) {
|
|
1896
|
+
let text;
|
|
1897
|
+
try {
|
|
1898
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1899
|
+
} catch {
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
let section = "";
|
|
1903
|
+
let thinkingEffort;
|
|
1904
|
+
let modelDefaultEffort;
|
|
1905
|
+
for (const rawLine of text.split("\n")) {
|
|
1906
|
+
const line = rawLine.trim();
|
|
1907
|
+
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
1908
|
+
if (header !== null) {
|
|
1909
|
+
section = header[1];
|
|
1257
1910
|
continue;
|
|
1258
1911
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
if (loopTurn !== void 0) {
|
|
1264
|
-
turn = Math.max(turn, loopTurn);
|
|
1265
|
-
turnSeen = true;
|
|
1266
|
-
}
|
|
1267
|
-
const part = loop.part;
|
|
1268
|
-
const lineTurn = turn === 0 ? 1 : turn;
|
|
1269
|
-
if (part?.type === "text" && part.text !== void 0 && part.text.trim() !== "") push({
|
|
1270
|
-
kind: "assistant",
|
|
1271
|
-
text: part.text,
|
|
1272
|
-
turn: lineTurn
|
|
1273
|
-
});
|
|
1274
|
-
else if (part?.type === "think" && part.think !== void 0 && part.think.trim() !== "") push({
|
|
1275
|
-
kind: "think",
|
|
1276
|
-
text: part.think,
|
|
1277
|
-
turn: lineTurn
|
|
1278
|
-
});
|
|
1279
|
-
else if (loop.type === "tool.call") {
|
|
1280
|
-
const call = loop;
|
|
1281
|
-
const name = call.toolCall?.name ?? call.name ?? "tool";
|
|
1282
|
-
const args = argsOf(name, call.toolCall?.args ?? call.args);
|
|
1283
|
-
const lineIndex = lines.length;
|
|
1284
|
-
push({
|
|
1285
|
-
kind: "tool",
|
|
1286
|
-
id: typeof call.toolCallId === "string" ? call.toolCallId : typeof call.uuid === "string" ? call.uuid : `kimi-tool-${lineIndex}`,
|
|
1287
|
-
name,
|
|
1288
|
-
...args === void 0 ? {} : { args },
|
|
1289
|
-
turn: lineTurn
|
|
1290
|
-
});
|
|
1291
|
-
if (typeof call.uuid === "string") callsByUuid.set(call.uuid, lineIndex);
|
|
1292
|
-
if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
|
|
1293
|
-
} else if (loop.type === "tool.result") {
|
|
1294
|
-
const result = loop;
|
|
1295
|
-
const output = result.result?.output;
|
|
1296
|
-
let target = typeof result.parentUuid === "string" ? callsByUuid.get(result.parentUuid) : void 0;
|
|
1297
|
-
if (target === void 0 && typeof result.toolCallId === "string") target = callsByToolCallId.get(result.toolCallId);
|
|
1298
|
-
if (target === void 0) {
|
|
1299
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) if (lines[index]?.kind === "tool") {
|
|
1300
|
-
target = index;
|
|
1301
|
-
break;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
if (output !== void 0 && output.trim() !== "" && target !== void 0) {
|
|
1305
|
-
const current = lines[target];
|
|
1306
|
-
if (current !== void 0 && current.kind === "tool") lines[target] = {
|
|
1307
|
-
...current,
|
|
1308
|
-
result: output
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
} else if (loop.type === "content.part" && part?.type !== void 0 && part.type !== "text" && part.type !== "think") push({
|
|
1312
|
-
kind: "assistant",
|
|
1313
|
-
text: `[未支持的内容类型 ${part.type}]`,
|
|
1314
|
-
turn: lineTurn
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1912
|
+
const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
|
|
1913
|
+
if (key === null) continue;
|
|
1914
|
+
if (section === "thinking" && key[1] === "effort" && thinkingEffort === void 0) thinkingEffort = key[2];
|
|
1915
|
+
if (section.startsWith("models.") && key[1] === "default_effort" && modelDefaultEffort === void 0) modelDefaultEffort = key[2];
|
|
1317
1916
|
}
|
|
1318
|
-
return
|
|
1319
|
-
sessionId,
|
|
1320
|
-
lines,
|
|
1321
|
-
usageRecords,
|
|
1322
|
-
...model === void 0 ? {} : { model },
|
|
1323
|
-
...modelTurn === void 0 ? {} : { modelTurn }
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* Map a wire `usage.record` payload onto the shared usage contract. kimi
|
|
1328
|
-
* reports input (uncached), cache read, cache creation, and output; the
|
|
1329
|
-
* creation counter is its cache-write analog. Non-numeric or missing
|
|
1330
|
-
* counters are dropped so the record stays usable when kimi omits a field.
|
|
1331
|
-
* @param raw - the event's `usage` value.
|
|
1332
|
-
* @returns the shared usage record, or undefined when nothing was reported.
|
|
1333
|
-
*/
|
|
1334
|
-
function usageFromWire(raw) {
|
|
1335
|
-
const counters = raw;
|
|
1336
|
-
if (counters === void 0 || typeof counters !== "object") return void 0;
|
|
1337
|
-
const num = (value) => {
|
|
1338
|
-
const n = Number(value);
|
|
1339
|
-
return Number.isFinite(n) ? n : void 0;
|
|
1340
|
-
};
|
|
1341
|
-
const input = num(counters.inputOther);
|
|
1342
|
-
const output = num(counters.output);
|
|
1343
|
-
const cacheRead = num(counters.inputCacheRead);
|
|
1344
|
-
const cacheWrite = num(counters.inputCacheCreation);
|
|
1345
|
-
if (input === void 0 && output === void 0 && cacheRead === void 0 && cacheWrite === void 0) return;
|
|
1346
|
-
const usage = {
|
|
1347
|
-
inputTokens: input ?? 0,
|
|
1348
|
-
outputTokens: output ?? 0
|
|
1349
|
-
};
|
|
1350
|
-
if (cacheRead !== void 0 && cacheRead > 0) usage.cacheReadTokens = cacheRead;
|
|
1351
|
-
if (cacheWrite !== void 0 && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
1352
|
-
return usage;
|
|
1917
|
+
return thinkingEffort ?? modelDefaultEffort;
|
|
1353
1918
|
}
|
|
1354
1919
|
/**
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1920
|
+
* Whether the scoped config's permission rules auto-approve the subagent's
|
|
1921
|
+
* tool use: the `Bash(*)` allow rule the provisioning gate
|
|
1922
|
+
* ({@link ensureKimiPermissions}) keys on, detected with the exact same
|
|
1923
|
+
* substring so the snapshot and the provisioning can never disagree about
|
|
1924
|
+
* the same file.
|
|
1925
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1926
|
+
* @returns whether the auto-approve rule is present.
|
|
1360
1927
|
*/
|
|
1361
|
-
function
|
|
1362
|
-
let
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
input += record.usage.inputTokens ?? 0;
|
|
1368
|
-
output += record.usage.outputTokens ?? 0;
|
|
1369
|
-
cacheRead += record.usage.cacheReadTokens ?? 0;
|
|
1370
|
-
cacheWrite += record.usage.cacheWriteTokens ?? 0;
|
|
1928
|
+
async function readKimiAutoApprove(homeDir) {
|
|
1929
|
+
let text;
|
|
1930
|
+
try {
|
|
1931
|
+
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
1932
|
+
} catch {
|
|
1933
|
+
return false;
|
|
1371
1934
|
}
|
|
1372
|
-
|
|
1373
|
-
const usage = {
|
|
1374
|
-
inputTokens: input,
|
|
1375
|
-
outputTokens: output
|
|
1376
|
-
};
|
|
1377
|
-
if (cacheRead > 0) usage.cacheReadTokens = cacheRead;
|
|
1378
|
-
if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
1379
|
-
return usage;
|
|
1935
|
+
return text.includes("pattern = \"Bash(*)");
|
|
1380
1936
|
}
|
|
1937
|
+
/** Permission rule block letting the kimi subagent run shell commands. */
|
|
1938
|
+
const KIMI_PERMISSION_BLOCK = `
|
|
1939
|
+
[[permission.rules]]
|
|
1940
|
+
decision = "allow"
|
|
1941
|
+
pattern = "Bash(*)"
|
|
1942
|
+
reason = "let the kimi subagent run shell commands"
|
|
1943
|
+
`;
|
|
1381
1944
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
* absence checks.
|
|
1387
|
-
* @param left - the running total, or undefined before the first window.
|
|
1388
|
-
* @param right - the next window's usage, or undefined when it had none.
|
|
1389
|
-
* @returns the merged total, or undefined when both operands are absent.
|
|
1945
|
+
* Ensure the scoped config allows the subagent's tool use. The one-shot
|
|
1946
|
+
* `kimi -p` child uses the config's permission rules, so without a rule it
|
|
1947
|
+
* answers without executing tools. Idempotent; an existing rule is respected.
|
|
1948
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1390
1949
|
*/
|
|
1391
|
-
function
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
usage: right
|
|
1400
|
-
}]);
|
|
1401
|
-
}
|
|
1402
|
-
/** Render a transcript as command-reply text. */
|
|
1403
|
-
function renderTranscript(transcript) {
|
|
1404
|
-
if (transcript.lines.length === 0) return `kimi session ${transcript.sessionId}: no readable transcript yet.`;
|
|
1405
|
-
const parts = [`kimi session ${transcript.sessionId}:`];
|
|
1406
|
-
for (const line of transcript.lines) if (line.kind === "user") parts.push(`\n用户: ${line.text}`);
|
|
1407
|
-
else if (line.kind === "assistant") parts.push(`\nkimi: ${line.text}`);
|
|
1408
|
-
else if (line.kind === "think") parts.push(`\n[思考] ${line.text}`);
|
|
1409
|
-
else {
|
|
1410
|
-
const call = `[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`;
|
|
1411
|
-
parts.push(`\n ↳ ${call}${line.result !== void 0 ? ` → ${line.result}` : ""}`);
|
|
1950
|
+
async function ensureKimiPermissions(homeDir) {
|
|
1951
|
+
const file = join(homeDir, "config.toml");
|
|
1952
|
+
let content;
|
|
1953
|
+
try {
|
|
1954
|
+
content = await readFile(file, "utf8");
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
if (error.code === "ENOENT") return;
|
|
1957
|
+
throw error;
|
|
1412
1958
|
}
|
|
1413
|
-
|
|
1959
|
+
if (content.includes("pattern = \"Bash(*)")) return;
|
|
1960
|
+
await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
|
|
1414
1961
|
}
|
|
1415
1962
|
//#endregion
|
|
1416
|
-
//#region lib/types/session-
|
|
1963
|
+
//#region lib/types/session-view.js
|
|
1417
1964
|
/**
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1965
|
+
* Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
|
|
1966
|
+
* and render a readable transcript (user prompts, assistant thinking and
|
|
1967
|
+
* replies, tool calls with arguments and results). The kimi ACP mode writes
|
|
1968
|
+
* responses to the session file rather than pushing them over the protocol,
|
|
1969
|
+
* and the dsh subagent record for a one-shot run carries only the final
|
|
1970
|
+
* output — this is the way to see what the kimi subagent actually did.
|
|
1971
|
+
*
|
|
1972
|
+
* The transcript is harness-comparison grade: every line is tagged with its
|
|
1973
|
+
* wire turn number (1-based, matching the dsh child session's `turn/start`
|
|
1974
|
+
* numbering), kimi's auto-permission `<system-reminder>` user messages are
|
|
1975
|
+
* filtered out, tool calls carry their rendered arguments, tool results are
|
|
1976
|
+
* matched to their owning call by `parentUuid`, and every `usage.record`
|
|
1977
|
+
* (kimi reports accounting per LLM request, not cumulative) is surfaced with
|
|
1978
|
+
* its transcript position so a delta mirror can sum a round's total.
|
|
1979
|
+
* @module @khorsheed/dsh-local-agent-kimi/session-view
|
|
1424
1980
|
*/
|
|
1425
|
-
/**
|
|
1426
|
-
function
|
|
1427
|
-
return
|
|
1428
|
-
content: [{
|
|
1429
|
-
type: "text",
|
|
1430
|
-
text
|
|
1431
|
-
}],
|
|
1432
|
-
source: { kind: "user" }
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
/** One assistant-role message event, attributed to the kimi route. */
|
|
1436
|
-
function assistantEvent(blocks, model) {
|
|
1437
|
-
return createAssistantMessage({
|
|
1438
|
-
content: blocks,
|
|
1439
|
-
source: {
|
|
1440
|
-
provider: "kimi-cli",
|
|
1441
|
-
model: model ?? "unobserved"
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
1981
|
+
/** The wire event log path for a session directory. */
|
|
1982
|
+
function wirePath(sessionDir) {
|
|
1983
|
+
return join(sessionDir, "agents", "main", "wire.jsonl");
|
|
1444
1984
|
}
|
|
1445
1985
|
/**
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
* place below it instead of the stream squatting on step 1.
|
|
1451
|
-
* @param childSession - the run's child session.
|
|
1452
|
-
* @param turn - the round's turn number.
|
|
1453
|
-
* @returns the next free step in the turn.
|
|
1986
|
+
* Find a session directory under `<homeDir>/sessions/wd_*` whose id matches.
|
|
1987
|
+
* @param homeDir - the `kimi` harness's scoped home.
|
|
1988
|
+
* @param sessionId - the kimi session id.
|
|
1989
|
+
* @returns the session directory, or undefined when absent.
|
|
1454
1990
|
*/
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1991
|
+
async function findKimiSessionDir(homeDir, sessionId) {
|
|
1992
|
+
const sessionsRoot = join(homeDir, "sessions");
|
|
1993
|
+
let workspaces;
|
|
1994
|
+
try {
|
|
1995
|
+
workspaces = await readdir(sessionsRoot);
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
if (error.code === "ENOENT") return void 0;
|
|
1998
|
+
throw error;
|
|
1460
1999
|
}
|
|
1461
|
-
|
|
2000
|
+
for (const workspace of workspaces) {
|
|
2001
|
+
const sessionDir = join(sessionsRoot, workspace, `session_${sessionId}`);
|
|
2002
|
+
try {
|
|
2003
|
+
await readFile(wirePath(sessionDir));
|
|
2004
|
+
return sessionDir;
|
|
2005
|
+
} catch {}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/** Whether a user message is kimi's auto-permission reminder, never real task text. */
|
|
2009
|
+
function isSystemReminder(text) {
|
|
2010
|
+
return text.startsWith("<system-reminder>");
|
|
2011
|
+
}
|
|
2012
|
+
/** Flatten the text blocks of a message content array. */
|
|
2013
|
+
function textOf(content) {
|
|
2014
|
+
return (content ?? []).filter((block) => typeof block === "object" && block !== null && block.type === "text").map((block) => block.text ?? "").join("");
|
|
1462
2015
|
}
|
|
1463
2016
|
/**
|
|
1464
|
-
*
|
|
1465
|
-
* the
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
2017
|
+
* Render a file-editing tool call's arguments apply-patch style: the path
|
|
2018
|
+
* plus the old/new content. Without this a delegation's edits collapsed to
|
|
2019
|
+
* the bare path (or nothing) and the transcript hid what the member actually
|
|
2020
|
+
* changed — the kimi twin of codex's fileChange → ApplyPatch fold.
|
|
2021
|
+
* @param name - the tool name the wire recorded.
|
|
2022
|
+
* @param record - the call's argument object.
|
|
2023
|
+
* @returns the patch-style summary, or undefined when the tool is not a
|
|
2024
|
+
* known file-editing shape.
|
|
1471
2025
|
*/
|
|
1472
|
-
function
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
2026
|
+
function editArgsOf(name, record) {
|
|
2027
|
+
const path = typeof record["path"] === "string" ? record["path"] : void 0;
|
|
2028
|
+
if (name === "Edit" || name === "MultiEdit") {
|
|
2029
|
+
if (path === void 0 || path.trim() === "") return void 0;
|
|
2030
|
+
const edits = Array.isArray(record["edits"]) ? record["edits"] : [record];
|
|
2031
|
+
const hunks = [];
|
|
2032
|
+
for (const edit of edits) {
|
|
2033
|
+
const oldText = typeof edit["old_string"] === "string" ? edit["old_string"] : "";
|
|
2034
|
+
const newText = typeof edit["new_string"] === "string" ? edit["new_string"] : "";
|
|
2035
|
+
if (oldText === "" && newText === "") continue;
|
|
2036
|
+
hunks.push("@@", ...oldText.split("\n").map((line) => `-${line}`), ...newText.split("\n").map((line) => `+${line}`));
|
|
2037
|
+
}
|
|
2038
|
+
if (hunks.length === 0) return void 0;
|
|
2039
|
+
return [`update: ${path}`, ...hunks].join("\n");
|
|
2040
|
+
}
|
|
2041
|
+
if (name === "Write") {
|
|
2042
|
+
if (path === void 0 || path.trim() === "") return void 0;
|
|
2043
|
+
const content = typeof record["content"] === "string" ? record["content"] : "";
|
|
2044
|
+
if (content === "") return void 0;
|
|
2045
|
+
return [`add: ${path}`, ...content.split("\n").map((line) => `+${line}`)].join("\n");
|
|
2046
|
+
}
|
|
2047
|
+
const patch = record["patch"] ?? record["diff"];
|
|
2048
|
+
if (typeof patch === "string" && patch.trim() !== "") return patch;
|
|
1481
2049
|
}
|
|
1482
|
-
/**
|
|
1483
|
-
function
|
|
1484
|
-
if (
|
|
1485
|
-
|
|
2050
|
+
/** Render a tool call's arguments into a compact summary. */
|
|
2051
|
+
function argsOf(name, args) {
|
|
2052
|
+
if (args === void 0) return void 0;
|
|
2053
|
+
if (typeof args === "string") return args.trim() === "" ? void 0 : args;
|
|
2054
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return void 0;
|
|
2055
|
+
const record = args;
|
|
2056
|
+
const edit = editArgsOf(name, record);
|
|
2057
|
+
if (edit !== void 0) return edit;
|
|
2058
|
+
for (const key of [
|
|
2059
|
+
"query",
|
|
2060
|
+
"url",
|
|
2061
|
+
"path",
|
|
2062
|
+
"command"
|
|
2063
|
+
]) {
|
|
2064
|
+
const value = record[key];
|
|
2065
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
2066
|
+
}
|
|
2067
|
+
const pairs = [];
|
|
2068
|
+
for (const [key, value] of Object.entries(record)) if (typeof value === "string" && value.trim() !== "") pairs.push(`${key}=${value.trim()}`);
|
|
2069
|
+
else if (typeof value === "number" || typeof value === "boolean") pairs.push(`${key}=${String(value)}`);
|
|
2070
|
+
if (pairs.length === 0) return void 0;
|
|
2071
|
+
const summary = pairs.join(" ");
|
|
2072
|
+
return summary.length > 200 ? `${summary.slice(0, 200)}…` : summary;
|
|
1486
2073
|
}
|
|
1487
2074
|
/**
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* are appended to persistence so the child session is viewable.
|
|
1492
|
-
*
|
|
1493
|
-
* A resumed round (or a live poll) passes the already-mirrored
|
|
1494
|
-
* transcript-line count so only the delta is appended — re-mirroring earlier
|
|
1495
|
-
* lines would duplicate their messages. Turn numbers come from the transcript
|
|
1496
|
-
* lines themselves (the wire's 1-based turn, matching the provider's
|
|
1497
|
-
* `turn/start` numbering); step numbers CONTINUE from the assistant messages
|
|
1498
|
-
* already in the child session, so a round mirrored across several live polls
|
|
1499
|
-
* does not restart step 1 per poll.
|
|
1500
|
-
*
|
|
1501
|
-
* Every folded step is wrapped in the `step/start`–`step/end` boundary pair
|
|
1502
|
-
* the live conversation assembler requires: without a boundary the step never
|
|
1503
|
-
* enters the location index, the assistant message resolves to a turn-level
|
|
1504
|
-
* location, and the real-time view renders nothing (only a full rebuild,
|
|
1505
|
-
* which mints step drafts from the explicit coordinates, recovers it). A
|
|
1506
|
-
* think/assistant line that completes an in-flight stream folds at the
|
|
1507
|
-
* stream's reserved step instead (see {@link KimiMirrorStreams}), and a
|
|
1508
|
-
* sequential fold never takes a step a stream reserved.
|
|
1509
|
-
*
|
|
1510
|
-
* Usage accounting is exactly-once ACROSS passes: each `usage.record` is
|
|
1511
|
-
* tagged with the transcript position it occurred at, and a pass attaches the
|
|
1512
|
-
* records in the half-open range `(fromLines, newTotal]` — the records that
|
|
1513
|
-
* trail THIS pass's lines — summed onto the delta's final assistant message.
|
|
1514
|
-
* (A record flushed only after its line was already mirrored belongs to no
|
|
1515
|
-
* later pass and is dropped; kimi flushes content and usage together per
|
|
1516
|
-
* request, so this window is a flush race, not the norm.)
|
|
1517
|
-
* @param ctx - host context carrying the session persistence service.
|
|
1518
|
-
* @param childSession - the dsh subagent session created for this delegation.
|
|
1519
|
-
* @param homeDir - the `kimi` harness's scoped home.
|
|
1520
|
-
* @param kimiSessionId - the kimi session to mirror; omitted mirrors the most
|
|
1521
|
-
* recent session (a one-shot `kimi -p` run always creates a fresh one).
|
|
1522
|
-
* @param fromLines - transcript lines already mirrored into the child session
|
|
1523
|
-
* (0 for the first round); only lines after this offset are appended.
|
|
1524
|
-
* @returns the new total and the newly mirrored lines' texts.
|
|
2075
|
+
* Parse a session's wire log into transcript lines plus every usage record.
|
|
2076
|
+
* @param sessionDir - the session directory.
|
|
2077
|
+
* @returns the transcript.
|
|
1525
2078
|
*/
|
|
1526
|
-
async function
|
|
1527
|
-
|
|
2079
|
+
async function readKimiTranscript(sessionDir) {
|
|
2080
|
+
const sessionId = sessionDir.split("session_").pop() ?? sessionDir;
|
|
2081
|
+
const lines = [];
|
|
2082
|
+
const usageRecords = [];
|
|
2083
|
+
const closedSteps = /* @__PURE__ */ new Set();
|
|
2084
|
+
let model;
|
|
2085
|
+
let modelTurn;
|
|
2086
|
+
let lineModel;
|
|
2087
|
+
let lineConfig;
|
|
2088
|
+
const push = (line) => {
|
|
2089
|
+
lines.push({
|
|
2090
|
+
...line,
|
|
2091
|
+
...lineModel === void 0 ? {} : { model: lineModel },
|
|
2092
|
+
...line.kind !== "user" && lineConfig !== void 0 ? { requestConfig: { ...lineConfig } } : {}
|
|
2093
|
+
});
|
|
2094
|
+
};
|
|
2095
|
+
let turn = 0;
|
|
2096
|
+
let turnSeen = false;
|
|
2097
|
+
const callsByUuid = /* @__PURE__ */ new Map();
|
|
2098
|
+
const callsByToolCallId = /* @__PURE__ */ new Map();
|
|
2099
|
+
let text;
|
|
1528
2100
|
try {
|
|
1529
|
-
|
|
1530
|
-
} catch {
|
|
1531
|
-
return {
|
|
1532
|
-
|
|
1533
|
-
|
|
2101
|
+
text = await readFile(wirePath(sessionDir), "utf8");
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
if (error.code === "ENOENT") return {
|
|
2104
|
+
sessionId,
|
|
2105
|
+
lines,
|
|
2106
|
+
usageRecords,
|
|
2107
|
+
closedSteps
|
|
1534
2108
|
};
|
|
2109
|
+
throw error;
|
|
1535
2110
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
continue;
|
|
1545
|
-
}
|
|
1546
|
-
if (transcript.lines.length > 0) break;
|
|
2111
|
+
for (const raw of text.split("\n")) {
|
|
2112
|
+
const line = raw.trim();
|
|
2113
|
+
if (line === "") continue;
|
|
2114
|
+
let event;
|
|
2115
|
+
try {
|
|
2116
|
+
event = JSON.parse(line);
|
|
2117
|
+
} catch {
|
|
2118
|
+
continue;
|
|
1547
2119
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
continue;
|
|
1556
|
-
}
|
|
1557
|
-
for (const name of sessions) {
|
|
1558
|
-
if (!name.startsWith("session_")) continue;
|
|
1559
|
-
const dir = join(homeDir, "sessions", workspace, name);
|
|
1560
|
-
const meta = await stat(dir);
|
|
1561
|
-
if (newest === void 0 || meta.mtimeMs > newest.time) newest = {
|
|
1562
|
-
dir,
|
|
1563
|
-
time: meta.mtimeMs
|
|
2120
|
+
if (event.type === "usage.record") {
|
|
2121
|
+
if (typeof event.model === "string" && event.model !== "") {
|
|
2122
|
+
model = lineModel = event.model;
|
|
2123
|
+
modelTurn = turn || 1;
|
|
2124
|
+
if (lineConfig !== void 0) lineConfig = {
|
|
2125
|
+
...lineConfig,
|
|
2126
|
+
model: event.model
|
|
1564
2127
|
};
|
|
1565
2128
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
transcript = void 0;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
if (transcript === void 0 || transcript.lines.length === 0) return {
|
|
1574
|
-
total: 0,
|
|
1575
|
-
texts: []
|
|
1576
|
-
};
|
|
1577
|
-
const newTotal = transcript.lines.length;
|
|
1578
|
-
const delta = transcript.lines.slice(fromLines);
|
|
1579
|
-
const observedModel = options?.turn === void 0 || transcript.modelTurn === options.turn ? transcript.model : void 0;
|
|
1580
|
-
const toolCalls = roundToolCalls(transcript.lines, options?.turn);
|
|
1581
|
-
const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= newTotal));
|
|
1582
|
-
let lastAssistant = -1;
|
|
1583
|
-
for (let index = 0; index < delta.length; index += 1) {
|
|
1584
|
-
const line = delta[index];
|
|
1585
|
-
if (line !== void 0 && line.kind !== "user" && line.kind !== "tool") lastAssistant = index;
|
|
1586
|
-
}
|
|
1587
|
-
const steps = /* @__PURE__ */ new Map();
|
|
1588
|
-
const openCalls = /* @__PURE__ */ new Map();
|
|
1589
|
-
const settledCalls = /* @__PURE__ */ new Set();
|
|
1590
|
-
for (const event of childSession.snapshotEvents()) {
|
|
1591
|
-
const data = event.data;
|
|
1592
|
-
if (typeof data.turn === "number" && typeof data.step === "number") steps.set(data.turn, Math.max(steps.get(data.turn) ?? 0, data.step + 1));
|
|
1593
|
-
if (event.type === "tool/call") {
|
|
1594
|
-
const call = event.data;
|
|
1595
|
-
openCalls.set(call.callId, {
|
|
1596
|
-
turn: call.turn,
|
|
1597
|
-
step: call.step,
|
|
1598
|
-
seq: event.seq
|
|
2129
|
+
const usage = usageFromWire(event.usage);
|
|
2130
|
+
if (usage !== void 0) usageRecords.push({
|
|
2131
|
+
line: lines.length,
|
|
2132
|
+
usage
|
|
1599
2133
|
});
|
|
1600
|
-
|
|
1601
|
-
const message = event.data.message;
|
|
1602
|
-
const id = message?.toolCallId ?? message?.content?.[0]?.toolCallId;
|
|
1603
|
-
if (id !== void 0) {
|
|
1604
|
-
openCalls.delete(id);
|
|
1605
|
-
settledCalls.add(id);
|
|
1606
|
-
}
|
|
2134
|
+
continue;
|
|
1607
2135
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const skip = new Set(reserved);
|
|
1621
|
-
while (skip.has(step)) step += 1;
|
|
2136
|
+
if (event.type === "llm.request") {
|
|
2137
|
+
lineModel = typeof event.model === "string" && event.model !== "" ? event.model : void 0;
|
|
2138
|
+
if (typeof event.model === "string" && event.model !== "") {
|
|
2139
|
+
model = event.model;
|
|
2140
|
+
modelTurn = turn || 1;
|
|
2141
|
+
}
|
|
2142
|
+
lineConfig = lineModel === void 0 ? void 0 : {
|
|
2143
|
+
model: lineModel,
|
|
2144
|
+
...typeof event.thinkingEffort === "string" && event.thinkingEffort !== "" ? { reasoningEffort: event.thinkingEffort } : {},
|
|
2145
|
+
...typeof event.maxTokens === "number" && Number.isFinite(event.maxTokens) ? { maxTokens: event.maxTokens } : {}
|
|
2146
|
+
};
|
|
2147
|
+
continue;
|
|
1622
2148
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
const call = openCalls.get(line.id);
|
|
1633
|
-
if (call === void 0) continue;
|
|
1634
|
-
openCalls.delete(line.id);
|
|
1635
|
-
settledCalls.add(line.id);
|
|
1636
|
-
childSession.append("tool/result", {
|
|
1637
|
-
turn: call.turn,
|
|
1638
|
-
step: call.step,
|
|
1639
|
-
message: createToolResultMessage({
|
|
1640
|
-
callId: line.id,
|
|
1641
|
-
content: [{
|
|
1642
|
-
type: "text",
|
|
1643
|
-
text: line.result
|
|
1644
|
-
}],
|
|
1645
|
-
isError: false
|
|
1646
|
-
})
|
|
1647
|
-
}, {
|
|
1648
|
-
surfaceOp: "append",
|
|
1649
|
-
sourceEventSeqs: [call.seq]
|
|
1650
|
-
});
|
|
1651
|
-
texts.push(kimiLineProgressText(line));
|
|
1652
|
-
}
|
|
1653
|
-
if (delta.length === 0) {
|
|
1654
|
-
if (texts.length > 0) await persistChildSession(ctx, childSession);
|
|
1655
|
-
return {
|
|
1656
|
-
total: newTotal,
|
|
1657
|
-
texts,
|
|
1658
|
-
...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
|
|
1659
|
-
...usageAttached ? { usageAttached: true } : {},
|
|
1660
|
-
...observedModel !== void 0 ? { model: observedModel } : {},
|
|
1661
|
-
...toolCalls !== void 0 ? { toolCalls } : {}
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
for (let index = 0; index < delta.length; index += 1) {
|
|
1665
|
-
const line = delta[index];
|
|
1666
|
-
if (line === void 0) continue;
|
|
1667
|
-
const turn = line.turn;
|
|
1668
|
-
if (line.kind === "user") {
|
|
1669
|
-
if (userAlreadyAppended(childSession, turn, line.text)) continue;
|
|
1670
|
-
steps.set(turn, 1);
|
|
1671
|
-
childSession.append("user/message", userEvent(line.text), { surfaceOp: "append" });
|
|
1672
|
-
texts.push(line.text);
|
|
1673
|
-
} else if (line.kind === "tool") {
|
|
1674
|
-
const reserved = options?.streams?.completeTool?.(line);
|
|
1675
|
-
const step = reserved?.step ?? nextFoldStep(turn);
|
|
1676
|
-
steps.set(turn, Math.max(steps.get(turn) ?? 1, step + 1));
|
|
1677
|
-
if (reserved?.opened !== true) childSession.append("step/start", {
|
|
1678
|
-
turn,
|
|
1679
|
-
step
|
|
2149
|
+
if (event.type === "turn.prompt") {
|
|
2150
|
+
lineModel = void 0;
|
|
2151
|
+
turnSeen = true;
|
|
2152
|
+
turn += 1;
|
|
2153
|
+
const prompt = textOf(event.input ?? []);
|
|
2154
|
+
if (prompt.trim() !== "" && !isSystemReminder(prompt)) push({
|
|
2155
|
+
kind: "user",
|
|
2156
|
+
text: prompt,
|
|
2157
|
+
turn: turn === 0 ? 1 : turn
|
|
1680
2158
|
});
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (event.type === "context.append_message") {
|
|
2162
|
+
const message = event.message;
|
|
2163
|
+
const role = message?.role;
|
|
2164
|
+
const textContent = textOf(message?.content ?? []);
|
|
2165
|
+
if (role === "user" && textContent.trim() !== "" && !isSystemReminder(textContent)) {
|
|
2166
|
+
const last = lines[lines.length - 1];
|
|
2167
|
+
if (turnSeen && last?.kind === "user" && last.text === textContent) continue;
|
|
2168
|
+
turnSeen = true;
|
|
2169
|
+
turn += 1;
|
|
2170
|
+
lineModel = void 0;
|
|
2171
|
+
push({
|
|
2172
|
+
kind: "user",
|
|
2173
|
+
text: textContent,
|
|
2174
|
+
turn: turn === 0 ? 1 : turn
|
|
2175
|
+
});
|
|
2176
|
+
} else if (role === "assistant" && textContent.trim() !== "") push({
|
|
2177
|
+
kind: "assistant",
|
|
2178
|
+
text: textContent,
|
|
2179
|
+
turn: turn === 0 ? 1 : turn
|
|
1687
2180
|
});
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (event.type === "context.append_loop_event") {
|
|
2184
|
+
const loop = event.event;
|
|
2185
|
+
if (loop === void 0) continue;
|
|
2186
|
+
const loopTurn = typeof loop.turnId === "number" ? loop.turnId + 1 : void 0;
|
|
2187
|
+
if (loopTurn !== void 0) {
|
|
2188
|
+
turn = Math.max(turn, loopTurn);
|
|
2189
|
+
turnSeen = true;
|
|
2190
|
+
}
|
|
2191
|
+
const wireStep = typeof loop.step === "number" ? loop.step : void 0;
|
|
2192
|
+
const part = loop.part;
|
|
2193
|
+
const lineTurn = turn === 0 ? 1 : turn;
|
|
2194
|
+
if (loop.type === "step.end" && wireStep !== void 0) closedSteps.add(`${lineTurn}:${wireStep}`);
|
|
2195
|
+
if (part?.type === "text" && part.text !== void 0 && part.text.trim() !== "") push({
|
|
2196
|
+
kind: "assistant",
|
|
2197
|
+
text: part.text,
|
|
2198
|
+
turn: lineTurn,
|
|
2199
|
+
...wireStep === void 0 ? {} : { wireStep }
|
|
1703
2200
|
});
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2201
|
+
else if (part?.type === "think" && part.think !== void 0 && part.think.trim() !== "") push({
|
|
2202
|
+
kind: "think",
|
|
2203
|
+
text: part.think,
|
|
2204
|
+
turn: lineTurn,
|
|
2205
|
+
...wireStep === void 0 ? {} : { wireStep }
|
|
1707
2206
|
});
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
turn,
|
|
1719
|
-
|
|
1720
|
-
message: assistantEvent(lineBlocks(line), line.model),
|
|
1721
|
-
stream: [],
|
|
1722
|
-
...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
|
|
1723
|
-
}, { surfaceOp: "append" });
|
|
1724
|
-
childSession.append("step/end", {
|
|
1725
|
-
turn,
|
|
1726
|
-
step: completion.step
|
|
2207
|
+
else if (loop.type === "tool.call") {
|
|
2208
|
+
const call = loop;
|
|
2209
|
+
const name = call.toolCall?.name ?? call.name ?? "tool";
|
|
2210
|
+
const args = argsOf(name, call.toolCall?.args ?? call.args);
|
|
2211
|
+
const lineIndex = lines.length;
|
|
2212
|
+
push({
|
|
2213
|
+
kind: "tool",
|
|
2214
|
+
id: typeof call.toolCallId === "string" ? call.toolCallId : typeof call.uuid === "string" ? call.uuid : `kimi-tool-${lineIndex}`,
|
|
2215
|
+
name,
|
|
2216
|
+
...args === void 0 ? {} : { args },
|
|
2217
|
+
turn: lineTurn,
|
|
2218
|
+
...wireStep === void 0 ? {} : { wireStep }
|
|
1727
2219
|
});
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2220
|
+
if (typeof call.uuid === "string") callsByUuid.set(call.uuid, lineIndex);
|
|
2221
|
+
if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
|
|
2222
|
+
} else if (loop.type === "tool.result") {
|
|
2223
|
+
const result = loop;
|
|
2224
|
+
const output = result.result?.output;
|
|
2225
|
+
let target = typeof result.parentUuid === "string" ? callsByUuid.get(result.parentUuid) : void 0;
|
|
2226
|
+
if (target === void 0 && typeof result.toolCallId === "string") target = callsByToolCallId.get(result.toolCallId);
|
|
2227
|
+
if (target === void 0) {
|
|
2228
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) if (lines[index]?.kind === "tool") {
|
|
2229
|
+
target = index;
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (output !== void 0 && output.trim() !== "" && target !== void 0) {
|
|
2234
|
+
const current = lines[target];
|
|
2235
|
+
if (current !== void 0 && current.kind === "tool") lines[target] = {
|
|
2236
|
+
...current,
|
|
2237
|
+
result: output
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
} else if (loop.type === "content.part" && part?.type !== void 0 && part.type !== "text" && part.type !== "think") push({
|
|
2241
|
+
kind: "assistant",
|
|
2242
|
+
text: `[未支持的内容类型 ${part.type}]`,
|
|
2243
|
+
turn: lineTurn
|
|
1747
2244
|
});
|
|
1748
|
-
texts.push(kimiLineProgressText(line));
|
|
1749
2245
|
}
|
|
1750
2246
|
}
|
|
1751
|
-
await persistChildSession(ctx, childSession);
|
|
1752
2247
|
return {
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
...
|
|
1758
|
-
...
|
|
2248
|
+
sessionId,
|
|
2249
|
+
lines,
|
|
2250
|
+
usageRecords,
|
|
2251
|
+
closedSteps,
|
|
2252
|
+
...model === void 0 ? {} : { model },
|
|
2253
|
+
...modelTurn === void 0 ? {} : { modelTurn }
|
|
1759
2254
|
};
|
|
1760
2255
|
}
|
|
1761
2256
|
/**
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1768
|
-
* @
|
|
2257
|
+
* Map a wire `usage.record` payload onto the shared usage contract. kimi
|
|
2258
|
+
* reports input (uncached), cache read, cache creation, and output; the
|
|
2259
|
+
* creation counter is its cache-write analog. Non-numeric or missing
|
|
2260
|
+
* counters are dropped so the record stays usable when kimi omits a field;
|
|
2261
|
+
* a reported cache-read counter is always written (zero included), so a
|
|
2262
|
+
* reader can tell "no hits" from "not reported".
|
|
2263
|
+
* @param raw - the event's `usage` value.
|
|
2264
|
+
* @returns the shared usage record, or undefined when nothing was reported.
|
|
1769
2265
|
*/
|
|
1770
|
-
function
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2266
|
+
function usageFromWire(raw) {
|
|
2267
|
+
const counters = raw;
|
|
2268
|
+
if (counters === void 0 || typeof counters !== "object") return void 0;
|
|
2269
|
+
const num = (value) => {
|
|
2270
|
+
const n = Number(value);
|
|
2271
|
+
return Number.isFinite(n) ? n : void 0;
|
|
2272
|
+
};
|
|
2273
|
+
const input = num(counters.inputOther);
|
|
2274
|
+
const output = num(counters.output);
|
|
2275
|
+
const cacheRead = num(counters.inputCacheRead);
|
|
2276
|
+
const cacheWrite = num(counters.inputCacheCreation);
|
|
2277
|
+
if (input === void 0 && output === void 0 && cacheRead === void 0 && cacheWrite === void 0) return;
|
|
2278
|
+
const usage = {
|
|
2279
|
+
inputTokens: input ?? 0,
|
|
2280
|
+
outputTokens: output ?? 0
|
|
1782
2281
|
};
|
|
2282
|
+
if (cacheRead !== void 0) usage.cacheReadTokens = cacheRead;
|
|
2283
|
+
if (cacheWrite !== void 0 && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
2284
|
+
return usage;
|
|
1783
2285
|
}
|
|
1784
2286
|
/**
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
2287
|
+
* Sum per-request usage records into one round total. Each `usage.record` is
|
|
2288
|
+
* a single LLM request's accounting, so a round's totals are the sum of the
|
|
2289
|
+
* records inside it.
|
|
2290
|
+
* @param records - the usage records to add.
|
|
2291
|
+
* @returns the summed usage, or undefined when the list is empty.
|
|
1788
2292
|
*/
|
|
1789
|
-
function
|
|
1790
|
-
let
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2293
|
+
function sumUsageRecords(records) {
|
|
2294
|
+
let input = 0;
|
|
2295
|
+
let output = 0;
|
|
2296
|
+
let cacheRead = 0;
|
|
2297
|
+
let cacheWrite = 0;
|
|
2298
|
+
for (const record of records) {
|
|
2299
|
+
input += record.usage.inputTokens ?? 0;
|
|
2300
|
+
output += record.usage.outputTokens ?? 0;
|
|
2301
|
+
cacheRead += record.usage.cacheReadTokens ?? 0;
|
|
2302
|
+
cacheWrite += record.usage.cacheWriteTokens ?? 0;
|
|
1796
2303
|
}
|
|
1797
|
-
return
|
|
2304
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return void 0;
|
|
2305
|
+
const usage = {
|
|
2306
|
+
inputTokens: input,
|
|
2307
|
+
outputTokens: output
|
|
2308
|
+
};
|
|
2309
|
+
if (cacheRead > 0) usage.cacheReadTokens = cacheRead;
|
|
2310
|
+
if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
2311
|
+
return usage;
|
|
1798
2312
|
}
|
|
1799
|
-
//#endregion
|
|
1800
|
-
//#region lib/types/credential-guard.js
|
|
1801
2313
|
/**
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
* -
|
|
1809
|
-
*
|
|
1810
|
-
* clears it too);
|
|
1811
|
-
* - restores the backup when the file is an exact empty shell (both tokens
|
|
1812
|
-
* empty strings) — the only corruption signature we trust;
|
|
1813
|
-
* - logs every state transition with a process snapshot, so the next wipe
|
|
1814
|
-
* can be attributed (which process, at which moment).
|
|
1815
|
-
*
|
|
1816
|
-
* Restore is deliberately narrow: a revoked-elsewhere credential never takes
|
|
1817
|
-
* the empty-shell shape, and a stale backup at worst produces one more loud
|
|
1818
|
-
* 401, never silent misbehavior.
|
|
1819
|
-
* @module @khorsheed/dsh-local-agent-kimi/credential-guard
|
|
2314
|
+
* Merge two usage windows into one running total — the delegation run's
|
|
2315
|
+
* per-round accumulator: each mirror pass (live poll, settle) returns its
|
|
2316
|
+
* own window's accounting, and the disjoint windows sum to the round's
|
|
2317
|
+
* total. `undefined` operands pass through, so callers accumulate without
|
|
2318
|
+
* absence checks.
|
|
2319
|
+
* @param left - the running total, or undefined before the first window.
|
|
2320
|
+
* @param right - the next window's usage, or undefined when it had none.
|
|
2321
|
+
* @returns the merged total, or undefined when both operands are absent.
|
|
1820
2322
|
*/
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
resolve(`(ps failed: ${error.message})`);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
resolve(stdout.split("\n").filter((line) => /\bkimi\b|dsh/.test(line) && !line.includes("grep")).slice(0, 12).join(" ; "));
|
|
1835
|
-
});
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
/** Classify the credential file's current content. */
|
|
1839
|
-
async function observe(file) {
|
|
1840
|
-
let raw;
|
|
1841
|
-
try {
|
|
1842
|
-
raw = await readFile(file, "utf8");
|
|
1843
|
-
} catch {
|
|
1844
|
-
return {
|
|
1845
|
-
state: "absent",
|
|
1846
|
-
mtimeMs: 0
|
|
1847
|
-
};
|
|
1848
|
-
}
|
|
1849
|
-
let mtimeMs = 0;
|
|
1850
|
-
try {
|
|
1851
|
-
mtimeMs = (await stat(file)).mtimeMs;
|
|
1852
|
-
} catch {}
|
|
1853
|
-
try {
|
|
1854
|
-
const parsed = JSON.parse(raw);
|
|
1855
|
-
const empty = (v) => typeof v === "string" && v.length === 0;
|
|
1856
|
-
if (empty(parsed.access_token) && empty(parsed.refresh_token)) return {
|
|
1857
|
-
state: "empty-shell",
|
|
1858
|
-
mtimeMs
|
|
1859
|
-
};
|
|
1860
|
-
return {
|
|
1861
|
-
state: "valid",
|
|
1862
|
-
mtimeMs
|
|
1863
|
-
};
|
|
1864
|
-
} catch {
|
|
1865
|
-
return {
|
|
1866
|
-
state: "absent",
|
|
1867
|
-
mtimeMs
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
2323
|
+
function addTokenUsage(left, right) {
|
|
2324
|
+
if (left === void 0) return right;
|
|
2325
|
+
if (right === void 0) return left;
|
|
2326
|
+
return sumUsageRecords([{
|
|
2327
|
+
line: 0,
|
|
2328
|
+
usage: left
|
|
2329
|
+
}, {
|
|
2330
|
+
line: 0,
|
|
2331
|
+
usage: right
|
|
2332
|
+
}]);
|
|
1870
2333
|
}
|
|
1871
|
-
/**
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
const { state, mtimeMs } = await observe(file);
|
|
1882
|
-
const signature = `${mtimeMs}:${state}`;
|
|
1883
|
-
const previous = lastSeen.get(homeDir);
|
|
1884
|
-
lastSeen.set(homeDir, signature);
|
|
1885
|
-
if (state === "valid") {
|
|
1886
|
-
if (previous !== void 0 && previous !== signature) log?.(`local-agent-kimi: credential refreshed (mtime ${new Date(mtimeMs).toISOString()}), snapshotting the backup`);
|
|
1887
|
-
await writeFile(backup, await readFile(file), { mode: 384 });
|
|
1888
|
-
return true;
|
|
1889
|
-
}
|
|
1890
|
-
if (state === "empty-shell") {
|
|
1891
|
-
const snapshot = await processSnapshot();
|
|
1892
|
-
log?.(`local-agent-kimi: CREDENTIAL EMPTY SHELL detected (mtime ${new Date(mtimeMs).toISOString()}); previous state ${previous ?? "unknown"}; processes: ${snapshot}`);
|
|
1893
|
-
let backupState = "absent";
|
|
1894
|
-
try {
|
|
1895
|
-
backupState = (await observe(backup)).state;
|
|
1896
|
-
} catch {}
|
|
1897
|
-
if (backupState === "valid") {
|
|
1898
|
-
await copyFile(backup, file);
|
|
1899
|
-
log?.("local-agent-kimi: credential restored from the sentinel backup — the next delegation should succeed");
|
|
1900
|
-
return true;
|
|
1901
|
-
}
|
|
1902
|
-
log?.("local-agent-kimi: no usable backup to restore; the harness needs a fresh login");
|
|
1903
|
-
return false;
|
|
2334
|
+
/** Render a transcript as command-reply text. */
|
|
2335
|
+
function renderTranscript(transcript) {
|
|
2336
|
+
if (transcript.lines.length === 0) return `kimi session ${transcript.sessionId}: no readable transcript yet.`;
|
|
2337
|
+
const parts = [`kimi session ${transcript.sessionId}:`];
|
|
2338
|
+
for (const line of transcript.lines) if (line.kind === "user") parts.push(`\n用户: ${line.text}`);
|
|
2339
|
+
else if (line.kind === "assistant") parts.push(`\nkimi: ${line.text}`);
|
|
2340
|
+
else if (line.kind === "think") parts.push(`\n[思考] ${line.text}`);
|
|
2341
|
+
else {
|
|
2342
|
+
const call = `[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`;
|
|
2343
|
+
parts.push(`\n ↳ ${call}${line.result !== void 0 ? ` → ${line.result}` : ""}`);
|
|
1904
2344
|
}
|
|
1905
|
-
return
|
|
2345
|
+
return parts.join("\n");
|
|
1906
2346
|
}
|
|
1907
2347
|
//#endregion
|
|
1908
|
-
//#region lib/types/
|
|
1909
|
-
/**
|
|
1910
|
-
* Kimi Code scoped-home provisioning: a working `config.toml` (provider and
|
|
1911
|
-
* model definitions) is required for the ACP server to authenticate. The
|
|
1912
|
-
* scoped home is provisioned once — from the user's own config, redacted —
|
|
1913
|
-
* so the scoped setup mirrors the user's models while carrying no secrets.
|
|
1914
|
-
* Sign-out removes the credentials and OAuth cache so a later login can
|
|
1915
|
-
* authorize a different account. The agent preset the delegation tool row
|
|
1916
|
-
* needs is bootstrapped into the user preset root on first boot, so
|
|
1917
|
-
* installing the bundle needs no manual copy step.
|
|
1918
|
-
* @module @khorsheed/dsh-local-agent-kimi/provision
|
|
1919
|
-
*/
|
|
1920
|
-
const REAL_CONFIG = join(homedir(), ".kimi-code", "config.toml");
|
|
1921
|
-
/** Credentials directory inside the scoped home; its presence marks auth. */
|
|
1922
|
-
const CREDENTIALS_DIR$1 = "credentials";
|
|
1923
|
-
/** OAuth token cache directory inside the scoped home. */
|
|
1924
|
-
const OAUTH_DIR = "oauth";
|
|
1925
|
-
/**
|
|
1926
|
-
* The managed provider's endpoint, written into a fresh scoped config and the
|
|
1927
|
-
* baseline the eval snapshot compares against: a scoped config still pointing
|
|
1928
|
-
* here routes through kimi's own service, i.e. no custom endpoint is in force.
|
|
1929
|
-
*/
|
|
1930
|
-
const KIMI_MANAGED_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
1931
|
-
/** Minimal managed config for a fresh home with no user config to mirror. */
|
|
1932
|
-
function minimalConfig(model, thinkingEffort) {
|
|
1933
|
-
return `default_model = "${model}"
|
|
1934
|
-
|
|
1935
|
-
[thinking]
|
|
1936
|
-
enabled = true
|
|
1937
|
-
effort = "${thinkingEffort}"
|
|
1938
|
-
|
|
1939
|
-
[providers."managed:kimi-code"]
|
|
1940
|
-
base_url = "${KIMI_MANAGED_BASE_URL}"
|
|
1941
|
-
type = "kimi"
|
|
1942
|
-
api_key = ""
|
|
1943
|
-
|
|
1944
|
-
[providers."managed:kimi-code".oauth]
|
|
1945
|
-
storage = "file"
|
|
1946
|
-
key = "oauth/kimi-code"
|
|
1947
|
-
|
|
1948
|
-
[models."${model}"]
|
|
1949
|
-
provider = "managed:kimi-code"
|
|
1950
|
-
model = "${model.split("/").pop()}"
|
|
1951
|
-
max_context_size = 1048576
|
|
1952
|
-
capabilities = [ "thinking", "always_thinking", "image_in", "video_in", "tool_use" ]
|
|
1953
|
-
display_name = "${model}"
|
|
1954
|
-
support_efforts = [ "low", "high", "max" ]
|
|
1955
|
-
default_effort = "${thinkingEffort}"
|
|
1956
|
-
`;
|
|
1957
|
-
}
|
|
1958
|
-
/**
|
|
1959
|
-
* Redact every api_key value so the scoped copy carries no secrets. The
|
|
1960
|
-
* kimi-managed provider keeps an empty api_key (its auth routes through
|
|
1961
|
-
* oauth), which is exactly what an empty value means here.
|
|
1962
|
-
* @param config - the user's config text.
|
|
1963
|
-
* @returns the same config with all api_key values blanked.
|
|
1964
|
-
*/
|
|
1965
|
-
function redactApiKeys(config) {
|
|
1966
|
-
return config.replace(/api_key\s*=\s*"[^"]*"/gu, "api_key = \"\"");
|
|
1967
|
-
}
|
|
2348
|
+
//#region lib/types/session-mirror.js
|
|
1968
2349
|
/**
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
1973
|
-
*
|
|
1974
|
-
*
|
|
1975
|
-
* never retroactively to an existing config.
|
|
1976
|
-
* @param homeDir - the `kimi` harness's scoped home.
|
|
1977
|
-
* @param model - the kimi-managed model id for a fresh minimal config.
|
|
1978
|
-
* @param thinkingEffort - the reasoning effort a fresh minimal config pins.
|
|
1979
|
-
* @returns true when a config was written, false when one already existed.
|
|
2350
|
+
* Mirror a kimi session's transcript into a dsh subagent session, so the
|
|
2351
|
+
* delegation shows up in the standard 子代理 surface and opening the child
|
|
2352
|
+
* session shows the full conversation (user prompts, kimi replies, thinking,
|
|
2353
|
+
* and tool activity with arguments and results) instead of a silent one-shot
|
|
2354
|
+
* call.
|
|
2355
|
+
* @module @khorsheed/dsh-local-agent-kimi/session-mirror
|
|
1980
2356
|
*/
|
|
1981
|
-
async function provisionKimiConfig(homeDir, model, thinkingEffort) {
|
|
1982
|
-
try {
|
|
1983
|
-
await readFile(join(homeDir, "config.toml"));
|
|
1984
|
-
return false;
|
|
1985
|
-
} catch (error) {
|
|
1986
|
-
if (error.code !== "ENOENT") throw error;
|
|
1987
|
-
}
|
|
1988
|
-
let config = minimalConfig(model, thinkingEffort);
|
|
1989
|
-
try {
|
|
1990
|
-
config = redactApiKeys(await readFile(REAL_CONFIG, "utf8"));
|
|
1991
|
-
} catch (error) {
|
|
1992
|
-
if (error.code !== "ENOENT") throw error;
|
|
1993
|
-
}
|
|
1994
|
-
await writeFile(join(homeDir, "config.toml"), config, { flag: "wx" });
|
|
1995
|
-
return true;
|
|
1996
|
-
}
|
|
1997
|
-
/** The managed-service provider section provisioned into the scoped config. */
|
|
1998
|
-
const MANAGED_PROVIDER_SECTION = "providers.\"managed:kimi-code\"";
|
|
1999
2357
|
/**
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
* and a user-edited value is respected untouched.
|
|
2005
|
-
* @param homeDir - the `kimi` harness's scoped home.
|
|
2006
|
-
* @returns the configured base URL, or undefined when absent/unreadable.
|
|
2358
|
+
* The synthesized result for a call whose result the wire never recorded
|
|
2359
|
+
* (a settle-pass fold, or a live reservation kimi never logged). The
|
|
2360
|
+
* released-log contract resolves every advertised call before its step
|
|
2361
|
+
* closes, so an interrupted call folds with this error result.
|
|
2007
2362
|
*/
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2363
|
+
const KIMI_INTERRUPTED_TOOL_RESULT = "(工具调用被中断,未能记录返回结果)";
|
|
2364
|
+
/** One user-role message event. */
|
|
2365
|
+
function userEvent(text) {
|
|
2366
|
+
return createUserMessage({
|
|
2367
|
+
content: [{
|
|
2368
|
+
type: "text",
|
|
2369
|
+
text
|
|
2370
|
+
}],
|
|
2371
|
+
source: { kind: "user" }
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
/** One assistant-role message event, attributed to the kimi route. */
|
|
2375
|
+
function assistantEvent(blocks, model) {
|
|
2376
|
+
return createAssistantMessage({
|
|
2377
|
+
content: blocks,
|
|
2378
|
+
source: {
|
|
2379
|
+
provider: "kimi-cli",
|
|
2380
|
+
model: model ?? "unobserved"
|
|
2026
2381
|
}
|
|
2027
|
-
}
|
|
2382
|
+
});
|
|
2028
2383
|
}
|
|
2029
2384
|
/**
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2033
|
-
*
|
|
2034
|
-
* and a
|
|
2035
|
-
*
|
|
2036
|
-
* @
|
|
2385
|
+
* The step the next fold pass would assign in one turn: the smallest step
|
|
2386
|
+
* nothing holds (never written, never reserved at call time — callers skip
|
|
2387
|
+
* their own reservations on top). Filling the smallest free step keeps the
|
|
2388
|
+
* stored log dense: the released-log reader admits only dense, strictly
|
|
2389
|
+
* sequential steps, and a reservation released mid-round (its content rode an
|
|
2390
|
+
* earlier fold) leaves a gap only the next first-free write fills.
|
|
2391
|
+
* @param childSession - the run's child session.
|
|
2392
|
+
* @param turn - the round's turn number.
|
|
2393
|
+
* @returns the smallest free step in the turn.
|
|
2037
2394
|
*/
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
let section = "";
|
|
2046
|
-
for (const rawLine of text.split("\n")) {
|
|
2047
|
-
const line = rawLine.trim();
|
|
2048
|
-
const header = /^\[+([^\]]+)\]+$/.exec(line);
|
|
2049
|
-
if (header !== null) {
|
|
2050
|
-
section = header[1];
|
|
2051
|
-
continue;
|
|
2052
|
-
}
|
|
2053
|
-
if (section !== "") continue;
|
|
2054
|
-
const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
|
|
2055
|
-
if (key !== null && key[1] === "default_model") return key[2];
|
|
2395
|
+
function nextKimiSessionStep(childSession, turn) {
|
|
2396
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2397
|
+
for (const event of childSession.snapshotEvents()) {
|
|
2398
|
+
const data = event.data;
|
|
2399
|
+
if (data.turn === turn && typeof data.step === "number") taken.add(data.step);
|
|
2056
2400
|
}
|
|
2401
|
+
let next = 1;
|
|
2402
|
+
while (taken.has(next)) next += 1;
|
|
2403
|
+
return next;
|
|
2057
2404
|
}
|
|
2058
2405
|
/**
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
2064
|
-
*
|
|
2065
|
-
* @returns the defined model identifiers, in file order, deduplicated.
|
|
2406
|
+
* The step a durable write takes: one past the highest step this turn has
|
|
2407
|
+
* opened in the log. The released-log reader admits steps only in order —
|
|
2408
|
+
* one past the last, never below it — so the coordinate is decided when the
|
|
2409
|
+
* step is written, never reserved ahead (a reservation filled later lands
|
|
2410
|
+
* below a higher step already written: a gap, then a step out of order).
|
|
2411
|
+
* Stream and tool reservations only key the live preview.
|
|
2066
2412
|
*/
|
|
2067
|
-
|
|
2068
|
-
let
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
}
|
|
2074
|
-
const models = [];
|
|
2075
|
-
for (const rawLine of text.split("\n")) {
|
|
2076
|
-
const header = /^\[+([^\]]+)\]+$/.exec(rawLine.trim());
|
|
2077
|
-
if (header === null) continue;
|
|
2078
|
-
const table = /^models\.(?:"([^"]+)"|([\w-]+))$/.exec(header[1]);
|
|
2079
|
-
const name = table?.[1] ?? table?.[2];
|
|
2080
|
-
if (name !== void 0 && name !== "" && !models.includes(name)) models.push(name);
|
|
2413
|
+
function nextWrittenKimiStep(childSession, turn) {
|
|
2414
|
+
let last = 0;
|
|
2415
|
+
for (const event of childSession.snapshotEvents()) {
|
|
2416
|
+
if (event.type !== "step/start") continue;
|
|
2417
|
+
const data = event.data;
|
|
2418
|
+
if (data.turn === turn && typeof data.step === "number" && data.step > last) last = data.step;
|
|
2081
2419
|
}
|
|
2082
|
-
return
|
|
2420
|
+
return last + 1;
|
|
2421
|
+
}
|
|
2422
|
+
/** The run-progress delta text for one transcript line (any kind). */
|
|
2423
|
+
function kimiLineProgressText(line) {
|
|
2424
|
+
if (line.kind !== "tool") return line.text;
|
|
2425
|
+
return `${`[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`}${line.result !== void 0 ? ` → ${line.result}` : ""}`;
|
|
2083
2426
|
}
|
|
2084
2427
|
/**
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
* and
|
|
2428
|
+
* Mirror one kimi session's transcript into a dsh subagent session: each
|
|
2429
|
+
* user prompt becomes a `user/message`, and each WIRE STEP (one LLM request:
|
|
2430
|
+
* its thinking, reply text and tool calls share the wire's own step number)
|
|
2431
|
+
* folds into one dsh step whose `assistant/message` carries the reasoning,
|
|
2432
|
+
* text and tool-call blocks together — the host session's own shape, so the
|
|
2433
|
+
* standard surfaces (conversation, Context Lens) read one request per LLM
|
|
2434
|
+
* call. Then the events are appended to persistence so the child session is
|
|
2435
|
+
* viewable.
|
|
2090
2436
|
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2096
|
-
*
|
|
2437
|
+
* A resumed round (or a live poll) passes the already-mirrored
|
|
2438
|
+
* transcript-line count so only the delta is appended — re-mirroring earlier
|
|
2439
|
+
* lines would duplicate their messages. Turn numbers come from the transcript
|
|
2440
|
+
* lines themselves (the wire's 1-based turn, matching the provider's
|
|
2441
|
+
* `turn/start` numbering); step numbers CONTINUE from the assistant messages
|
|
2442
|
+
* already in the child session, so a round mirrored across several live polls
|
|
2443
|
+
* does not restart step 1 per poll.
|
|
2444
|
+
*
|
|
2445
|
+
* Every folded step is one ATOMIC `step/start`–`step/end` triple, written
|
|
2446
|
+
* only when its content lands: the released-log reader admits only dense,
|
|
2447
|
+
* strictly sequential steps (one open at a time — a second `step/start`
|
|
2448
|
+
* while another step is open fails the session's reload validation), and the
|
|
2449
|
+
* live conversation assembler needs the boundary to materialize the step at
|
|
2450
|
+
* all. A fold paired with in-flight streams lands at the earliest consumed
|
|
2451
|
+
* reservation (see {@link KimiMirrorStreams}), after `finalizeBefore` has
|
|
2452
|
+
* closed every stream reservation below it; a sequential fold never takes a
|
|
2453
|
+
* step a stream reserved. Mid-round, a wire step the wire has not CLOSED yet
|
|
2454
|
+
* (no `step.end`) holds the pass — its parts may still arrive — and so does
|
|
2455
|
+
* an unresulted tool call (the reader resolves every advertised call before
|
|
2456
|
+
* the step closes; the result merges into the call's line in place, so the
|
|
2457
|
+
* next pass sees it). The settle pass folds the remainder with a synthesized
|
|
2458
|
+
* interrupted result. Lines the wire never step-tagged (older kimi) fold one
|
|
2459
|
+
* by one, immediately, as before.
|
|
2460
|
+
*
|
|
2461
|
+
* Usage accounting is exactly-once ACROSS passes: each `usage.record` is
|
|
2462
|
+
* tagged with the transcript position it occurred at, a pass owns the records
|
|
2463
|
+
* between the old offset and its own consumed end, and inside the window each
|
|
2464
|
+
* record rides the unit whose line span encloses it — one record per wire
|
|
2465
|
+
* step, landing on its own step's message. (A record raced PAST its content
|
|
2466
|
+
* encloses into the next unit instead: attribution off by one in a flush
|
|
2467
|
+
* race, totals still exact.)
|
|
2468
|
+
* @param ctx - host context carrying the session persistence service.
|
|
2469
|
+
* @param childSession - the dsh subagent session created for this delegation.
|
|
2097
2470
|
* @param homeDir - the `kimi` harness's scoped home.
|
|
2098
|
-
* @param
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2471
|
+
* @param kimiSessionId - the kimi session to mirror; omitted mirrors the most
|
|
2472
|
+
* recent session (a one-shot `kimi -p` run always creates a fresh one).
|
|
2473
|
+
* @param fromLines - transcript lines already mirrored into the child session
|
|
2474
|
+
* (0 for the first round); only lines after this offset are appended.
|
|
2475
|
+
* @returns the new mirror offset and the newly mirrored lines' texts. A held
|
|
2476
|
+
* unit (open wire step or unresulted tool) stops the offset before itself;
|
|
2477
|
+
* the next pass retries from there.
|
|
2101
2478
|
*/
|
|
2102
|
-
async function
|
|
2103
|
-
|
|
2104
|
-
let text;
|
|
2479
|
+
async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, fromLines = 0, options) {
|
|
2480
|
+
let workspaces;
|
|
2105
2481
|
try {
|
|
2106
|
-
|
|
2482
|
+
workspaces = await readdir(join(homeDir, "sessions"));
|
|
2107
2483
|
} catch {
|
|
2108
|
-
return
|
|
2484
|
+
return {
|
|
2485
|
+
total: 0,
|
|
2486
|
+
texts: []
|
|
2487
|
+
};
|
|
2109
2488
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2489
|
+
let transcript;
|
|
2490
|
+
if (kimiSessionId !== void 0) {
|
|
2491
|
+
const dirName = kimiSessionId.startsWith("session_") ? kimiSessionId : `session_${kimiSessionId}`;
|
|
2492
|
+
for (const workspace of workspaces) {
|
|
2493
|
+
const dir = join(homeDir, "sessions", workspace, dirName);
|
|
2494
|
+
try {
|
|
2495
|
+
transcript = await readKimiTranscript(dir);
|
|
2496
|
+
} catch {
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
if (transcript.lines.length > 0) break;
|
|
2500
|
+
}
|
|
2501
|
+
} else {
|
|
2502
|
+
let newest;
|
|
2503
|
+
for (const workspace of workspaces) {
|
|
2504
|
+
let sessions;
|
|
2505
|
+
try {
|
|
2506
|
+
sessions = await readdir(join(homeDir, "sessions", workspace));
|
|
2507
|
+
} catch {
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
for (const name of sessions) {
|
|
2511
|
+
if (!name.startsWith("session_")) continue;
|
|
2512
|
+
const dir = join(homeDir, "sessions", workspace, name);
|
|
2513
|
+
const meta = await stat(dir);
|
|
2514
|
+
if (newest === void 0 || meta.mtimeMs > newest.time) newest = {
|
|
2515
|
+
dir,
|
|
2516
|
+
time: meta.mtimeMs
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
if (newest !== void 0) try {
|
|
2521
|
+
transcript = await readKimiTranscript(newest.dir);
|
|
2522
|
+
} catch {
|
|
2523
|
+
transcript = void 0;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
if (transcript === void 0 || transcript.lines.length === 0) return {
|
|
2527
|
+
total: 0,
|
|
2528
|
+
texts: []
|
|
2529
|
+
};
|
|
2530
|
+
const delta = transcript.lines.slice(fromLines);
|
|
2531
|
+
const observedModel = options?.turn === void 0 || transcript.modelTurn === options.turn ? transcript.model : void 0;
|
|
2532
|
+
const toolCalls = roundToolCalls(transcript.lines, options?.turn);
|
|
2533
|
+
const units = [];
|
|
2534
|
+
for (const line of delta) {
|
|
2535
|
+
if (line === void 0) continue;
|
|
2536
|
+
const last = units.at(-1);
|
|
2537
|
+
const head = last?.[0];
|
|
2538
|
+
if (last !== void 0 && head !== void 0 && line.kind !== "user" && head.kind !== "user" && line.wireStep !== void 0 && line.wireStep === head.wireStep && line.turn === head.turn) {
|
|
2539
|
+
last.push(line);
|
|
2118
2540
|
continue;
|
|
2119
2541
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2542
|
+
units.push([line]);
|
|
2543
|
+
}
|
|
2544
|
+
let holdAt = -1;
|
|
2545
|
+
if (options?.settle !== true) for (let index = 0; index < units.length; index += 1) {
|
|
2546
|
+
const unit = units[index];
|
|
2547
|
+
const head = unit[0];
|
|
2548
|
+
if (head.kind !== "user" && head.wireStep !== void 0 && !transcript.closedSteps.has(`${head.turn}:${head.wireStep}`) || unit.some((line) => line.kind === "tool" && line.result === void 0)) {
|
|
2549
|
+
holdAt = index;
|
|
2550
|
+
break;
|
|
2551
|
+
}
|
|
2127
2552
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
text = await readFile(join(homeDir, "config.toml"), "utf8");
|
|
2145
|
-
} catch {
|
|
2146
|
-
return;
|
|
2553
|
+
const foldedUnits = holdAt < 0 ? units : units.slice(0, holdAt);
|
|
2554
|
+
const consumedTotal = fromLines + foldedUnits.reduce((n, unit) => n + unit.length, 0);
|
|
2555
|
+
const unitUsage = [];
|
|
2556
|
+
const claimed = [];
|
|
2557
|
+
let unitStart = fromLines;
|
|
2558
|
+
for (const unit of foldedUnits) {
|
|
2559
|
+
const start = unitStart;
|
|
2560
|
+
unitStart += unit.length;
|
|
2561
|
+
const head = unit[0];
|
|
2562
|
+
if (head.kind !== "user" && head.wireStep !== void 0) {
|
|
2563
|
+
claimed.push({
|
|
2564
|
+
from: start,
|
|
2565
|
+
to: unitStart
|
|
2566
|
+
});
|
|
2567
|
+
unitUsage.push(sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= start && record.line < unitStart)));
|
|
2568
|
+
} else unitUsage.push(void 0);
|
|
2147
2569
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
for (const
|
|
2152
|
-
const
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2570
|
+
const unclaimedUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal && !claimed.some((span) => record.line >= span.from && record.line < span.to)));
|
|
2571
|
+
const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal));
|
|
2572
|
+
const takenSteps = /* @__PURE__ */ new Map();
|
|
2573
|
+
for (const event of childSession.snapshotEvents()) {
|
|
2574
|
+
const data = event.data;
|
|
2575
|
+
if (typeof data.turn === "number" && typeof data.step === "number") {
|
|
2576
|
+
const taken = takenSteps.get(data.turn) ?? /* @__PURE__ */ new Set();
|
|
2577
|
+
taken.add(data.step);
|
|
2578
|
+
takenSteps.set(data.turn, taken);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* The fold's coordinate: the smallest step of the turn that nothing holds
|
|
2583
|
+
* — neither written nor currently reserved. Filling the smallest free step
|
|
2584
|
+
* keeps the stored log dense even when a stream reservation below was
|
|
2585
|
+
* released mid-round (its content rode an earlier fold's step).
|
|
2586
|
+
*/
|
|
2587
|
+
const nextFoldStep = (turn) => {
|
|
2588
|
+
const taken = new Set(takenSteps.get(turn));
|
|
2589
|
+
for (const reserved of options?.streams?.reservedSteps(turn) ?? []) taken.add(reserved);
|
|
2590
|
+
let step = 1;
|
|
2591
|
+
while (taken.has(step)) step += 1;
|
|
2592
|
+
return step;
|
|
2593
|
+
};
|
|
2594
|
+
/** Record a fold's coordinate chosen by a stream pairing. */
|
|
2595
|
+
const markTaken = (turn, step) => {
|
|
2596
|
+
const written = takenSteps.get(turn) ?? /* @__PURE__ */ new Set();
|
|
2597
|
+
written.add(step);
|
|
2598
|
+
takenSteps.set(turn, written);
|
|
2599
|
+
};
|
|
2600
|
+
/** The window's accounting that no fold carried (dropped units' sums). */
|
|
2601
|
+
let carryUsage;
|
|
2602
|
+
let lastContentUnit = -1;
|
|
2603
|
+
for (let index = foldedUnits.length - 1; index >= 0; index -= 1) if (foldedUnits[index][0]?.kind !== "user") {
|
|
2604
|
+
lastContentUnit = index;
|
|
2605
|
+
break;
|
|
2606
|
+
}
|
|
2607
|
+
const texts = [];
|
|
2608
|
+
for (let unitIndex = 0; unitIndex < foldedUnits.length; unitIndex += 1) {
|
|
2609
|
+
const unit = foldedUnits[unitIndex];
|
|
2610
|
+
const head = unit[0];
|
|
2611
|
+
let usage = addTokenUsage(carryUsage, unitUsage[unitIndex]);
|
|
2612
|
+
carryUsage = void 0;
|
|
2613
|
+
if (unitIndex === lastContentUnit) usage = addTokenUsage(usage, unclaimedUsage);
|
|
2614
|
+
const turn = head.turn;
|
|
2615
|
+
if (head.kind === "user") {
|
|
2616
|
+
if (userAlreadyAppended(childSession, turn, head.text)) continue;
|
|
2617
|
+
childSession.append("user/message", userEvent(head.text), { surfaceOp: "append" });
|
|
2618
|
+
texts.push(head.text);
|
|
2619
|
+
carryUsage = usage;
|
|
2156
2620
|
continue;
|
|
2157
2621
|
}
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2622
|
+
const toolLines = unit.filter((line) => line.kind === "tool");
|
|
2623
|
+
const reasoning = unit.filter((line) => line.kind === "think").map((line) => line.text).join("");
|
|
2624
|
+
const text = unit.filter((line) => line.kind === "assistant").map((line) => line.text).join("");
|
|
2625
|
+
const kinds = [...reasoning.trim() === "" ? [] : ["think"], ...text.trim() === "" ? [] : ["text"]];
|
|
2626
|
+
const pairing = kinds.length === 0 ? void 0 : options?.streams?.completeStreams(turn, kinds);
|
|
2627
|
+
const sealed = new Set(pairing?.sealed ?? []);
|
|
2628
|
+
const blocks = [];
|
|
2629
|
+
if (reasoning.trim() !== "" && !sealed.has("think")) blocks.push({
|
|
2630
|
+
type: "reasoning",
|
|
2631
|
+
text: reasoning
|
|
2632
|
+
});
|
|
2633
|
+
if (text.trim() !== "" && !sealed.has("text")) blocks.push({
|
|
2634
|
+
type: "text",
|
|
2635
|
+
text
|
|
2636
|
+
});
|
|
2637
|
+
for (const line of toolLines) blocks.push({
|
|
2638
|
+
type: "tool-call",
|
|
2639
|
+
id: line.id,
|
|
2640
|
+
name: line.name,
|
|
2641
|
+
arguments: line.args ?? ""
|
|
2642
|
+
});
|
|
2643
|
+
for (const line of unit) texts.push(kimiLineProgressText(line));
|
|
2644
|
+
if (blocks.length === 0) {
|
|
2645
|
+
carryUsage = usage;
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
for (const line of toolLines) options?.streams?.releaseTool?.(line);
|
|
2649
|
+
let step = pairing?.step ?? nextFoldStep(turn);
|
|
2650
|
+
options?.streams?.finalizeBefore?.(turn, step);
|
|
2651
|
+
let sequential = nextKimiSessionStep(childSession, turn);
|
|
2652
|
+
const reservedAfter = options?.streams?.reservedSteps(turn) ?? [];
|
|
2653
|
+
while (reservedAfter.includes(sequential)) sequential += 1;
|
|
2654
|
+
step = Math.min(step, sequential);
|
|
2655
|
+
markTaken(turn, step);
|
|
2656
|
+
step = nextWrittenKimiStep(childSession, turn);
|
|
2657
|
+
const requestConfig = unit.map((line) => line.kind !== "user" ? line.requestConfig : void 0).find((config) => config !== void 0);
|
|
2658
|
+
if (requestConfig?.model !== void 0) appendRequestHeaderIfChanged(childSession, {
|
|
2659
|
+
provider: "kimi-cli",
|
|
2660
|
+
model: requestConfig.model,
|
|
2661
|
+
...requestConfig.reasoningEffort === void 0 ? {} : { reasoningEffort: requestConfig.reasoningEffort },
|
|
2662
|
+
...requestConfig.maxTokens === void 0 ? {} : { maxTokens: requestConfig.maxTokens }
|
|
2663
|
+
});
|
|
2664
|
+
const model = [...unit].reverse().find((line) => line.model !== void 0)?.model;
|
|
2665
|
+
childSession.append("step/start", {
|
|
2666
|
+
turn,
|
|
2667
|
+
step
|
|
2668
|
+
});
|
|
2669
|
+
childSession.append("assistant/message", {
|
|
2670
|
+
turn,
|
|
2671
|
+
step,
|
|
2672
|
+
message: assistantEvent(blocks, model),
|
|
2673
|
+
stream: [],
|
|
2674
|
+
...usage === void 0 ? {} : { usage }
|
|
2675
|
+
}, { surfaceOp: "append" });
|
|
2676
|
+
for (const line of toolLines) {
|
|
2677
|
+
const call = childSession.append("tool/call", {
|
|
2678
|
+
turn,
|
|
2679
|
+
step,
|
|
2680
|
+
callId: line.id,
|
|
2681
|
+
name: line.name,
|
|
2682
|
+
arguments: line.args ?? ""
|
|
2683
|
+
});
|
|
2684
|
+
childSession.append("tool/result", {
|
|
2685
|
+
turn,
|
|
2686
|
+
step,
|
|
2687
|
+
message: createToolResultMessage({
|
|
2688
|
+
callId: line.id,
|
|
2689
|
+
content: [{
|
|
2690
|
+
type: "text",
|
|
2691
|
+
text: line.result ?? "(工具调用被中断,未能记录返回结果)"
|
|
2692
|
+
}],
|
|
2693
|
+
isError: line.result === void 0
|
|
2694
|
+
})
|
|
2695
|
+
}, {
|
|
2696
|
+
surfaceOp: "append",
|
|
2697
|
+
sourceEventSeqs: [call.seq]
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
childSession.append("step/end", {
|
|
2701
|
+
turn,
|
|
2702
|
+
step
|
|
2703
|
+
});
|
|
2162
2704
|
}
|
|
2163
|
-
|
|
2705
|
+
await persistChildSession(ctx, childSession);
|
|
2706
|
+
const remainder = addTokenUsage(carryUsage, lastContentUnit === -1 ? unclaimedUsage : void 0);
|
|
2707
|
+
return {
|
|
2708
|
+
total: consumedTotal,
|
|
2709
|
+
texts,
|
|
2710
|
+
...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
|
|
2711
|
+
...deltaUsage !== void 0 && remainder === void 0 ? { usageAttached: true } : {},
|
|
2712
|
+
...remainder !== void 0 ? { usageUnattached: remainder } : {},
|
|
2713
|
+
...observedModel !== void 0 ? { model: observedModel } : {},
|
|
2714
|
+
...toolCalls !== void 0 ? { toolCalls } : {}
|
|
2715
|
+
};
|
|
2164
2716
|
}
|
|
2165
2717
|
/**
|
|
2166
|
-
*
|
|
2167
|
-
* tool
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2170
|
-
* the
|
|
2171
|
-
* @param
|
|
2172
|
-
* @returns
|
|
2718
|
+
* Count one round's tool calls out of a kimi transcript, keyed by the name the
|
|
2719
|
+
* wire gave each tool. No turn named, or no `tool.call` line carrying it,
|
|
2720
|
+
* yields undefined — a round reports nothing rather than a zero it did not
|
|
2721
|
+
* observe. Reads only the transcript lines the mirror already folded.
|
|
2722
|
+
* @param lines - the whole session transcript.
|
|
2723
|
+
* @param turn - the round's 1-based turn, when the caller named one.
|
|
2724
|
+
* @returns the round's accounting, or undefined.
|
|
2173
2725
|
*/
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2726
|
+
function roundToolCalls(lines, turn) {
|
|
2727
|
+
if (turn === void 0) return void 0;
|
|
2728
|
+
const byName = {};
|
|
2729
|
+
let count = 0;
|
|
2730
|
+
for (const line of lines) {
|
|
2731
|
+
if (line.kind !== "tool" || line.turn !== turn) continue;
|
|
2732
|
+
count += 1;
|
|
2733
|
+
byName[line.name] = (byName[line.name] ?? 0) + 1;
|
|
2180
2734
|
}
|
|
2181
|
-
return
|
|
2735
|
+
return count === 0 ? void 0 : {
|
|
2736
|
+
count,
|
|
2737
|
+
byName
|
|
2738
|
+
};
|
|
2182
2739
|
}
|
|
2183
|
-
/** Permission rule block letting the kimi subagent run shell commands. */
|
|
2184
|
-
const KIMI_PERMISSION_BLOCK = `
|
|
2185
|
-
[[permission.rules]]
|
|
2186
|
-
decision = "allow"
|
|
2187
|
-
pattern = "Bash(*)"
|
|
2188
|
-
reason = "let the kimi subagent run shell commands"
|
|
2189
|
-
`;
|
|
2190
2740
|
/**
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2193
|
-
*
|
|
2194
|
-
*
|
|
2741
|
+
* Write the fold's request/header when the wire's declared config differs from
|
|
2742
|
+
* the child log's last one (the host logs changed snapshots, not per-call
|
|
2743
|
+
* duplicates): first write is `initial`, a differing one `change`, an
|
|
2744
|
+
* identical one skipped.
|
|
2195
2745
|
*/
|
|
2196
|
-
|
|
2197
|
-
const
|
|
2198
|
-
let
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
}
|
|
2205
|
-
if (content.includes("pattern = \"Bash(*)")) return;
|
|
2206
|
-
await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
|
|
2746
|
+
function appendRequestHeaderIfChanged(childSession, config) {
|
|
2747
|
+
const serialized = JSON.stringify(config);
|
|
2748
|
+
let last;
|
|
2749
|
+
for (const event of childSession.snapshotEvents()) if (event.type === "request/header") last = JSON.stringify(event.data.header?.config);
|
|
2750
|
+
if (last === serialized) return;
|
|
2751
|
+
childSession.append("request/header", {
|
|
2752
|
+
header: { config },
|
|
2753
|
+
reason: last === void 0 ? "initial" : "change"
|
|
2754
|
+
});
|
|
2207
2755
|
}
|
|
2208
2756
|
/**
|
|
2209
|
-
*
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
* scoped files are the logout path.
|
|
2213
|
-
* @param homeDir - the `kimi` harness's scoped home.
|
|
2757
|
+
* Whether the turn already carries a user/message with this exact text (the
|
|
2758
|
+
* live driver's round-start append). Scoped to the events at or after this
|
|
2759
|
+
* turn's turn/start so identical prompts across rounds stay independent.
|
|
2214
2760
|
*/
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
}
|
|
2761
|
+
function userAlreadyAppended(childSession, turn, text) {
|
|
2762
|
+
let turnStartSeq = -1;
|
|
2763
|
+
for (const event of childSession.snapshotEvents()) if (event.type === "turn/start" && event.data.turn === turn) turnStartSeq = event.seq;
|
|
2764
|
+
if (turnStartSeq < 0) return false;
|
|
2765
|
+
for (const event of childSession.snapshotEvents()) {
|
|
2766
|
+
if (event.seq < turnStartSeq || event.type !== "user/message") continue;
|
|
2767
|
+
if ((event.data.content ?? []).map((block) => block.text ?? "").join("") === text) return true;
|
|
2768
|
+
}
|
|
2769
|
+
return false;
|
|
2223
2770
|
}
|
|
2224
2771
|
//#endregion
|
|
2225
2772
|
//#region lib/types/live-driver.js
|
|
@@ -2241,11 +2788,14 @@ async function kimiLogout(homeDir) {
|
|
|
2241
2788
|
* throttled `mirrorKimiDelta` passes and the settle pass stays authoritative
|
|
2242
2789
|
* (one fold, one offset, zero divergence between the two drivers). Every
|
|
2243
2790
|
* transcript item folds 1:1 into the child session log.
|
|
2244
|
-
* Live output
|
|
2245
|
-
*
|
|
2246
|
-
*
|
|
2247
|
-
*
|
|
2248
|
-
*
|
|
2791
|
+
* Live output rides the `local-agent/stream` channel: chunk deltas
|
|
2792
|
+
* accumulate per streaming item and publish throttled updates at the item's
|
|
2793
|
+
* reserved (turn, step) — the member live node renders them as-is — while
|
|
2794
|
+
* the item's durable content lands as ONE atomic step/start→…→step/end
|
|
2795
|
+
* triple at that step, written by the completing wire line's fold or at
|
|
2796
|
+
* settle. Durable steps stay dense and strictly sequential (the released-log
|
|
2797
|
+
* reader validates exactly that on every cold open); snapshots never hold a
|
|
2798
|
+
* step open across appends.
|
|
2249
2799
|
*
|
|
2250
2800
|
* Lifecycle mirrors the dsh/codex live drivers' discipline: lazy spawn with an
|
|
2251
2801
|
* initialize handshake (loadSession capability required — crash recovery is
|
|
@@ -2719,6 +3269,8 @@ var KimiAcpLiveDriver = class {
|
|
|
2719
3269
|
this.ctx.logger.warn(`local-agent-kimi: pinning default_model for the resident runtime failed: ${thrown$1(error).message}`);
|
|
2720
3270
|
return false;
|
|
2721
3271
|
});
|
|
3272
|
+
const endpoint = this.config.endpoint === void 0 ? void 0 : await this.config.endpoint(spec.homeDir).catch(() => void 0);
|
|
3273
|
+
await ensureKimiEndpointProvider(spec.homeDir, hasEndpointCredential(endpoint) ? endpoint.baseUrl ?? "https://api.kimi.com/coding/v1" : void 0).catch(() => false);
|
|
2722
3274
|
const spawnSpec = {
|
|
2723
3275
|
argv: [
|
|
2724
3276
|
"kimi",
|
|
@@ -2732,7 +3284,10 @@ var KimiAcpLiveDriver = class {
|
|
|
2732
3284
|
stderr: "pipe"
|
|
2733
3285
|
},
|
|
2734
3286
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
2735
|
-
env: delegationEnv({
|
|
3287
|
+
env: delegationEnv({
|
|
3288
|
+
KIMI_CODE_HOME: spec.homeDir,
|
|
3289
|
+
...endpointAuthEnv(endpoint)
|
|
3290
|
+
})
|
|
2736
3291
|
};
|
|
2737
3292
|
let child;
|
|
2738
3293
|
try {
|
|
@@ -2800,11 +3355,8 @@ var KimiAcpLiveDriver = class {
|
|
|
2800
3355
|
* never exits: the match runs against the settled error instead). Degrades
|
|
2801
3356
|
* silently on a core predating reportAuthFailure.
|
|
2802
3357
|
*/
|
|
2803
|
-
reportAuthIfShaped(error
|
|
3358
|
+
reportAuthIfShaped(error) {
|
|
2804
3359
|
if (!KIMI_LIVE_AUTH_FAILURE.test(error.message)) return;
|
|
2805
|
-
if (homeDir !== void 0) guardKimiCredential(homeDir, (message) => {
|
|
2806
|
-
this.ctx.logger.warn(message);
|
|
2807
|
-
});
|
|
2808
3360
|
const registry = this.ctx.localAgent;
|
|
2809
3361
|
if (typeof registry.reportAuthFailure === "function") registry.reportAuthFailure("kimi", error.message.split("\n").find((line) => line.trim() !== "") ?? error.message);
|
|
2810
3362
|
}
|
|
@@ -2928,25 +3480,17 @@ var KimiAcpLiveDriver = class {
|
|
|
2928
3480
|
/** The round's accumulated assistant text (the run output — chunks are the only source). */
|
|
2929
3481
|
let roundText = "";
|
|
2930
3482
|
/**
|
|
2931
|
-
* The step ledger:
|
|
2932
|
-
*
|
|
2933
|
-
*
|
|
2934
|
-
*
|
|
2935
|
-
*
|
|
2936
|
-
*
|
|
3483
|
+
* The step ledger: the steps in-flight reservations currently hold this
|
|
3484
|
+
* round. A reservation's durable content lands at the fold that consumes
|
|
3485
|
+
* it (possibly carrying several reservations' kinds in one message — the
|
|
3486
|
+
* wire step's fold), which RELEASES the other consumed coordinates for
|
|
3487
|
+
* later folds; only a reservation kimi never completes (a plan stream, an
|
|
3488
|
+
* unwired tool notification) stays held until its settle-time finalize.
|
|
3489
|
+
* The file fold skips held steps for its sequential folds, so a tool card
|
|
3490
|
+
* folded mid-stream lands ABOVE the stream's step and the projection
|
|
3491
|
+
* renders the round's chronological order.
|
|
2937
3492
|
*/
|
|
2938
3493
|
const reservedSteps = [];
|
|
2939
|
-
/**
|
|
2940
|
-
* The streaming items seen this round . kimi's ACP
|
|
2941
|
-
* chunks carry no item id, so the stream key is synthetic per kind per
|
|
2942
|
-
* round (the codex live driver's fallback shape): thought chunks share
|
|
2943
|
-
* one stream, message chunks another. Deltas accumulate into throttled
|
|
2944
|
-
* snapshot assistant/messages appended at the stream's reserved
|
|
2945
|
-
* (turn, step) — the host folds repeated settles at one coordinate into
|
|
2946
|
-
* one live-updating chat node. The wire line that completes the stream's
|
|
2947
|
-
* kind folds at the same step and finalizes it; an entry whose completion
|
|
2948
|
-
* never folds is force-finalized at settle.
|
|
2949
|
-
*/
|
|
2950
3494
|
const streams = /* @__PURE__ */ new Map();
|
|
2951
3495
|
/** The stream the latest delta accumulated into (a kind switch force-snapshots it). */
|
|
2952
3496
|
let activeStream;
|
|
@@ -2974,7 +3518,23 @@ var KimiAcpLiveDriver = class {
|
|
|
2974
3518
|
};
|
|
2975
3519
|
/** A stream's synthetic key: kimi's ACP deltas carry no item id, so kind + turn pairs them. */
|
|
2976
3520
|
let generation = 0;
|
|
3521
|
+
/**
|
|
3522
|
+
* One step reservation per generation, shared by the think and text
|
|
3523
|
+
* streams of the same response segment: the host's own live shape is
|
|
3524
|
+
* reasoning + text on ONE step, and a per-kind step would leave the
|
|
3525
|
+
* consumed reservation's stream frames pointing at a step that never
|
|
3526
|
+
* materializes (an empty trailing step breaks the host's answer
|
|
3527
|
+
* detection, folding the reply into the collapsed process block).
|
|
3528
|
+
*/
|
|
3529
|
+
const generationSteps = /* @__PURE__ */ new Map();
|
|
2977
3530
|
const seenToolCalls = /* @__PURE__ */ new Set();
|
|
3531
|
+
/**
|
|
3532
|
+
* ACP tool_call notifications' chronological reservations (call id →
|
|
3533
|
+
* step, plus the notification's display title). No durable event lands at
|
|
3534
|
+
* notification time: the native lifecycle (advertisement, call, result in
|
|
3535
|
+
* one closed step) is written by the wire line's fold, or at settle with
|
|
3536
|
+
* an interrupted placeholder when kimi never logged the call.
|
|
3537
|
+
*/
|
|
2978
3538
|
const toolSteps = /* @__PURE__ */ new Map();
|
|
2979
3539
|
const streamKey = (kind) => `kimi-stream-${kind}-${turn}-${generation}`;
|
|
2980
3540
|
/** The plan stream's key: a snapshot stream beside the think/text delta streams. */
|
|
@@ -2992,7 +3552,7 @@ var KimiAcpLiveDriver = class {
|
|
|
2992
3552
|
const stream = streams.get(key);
|
|
2993
3553
|
if (stream !== void 0) {
|
|
2994
3554
|
stream.text += text;
|
|
2995
|
-
|
|
3555
|
+
publishStreamSnapshot(stream, false);
|
|
2996
3556
|
}
|
|
2997
3557
|
localAgent?.reportRunProgress(childSession.id, {
|
|
2998
3558
|
kind: "delta",
|
|
@@ -3018,135 +3578,283 @@ var KimiAcpLiveDriver = class {
|
|
|
3018
3578
|
const stream = streams.get(planKey);
|
|
3019
3579
|
if (stream !== void 0 && stream.text !== plan) {
|
|
3020
3580
|
stream.text = plan;
|
|
3021
|
-
|
|
3581
|
+
publishStreamSnapshot(stream, true);
|
|
3022
3582
|
}
|
|
3023
3583
|
};
|
|
3024
3584
|
/**
|
|
3025
|
-
*
|
|
3026
|
-
*
|
|
3027
|
-
*
|
|
3028
|
-
* legitimately) and, when `withUsage`, the round's uncarried usage. An
|
|
3029
|
-
* empty stream leaves no boundary at all.
|
|
3585
|
+
* The live-output channel. Snapshots are browser-only previews (never
|
|
3586
|
+
* `local-agent/stream` log events) — durable step content lands once per
|
|
3587
|
+
* stream, at finalization, as the fold's atomic triple.
|
|
3030
3588
|
*/
|
|
3031
3589
|
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)}`));
|
|
3032
3590
|
const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
3033
3591
|
const pendingStreamArrival = /* @__PURE__ */ new Map();
|
|
3034
|
-
|
|
3035
|
-
|
|
3592
|
+
/**
|
|
3593
|
+
* Publish one snapshot of a streaming item at its reserved (turn, step)
|
|
3594
|
+
* through the live channel. Batched per stream on a bounded deadline
|
|
3595
|
+
* unless `force`. Never writes durable content.
|
|
3596
|
+
*/
|
|
3597
|
+
const publishStreamSnapshot = (stream, force) => {
|
|
3598
|
+
if (stream.text.trim() === "" || stream.sealed === true) return;
|
|
3036
3599
|
const now = Date.now();
|
|
3037
3600
|
if (!force) {
|
|
3038
3601
|
if (!pendingStreamArrival.has(stream.step)) pendingStreamArrival.set(stream.step, now);
|
|
3039
|
-
liveFlush.schedule(stream.step, () =>
|
|
3602
|
+
liveFlush.schedule(stream.step, () => publishStreamSnapshot(stream, true));
|
|
3040
3603
|
return;
|
|
3041
3604
|
}
|
|
3042
3605
|
liveFlush.cancel(stream.step);
|
|
3043
3606
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
3044
3607
|
pendingStreamArrival.delete(stream.step);
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3608
|
+
streamPublisher?.preview(stream.step, stream.kind, stream.text, {
|
|
3609
|
+
receivedAt,
|
|
3610
|
+
itemId: stream.kind
|
|
3611
|
+
});
|
|
3612
|
+
stream.lastSnapshotLen = stream.text.length;
|
|
3613
|
+
};
|
|
3614
|
+
/**
|
|
3615
|
+
* Write the stream's durable content as ONE atomic
|
|
3616
|
+
* step/start→assistant/message→step/end triple at its reserved step and
|
|
3617
|
+
* seal the reservation (its completing wire line, when one exists, pairs
|
|
3618
|
+
* and drops). `interrupted` marks a non-completed round's partial (a
|
|
3619
|
+
* cancelled turn reads 已停止 legitimately); `withUsage` attaches the
|
|
3620
|
+
* round's uncarried usage.
|
|
3621
|
+
*/
|
|
3622
|
+
/**
|
|
3623
|
+
* Write the durable content of every stream sharing one step as ONE atomic
|
|
3624
|
+
* step/start→assistant/message→step/end triple (a generation's think and
|
|
3625
|
+
* text merge into a single message — the host's own shape) and seal the
|
|
3626
|
+
* reservations. `interrupted` marks a non-completed round's partial (a
|
|
3627
|
+
* cancelled turn reads 已停止 legitimately); `withUsage` attaches the
|
|
3628
|
+
* round's uncarried usage.
|
|
3629
|
+
*/
|
|
3630
|
+
const finalizeStep = (step, entries, interrupted, withUsage) => {
|
|
3631
|
+
liveFlush.cancel(step);
|
|
3632
|
+
pendingStreamArrival.delete(step);
|
|
3633
|
+
const reasoning = entries.filter(([, stream]) => stream.kind === "think").map(([, stream]) => stream.text).join("");
|
|
3634
|
+
const text = entries.filter(([, stream]) => stream.kind === "text").map(([, stream]) => stream.text).join("");
|
|
3635
|
+
const blocks = [];
|
|
3636
|
+
if (reasoning.trim() !== "") blocks.push({
|
|
3637
|
+
type: "reasoning",
|
|
3638
|
+
text: reasoning
|
|
3639
|
+
});
|
|
3640
|
+
if (text.trim() !== "") blocks.push({
|
|
3641
|
+
type: "text",
|
|
3642
|
+
text
|
|
3643
|
+
});
|
|
3644
|
+
if (blocks.length === 0) {
|
|
3645
|
+
streamPublisher?.finish(step);
|
|
3646
|
+
const keys = new Set(entries.map(([key]) => key));
|
|
3647
|
+
if (!new Set([...streams.entries()].filter(([key, stream]) => !keys.has(key) && stream.sealed !== true).map(([, stream]) => stream.step)).has(step)) {
|
|
3648
|
+
const index = reservedSteps.indexOf(step);
|
|
3649
|
+
if (index >= 0) reservedSteps.splice(index, 1);
|
|
3650
|
+
}
|
|
3651
|
+
for (const [key, stream] of entries) {
|
|
3652
|
+
stream.sealed = true;
|
|
3653
|
+
generationSteps.delete(stream.generation);
|
|
3654
|
+
if (activeStream === key) activeStream = void 0;
|
|
3052
3655
|
}
|
|
3053
|
-
streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
|
|
3054
|
-
stream.lastSnapshotAt = now;
|
|
3055
|
-
stream.lastSnapshotLen = stream.text.length;
|
|
3056
3656
|
return;
|
|
3057
3657
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
stream.opened = true;
|
|
3064
|
-
}
|
|
3658
|
+
const durable = nextWrittenKimiStep(childSession, turn);
|
|
3659
|
+
childSession.append("step/start", {
|
|
3660
|
+
turn,
|
|
3661
|
+
step: durable
|
|
3662
|
+
});
|
|
3065
3663
|
childSession.append("assistant/message", {
|
|
3066
3664
|
turn,
|
|
3067
|
-
step:
|
|
3068
|
-
message: assistantEvent(
|
|
3069
|
-
type: "reasoning",
|
|
3070
|
-
text: stream.text
|
|
3071
|
-
} : {
|
|
3072
|
-
type: "text",
|
|
3073
|
-
text: stream.text
|
|
3074
|
-
}], runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
3665
|
+
step: durable,
|
|
3666
|
+
message: assistantEvent(blocks, runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
3075
3667
|
stream: [],
|
|
3076
3668
|
...withUsage && roundUsage !== void 0 ? { usage: roundUsage } : {},
|
|
3077
3669
|
...interrupted ? { interrupted: true } : {}
|
|
3078
3670
|
}, { surfaceOp: "append" });
|
|
3079
3671
|
if (withUsage) roundUsage = void 0;
|
|
3080
|
-
|
|
3081
|
-
|
|
3672
|
+
streamPublisher?.finish(step);
|
|
3673
|
+
childSession.append("step/end", {
|
|
3674
|
+
turn,
|
|
3675
|
+
step: durable
|
|
3676
|
+
});
|
|
3677
|
+
for (const [key, stream] of entries) {
|
|
3678
|
+
stream.sealed = true;
|
|
3679
|
+
generationSteps.delete(stream.generation);
|
|
3680
|
+
if (activeStream === key) activeStream = void 0;
|
|
3681
|
+
}
|
|
3082
3682
|
persist();
|
|
3083
3683
|
};
|
|
3084
3684
|
/**
|
|
3685
|
+
* Write the durable step for an ACP tool call kimi never logged to the
|
|
3686
|
+
* wire (a crash between the notification and the wire flush): the
|
|
3687
|
+
* notification's title stands in for the name and the result is the
|
|
3688
|
+
* shared interrupted placeholder — the native lifecycle in one step.
|
|
3689
|
+
*/
|
|
3690
|
+
/** Whether this turn's log already carries the tool call (folded from the wire). */
|
|
3691
|
+
const toolLogged = (id) => childSession.snapshotEvents().some((event) => event.type === "tool/call" && event.data.turn === turn && event.data.callId === id);
|
|
3692
|
+
const finalizeOrphanTool = (id, entry) => {
|
|
3693
|
+
const name = entry.title ?? "tool";
|
|
3694
|
+
const step = nextWrittenKimiStep(childSession, turn);
|
|
3695
|
+
childSession.append("step/start", {
|
|
3696
|
+
turn,
|
|
3697
|
+
step
|
|
3698
|
+
});
|
|
3699
|
+
childSession.append("assistant/message", {
|
|
3700
|
+
turn,
|
|
3701
|
+
step,
|
|
3702
|
+
message: assistantEvent([{
|
|
3703
|
+
type: "tool-call",
|
|
3704
|
+
id,
|
|
3705
|
+
name,
|
|
3706
|
+
arguments: ""
|
|
3707
|
+
}], runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
3708
|
+
stream: []
|
|
3709
|
+
}, { surfaceOp: "append" });
|
|
3710
|
+
const call = childSession.append("tool/call", {
|
|
3711
|
+
turn,
|
|
3712
|
+
step,
|
|
3713
|
+
callId: id,
|
|
3714
|
+
name,
|
|
3715
|
+
arguments: ""
|
|
3716
|
+
});
|
|
3717
|
+
childSession.append("tool/result", {
|
|
3718
|
+
turn,
|
|
3719
|
+
step,
|
|
3720
|
+
message: createToolResultMessage({
|
|
3721
|
+
callId: id,
|
|
3722
|
+
content: [{
|
|
3723
|
+
type: "text",
|
|
3724
|
+
text: KIMI_INTERRUPTED_TOOL_RESULT
|
|
3725
|
+
}],
|
|
3726
|
+
isError: true
|
|
3727
|
+
})
|
|
3728
|
+
}, {
|
|
3729
|
+
surfaceOp: "append",
|
|
3730
|
+
sourceEventSeqs: [call.seq]
|
|
3731
|
+
});
|
|
3732
|
+
childSession.append("step/end", {
|
|
3733
|
+
turn,
|
|
3734
|
+
step
|
|
3735
|
+
});
|
|
3736
|
+
};
|
|
3737
|
+
/**
|
|
3738
|
+
* The fold's gap fill: a fold about to write at `step` first finalizes
|
|
3739
|
+
* every live stream reserved below it (ascending), so the stored log's
|
|
3740
|
+
* steps land dense and strictly sequential. Only streams can sit in the
|
|
3741
|
+
* gap: an unresulted tool line holds its fold instead (session-mirror).
|
|
3742
|
+
*/
|
|
3743
|
+
const finalizeBefore = (targetTurn, step) => {
|
|
3744
|
+
if (targetTurn !== turn) return;
|
|
3745
|
+
const pending = [...streams.entries()].filter(([, stream]) => stream.sealed !== true && stream.step < step).sort((a, b) => a[1].step - b[1].step);
|
|
3746
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
3747
|
+
for (const entry of pending) {
|
|
3748
|
+
const group = byStep.get(entry[1].step) ?? [];
|
|
3749
|
+
group.push(entry);
|
|
3750
|
+
byStep.set(entry[1].step, group);
|
|
3751
|
+
}
|
|
3752
|
+
for (const [at, entries] of byStep) finalizeStep(at, entries, false, false);
|
|
3753
|
+
};
|
|
3754
|
+
/**
|
|
3085
3755
|
* Reserve the step one streaming item will occupy — past every folded
|
|
3086
3756
|
* line (the session's own event ledger: the file fold writes events
|
|
3087
|
-
* synchronously) and every earlier reservation. A kind switch
|
|
3088
|
-
* freshness snapshot of the current stream (skipped when its text has
|
|
3089
|
-
* grown since the last one); its
|
|
3090
|
-
* reserved step when the wire line
|
|
3757
|
+
* synchronously) and every earlier reservation. A kind switch publishes
|
|
3758
|
+
* one freshness snapshot of the current stream (skipped when its text has
|
|
3759
|
+
* not grown since the last one); its durable triple still lands at its
|
|
3760
|
+
* own reserved step when the wire line folds or the round settles.
|
|
3091
3761
|
*/
|
|
3092
3762
|
const reserveStream = (key, kind) => {
|
|
3093
3763
|
if (activeStream !== void 0 && activeStream !== key) {
|
|
3094
3764
|
const previous = streams.get(activeStream);
|
|
3095
|
-
if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen)
|
|
3765
|
+
if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen) publishStreamSnapshot(previous, true);
|
|
3096
3766
|
activeStream = void 0;
|
|
3097
3767
|
}
|
|
3098
3768
|
let stream = streams.get(key);
|
|
3769
|
+
if (stream?.sealed === true) {
|
|
3770
|
+
streams.delete(key);
|
|
3771
|
+
stream = void 0;
|
|
3772
|
+
}
|
|
3099
3773
|
if (stream === void 0) {
|
|
3100
|
-
let step =
|
|
3101
|
-
|
|
3102
|
-
|
|
3774
|
+
let step = key === planKey ? void 0 : generationSteps.get(generation);
|
|
3775
|
+
if (step === void 0) {
|
|
3776
|
+
step = nextKimiSessionStep(childSession, turn);
|
|
3777
|
+
while (reservedSteps.includes(step)) step += 1;
|
|
3778
|
+
reservedSteps.push(step);
|
|
3779
|
+
if (key !== planKey) generationSteps.set(generation, step);
|
|
3780
|
+
}
|
|
3103
3781
|
stream = {
|
|
3104
3782
|
kind,
|
|
3105
3783
|
step,
|
|
3784
|
+
generation: key === planKey ? -1 : generation,
|
|
3106
3785
|
text: "",
|
|
3107
|
-
|
|
3108
|
-
lastSnapshotLen: 0,
|
|
3109
|
-
opened: false
|
|
3786
|
+
lastSnapshotLen: 0
|
|
3110
3787
|
};
|
|
3111
3788
|
streams.set(key, stream);
|
|
3112
3789
|
}
|
|
3113
3790
|
activeStream = key;
|
|
3114
3791
|
};
|
|
3115
3792
|
/**
|
|
3116
|
-
* The fold-pass coordination surface
|
|
3117
|
-
* fold skips reserved steps for sequential folds
|
|
3118
|
-
*
|
|
3119
|
-
*
|
|
3793
|
+
* The fold-pass coordination surface: the file
|
|
3794
|
+
* fold skips reserved steps for sequential folds, pairs one fold with the
|
|
3795
|
+
* round's in-flight streams of the fold's kinds (consuming both — a wire
|
|
3796
|
+
* step's think and text ride ONE durable step), and closes every
|
|
3797
|
+
* reservation below each fold step first (finalizeBefore) — scoped to
|
|
3798
|
+
* THIS round's turn, so an earlier round's leftover lines never consume
|
|
3799
|
+
* this round's stream.
|
|
3120
3800
|
*/
|
|
3121
3801
|
const mirrorStreams = {
|
|
3122
3802
|
reservedSteps: (t) => t === turn ? reservedSteps : [],
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3803
|
+
finalizeBefore,
|
|
3804
|
+
releaseTool: (line) => {
|
|
3805
|
+
if (line.turn !== turn) return;
|
|
3806
|
+
const entry = toolSteps.get(line.id);
|
|
3807
|
+
if (entry === void 0) return;
|
|
3127
3808
|
toolSteps.delete(line.id);
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
opened: true
|
|
3131
|
-
};
|
|
3809
|
+
const index = reservedSteps.indexOf(entry.step);
|
|
3810
|
+
if (index >= 0) reservedSteps.splice(index, 1);
|
|
3132
3811
|
},
|
|
3133
|
-
|
|
3134
|
-
if (
|
|
3135
|
-
const
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3812
|
+
completeStreams: (t, kinds) => {
|
|
3813
|
+
if (t !== turn) return void 0;
|
|
3814
|
+
const picked = [];
|
|
3815
|
+
for (const kind of kinds) {
|
|
3816
|
+
const entry = [...streams.entries()].find(([key, stream]) => key !== planKey && stream.kind === kind);
|
|
3817
|
+
if (entry !== void 0) picked.push(entry);
|
|
3818
|
+
}
|
|
3819
|
+
if (picked.length === 0) return void 0;
|
|
3820
|
+
const sealed = [];
|
|
3821
|
+
for (const [key, stream] of picked) {
|
|
3822
|
+
streams.delete(key);
|
|
3823
|
+
generationSteps.delete(stream.generation);
|
|
3824
|
+
if (activeStream === key) activeStream = void 0;
|
|
3825
|
+
if (stream.sealed === true) {
|
|
3826
|
+
sealed.push(stream.kind);
|
|
3827
|
+
continue;
|
|
3828
|
+
}
|
|
3829
|
+
liveFlush.cancel(stream.step);
|
|
3830
|
+
}
|
|
3831
|
+
const open = picked.filter(([, stream]) => stream.sealed !== true);
|
|
3832
|
+
if (open.length === 0) return { sealed };
|
|
3833
|
+
const held = new Set([...streams.values()].filter((stream) => stream.sealed !== true).map((stream) => stream.step));
|
|
3834
|
+
for (const [, stream] of open) {
|
|
3835
|
+
if (held.has(stream.step)) continue;
|
|
3836
|
+
const index = reservedSteps.indexOf(stream.step);
|
|
3837
|
+
if (index >= 0) reservedSteps.splice(index, 1);
|
|
3838
|
+
}
|
|
3839
|
+
for (const step of new Set(open.map(([, stream]) => stream.step))) streamPublisher?.finish(step);
|
|
3840
|
+
const written = /* @__PURE__ */ new Set();
|
|
3841
|
+
for (const event of childSession.snapshotEvents()) {
|
|
3842
|
+
const data = event.data;
|
|
3843
|
+
if (event.type === "step/start" && data.turn === turn && typeof data.step === "number") written.add(data.step);
|
|
3844
|
+
}
|
|
3845
|
+
let free = nextKimiSessionStep(childSession, turn);
|
|
3846
|
+
while (reservedSteps.includes(free)) free += 1;
|
|
3847
|
+
const candidates = [...new Set(open.map(([, stream]) => stream.step))].filter((step) => !written.has(step));
|
|
3143
3848
|
return {
|
|
3144
|
-
step:
|
|
3145
|
-
|
|
3849
|
+
step: Math.min(free, ...candidates),
|
|
3850
|
+
sealed
|
|
3146
3851
|
};
|
|
3147
3852
|
}
|
|
3148
3853
|
};
|
|
3149
|
-
const mirrorOptions = () => ({
|
|
3854
|
+
const mirrorOptions = (settle = false) => ({
|
|
3855
|
+
streams: mirrorStreams,
|
|
3856
|
+
...settle ? { settle: true } : {}
|
|
3857
|
+
});
|
|
3150
3858
|
/**
|
|
3151
3859
|
* Observation accounting across mirror passes: a pass whose window
|
|
3152
3860
|
* attached its usage to a folded message is already carried; anything else
|
|
@@ -3155,7 +3863,7 @@ var KimiAcpLiveDriver = class {
|
|
|
3155
3863
|
* the latest.
|
|
3156
3864
|
*/
|
|
3157
3865
|
const noteMirrorUsage = (delta) => {
|
|
3158
|
-
if (delta.
|
|
3866
|
+
if (delta.usageUnattached !== void 0) roundUsage = addTokenUsage(roundUsage, delta.usageUnattached);
|
|
3159
3867
|
if (delta.model !== void 0) roundModel = delta.model;
|
|
3160
3868
|
};
|
|
3161
3869
|
/** Throttled push-triggered mirror pass; the file fold stays the only transcript source. */
|
|
@@ -3207,23 +3915,22 @@ var KimiAcpLiveDriver = class {
|
|
|
3207
3915
|
const stream = streams.get(key);
|
|
3208
3916
|
if (stream !== void 0) {
|
|
3209
3917
|
stream.text += text;
|
|
3210
|
-
|
|
3918
|
+
publishStreamSnapshot(stream, false);
|
|
3211
3919
|
}
|
|
3212
3920
|
}
|
|
3213
3921
|
} else if (kind === "tool_call") {
|
|
3214
3922
|
const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0;
|
|
3215
3923
|
if (id === void 0 || !seenToolCalls.has(id)) {
|
|
3216
|
-
if (id !== void 0)
|
|
3924
|
+
if (id !== void 0 && toolLogged(id)) seenToolCalls.add(id);
|
|
3925
|
+
else if (id !== void 0) {
|
|
3217
3926
|
seenToolCalls.add(id);
|
|
3218
3927
|
let step = nextKimiSessionStep(childSession, turn);
|
|
3219
|
-
|
|
3928
|
+
while (reservedSteps.includes(step)) step += 1;
|
|
3220
3929
|
reservedSteps.push(step);
|
|
3221
|
-
toolSteps.set(id,
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
step
|
|
3930
|
+
toolSteps.set(id, {
|
|
3931
|
+
step,
|
|
3932
|
+
...typeof update["title"] === "string" ? { title: update["title"] } : {}
|
|
3225
3933
|
});
|
|
3226
|
-
persist();
|
|
3227
3934
|
}
|
|
3228
3935
|
generation++;
|
|
3229
3936
|
}
|
|
@@ -3293,7 +4000,7 @@ var KimiAcpLiveDriver = class {
|
|
|
3293
4000
|
let stableReads = 0;
|
|
3294
4001
|
const deadline = Date.now() + SETTLE_MIRROR_QUIESCE_MS;
|
|
3295
4002
|
for (;;) {
|
|
3296
|
-
const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions());
|
|
4003
|
+
const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions(true));
|
|
3297
4004
|
noteMirrorUsage(delta);
|
|
3298
4005
|
localAgent?.reportRunProgress(childSession.id, {
|
|
3299
4006
|
kind: "mirror",
|
|
@@ -3314,7 +4021,7 @@ var KimiAcpLiveDriver = class {
|
|
|
3314
4021
|
const diagnostics = runtime?.diagnostics ?? "";
|
|
3315
4022
|
const suffix = diagnostics === "" ? "" : `; ${diagnostics}`;
|
|
3316
4023
|
this.ctx.logger.warn(`subagent-kimi: live round failed (${stopReason}): ${error.message}${suffix}`);
|
|
3317
|
-
this.reportAuthIfShaped(error
|
|
4024
|
+
this.reportAuthIfShaped(error);
|
|
3318
4025
|
},
|
|
3319
4026
|
signal: request.signal,
|
|
3320
4027
|
onAbort
|
|
@@ -3328,25 +4035,28 @@ var KimiAcpLiveDriver = class {
|
|
|
3328
4035
|
} catch (error) {
|
|
3329
4036
|
this.ctx.logger.warn(`subagent-kimi: live settle mirror failed: ${thrown$1(error).message}`);
|
|
3330
4037
|
}
|
|
3331
|
-
|
|
3332
|
-
const
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
4038
|
+
const pending = [...[...[...streams.entries()].filter(([, stream]) => stream.sealed !== true).reduce((byStep, entry) => {
|
|
4039
|
+
const group = byStep.get(entry[1].step) ?? [];
|
|
4040
|
+
group.push(entry);
|
|
4041
|
+
byStep.set(entry[1].step, group);
|
|
4042
|
+
return byStep;
|
|
4043
|
+
}, /* @__PURE__ */ new Map()).entries()].map(([step, entries]) => ({
|
|
4044
|
+
step,
|
|
4045
|
+
entries
|
|
4046
|
+
})), ...[...toolSteps.entries()].map(([toolId, tool]) => ({
|
|
4047
|
+
step: tool.step,
|
|
4048
|
+
toolId,
|
|
4049
|
+
tool
|
|
4050
|
+
}))].sort((a, b) => a.step - b.step);
|
|
4051
|
+
const lastStream = pending.map((entry, index) => "entries" in entry ? index : -1).filter((index) => index >= 0).at(-1);
|
|
4052
|
+
pending.forEach((entry, index) => {
|
|
4053
|
+
if ("entries" in entry) finalizeStep(entry.step, entry.entries, settled.stopReason !== "completed", index === lastStream);
|
|
4054
|
+
else if (!toolLogged(entry.toolId)) finalizeOrphanTool(entry.toolId, entry.tool);
|
|
3348
4055
|
});
|
|
4056
|
+
streams.clear();
|
|
3349
4057
|
toolSteps.clear();
|
|
4058
|
+
activeStream = void 0;
|
|
4059
|
+
if (pending.length > 0) persist();
|
|
3350
4060
|
if (settled.stopReason === "completed") childSession.append("turn/end", {
|
|
3351
4061
|
turn,
|
|
3352
4062
|
reason: { kind: "completed" }
|
|
@@ -3386,7 +4096,7 @@ var KimiAcpLiveDriver = class {
|
|
|
3386
4096
|
});
|
|
3387
4097
|
await Promise.race([accepted, abortBranch.catch(() => void 0)]);
|
|
3388
4098
|
return subprocessRunHandle({
|
|
3389
|
-
id:
|
|
4099
|
+
id: SessionId(randomUUID()),
|
|
3390
4100
|
result,
|
|
3391
4101
|
signal: request.signal,
|
|
3392
4102
|
onAbort,
|
|
@@ -3551,6 +4261,7 @@ var KimiCliProvider = class {
|
|
|
3551
4261
|
live;
|
|
3552
4262
|
model;
|
|
3553
4263
|
memberModels;
|
|
4264
|
+
endpoint;
|
|
3554
4265
|
name = "kimi-cli";
|
|
3555
4266
|
capabilities = NO_START_CAPABILITIES;
|
|
3556
4267
|
inheritsParentContext = false;
|
|
@@ -3561,19 +4272,33 @@ var KimiCliProvider = class {
|
|
|
3561
4272
|
* while a retiring generation still hosts it).
|
|
3562
4273
|
* @param model - resolver for the configured model, read PER ROUND so a
|
|
3563
4274
|
* settings-card write takes effect on the next delegation without a
|
|
3564
|
-
* reload
|
|
3565
|
-
*
|
|
3566
|
-
*
|
|
4275
|
+
* reload; it receives the round's scope (a named scope's curated list
|
|
4276
|
+
* answers first). Undefined (the resolver absent, or returning
|
|
4277
|
+
* undefined) leaves the argv exactly as it was before the key rode every
|
|
4278
|
+
* round — the scoped config.toml's `default_model` then decides, as it
|
|
4279
|
+
* always did.
|
|
3567
4280
|
* @param memberModels - the broker's member-model ledger: the provider
|
|
3568
4281
|
* records each delegation's start model here (the live driver's spawn
|
|
3569
4282
|
* resolver binds it) and reads the session-level override as the exec
|
|
3570
4283
|
* path's top layer. Absent (older wiring), both drop out.
|
|
3571
4284
|
*/
|
|
3572
|
-
constructor(ctx, live, model, memberModels) {
|
|
4285
|
+
constructor(ctx, live, model, memberModels, endpoint) {
|
|
3573
4286
|
this.ctx = ctx;
|
|
3574
4287
|
this.live = live;
|
|
3575
4288
|
this.model = model;
|
|
3576
4289
|
this.memberModels = memberModels;
|
|
4290
|
+
this.endpoint = endpoint;
|
|
4291
|
+
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Resolve the scope's endpoint for this round and land its baseUrl on the
|
|
4294
|
+
* managed provider (the surgery writes only on drift), so a hand-edited
|
|
4295
|
+
* endpoint-auth.json applies without a restart.
|
|
4296
|
+
*/
|
|
4297
|
+
async prepareEndpoint(homeDir) {
|
|
4298
|
+
if (this.endpoint === void 0) return void 0;
|
|
4299
|
+
const endpoint = await this.endpoint(homeDir).catch(() => void 0);
|
|
4300
|
+
await ensureKimiEndpointProvider(homeDir, hasEndpointCredential(endpoint) ? endpoint.baseUrl ?? "https://api.kimi.com/coding/v1" : void 0).catch(() => false);
|
|
4301
|
+
return endpoint;
|
|
3577
4302
|
}
|
|
3578
4303
|
/** Resolve the live driver for one round's member, if live is on for it. */
|
|
3579
4304
|
liveDriver(childSessionId) {
|
|
@@ -3646,7 +4371,7 @@ var KimiCliProvider = class {
|
|
|
3646
4371
|
...record?.configurationLock === void 0 ? {} : { configurationLock: record.configurationLock }
|
|
3647
4372
|
}, (configuration) => this.startKimiResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
|
|
3648
4373
|
}
|
|
3649
|
-
const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
|
|
4374
|
+
const childSessionId = SessionId(intent?.preparedMemberId ?? intent?.childSessionId ?? randomUUID());
|
|
3650
4375
|
if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") {
|
|
3651
4376
|
if (intent?.effort !== void 0) throw new Error("Kimi effort requires the configuration admission core");
|
|
3652
4377
|
return this.startKimiFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId);
|
|
@@ -3675,16 +4400,19 @@ var KimiCliProvider = class {
|
|
|
3675
4400
|
delegationDepth: (request.parent.session.header.delegationDepth ?? 0) + 1
|
|
3676
4401
|
} });
|
|
3677
4402
|
const harness = this.ctx.localAgent.get("kimi");
|
|
3678
|
-
const label = subagentDelegationLabel(harness?.displayName ?? "kimi", request.
|
|
4403
|
+
const label = subagentDelegationLabel(harness?.displayName ?? "kimi", request.label || void 0);
|
|
3679
4404
|
childSession.append("subagent/descriptor", {
|
|
3680
|
-
|
|
4405
|
+
version: SUBAGENT_DESCRIPTOR_VERSION,
|
|
4406
|
+
mode: "one-shot",
|
|
4407
|
+
provider: this.name,
|
|
3681
4408
|
label
|
|
3682
4409
|
});
|
|
3683
4410
|
establishSubagentCatalogChild(request.parent.session, childSession.header, label);
|
|
3684
4411
|
} catch (error) {
|
|
3685
4412
|
this.ctx.logger.warn(`subagent-kimi: subagent session record failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3686
4413
|
}
|
|
3687
|
-
const
|
|
4414
|
+
const endpoint = await this.prepareEndpoint(homeDir);
|
|
4415
|
+
const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl : await readKimiBaseUrl(homeDir).catch(() => void 0);
|
|
3688
4416
|
this.ctx.logger.info(`subagent-kimi: delegating via ${baseUrl ?? "kimi default endpoint"}`);
|
|
3689
4417
|
const live = exec === void 0 ? this.liveDriver(runId) : void 0;
|
|
3690
4418
|
if (live !== void 0 && childSession !== void 0 && !live.disabled) {
|
|
@@ -3717,10 +4445,13 @@ var KimiCliProvider = class {
|
|
|
3717
4445
|
try {
|
|
3718
4446
|
const run = await startKimiCliRun(request, {
|
|
3719
4447
|
cwd,
|
|
3720
|
-
env: delegationEnv({
|
|
4448
|
+
env: delegationEnv({
|
|
4449
|
+
KIMI_CODE_HOME: homeDir,
|
|
4450
|
+
...endpointAuthEnv(endpoint)
|
|
4451
|
+
}),
|
|
3721
4452
|
...exec === void 0 ? {} : { exec },
|
|
3722
4453
|
endpointLabel: baseUrl,
|
|
3723
|
-
...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(runId) ?? requestedModel, this.model),
|
|
4454
|
+
...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(runId) ?? requestedModel, () => this.model?.(scope)),
|
|
3724
4455
|
...configuration === void 0 ? {} : { controlled: true },
|
|
3725
4456
|
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
3726
4457
|
spawn: (spec) => this.ctx.subprocess.spawn(spec),
|
|
@@ -3728,9 +4459,6 @@ var KimiCliProvider = class {
|
|
|
3728
4459
|
this.ctx.logger.warn(`subagent-kimi: child run failed (${stopReason}) via ${baseUrl ?? "kimi default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3729
4460
|
},
|
|
3730
4461
|
onAuthFailure: (detail) => {
|
|
3731
|
-
guardKimiCredential(homeDir, (message) => {
|
|
3732
|
-
this.ctx.logger.warn(message);
|
|
3733
|
-
});
|
|
3734
4462
|
this.ctx.localAgent.reportAuthFailure("kimi", detail, scope);
|
|
3735
4463
|
},
|
|
3736
4464
|
onAuthSuccess: () => {
|
|
@@ -3797,16 +4525,20 @@ var KimiCliProvider = class {
|
|
|
3797
4525
|
}
|
|
3798
4526
|
}
|
|
3799
4527
|
const member = exec === void 0 && scope === void 0 ? this.memberRun(intent.childSessionId, request.parent.session.id, homeDir) : void 0;
|
|
3800
|
-
const
|
|
4528
|
+
const endpoint = await this.prepareEndpoint(homeDir);
|
|
4529
|
+
const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl : await readKimiBaseUrl(homeDir).catch(() => void 0);
|
|
3801
4530
|
this.ctx.logger.info(`subagent-kimi: resuming via ${baseUrl ?? "kimi default endpoint"}`);
|
|
3802
4531
|
let run;
|
|
3803
4532
|
try {
|
|
3804
4533
|
run = await startKimiCliRun(request, {
|
|
3805
4534
|
cwd,
|
|
3806
|
-
env: delegationEnv({
|
|
4535
|
+
env: delegationEnv({
|
|
4536
|
+
KIMI_CODE_HOME: homeDir,
|
|
4537
|
+
...endpointAuthEnv(endpoint)
|
|
4538
|
+
}),
|
|
3807
4539
|
...exec === void 0 ? {} : { exec },
|
|
3808
4540
|
endpointLabel: baseUrl,
|
|
3809
|
-
...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(intent.childSessionId) ?? requestedModel, this.model),
|
|
4541
|
+
...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(intent.childSessionId) ?? requestedModel, () => this.model?.(scope)),
|
|
3810
4542
|
...configuration === void 0 ? {} : { controlled: true },
|
|
3811
4543
|
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
3812
4544
|
spawn: (spec) => this.ctx.subprocess.spawn(spec),
|
|
@@ -3814,9 +4546,6 @@ var KimiCliProvider = class {
|
|
|
3814
4546
|
this.ctx.logger.warn(`subagent-kimi: child run failed (${stopReason}) via ${baseUrl ?? "kimi default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3815
4547
|
},
|
|
3816
4548
|
onAuthFailure: (detail) => {
|
|
3817
|
-
guardKimiCredential(homeDir, (message) => {
|
|
3818
|
-
this.ctx.logger.warn(message);
|
|
3819
|
-
});
|
|
3820
4549
|
this.ctx.localAgent.reportAuthFailure("kimi", detail, scope);
|
|
3821
4550
|
},
|
|
3822
4551
|
onAuthSuccess: () => {
|
|
@@ -3926,7 +4655,10 @@ async function mirrorKimiAfterExit(spec, stderr) {
|
|
|
3926
4655
|
try {
|
|
3927
4656
|
const kimiSessionId = spec.resume?.cliSessionId ?? kimiSessionIdFromOutput(stderr);
|
|
3928
4657
|
if (spec.resume === void 0) spec.onCliSessionId?.(kimiSessionId);
|
|
3929
|
-
const delta = await mirrorKimiDelta(spec.ctx, spec.childSession, spec.homeDir, kimiSessionId, {
|
|
4658
|
+
const delta = await mirrorKimiDelta(spec.ctx, spec.childSession, spec.homeDir, kimiSessionId, {
|
|
4659
|
+
turn: spec.resume?.turn ?? 1,
|
|
4660
|
+
settle: true
|
|
4661
|
+
});
|
|
3930
4662
|
spec.ctx.get("localAgent")?.reportRunProgress(spec.childSession.id, {
|
|
3931
4663
|
kind: "mirror",
|
|
3932
4664
|
mirroredLines: delta.total
|
|
@@ -4092,29 +4824,6 @@ async function startKimiCliRun(request, spec) {
|
|
|
4092
4824
|
onAbort
|
|
4093
4825
|
}).then((settled) => {
|
|
4094
4826
|
if (settled.stopReason === "completed") spec.onAuthSuccess?.();
|
|
4095
|
-
if (spec.childSession !== void 0) {
|
|
4096
|
-
if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
|
|
4097
|
-
turn,
|
|
4098
|
-
reason: { kind: "completed" }
|
|
4099
|
-
});
|
|
4100
|
-
else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
|
|
4101
|
-
turn,
|
|
4102
|
-
reason: {
|
|
4103
|
-
kind: "aborted",
|
|
4104
|
-
reason: { kind: "parent" }
|
|
4105
|
-
}
|
|
4106
|
-
});
|
|
4107
|
-
else spec.childSession.append("turn/end", {
|
|
4108
|
-
turn,
|
|
4109
|
-
reason: {
|
|
4110
|
-
kind: "error",
|
|
4111
|
-
error: {
|
|
4112
|
-
message: "kimi -p exited before the run completed",
|
|
4113
|
-
code: "UNKNOWN"
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4116
|
-
});
|
|
4117
|
-
}
|
|
4118
4827
|
return settled;
|
|
4119
4828
|
});
|
|
4120
4829
|
result.then(() => child.done).then(() => {
|
|
@@ -4123,19 +4832,46 @@ async function startKimiCliRun(request, spec) {
|
|
|
4123
4832
|
if (KIMI_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
|
|
4124
4833
|
}
|
|
4125
4834
|
enqueueMirror(async () => {
|
|
4126
|
-
const settled = await
|
|
4127
|
-
|
|
4835
|
+
const settled = await result;
|
|
4836
|
+
const mirrored = await mirrorKimiAfterExit(spec, stderr);
|
|
4837
|
+
addRoundUsage(mirrored?.usage);
|
|
4838
|
+
if (spec.childSession !== void 0) {
|
|
4839
|
+
if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
|
|
4840
|
+
turn,
|
|
4841
|
+
reason: { kind: "completed" }
|
|
4842
|
+
});
|
|
4843
|
+
else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
|
|
4844
|
+
turn,
|
|
4845
|
+
reason: {
|
|
4846
|
+
kind: "aborted",
|
|
4847
|
+
reason: { kind: "parent" }
|
|
4848
|
+
}
|
|
4849
|
+
});
|
|
4850
|
+
else spec.childSession.append("turn/end", {
|
|
4851
|
+
turn,
|
|
4852
|
+
reason: {
|
|
4853
|
+
kind: "error",
|
|
4854
|
+
error: {
|
|
4855
|
+
message: "kimi -p exited before the run completed",
|
|
4856
|
+
code: "UNKNOWN"
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
});
|
|
4860
|
+
if (spec.ctx !== void 0) await persistChildSession(spec.ctx, spec.childSession).catch((error) => {
|
|
4861
|
+
spec.ctx?.logger.warn(`subagent-kimi: turn/end persistence failed: ${thrown(error).message}`);
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4128
4864
|
const cliVersion = await spec.cliVersion?.();
|
|
4129
4865
|
spec.onRoundSettled?.({
|
|
4130
|
-
...
|
|
4866
|
+
...mirrored?.model === void 0 ? {} : { observedModel: mirrored.model },
|
|
4131
4867
|
...cliVersion === void 0 ? {} : { cliVersion },
|
|
4132
4868
|
...roundUsage === void 0 ? {} : { usage: roundUsage },
|
|
4133
|
-
...
|
|
4869
|
+
...mirrored?.toolCalls === void 0 ? {} : { toolCalls: mirrored.toolCalls }
|
|
4134
4870
|
});
|
|
4135
4871
|
});
|
|
4136
4872
|
}, () => {});
|
|
4137
4873
|
return Promise.resolve(subprocessRunHandle({
|
|
4138
|
-
id:
|
|
4874
|
+
id: SessionId(randomUUID()),
|
|
4139
4875
|
result,
|
|
4140
4876
|
signal: request.signal,
|
|
4141
4877
|
onAbort,
|
|
@@ -4161,15 +4897,17 @@ var LiveDriverSwitch = class {
|
|
|
4161
4897
|
ctx;
|
|
4162
4898
|
liveIdleMs;
|
|
4163
4899
|
model;
|
|
4900
|
+
endpoint;
|
|
4164
4901
|
liveOn = false;
|
|
4165
4902
|
active;
|
|
4166
4903
|
/** Retiring generations still draining their in-flight rounds. */
|
|
4167
4904
|
draining = /* @__PURE__ */ new Set();
|
|
4168
4905
|
unwatch;
|
|
4169
|
-
constructor(ctx, face, liveIdleMs, model) {
|
|
4906
|
+
constructor(ctx, face, liveIdleMs, model, endpoint) {
|
|
4170
4907
|
this.ctx = ctx;
|
|
4171
4908
|
this.liveIdleMs = liveIdleMs;
|
|
4172
4909
|
this.model = model;
|
|
4910
|
+
this.endpoint = endpoint;
|
|
4173
4911
|
this.apply(face.get());
|
|
4174
4912
|
this.unwatch = face.watch((next) => {
|
|
4175
4913
|
this.apply(next);
|
|
@@ -4193,7 +4931,8 @@ var LiveDriverSwitch = class {
|
|
|
4193
4931
|
const retiring = this.active;
|
|
4194
4932
|
this.active = next.live ? new KimiAcpLiveDriver(this.ctx, {
|
|
4195
4933
|
...this.liveIdleMs === void 0 ? {} : { liveIdleMs: this.liveIdleMs },
|
|
4196
|
-
...this.model === void 0 ? {} : { model: this.model }
|
|
4934
|
+
...this.model === void 0 ? {} : { model: this.model },
|
|
4935
|
+
...this.endpoint === void 0 ? {} : { endpoint: this.endpoint }
|
|
4197
4936
|
}) : void 0;
|
|
4198
4937
|
if (retiring !== void 0) {
|
|
4199
4938
|
this.draining.add(retiring);
|
|
@@ -4280,6 +5019,47 @@ function dedupe(candidates) {
|
|
|
4280
5019
|
return out;
|
|
4281
5020
|
}
|
|
4282
5021
|
/**
|
|
5022
|
+
* Scope the served directory to the surface asking for it (the same helper
|
|
5023
|
+
* lives in each provider's broker — fold into the family core when the
|
|
5024
|
+
* directory faces next evolve):
|
|
5025
|
+
*
|
|
5026
|
+
* - memberless (the card's add-model dialog, the invite picker): the
|
|
5027
|
+
* endpoint's served catalog only — the configuration/history extras the
|
|
5028
|
+
* broker's info extension adds for STATUS display (a config-defined model
|
|
5029
|
+
* the endpoint does not serve) must never become add candidates;
|
|
5030
|
+
* - a member whose harness has a curated list: the list IS the vocabulary,
|
|
5031
|
+
* in the list's own order (the same rule `choices` already follows) — a
|
|
5032
|
+
* curated value the endpoint no longer serves stays as a bare row (the
|
|
5033
|
+
* card shows it the same way), and the member's current effective model is
|
|
5034
|
+
* kept visible as a bare entry when it sits outside the list, so a pre-list
|
|
5035
|
+
* binding never renders blank;
|
|
5036
|
+
* - a member without a curated list: the legacy extended vocabulary.
|
|
5037
|
+
*/
|
|
5038
|
+
function scopeServedDirectory(directory, opts) {
|
|
5039
|
+
if (opts.member && opts.curated.length === 0) return directory;
|
|
5040
|
+
const served = directory.entries.filter((entry) => entry.source === "native");
|
|
5041
|
+
if (!opts.member) return {
|
|
5042
|
+
...directory,
|
|
5043
|
+
entries: served
|
|
5044
|
+
};
|
|
5045
|
+
const byValue = new Map(served.flatMap((entry) => entry.resolvedModel === void 0 ? [[entry.value, entry]] : [[entry.value, entry], [entry.resolvedModel, entry]]));
|
|
5046
|
+
const entries = opts.curated.map((value) => byValue.get(value) ?? {
|
|
5047
|
+
value,
|
|
5048
|
+
label: value,
|
|
5049
|
+
source: "configuration"
|
|
5050
|
+
});
|
|
5051
|
+
const current = opts.current?.trim();
|
|
5052
|
+
if (current !== void 0 && current !== "" && !entries.some((entry) => entry.value === current || entry.resolvedModel === current)) entries.push({
|
|
5053
|
+
value: current,
|
|
5054
|
+
label: current,
|
|
5055
|
+
source: "configuration"
|
|
5056
|
+
});
|
|
5057
|
+
return {
|
|
5058
|
+
...directory,
|
|
5059
|
+
entries
|
|
5060
|
+
};
|
|
5061
|
+
}
|
|
5062
|
+
/**
|
|
4283
5063
|
* The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
|
|
4284
5064
|
* member-model ledger: the provider records each delegation's start model
|
|
4285
5065
|
* here and reads the session-level override for its exec-path resolution, so
|
|
@@ -4306,7 +5086,7 @@ var KimiModelBroker = class {
|
|
|
4306
5086
|
configurationAdapter(binding) {
|
|
4307
5087
|
const home = this.ctx.localAgent.homeDir("kimi", binding.scope);
|
|
4308
5088
|
const resolve = async (selection) => {
|
|
4309
|
-
const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel() ?? await readKimiDefaultModel(home);
|
|
5089
|
+
const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel(home) ?? await readKimiDefaultModel(home);
|
|
4310
5090
|
const configured = await readKimiModelConfiguration(home, model);
|
|
4311
5091
|
const entry = this.deps.liveSwitch.memberRuntimeConfiguration(binding.childSessionId)?.directory.entries.find((entry) => entry.value === model) ?? configured.entries.find((entry) => entry.value === model);
|
|
4312
5092
|
const effort = (selection.effort.mode === "value" ? selection.effort.value : selection.effort.mode === "inherit" ? binding.effort : void 0) ?? configured.effort;
|
|
@@ -4344,14 +5124,19 @@ var KimiModelBroker = class {
|
|
|
4344
5124
|
}
|
|
4345
5125
|
async modelDirectory(childSessionId, refresh = false) {
|
|
4346
5126
|
if (refresh) await this.deps.catalog?.refresh(childSessionId);
|
|
4347
|
-
|
|
5127
|
+
const info = await this.modelInfo(childSessionId);
|
|
5128
|
+
return scopeServedDirectory(info.directory ?? {
|
|
4348
5129
|
entries: [],
|
|
4349
5130
|
complete: false,
|
|
4350
5131
|
customInput: true,
|
|
4351
5132
|
status: "unsupported",
|
|
4352
5133
|
refreshing: false,
|
|
4353
5134
|
revision: 0
|
|
4354
|
-
}
|
|
5135
|
+
}, {
|
|
5136
|
+
member: childSessionId !== void 0,
|
|
5137
|
+
curated: this.deps.modelList?.(this.deps.homeDir(childSessionId)) ?? [],
|
|
5138
|
+
current: info.effective
|
|
5139
|
+
});
|
|
4355
5140
|
}
|
|
4356
5141
|
async *followModelDirectory(childSessionId, signal) {
|
|
4357
5142
|
if (this.deps.catalog === void 0) {
|
|
@@ -4383,7 +5168,7 @@ var KimiModelBroker = class {
|
|
|
4383
5168
|
* nothing — the scoped config's own `default_model` decides, as it always
|
|
4384
5169
|
* did.
|
|
4385
5170
|
*/
|
|
4386
|
-
spawnModel = (childSessionId) => this.overrideFor(childSessionId) ?? this.startModels.get(childSessionId) ?? this.deps.settingsModel();
|
|
5171
|
+
spawnModel = (childSessionId) => this.overrideFor(childSessionId) ?? this.startModels.get(childSessionId) ?? this.deps.settingsModel(this.deps.homeDir(childSessionId));
|
|
4387
5172
|
/**
|
|
4388
5173
|
* Read the model surface. With no member this is the harness default (the
|
|
4389
5174
|
* settings card's "what would a round run with"): override and delegation
|
|
@@ -4392,11 +5177,12 @@ var KimiModelBroker = class {
|
|
|
4392
5177
|
async modelInfo(childSessionId, delegationModel) {
|
|
4393
5178
|
const override = childSessionId === void 0 ? void 0 : this.overrideFor(childSessionId);
|
|
4394
5179
|
const delegation = childSessionId === void 0 ? void 0 : delegationModel ?? this.startModels.get(childSessionId);
|
|
4395
|
-
const settings = this.deps.settingsModel();
|
|
4396
5180
|
const homeDir = this.deps.homeDir(childSessionId);
|
|
5181
|
+
const settings = this.deps.settingsModel(homeDir);
|
|
4397
5182
|
const native = this.deps.catalog?.read(childSessionId);
|
|
4398
5183
|
const [cliDefault, discovered] = await Promise.all([readKimiDefaultModel(homeDir).catch(() => void 0), listKimiConfigModels(homeDir).catch(() => [])]);
|
|
4399
|
-
const
|
|
5184
|
+
const curated = this.deps.modelList?.(homeDir) ?? [];
|
|
5185
|
+
const choices = curated.length > 0 ? [...curated] : dedupe([
|
|
4400
5186
|
settings,
|
|
4401
5187
|
cliDefault,
|
|
4402
5188
|
...discovered,
|
|
@@ -4417,7 +5203,7 @@ var KimiModelBroker = class {
|
|
|
4417
5203
|
settings,
|
|
4418
5204
|
cliDefault,
|
|
4419
5205
|
...discovered
|
|
4420
|
-
], this.deps.recentModels()) },
|
|
5206
|
+
], curated.length > 0 ? [] : this.deps.recentModels()) },
|
|
4421
5207
|
live: this.deps.isLive(),
|
|
4422
5208
|
switchable: !inFlight,
|
|
4423
5209
|
...inFlight ? { reason: SWITCH_IN_FLIGHT_REASON } : {}
|
|
@@ -4454,42 +5240,6 @@ var KimiModelBroker = class {
|
|
|
4454
5240
|
* `session_index.jsonl`, kimi's own append-only index.
|
|
4455
5241
|
* @module @khorsheed/dsh-local-agent-kimi/records
|
|
4456
5242
|
*/
|
|
4457
|
-
/** Credentials live under this directory inside the scoped home. */
|
|
4458
|
-
const CREDENTIALS_DIR = "credentials";
|
|
4459
|
-
/**
|
|
4460
|
-
* Whether the scoped home holds usable login credentials, guarded by the
|
|
4461
|
-
* credential sentinel: a VALID credential file (both tokens non-empty) reads
|
|
4462
|
-
* true and refreshes the backup; an empty shell (the kimi CLI's failure-path
|
|
4463
|
-
* wipe) restores the backup when one exists and reads false otherwise. The
|
|
4464
|
-
* pre-sentinel "directory non-empty" check could not tell the empty shell
|
|
4465
|
-
* from a real credential at all.
|
|
4466
|
-
* @param homeDir - the `kimi` harness's scoped home.
|
|
4467
|
-
* @returns true when usable credentials are present (after any restore).
|
|
4468
|
-
*/
|
|
4469
|
-
async function kimiAuthenticated(homeDir) {
|
|
4470
|
-
return guardKimiCredential(homeDir);
|
|
4471
|
-
}
|
|
4472
|
-
/**
|
|
4473
|
-
* The newest credential file's modification stamp (epoch ms), undefined when
|
|
4474
|
-
* no credential exists. A completed device-code login rewrites the
|
|
4475
|
-
* credentials directory, so the stamp distinguishes a fresh login from a
|
|
4476
|
-
* leftover (possibly expired) credential.
|
|
4477
|
-
* @param homeDir - the `kimi` harness's scoped home.
|
|
4478
|
-
* @returns the newest credential mtime, or undefined when none exists.
|
|
4479
|
-
*/
|
|
4480
|
-
async function kimiCredentialStamp(homeDir) {
|
|
4481
|
-
try {
|
|
4482
|
-
const entries = await readdir(join(homeDir, CREDENTIALS_DIR));
|
|
4483
|
-
let newest;
|
|
4484
|
-
for (const entry of entries) {
|
|
4485
|
-
const { mtimeMs } = await stat(join(homeDir, CREDENTIALS_DIR, entry));
|
|
4486
|
-
if (newest === void 0 || mtimeMs > newest) newest = mtimeMs;
|
|
4487
|
-
}
|
|
4488
|
-
return newest;
|
|
4489
|
-
} catch {
|
|
4490
|
-
return;
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
4493
5243
|
/**
|
|
4494
5244
|
* Read the scoped home's session index. The index is kimi's own
|
|
4495
5245
|
* newline-delimited JSONL; a torn tail line (the file is append-only) is
|
|
@@ -4563,8 +5313,10 @@ async function removeLegacyVariants(ctx) {
|
|
|
4563
5313
|
/**
|
|
4564
5314
|
* Kimi Code harness instantiation of the local-agent family. Registers the
|
|
4565
5315
|
* `kimi` harness into the core registry: scoped home under the shared homes
|
|
4566
|
-
* root (`KIMI_CODE_HOME`),
|
|
4567
|
-
*
|
|
5316
|
+
* root (`KIMI_CODE_HOME`), endpoint-key authentication (endpoint-auth.ts —
|
|
5317
|
+
* the managed provider reads the key from the `api_key_env` environment
|
|
5318
|
+
* indirection; no OAuth anywhere), and the `session_index.jsonl` records
|
|
5319
|
+
* adapter. Delegation mounts through the
|
|
4568
5320
|
* `kimi-cli` provider (one-shot `kimi -p` under the scoped home); the
|
|
4569
5321
|
* bundle patch's `tool-subagent-kimi` row puts the `subagent_kimi` tool on
|
|
4570
5322
|
* the profile root, so every agent preset can delegate without variants.
|
|
@@ -4608,6 +5360,8 @@ const Config = z.object({
|
|
|
4608
5360
|
z.const("high"),
|
|
4609
5361
|
z.const("max")
|
|
4610
5362
|
]),
|
|
5363
|
+
baseUrl: z.string(),
|
|
5364
|
+
apiKey: z.string(),
|
|
4611
5365
|
live: volatilize(SETTINGS_FIELDS.live),
|
|
4612
5366
|
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
4613
5367
|
liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
|
|
@@ -4669,10 +5423,15 @@ function apply(ctx, config) {
|
|
|
4669
5423
|
ctx.effect(() => {
|
|
4670
5424
|
const homeDir = ctx.localAgent.homeDir("kimi");
|
|
4671
5425
|
const thinkingEffort = config.thinkingEffort ?? "high";
|
|
5426
|
+
const endpointConfig = {
|
|
5427
|
+
...config.baseUrl === void 0 ? {} : { baseUrl: config.baseUrl },
|
|
5428
|
+
...config.apiKey === void 0 ? {} : { apiKey: config.apiKey }
|
|
5429
|
+
};
|
|
5430
|
+
const resolveEndpoint = (scopeHome) => resolveEndpointAuth(scopeHome, endpointConfig);
|
|
4672
5431
|
removeLegacyVariants(ctx).catch((error) => {
|
|
4673
5432
|
ctx.logger.warn(`local-agent-kimi: legacy preset cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4674
5433
|
});
|
|
4675
|
-
provisionKimiConfig(homeDir, vol(config.model) ?? "kimi-code/k3", thinkingEffort).then(() => ensureKimiPermissions(homeDir)).catch((error) => {
|
|
5434
|
+
provisionKimiConfig(homeDir, vol(config.model) ?? "kimi-code/k3", thinkingEffort, config.baseUrl).then(() => ensureKimiPermissions(homeDir)).catch((error) => {
|
|
4676
5435
|
ctx.logger.warn(`local-agent-kimi: config provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4677
5436
|
});
|
|
4678
5437
|
const readSettings = () => {
|
|
@@ -4700,12 +5459,14 @@ function apply(ctx, config) {
|
|
|
4700
5459
|
legacyBase,
|
|
4701
5460
|
read: readSettings
|
|
4702
5461
|
});
|
|
4703
|
-
const
|
|
5462
|
+
const resolveModelKey = () => {
|
|
4704
5463
|
const model = face.get().model?.trim();
|
|
4705
5464
|
return model === void 0 || model === "" ? void 0 : model;
|
|
4706
5465
|
};
|
|
5466
|
+
const resolveModelFor = (homeDir) => firstModelOf(readModelListSync(homeDir)) ?? resolveModelKey();
|
|
5467
|
+
const resolveModel = (scope) => resolveModelFor(ctx.localAgent.homeDir("kimi", scope));
|
|
4707
5468
|
let broker;
|
|
4708
|
-
const liveSwitch = new LiveDriverSwitch(ctx, face, config.liveIdleMs, (child) => broker.spawnModel(child));
|
|
5469
|
+
const liveSwitch = new LiveDriverSwitch(ctx, face, config.liveIdleMs, (child) => broker.spawnModel(child), resolveEndpoint);
|
|
4709
5470
|
const modelCatalog = new KimiModelCatalog((childSessionId) => {
|
|
4710
5471
|
const record = childSessionId === void 0 ? void 0 : ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId);
|
|
4711
5472
|
const native = childSessionId === void 0 ? void 0 : liveSwitch.memberRuntimeConfiguration(childSessionId);
|
|
@@ -4714,16 +5475,20 @@ function apply(ctx, config) {
|
|
|
4714
5475
|
...record?.cwd === void 0 ? {} : { cwd: record.cwd },
|
|
4715
5476
|
...native === void 0 ? {} : { native }
|
|
4716
5477
|
};
|
|
5478
|
+
}, {
|
|
5479
|
+
endpoint: resolveEndpoint,
|
|
5480
|
+
warn: (message) => ctx.logger.warn(message)
|
|
4717
5481
|
});
|
|
4718
5482
|
broker = new KimiModelBroker(ctx, {
|
|
4719
5483
|
homeDir: (childSessionId) => ctx.localAgent.homeDir("kimi", childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
|
|
4720
5484
|
catalog: modelCatalog,
|
|
4721
|
-
settingsModel:
|
|
5485
|
+
settingsModel: resolveModelFor,
|
|
5486
|
+
modelList: (homeDir) => readModelListSync(homeDir),
|
|
4722
5487
|
recentModels: () => face.get().recentModels ?? [],
|
|
4723
5488
|
isLive: () => face.get().live,
|
|
4724
5489
|
liveSwitch
|
|
4725
5490
|
});
|
|
4726
|
-
const disposeProvider = ctx.subagents.registerProvider(new KimiCliProvider(ctx, liveSwitch.resolve, resolveModel, broker));
|
|
5491
|
+
const disposeProvider = ctx.subagents.registerProvider(new KimiCliProvider(ctx, liveSwitch.resolve, resolveModel, broker, resolveEndpoint));
|
|
4727
5492
|
const disposeHarness = ctx.localAgent.register({
|
|
4728
5493
|
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
4729
5494
|
const driver = liveSwitch.resolve(binding.childSessionId);
|
|
@@ -4740,31 +5505,76 @@ function apply(ctx, config) {
|
|
|
4740
5505
|
displayName: "Kimi Code",
|
|
4741
5506
|
homeEnvVar: "KIMI_CODE_HOME",
|
|
4742
5507
|
delegationProvider: "kimi-cli",
|
|
4743
|
-
login: {
|
|
4744
|
-
command: "kimi",
|
|
4745
|
-
args: ["login"]
|
|
4746
|
-
},
|
|
4747
5508
|
records: { listSessions: (homeDir) => listKimiSessions(homeDir) },
|
|
4748
5509
|
provision: async (scopedHome) => {
|
|
4749
|
-
await provisionKimiConfig(scopedHome, vol(config.model) ?? "kimi-code/k3", thinkingEffort);
|
|
5510
|
+
await provisionKimiConfig(scopedHome, vol(config.model) ?? "kimi-code/k3", thinkingEffort, config.baseUrl);
|
|
4750
5511
|
await ensureKimiPermissions(scopedHome);
|
|
4751
5512
|
},
|
|
4752
|
-
isAuthenticated:
|
|
4753
|
-
|
|
4754
|
-
|
|
5513
|
+
isAuthenticated: async (home) => hasEndpointCredential(await resolveEndpoint(home)),
|
|
5514
|
+
checkEndpoint: (home) => resolveEndpoint(home).then((endpoint) => checkKimiEndpoint(endpoint)),
|
|
5515
|
+
endpointAuth: {
|
|
5516
|
+
read: async (home) => {
|
|
5517
|
+
const endpoint = await resolveEndpoint(home);
|
|
5518
|
+
return {
|
|
5519
|
+
...endpoint.baseUrl === void 0 ? {} : { baseUrl: endpoint.baseUrl },
|
|
5520
|
+
apiKeySet: endpoint.apiKey !== void 0,
|
|
5521
|
+
authTokenSet: false
|
|
5522
|
+
};
|
|
5523
|
+
},
|
|
5524
|
+
write: async (home, fields) => {
|
|
5525
|
+
const next = { ...await readEndpointAuthFile(home) ?? {} };
|
|
5526
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
5527
|
+
if (field !== "baseUrl" && field !== "apiKey") continue;
|
|
5528
|
+
if (value === void 0) continue;
|
|
5529
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
5530
|
+
if (value === null || trimmed === "") delete next[field];
|
|
5531
|
+
else next[field] = trimmed;
|
|
5532
|
+
}
|
|
5533
|
+
if (Object.keys(next).length === 0) await clearEndpointAuth(home);
|
|
5534
|
+
else await writeEndpointAuth(home, next);
|
|
5535
|
+
const resolved = await resolveEndpoint(home);
|
|
5536
|
+
await ensureKimiEndpointProvider(home, hasEndpointCredential(resolved) ? resolved.baseUrl ?? "https://api.kimi.com/coding/v1" : void 0);
|
|
5537
|
+
}
|
|
5538
|
+
},
|
|
5539
|
+
credentialStamp: endpointAuthStamp,
|
|
5540
|
+
logout: async (home) => {
|
|
5541
|
+
if (await endpointAuthFileExists(home)) {
|
|
5542
|
+
await clearEndpointAuth(home);
|
|
5543
|
+
return;
|
|
5544
|
+
}
|
|
5545
|
+
throw new Error("kimi has no scoped endpoint-auth.json to clear — the credential comes from the settings card or the profile row (baseUrl/apiKey); clear it there instead");
|
|
5546
|
+
},
|
|
4755
5547
|
subcommand: handleSubcommand,
|
|
4756
5548
|
modelBroker: broker,
|
|
5549
|
+
modelList: {
|
|
5550
|
+
read: (home) => readModelList(home),
|
|
5551
|
+
write: async (home, models) => {
|
|
5552
|
+
const written = await writeModelList(home, models);
|
|
5553
|
+
await ensureKimiModelTables(home, written, (value) => {
|
|
5554
|
+
const entry = modelCatalog.readHome(home).entries.find((candidate) => candidate.value === value);
|
|
5555
|
+
return entry === void 0 ? void 0 : {
|
|
5556
|
+
label: entry.label,
|
|
5557
|
+
...entry.reasoning === void 0 ? {} : {
|
|
5558
|
+
efforts: entry.reasoning.options.map((option) => option.value),
|
|
5559
|
+
...entry.reasoning.default === void 0 ? {} : { defaultEffort: entry.reasoning.default }
|
|
5560
|
+
}
|
|
5561
|
+
};
|
|
5562
|
+
});
|
|
5563
|
+
return written;
|
|
5564
|
+
}
|
|
5565
|
+
},
|
|
4757
5566
|
effectiveSettings: async (scopedHome) => {
|
|
4758
|
-
const [reasoningEffort,
|
|
5567
|
+
const [reasoningEffort, endpoint, autoApprove, defaultModel, cliVersion] = await Promise.all([
|
|
4759
5568
|
readKimiReasoningEffort(scopedHome).catch(() => void 0),
|
|
4760
|
-
|
|
5569
|
+
resolveEndpoint(scopedHome).catch(() => void 0),
|
|
4761
5570
|
readKimiAutoApprove(scopedHome).catch(() => false),
|
|
4762
5571
|
readKimiDefaultModel(scopedHome).catch(() => void 0),
|
|
4763
5572
|
kimiCliVersion(ctx, scopedHome).catch(() => void 0)
|
|
4764
5573
|
]);
|
|
5574
|
+
const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl ?? "https://api.kimi.com/coding/v1" : await readKimiBaseUrl(scopedHome).catch(() => void 0);
|
|
4765
5575
|
const custom = baseUrl !== void 0 && baseUrl !== "https://api.kimi.com/coding/v1";
|
|
4766
5576
|
const baseUrlHost = custom ? endpointHost(baseUrl) : void 0;
|
|
4767
|
-
const model =
|
|
5577
|
+
const model = resolveModelFor(scopedHome) ?? defaultModel;
|
|
4768
5578
|
return {
|
|
4769
5579
|
drive: face.get().live ? "live" : "exec",
|
|
4770
5580
|
autoApprove,
|