@khorsheed/dsh-local-agent-kimi 0.2.0 → 0.2.2
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 +17 -0
- package/README.en.md +1 -1
- package/README.i18n.yaml +2 -2
- package/README.md +1 -1
- package/lib/client.js +161 -76
- package/lib/index.js +176 -107
- package/lib/types/client/SettingsCard.js +11 -3
- package/lib/types/client/index.js +2 -2
- package/lib/types/index.js +4 -2
- package/lib/types/live-driver.d.ts +0 -1
- package/lib/types/live-driver.js +44 -24
- package/lib/types/member-bridge-config.d.ts +2 -0
- package/lib/types/member-bridge-config.js +5 -1
- package/lib/types/session-mirror.js +37 -3
- package/lib/types/session-view.d.ts +11 -0
- package/lib/types/session-view.js +45 -1
- package/package.json +2 -2
|
@@ -12,6 +12,8 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
/** Kimi's user-level MCP config filename inside the scoped home. */
|
|
14
14
|
const MCP_CONFIG_FILENAME = 'mcp.json';
|
|
15
|
+
/** The member bridge's per-call bound: as long as a question card waits for the human (30 min). */
|
|
16
|
+
export const MEMBER_TOOL_TIMEOUT_MS = 30 * 60_000;
|
|
15
17
|
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
16
18
|
export const MEMBER_BRIDGE_SERVER_PREFIX = 'dsh-member-';
|
|
17
19
|
/**
|
|
@@ -60,7 +62,9 @@ export function injectMemberBridge(homeDir, key, command, env) {
|
|
|
60
62
|
if (name.startsWith(MEMBER_BRIDGE_SERVER_PREFIX) && name !== key)
|
|
61
63
|
delete servers[name];
|
|
62
64
|
}
|
|
63
|
-
|
|
65
|
+
// A bridge call may wait for the human (ask_user_question, a mode tool's
|
|
66
|
+
// approval): kimi's 60s MCP default fails it first and the answer is lost.
|
|
67
|
+
servers[key] = { command: command.command, args: command.args, env, toolTimeoutMs: MEMBER_TOOL_TIMEOUT_MS };
|
|
64
68
|
writeMcpConfig(file, { ...config, mcpServers: servers });
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
@@ -71,6 +71,14 @@ export function nextWrittenKimiStep(childSession, turn) {
|
|
|
71
71
|
}
|
|
72
72
|
return last + 1;
|
|
73
73
|
}
|
|
74
|
+
/** Wire usage records already counted, per child session (see mirrorKimiSessionDelta). */
|
|
75
|
+
const countedUsage = new WeakMap();
|
|
76
|
+
const usageSeen = (childSession) => {
|
|
77
|
+
let seen = countedUsage.get(childSession);
|
|
78
|
+
if (seen === undefined)
|
|
79
|
+
countedUsage.set(childSession, seen = new Set());
|
|
80
|
+
return seen;
|
|
81
|
+
};
|
|
74
82
|
/** The run-progress delta text for one transcript line (any kind). */
|
|
75
83
|
export function kimiLineProgressText(line) {
|
|
76
84
|
if (line.kind !== 'tool')
|
|
@@ -245,6 +253,13 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
245
253
|
// step-tagged unit claims its own line span's records; records no tagged
|
|
246
254
|
// unit claims (untagged spans on older wires, or the boundary) sum onto the
|
|
247
255
|
// pass's last folded content unit — the pre-tagging shape.
|
|
256
|
+
// A record counts once per child session, ever: the boundary is inclusive, so
|
|
257
|
+
// a pass whose offset cannot advance (a held unit) re-reads the same boundary
|
|
258
|
+
// record on every poll — one 320k-token record was carried ten times onto a
|
|
259
|
+
// cancelled round's step (3.2M). Wire line numbers only grow, so the line
|
|
260
|
+
// identifies the record across passes and rounds.
|
|
261
|
+
const seen = usageSeen(childSession);
|
|
262
|
+
const usageRecords = transcript.usageRecords.filter(record => !seen.has(record.line));
|
|
248
263
|
const unitUsage = [];
|
|
249
264
|
const claimed = [];
|
|
250
265
|
let unitStart = fromLines;
|
|
@@ -254,15 +269,18 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
254
269
|
const head = unit[0];
|
|
255
270
|
if (head.kind !== 'user' && head.wireStep !== undefined) {
|
|
256
271
|
claimed.push({ from: start, to: unitStart });
|
|
257
|
-
unitUsage.push(sumUsageRecords(
|
|
272
|
+
unitUsage.push(sumUsageRecords(usageRecords.filter(record => record.line >= start && record.line < unitStart)));
|
|
258
273
|
}
|
|
259
274
|
else {
|
|
260
275
|
unitUsage.push(undefined);
|
|
261
276
|
}
|
|
262
277
|
}
|
|
263
|
-
const unclaimedUsage = sumUsageRecords(
|
|
278
|
+
const unclaimedUsage = sumUsageRecords(usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal
|
|
264
279
|
&& !claimed.some(span => record.line >= span.from && record.line < span.to)));
|
|
265
|
-
const
|
|
280
|
+
const windowRecords = usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal);
|
|
281
|
+
const deltaUsage = sumUsageRecords(windowRecords);
|
|
282
|
+
for (const record of windowRecords)
|
|
283
|
+
seen.add(record.line);
|
|
266
284
|
// The steps a fold may NOT take: every step already written (the session's
|
|
267
285
|
// own event ledger), consulted per turn. A round mirrored across several
|
|
268
286
|
// live polls keeps its earlier writes, so later polls never reuse them.
|
|
@@ -296,6 +314,20 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
296
314
|
written.add(step);
|
|
297
315
|
takenSteps.set(turn, written);
|
|
298
316
|
};
|
|
317
|
+
// Turns the log has already closed take no more writes: the released-log
|
|
318
|
+
// reader admits events only inside the open turn. A backlog the offset never
|
|
319
|
+
// covered (a fold that failed for rounds, then an offset lost on restart)
|
|
320
|
+
// would otherwise land old content inside a finished turn — or, after the
|
|
321
|
+
// next turn opened, corrupt the whole log. Those units are skipped; the
|
|
322
|
+
// offset still moves past them.
|
|
323
|
+
const closedTurns = new Set();
|
|
324
|
+
for (const event of childSession.snapshotEvents()) {
|
|
325
|
+
if (event.type !== 'turn/end')
|
|
326
|
+
continue;
|
|
327
|
+
const closed = event.data.turn;
|
|
328
|
+
if (typeof closed === 'number')
|
|
329
|
+
closedTurns.add(closed);
|
|
330
|
+
}
|
|
299
331
|
/** The window's accounting that no fold carried (dropped units' sums). */
|
|
300
332
|
let carryUsage;
|
|
301
333
|
// The pass's last content unit carries the records no tagged unit claimed.
|
|
@@ -319,6 +351,8 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
319
351
|
// numbering) is the dsh round; no offset is needed because both count the
|
|
320
352
|
// same rounds.
|
|
321
353
|
const turn = head.turn;
|
|
354
|
+
if (closedTurns.has(turn))
|
|
355
|
+
continue;
|
|
322
356
|
if (head.kind === 'user') {
|
|
323
357
|
// The live driver appends the round's user/message at turn start (so the
|
|
324
358
|
// question renders before the streamed think/text instead of after it).
|
|
@@ -104,6 +104,17 @@ export declare function findKimiSessionDir(homeDir: string, sessionId: string):
|
|
|
104
104
|
* @returns the transcript.
|
|
105
105
|
*/
|
|
106
106
|
export declare function readKimiTranscript(sessionDir: string): Promise<KimiSessionTranscript>;
|
|
107
|
+
/**
|
|
108
|
+
* A wire tool result's output as display text. Most tools report a string;
|
|
109
|
+
* a media read (ReadMediaFile) reports content parts — text parts keep their
|
|
110
|
+
* text, an image part becomes a short marker (its bytes stay in kimi's blob
|
|
111
|
+
* store). Anything else reads as no output, never a parse failure: one
|
|
112
|
+
* unreadable result must not stop the whole transcript (and the round's
|
|
113
|
+
* usage) from folding.
|
|
114
|
+
* @param raw - the result's `output` field.
|
|
115
|
+
* @returns the text, or undefined when there is none.
|
|
116
|
+
*/
|
|
117
|
+
export declare function toolOutputText(raw: unknown): string | undefined;
|
|
107
118
|
/**
|
|
108
119
|
* Map a wire `usage.record` payload onto the shared usage contract. kimi
|
|
109
120
|
* reports input (uncached), cache read, cache creation, and output; the
|
|
@@ -105,6 +105,8 @@ function editArgsOf(name, record) {
|
|
|
105
105
|
return patch;
|
|
106
106
|
return undefined;
|
|
107
107
|
}
|
|
108
|
+
/** Cap on a structured tool call's mirrored JSON arguments. */
|
|
109
|
+
const STRUCTURED_ARGS_MAX = 16_000;
|
|
108
110
|
/** Render a tool call's arguments into a compact summary. */
|
|
109
111
|
function argsOf(name, args) {
|
|
110
112
|
if (args === undefined)
|
|
@@ -119,6 +121,13 @@ function argsOf(name, args) {
|
|
|
119
121
|
const edit = editArgsOf(name, record);
|
|
120
122
|
if (edit !== undefined)
|
|
121
123
|
return edit;
|
|
124
|
+
// Structured arguments (an MCP tool's nested questions, options, lists)
|
|
125
|
+
// keep their full JSON: the one-line summary below keeps only scalars and
|
|
126
|
+
// dropped them, so the member's ask_user_question read as called with nothing.
|
|
127
|
+
if (Object.values(record).some(value => typeof value === 'object' && value !== null)) {
|
|
128
|
+
const json = JSON.stringify(record);
|
|
129
|
+
return json.length <= STRUCTURED_ARGS_MAX ? json : `${json.slice(0, STRUCTURED_ARGS_MAX)}…`;
|
|
130
|
+
}
|
|
122
131
|
// WebSearch/FetchURL expose a query/url; prefer the most meaningful scalar.
|
|
123
132
|
for (const key of ['query', 'url', 'path', 'command']) {
|
|
124
133
|
const value = record[key];
|
|
@@ -168,6 +177,8 @@ export async function readKimiTranscript(sessionDir) {
|
|
|
168
177
|
// turn/start numbering.
|
|
169
178
|
let turn = 0;
|
|
170
179
|
let turnSeen = false;
|
|
180
|
+
/** The wire marks rounds with turn.prompt (current kimi): a user message it did not prompt is an injection. */
|
|
181
|
+
let promptsMarkTurns = false;
|
|
171
182
|
// Tool calls by uuid and toolCallId, so a result attaches to its own call
|
|
172
183
|
// even when several calls run in parallel within one step.
|
|
173
184
|
const callsByUuid = new Map();
|
|
@@ -228,6 +239,7 @@ export async function readKimiTranscript(sessionDir) {
|
|
|
228
239
|
continue;
|
|
229
240
|
}
|
|
230
241
|
if (event.type === 'turn.prompt') {
|
|
242
|
+
promptsMarkTurns = true;
|
|
231
243
|
lineModel = undefined;
|
|
232
244
|
// Each round's user prompt opens the next turn; the matching
|
|
233
245
|
// context.append_message below is deduped against this text.
|
|
@@ -254,6 +266,13 @@ export async function readKimiTranscript(sessionDir) {
|
|
|
254
266
|
// Duplicate of the prompt; skip.
|
|
255
267
|
continue;
|
|
256
268
|
}
|
|
269
|
+
// A wire that opens every round with turn.prompt: any other user
|
|
270
|
+
// message is kimi's own injection inside the round (a Skill's loaded
|
|
271
|
+
// instructions, a steer) — its loop events keep the same turnId. It
|
|
272
|
+
// opens no turn (the dsh round is still running: a turn here wrote
|
|
273
|
+
// steps into a turn the log never opened) and is not the human's.
|
|
274
|
+
if (promptsMarkTurns)
|
|
275
|
+
continue;
|
|
257
276
|
turnSeen = true;
|
|
258
277
|
turn += 1;
|
|
259
278
|
lineModel = undefined;
|
|
@@ -309,7 +328,7 @@ export async function readKimiTranscript(sessionDir) {
|
|
|
309
328
|
}
|
|
310
329
|
else if (loop.type === 'tool.result') {
|
|
311
330
|
const result = loop;
|
|
312
|
-
const output = result.result?.output;
|
|
331
|
+
const output = toolOutputText(result.result?.output);
|
|
313
332
|
let target = typeof result.parentUuid === 'string'
|
|
314
333
|
? callsByUuid.get(result.parentUuid)
|
|
315
334
|
: undefined;
|
|
@@ -346,6 +365,31 @@ export async function readKimiTranscript(sessionDir) {
|
|
|
346
365
|
}
|
|
347
366
|
return { sessionId, lines, usageRecords, closedSteps, ...model === undefined ? {} : { model }, ...modelTurn === undefined ? {} : { modelTurn } };
|
|
348
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* A wire tool result's output as display text. Most tools report a string;
|
|
370
|
+
* a media read (ReadMediaFile) reports content parts — text parts keep their
|
|
371
|
+
* text, an image part becomes a short marker (its bytes stay in kimi's blob
|
|
372
|
+
* store). Anything else reads as no output, never a parse failure: one
|
|
373
|
+
* unreadable result must not stop the whole transcript (and the round's
|
|
374
|
+
* usage) from folding.
|
|
375
|
+
* @param raw - the result's `output` field.
|
|
376
|
+
* @returns the text, or undefined when there is none.
|
|
377
|
+
*/
|
|
378
|
+
export function toolOutputText(raw) {
|
|
379
|
+
if (typeof raw === 'string')
|
|
380
|
+
return raw;
|
|
381
|
+
if (!Array.isArray(raw))
|
|
382
|
+
return undefined;
|
|
383
|
+
const parts = raw.map((part) => {
|
|
384
|
+
if (part === null || typeof part !== 'object')
|
|
385
|
+
return '';
|
|
386
|
+
const entry = part;
|
|
387
|
+
if (entry.type === 'text' && typeof entry.text === 'string')
|
|
388
|
+
return entry.text;
|
|
389
|
+
return typeof entry.type === 'string' && entry.type !== '' ? `[${entry.type}]` : '';
|
|
390
|
+
});
|
|
391
|
+
return parts.join('');
|
|
392
|
+
}
|
|
349
393
|
/**
|
|
350
394
|
* Map a wire `usage.record` payload onto the shared usage contract. kimi
|
|
351
395
|
* reports input (uncached), cache read, cache creation, and output; the
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"local-agent",
|
|
9
9
|
"kimi"
|
|
10
10
|
],
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.2",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "lib/index.js",
|
|
14
14
|
"types": "lib/types/index.d.ts",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"dsh": ">=0.1.5-rc.1 <0.3.0-0"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@khorsheed/dsh-local-agent": "^0.2.
|
|
72
|
+
"@khorsheed/dsh-local-agent": "^0.2.2"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@deepseek-ai/cordis": "^4.0.1",
|