@khorsheed/dsh-local-agent-kimi 0.1.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.en.md +132 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +132 -0
  4. package/cordis.patch.yml +34 -0
  5. package/lib/client.js +1066 -0
  6. package/lib/index.js +4716 -0
  7. package/lib/invariant.js +31 -0
  8. package/lib/types/client/SettingsCard.d.ts +77 -0
  9. package/lib/types/client/SettingsCard.js +197 -0
  10. package/lib/types/client/icons.d.ts +18 -0
  11. package/lib/types/client/icons.js +2 -0
  12. package/lib/types/client/index.d.ts +42 -0
  13. package/lib/types/client/index.js +89 -0
  14. package/lib/types/client/locales.d.ts +64 -0
  15. package/lib/types/client/locales.js +55 -0
  16. package/lib/types/credential-guard.d.ts +29 -0
  17. package/lib/types/credential-guard.js +107 -0
  18. package/lib/types/index.d.ts +69 -0
  19. package/lib/types/index.js +270 -0
  20. package/lib/types/invariant.d.ts +16 -0
  21. package/lib/types/invariant.js +33 -0
  22. package/lib/types/kimi-cli-provider.d.ts +215 -0
  23. package/lib/types/kimi-cli-provider.js +855 -0
  24. package/lib/types/live-driver.d.ts +245 -0
  25. package/lib/types/live-driver.js +1312 -0
  26. package/lib/types/live-switch.d.ts +78 -0
  27. package/lib/types/live-switch.js +112 -0
  28. package/lib/types/member-bridge-config.d.ts +43 -0
  29. package/lib/types/member-bridge-config.js +80 -0
  30. package/lib/types/model-broker.d.ts +97 -0
  31. package/lib/types/model-broker.js +205 -0
  32. package/lib/types/model-catalog.d.ts +26 -0
  33. package/lib/types/model-catalog.js +86 -0
  34. package/lib/types/model-configuration.d.ts +11 -0
  35. package/lib/types/model-configuration.js +55 -0
  36. package/lib/types/preset-tools.d.ts +20 -0
  37. package/lib/types/preset-tools.js +36 -0
  38. package/lib/types/provision.d.ts +131 -0
  39. package/lib/types/provision.js +363 -0
  40. package/lib/types/records.d.ts +35 -0
  41. package/lib/types/records.js +85 -0
  42. package/lib/types/session-configuration.d.ts +5 -0
  43. package/lib/types/session-configuration.js +26 -0
  44. package/lib/types/session-mirror.d.ts +180 -0
  45. package/lib/types/session-mirror.js +446 -0
  46. package/lib/types/session-view.d.ts +121 -0
  47. package/lib/types/session-view.js +423 -0
  48. package/locale/en.json +6 -0
  49. package/locale/zh.json +6 -0
  50. package/package.json +107 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
3
+ * and render a readable transcript (user prompts, assistant thinking and
4
+ * replies, tool calls with arguments and results). The kimi ACP mode writes
5
+ * responses to the session file rather than pushing them over the protocol,
6
+ * and the dsh subagent record for a one-shot run carries only the final
7
+ * output — this is the way to see what the kimi subagent actually did.
8
+ *
9
+ * The transcript is harness-comparison grade: every line is tagged with its
10
+ * wire turn number (1-based, matching the dsh child session's `turn/start`
11
+ * numbering), kimi's auto-permission `<system-reminder>` user messages are
12
+ * filtered out, tool calls carry their rendered arguments, tool results are
13
+ * matched to their owning call by `parentUuid`, and every `usage.record`
14
+ * (kimi reports accounting per LLM request, not cumulative) is surfaced with
15
+ * its transcript position so a delta mirror can sum a round's total.
16
+ * @module @khorsheed/dsh-local-agent-kimi/session-view
17
+ */
18
+ import type { TokenUsage } from '@deepseek-ai/dsh-llm';
19
+ /** One rendered transcript line. */
20
+ export type KimiTranscriptLine = ({
21
+ kind: 'user';
22
+ text: string;
23
+ turn: number;
24
+ } | {
25
+ kind: 'assistant';
26
+ text: string;
27
+ turn: number;
28
+ } | {
29
+ kind: 'think';
30
+ text: string;
31
+ turn: number;
32
+ }
33
+ /**
34
+ * Tool activity: one call with its (possibly still pending) result. `id`
35
+ * is the wire's toolCallId/uuid when present, else a synthesized
36
+ * position-based id — stable across mirror passes either way, so the
37
+ * child session's `tool/call`/`tool/result` events pair by it.
38
+ */
39
+ | {
40
+ kind: 'tool';
41
+ id: string;
42
+ name: string;
43
+ args?: string;
44
+ result?: string;
45
+ turn: number;
46
+ }) & {
47
+ model?: string;
48
+ };
49
+ /** A parsed transcript of one kimi session. */
50
+ export interface KimiSessionTranscript {
51
+ /** Session id. */
52
+ sessionId: string;
53
+ /** Transcript lines in event order, each tagged with its wire turn number. */
54
+ lines: readonly KimiTranscriptLine[];
55
+ /**
56
+ * Every `usage.record` the wire reported, tagged with the transcript-line
57
+ * position it occurred at. Each record is ONE LLM request's accounting
58
+ * (kimi does not accumulate within a turn), so a mirror sums the records
59
+ * whose position falls inside the delta it appends.
60
+ */
61
+ usageRecords: readonly {
62
+ readonly line: number;
63
+ readonly usage: TokenUsage;
64
+ }[];
65
+ /**
66
+ * The model identifier the wire named: the `model` field of a `usage.record`
67
+ * or `llm.request` event, LAST one seen (a resumed session's later rounds
68
+ * append later records, so the last record is the latest round's). Absent
69
+ * when the wire named none — older kimi releases omit it.
70
+ */
71
+ model?: string;
72
+ /** Turn containing the latest native model observation. */
73
+ modelTurn?: number;
74
+ }
75
+ /**
76
+ * Find a session directory under `<homeDir>/sessions/wd_*` whose id matches.
77
+ * @param homeDir - the `kimi` harness's scoped home.
78
+ * @param sessionId - the kimi session id.
79
+ * @returns the session directory, or undefined when absent.
80
+ */
81
+ export declare function findKimiSessionDir(homeDir: string, sessionId: string): Promise<string | undefined>;
82
+ /**
83
+ * Parse a session's wire log into transcript lines plus every usage record.
84
+ * @param sessionDir - the session directory.
85
+ * @returns the transcript.
86
+ */
87
+ export declare function readKimiTranscript(sessionDir: string): Promise<KimiSessionTranscript>;
88
+ /**
89
+ * Map a wire `usage.record` payload onto the shared usage contract. kimi
90
+ * reports input (uncached), cache read, cache creation, and output; the
91
+ * creation counter is its cache-write analog. Non-numeric or missing
92
+ * counters are dropped so the record stays usable when kimi omits a field.
93
+ * @param raw - the event's `usage` value.
94
+ * @returns the shared usage record, or undefined when nothing was reported.
95
+ */
96
+ export declare function usageFromWire(raw: unknown): TokenUsage | undefined;
97
+ /**
98
+ * Sum per-request usage records into one round total. Each `usage.record` is
99
+ * a single LLM request's accounting, so a round's totals are the sum of the
100
+ * records inside it.
101
+ * @param records - the usage records to add.
102
+ * @returns the summed usage, or undefined when the list is empty.
103
+ */
104
+ export declare function sumUsageRecords(records: readonly {
105
+ readonly line: number;
106
+ readonly usage: TokenUsage;
107
+ }[]): TokenUsage | undefined;
108
+ /**
109
+ * Merge two usage windows into one running total — the delegation run's
110
+ * per-round accumulator: each mirror pass (live poll, settle) returns its
111
+ * own window's accounting, and the disjoint windows sum to the round's
112
+ * total. `undefined` operands pass through, so callers accumulate without
113
+ * absence checks.
114
+ * @param left - the running total, or undefined before the first window.
115
+ * @param right - the next window's usage, or undefined when it had none.
116
+ * @returns the merged total, or undefined when both operands are absent.
117
+ */
118
+ export declare function addTokenUsage(left: TokenUsage | undefined, right: TokenUsage | undefined): TokenUsage | undefined;
119
+ /** Render a transcript as command-reply text. */
120
+ export declare function renderTranscript(transcript: KimiSessionTranscript): string;
121
+ //# sourceMappingURL=session-view.d.ts.map
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
3
+ * and render a readable transcript (user prompts, assistant thinking and
4
+ * replies, tool calls with arguments and results). The kimi ACP mode writes
5
+ * responses to the session file rather than pushing them over the protocol,
6
+ * and the dsh subagent record for a one-shot run carries only the final
7
+ * output — this is the way to see what the kimi subagent actually did.
8
+ *
9
+ * The transcript is harness-comparison grade: every line is tagged with its
10
+ * wire turn number (1-based, matching the dsh child session's `turn/start`
11
+ * numbering), kimi's auto-permission `<system-reminder>` user messages are
12
+ * filtered out, tool calls carry their rendered arguments, tool results are
13
+ * matched to their owning call by `parentUuid`, and every `usage.record`
14
+ * (kimi reports accounting per LLM request, not cumulative) is surfaced with
15
+ * its transcript position so a delta mirror can sum a round's total.
16
+ * @module @khorsheed/dsh-local-agent-kimi/session-view
17
+ */
18
+ import { readdir, readFile } from 'node:fs/promises';
19
+ import { join } from 'node:path';
20
+ /** The wire event log path for a session directory. */
21
+ function wirePath(sessionDir) {
22
+ return join(sessionDir, 'agents', 'main', 'wire.jsonl');
23
+ }
24
+ /**
25
+ * Find a session directory under `<homeDir>/sessions/wd_*` whose id matches.
26
+ * @param homeDir - the `kimi` harness's scoped home.
27
+ * @param sessionId - the kimi session id.
28
+ * @returns the session directory, or undefined when absent.
29
+ */
30
+ export async function findKimiSessionDir(homeDir, sessionId) {
31
+ const sessionsRoot = join(homeDir, 'sessions');
32
+ let workspaces;
33
+ try {
34
+ workspaces = await readdir(sessionsRoot);
35
+ }
36
+ catch (error) {
37
+ if (error.code === 'ENOENT')
38
+ return undefined;
39
+ throw error;
40
+ }
41
+ for (const workspace of workspaces) {
42
+ const sessionDir = join(sessionsRoot, workspace, `session_${sessionId}`);
43
+ try {
44
+ await readFile(wirePath(sessionDir));
45
+ return sessionDir;
46
+ }
47
+ catch {
48
+ // Not this workspace's session; keep looking.
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ /** Whether a user message is kimi's auto-permission reminder, never real task text. */
54
+ function isSystemReminder(text) {
55
+ return text.startsWith('<system-reminder>');
56
+ }
57
+ /** Flatten the text blocks of a message content array. */
58
+ function textOf(content) {
59
+ return (content ?? [])
60
+ .filter((block) => typeof block === 'object' && block !== null && block.type === 'text')
61
+ .map(block => block.text ?? '')
62
+ .join('');
63
+ }
64
+ /**
65
+ * Render a file-editing tool call's arguments apply-patch style: the path
66
+ * plus the old/new content. Without this a delegation's edits collapsed to
67
+ * the bare path (or nothing) and the transcript hid what the member actually
68
+ * changed — the kimi twin of codex's fileChange → ApplyPatch fold.
69
+ * @param name - the tool name the wire recorded.
70
+ * @param record - the call's argument object.
71
+ * @returns the patch-style summary, or undefined when the tool is not a
72
+ * known file-editing shape.
73
+ */
74
+ function editArgsOf(name, record) {
75
+ const path = typeof record['path'] === 'string' ? record['path'] : undefined;
76
+ if (name === 'Edit' || name === 'MultiEdit') {
77
+ if (path === undefined || path.trim() === '')
78
+ return undefined;
79
+ const edits = Array.isArray(record['edits']) ? record['edits'] : [record];
80
+ const hunks = [];
81
+ for (const edit of edits) {
82
+ const oldText = typeof edit['old_string'] === 'string' ? edit['old_string'] : '';
83
+ const newText = typeof edit['new_string'] === 'string' ? edit['new_string'] : '';
84
+ if (oldText === '' && newText === '')
85
+ continue;
86
+ hunks.push('@@', ...oldText.split('\n').map(line => `-${line}`), ...newText.split('\n').map(line => `+${line}`));
87
+ }
88
+ // An edit call whose content the wire did not carry keeps the scalar path.
89
+ if (hunks.length === 0)
90
+ return undefined;
91
+ return [`update: ${path}`, ...hunks].join('\n');
92
+ }
93
+ if (name === 'Write') {
94
+ if (path === undefined || path.trim() === '')
95
+ return undefined;
96
+ const content = typeof record['content'] === 'string' ? record['content'] : '';
97
+ // A Write whose content the wire did not carry keeps the scalar path.
98
+ if (content === '')
99
+ return undefined;
100
+ return [`add: ${path}`, ...content.split('\n').map(line => `+${line}`)].join('\n');
101
+ }
102
+ // Patch-style tools (ApplyPatch and friends): the patch text IS the argument.
103
+ const patch = record['patch'] ?? record['diff'];
104
+ if (typeof patch === 'string' && patch.trim() !== '')
105
+ return patch;
106
+ return undefined;
107
+ }
108
+ /** Render a tool call's arguments into a compact summary. */
109
+ function argsOf(name, args) {
110
+ if (args === undefined)
111
+ return undefined;
112
+ if (typeof args === 'string')
113
+ return args.trim() === '' ? undefined : args;
114
+ if (typeof args !== 'object' || args === null || Array.isArray(args))
115
+ return undefined;
116
+ const record = args;
117
+ // File-editing tools first: their edit content outranks the bare path the
118
+ // scalar preference below would otherwise reduce them to.
119
+ const edit = editArgsOf(name, record);
120
+ if (edit !== undefined)
121
+ return edit;
122
+ // WebSearch/FetchURL expose a query/url; prefer the most meaningful scalar.
123
+ for (const key of ['query', 'url', 'path', 'command']) {
124
+ const value = record[key];
125
+ if (typeof value === 'string' && value.trim() !== '')
126
+ return value.trim();
127
+ }
128
+ // A tool whose meaningful arg is none of the four (Agent's prompt, Grep's
129
+ // pattern, …): a short key=value summary beats dropping the call entirely.
130
+ const pairs = [];
131
+ for (const [key, value] of Object.entries(record)) {
132
+ if (typeof value === 'string' && value.trim() !== '')
133
+ pairs.push(`${key}=${value.trim()}`);
134
+ else if (typeof value === 'number' || typeof value === 'boolean')
135
+ pairs.push(`${key}=${String(value)}`);
136
+ }
137
+ if (pairs.length === 0)
138
+ return undefined;
139
+ const summary = pairs.join(' ');
140
+ return summary.length > 200 ? `${summary.slice(0, 200)}…` : summary;
141
+ }
142
+ /**
143
+ * Parse a session's wire log into transcript lines plus every usage record.
144
+ * @param sessionDir - the session directory.
145
+ * @returns the transcript.
146
+ */
147
+ export async function readKimiTranscript(sessionDir) {
148
+ const sessionId = sessionDir.split('session_').pop() ?? sessionDir;
149
+ const lines = [];
150
+ const usageRecords = [];
151
+ let model;
152
+ let modelTurn;
153
+ let lineModel;
154
+ const push = (line) => {
155
+ lines.push({ ...line, ...lineModel === undefined ? {} : { model: lineModel } });
156
+ };
157
+ // Wire turns are 0-based on loop events; a `turn.prompt` also opens a new
158
+ // round. The transcript uses 1-based turns matching the dsh child session's
159
+ // turn/start numbering.
160
+ let turn = 0;
161
+ let turnSeen = false;
162
+ // Tool calls by uuid and toolCallId, so a result attaches to its own call
163
+ // even when several calls run in parallel within one step.
164
+ const callsByUuid = new Map();
165
+ const callsByToolCallId = new Map();
166
+ let text;
167
+ try {
168
+ text = await readFile(wirePath(sessionDir), 'utf8');
169
+ }
170
+ catch (error) {
171
+ if (error.code === 'ENOENT') {
172
+ return { sessionId, lines, usageRecords };
173
+ }
174
+ throw error;
175
+ }
176
+ for (const raw of text.split('\n')) {
177
+ const line = raw.trim();
178
+ if (line === '')
179
+ continue;
180
+ let event;
181
+ try {
182
+ event = JSON.parse(line);
183
+ }
184
+ catch {
185
+ continue;
186
+ }
187
+ if (event.type === 'usage.record') {
188
+ // The record's own `model` field is the observed model identifier
189
+ // (verified against kimi 0.39.x wires); the `llm.request` fallback
190
+ // below covers wires whose usage records omit it. Last one seen wins:
191
+ // a resumed session's later rounds append later records.
192
+ if (typeof event.model === 'string' && event.model !== '') {
193
+ model = lineModel = event.model;
194
+ modelTurn = turn || 1;
195
+ }
196
+ const usage = usageFromWire(event.usage);
197
+ if (usage !== undefined)
198
+ usageRecords.push({ line: lines.length, usage });
199
+ continue;
200
+ }
201
+ if (event.type === 'llm.request') {
202
+ lineModel = typeof event.model === 'string' && event.model !== '' ? event.model : undefined;
203
+ if (typeof event.model === 'string' && event.model !== '') {
204
+ model = event.model;
205
+ modelTurn = turn || 1;
206
+ }
207
+ continue;
208
+ }
209
+ if (event.type === 'turn.prompt') {
210
+ lineModel = undefined;
211
+ // Each round's user prompt opens the next turn; the matching
212
+ // context.append_message below is deduped against this text.
213
+ turnSeen = true;
214
+ turn += 1;
215
+ const prompt = textOf(event.input ?? []);
216
+ if (prompt.trim() !== '' && !isSystemReminder(prompt)) {
217
+ push({ kind: 'user', text: prompt, turn: turn === 0 ? 1 : turn });
218
+ }
219
+ continue;
220
+ }
221
+ if (event.type === 'context.append_message') {
222
+ const message = event.message;
223
+ const role = message?.role;
224
+ const textContent = textOf(message?.content ?? []);
225
+ if (role === 'user' && textContent.trim() !== '' && !isSystemReminder(textContent)) {
226
+ // A user append_message that duplicates the current turn.prompt (the
227
+ // ACP server replays the prompt into the context) must not open a new
228
+ // turn; when no turn.prompt preceded it, treat it as the round start.
229
+ // The dedupe compares TEXT, not just kind: a second DISTINCT user
230
+ // message in a row (a mid-round injection) is real content.
231
+ const last = lines[lines.length - 1];
232
+ if (turnSeen && last?.kind === 'user' && last.text === textContent) {
233
+ // Duplicate of the prompt; skip.
234
+ continue;
235
+ }
236
+ turnSeen = true;
237
+ turn += 1;
238
+ lineModel = undefined;
239
+ push({ kind: 'user', text: textContent, turn: turn === 0 ? 1 : turn });
240
+ }
241
+ else if (role === 'assistant' && textContent.trim() !== '') {
242
+ push({ kind: 'assistant', text: textContent, turn: turn === 0 ? 1 : turn });
243
+ }
244
+ continue;
245
+ }
246
+ if (event.type === 'context.append_loop_event') {
247
+ const loop = event.event;
248
+ if (loop === undefined)
249
+ continue;
250
+ const loopTurn = typeof loop.turnId === 'number' ? loop.turnId + 1 : undefined;
251
+ if (loopTurn !== undefined) {
252
+ turn = Math.max(turn, loopTurn);
253
+ turnSeen = true;
254
+ }
255
+ const part = loop.part;
256
+ const lineTurn = turn === 0 ? 1 : turn;
257
+ if (part?.type === 'text' && part.text !== undefined && part.text.trim() !== '') {
258
+ push({ kind: 'assistant', text: part.text, turn: lineTurn });
259
+ }
260
+ else if (part?.type === 'think' && part.think !== undefined && part.think.trim() !== '') {
261
+ push({ kind: 'think', text: part.think, turn: lineTurn });
262
+ }
263
+ else if (loop.type === 'tool.call') {
264
+ const call = loop;
265
+ const name = call.toolCall?.name ?? call.name ?? 'tool';
266
+ const args = argsOf(name, call.toolCall?.args ?? call.args);
267
+ const lineIndex = lines.length;
268
+ push({
269
+ kind: 'tool',
270
+ id: typeof call.toolCallId === 'string'
271
+ ? call.toolCallId
272
+ : typeof call.uuid === 'string' ? call.uuid : `kimi-tool-${lineIndex}`,
273
+ name,
274
+ ...args === undefined ? {} : { args },
275
+ turn: lineTurn,
276
+ });
277
+ if (typeof call.uuid === 'string')
278
+ callsByUuid.set(call.uuid, lineIndex);
279
+ if (typeof call.toolCallId === 'string')
280
+ callsByToolCallId.set(call.toolCallId, lineIndex);
281
+ }
282
+ else if (loop.type === 'tool.result') {
283
+ const result = loop;
284
+ const output = result.result?.output;
285
+ let target = typeof result.parentUuid === 'string'
286
+ ? callsByUuid.get(result.parentUuid)
287
+ : undefined;
288
+ if (target === undefined && typeof result.toolCallId === 'string') {
289
+ target = callsByToolCallId.get(result.toolCallId);
290
+ }
291
+ if (target === undefined) {
292
+ // No id to pair with: fall back to the most recent tool line so a
293
+ // wire without ids still renders the result on its call.
294
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
295
+ if (lines[index]?.kind === 'tool') {
296
+ target = index;
297
+ break;
298
+ }
299
+ }
300
+ }
301
+ if (output !== undefined && output.trim() !== '' && target !== undefined) {
302
+ const current = lines[target];
303
+ if (current !== undefined && current.kind === 'tool') {
304
+ lines[target] = { ...current, result: output };
305
+ }
306
+ }
307
+ }
308
+ else if (loop.type === 'content.part'
309
+ && part?.type !== undefined
310
+ && part.type !== 'text'
311
+ && part.type !== 'think') {
312
+ // A content part type this fold does not know (image, audio, a future
313
+ // plan part, …): keep a visible marker instead of the content
314
+ // vanishing from the transcript.
315
+ push({ kind: 'assistant', text: `[未支持的内容类型 ${part.type}]`, turn: lineTurn });
316
+ }
317
+ }
318
+ }
319
+ return { sessionId, lines, usageRecords, ...model === undefined ? {} : { model }, ...modelTurn === undefined ? {} : { modelTurn } };
320
+ }
321
+ /**
322
+ * Map a wire `usage.record` payload onto the shared usage contract. kimi
323
+ * reports input (uncached), cache read, cache creation, and output; the
324
+ * creation counter is its cache-write analog. Non-numeric or missing
325
+ * counters are dropped so the record stays usable when kimi omits a field.
326
+ * @param raw - the event's `usage` value.
327
+ * @returns the shared usage record, or undefined when nothing was reported.
328
+ */
329
+ export function usageFromWire(raw) {
330
+ const counters = raw;
331
+ if (counters === undefined || typeof counters !== 'object')
332
+ return undefined;
333
+ const num = (value) => {
334
+ const n = Number(value);
335
+ return Number.isFinite(n) ? n : undefined;
336
+ };
337
+ const input = num(counters.inputOther);
338
+ const output = num(counters.output);
339
+ const cacheRead = num(counters.inputCacheRead);
340
+ const cacheWrite = num(counters.inputCacheCreation);
341
+ if (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined) {
342
+ return undefined;
343
+ }
344
+ const usage = {
345
+ inputTokens: input ?? 0,
346
+ outputTokens: output ?? 0,
347
+ };
348
+ if (cacheRead !== undefined && cacheRead > 0)
349
+ usage.cacheReadTokens = cacheRead;
350
+ if (cacheWrite !== undefined && cacheWrite > 0)
351
+ usage.cacheWriteTokens = cacheWrite;
352
+ return usage;
353
+ }
354
+ /**
355
+ * Sum per-request usage records into one round total. Each `usage.record` is
356
+ * a single LLM request's accounting, so a round's totals are the sum of the
357
+ * records inside it.
358
+ * @param records - the usage records to add.
359
+ * @returns the summed usage, or undefined when the list is empty.
360
+ */
361
+ export function sumUsageRecords(records) {
362
+ let input = 0;
363
+ let output = 0;
364
+ let cacheRead = 0;
365
+ let cacheWrite = 0;
366
+ for (const record of records) {
367
+ input += record.usage.inputTokens ?? 0;
368
+ output += record.usage.outputTokens ?? 0;
369
+ cacheRead += record.usage.cacheReadTokens ?? 0;
370
+ cacheWrite += record.usage.cacheWriteTokens ?? 0;
371
+ }
372
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0)
373
+ return undefined;
374
+ const usage = { inputTokens: input, outputTokens: output };
375
+ if (cacheRead > 0)
376
+ usage.cacheReadTokens = cacheRead;
377
+ if (cacheWrite > 0)
378
+ usage.cacheWriteTokens = cacheWrite;
379
+ return usage;
380
+ }
381
+ /**
382
+ * Merge two usage windows into one running total — the delegation run's
383
+ * per-round accumulator: each mirror pass (live poll, settle) returns its
384
+ * own window's accounting, and the disjoint windows sum to the round's
385
+ * total. `undefined` operands pass through, so callers accumulate without
386
+ * absence checks.
387
+ * @param left - the running total, or undefined before the first window.
388
+ * @param right - the next window's usage, or undefined when it had none.
389
+ * @returns the merged total, or undefined when both operands are absent.
390
+ */
391
+ export function addTokenUsage(left, right) {
392
+ if (left === undefined)
393
+ return right;
394
+ if (right === undefined)
395
+ return left;
396
+ return sumUsageRecords([
397
+ { line: 0, usage: left },
398
+ { line: 0, usage: right },
399
+ ]);
400
+ }
401
+ /** Render a transcript as command-reply text. */
402
+ export function renderTranscript(transcript) {
403
+ if (transcript.lines.length === 0)
404
+ return `kimi session ${transcript.sessionId}: no readable transcript yet.`;
405
+ const parts = [`kimi session ${transcript.sessionId}:`];
406
+ for (const line of transcript.lines) {
407
+ if (line.kind === 'user') {
408
+ parts.push(`\n用户: ${line.text}`);
409
+ }
410
+ else if (line.kind === 'assistant') {
411
+ parts.push(`\nkimi: ${line.text}`);
412
+ }
413
+ else if (line.kind === 'think') {
414
+ parts.push(`\n[思考] ${line.text}`);
415
+ }
416
+ else {
417
+ // The other kinds are exhausted above; this is the tool line.
418
+ const call = `[工具 ${line.name}]${line.args !== undefined ? ` ${line.args}` : ''}`;
419
+ parts.push(`\n ↳ ${call}${line.result !== undefined ? ` → ${line.result}` : ''}`);
420
+ }
421
+ }
422
+ return parts.join('\n');
423
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Kimi Delegation",
4
+ "description": "Delegate tasks to the Kimi Code CLI from any session."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Kimi 委派",
4
+ "description": "从任意会话委派任务给 Kimi Code CLI。"
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@khorsheed/dsh-local-agent-kimi",
3
+ "description": "Kimi Code harness for dsh-local-agent: scoped KIMI_CODE_HOME, one-shot kimi -p delegation visible in the 子代理 surface, session records and preset wiring",
4
+ "keywords": [
5
+ "dsh",
6
+ "dsh-plugin",
7
+ "cordis",
8
+ "local-agent",
9
+ "kimi"
10
+ ],
11
+ "version": "0.1.0-rc.6",
12
+ "type": "module",
13
+ "main": "lib/index.js",
14
+ "types": "lib/types/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./lib/types/index.d.ts",
18
+ "default": "./lib/index.js"
19
+ },
20
+ "./invariant": {
21
+ "types": "./lib/types/invariant.d.ts",
22
+ "default": "./lib/invariant.js"
23
+ },
24
+ "./records": {
25
+ "types": "./lib/types/records.d.ts",
26
+ "default": "./lib/records.js"
27
+ },
28
+ "./client": {
29
+ "types": "./lib/types/client/index.d.ts",
30
+ "default": "./lib/client.js"
31
+ },
32
+ "./src/*": "./src/*",
33
+ "./cordis.patch.yml": "./cordis.patch.yml",
34
+ "./package.json": "./package.json",
35
+ "./locale/*.json": "./locale/*.json"
36
+ },
37
+ "dsh": {
38
+ "references": [
39
+ "@khorsheed/dsh-bundle-local-agent"
40
+ ],
41
+ "client": {
42
+ "inject": [
43
+ "@deepseek-ai/dsh-api-remotes",
44
+ "@deepseek-ai/dsh-client-locale",
45
+ "@deepseek-ai/dsh-client-ui-primitives",
46
+ "@deepseek-ai/dsh-client-ui-settings",
47
+ "@deepseek-ai/dsh-client-ui-slots"
48
+ ],
49
+ "platform": "web"
50
+ },
51
+ "bundle": {
52
+ "patch": "./cordis.patch.yml"
53
+ },
54
+ "compat": {
55
+ "minHost": "0.1.5-rc.1",
56
+ "notes": "liveMirrorGranularity token: host 0.1.5 retired the per-chunk session event, so token-mode deltas no longer land in the child session log — they ride the run-progress channel and the round settles as one combined assistant/message (identical final text); a late-arriving usage whose carrier message already mirrored is dropped with a warn (no usage-backfill event exists). The thinkingEffort config item (default high, provision-time only) and the effectiveSettings eval snapshot are plugin-level — scoped-file provisioning and reads with no host-API dependency on either host line",
57
+ "verifiedHost": "0.1.5-rc.1"
58
+ }
59
+ },
60
+ "files": [
61
+ "lib",
62
+ "cordis.patch.yml",
63
+ "locale/*.json"
64
+ ],
65
+ "license": "MIT",
66
+ "dependencies": {
67
+ "@khorsheed/dsh-local-agent": "^0.1.0-rc.6",
68
+ "@khorsheed/dsh-local-agent-tool-subagent": "^0.1.0-rc.6"
69
+ },
70
+ "peerDependencies": {
71
+ "@deepseek-ai/cordis": "^4.0.1",
72
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
79
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
80
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
81
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
82
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
83
+ "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
84
+ "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
85
+ "react": "^18.2.0"
86
+ },
87
+ "peerDependenciesMeta": {
88
+ "@deepseek-ai/dsh-api-remotes": {
89
+ "optional": true
90
+ },
91
+ "@deepseek-ai/dsh-client-locale": {
92
+ "optional": true
93
+ },
94
+ "@deepseek-ai/dsh-client-ui-primitives": {
95
+ "optional": true
96
+ },
97
+ "@deepseek-ai/dsh-client-ui-settings": {
98
+ "optional": true
99
+ },
100
+ "@deepseek-ai/dsh-client-ui-slots": {
101
+ "optional": true
102
+ },
103
+ "react": {
104
+ "optional": true
105
+ }
106
+ }
107
+ }