@khorsheed/dsh-local-agent-kimi 0.2.0-rc.3 → 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 +16 -1
- package/README.en.md +24 -112
- package/README.i18n.yaml +2 -2
- package/README.md +24 -112
- package/lib/client.js +63 -63
- package/lib/index.js +618 -343
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/kimi-cli-provider.js +39 -26
- package/lib/types/live-driver.d.ts +8 -5
- package/lib/types/live-driver.js +324 -119
- package/lib/types/model-broker.js +42 -1
- 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/package.json +2 -2
|
@@ -10,7 +10,14 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { readdir, stat } from 'node:fs/promises';
|
|
11
11
|
import { createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
12
12
|
import { persistChildSession } from '@khorsheed/dsh-local-agent';
|
|
13
|
-
import { readKimiTranscript, sumUsageRecords } from './session-view.js';
|
|
13
|
+
import { addTokenUsage, readKimiTranscript, sumUsageRecords } from './session-view.js';
|
|
14
|
+
/**
|
|
15
|
+
* The synthesized result for a call whose result the wire never recorded
|
|
16
|
+
* (a settle-pass fold, or a live reservation kimi never logged). The
|
|
17
|
+
* released-log contract resolves every advertised call before its step
|
|
18
|
+
* closes, so an interrupted call folds with this error result.
|
|
19
|
+
*/
|
|
20
|
+
export const KIMI_INTERRUPTED_TOOL_RESULT = '(工具调用被中断,未能记录返回结果)';
|
|
14
21
|
/** One user-role message event. */
|
|
15
22
|
function userEvent(text) {
|
|
16
23
|
return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } });
|
|
@@ -23,38 +30,46 @@ export function assistantEvent(blocks, model) {
|
|
|
23
30
|
});
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
|
-
* The step the next fold pass would assign in one turn
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
33
|
+
* The step the next fold pass would assign in one turn: the smallest step
|
|
34
|
+
* nothing holds (never written, never reserved at call time — callers skip
|
|
35
|
+
* their own reservations on top). Filling the smallest free step keeps the
|
|
36
|
+
* stored log dense: the released-log reader admits only dense, strictly
|
|
37
|
+
* sequential steps, and a reservation released mid-round (its content rode an
|
|
38
|
+
* earlier fold) leaves a gap only the next first-free write fills.
|
|
31
39
|
* @param childSession - the run's child session.
|
|
32
40
|
* @param turn - the round's turn number.
|
|
33
|
-
* @returns the
|
|
41
|
+
* @returns the smallest free step in the turn.
|
|
34
42
|
*/
|
|
35
43
|
export function nextKimiSessionStep(childSession, turn) {
|
|
36
|
-
|
|
44
|
+
const taken = new Set();
|
|
37
45
|
for (const event of childSession.snapshotEvents()) {
|
|
38
46
|
const data = event.data;
|
|
39
47
|
if (data.turn === turn && typeof data.step === 'number')
|
|
40
|
-
|
|
48
|
+
taken.add(data.step);
|
|
41
49
|
}
|
|
50
|
+
let next = 1;
|
|
51
|
+
while (taken.has(next))
|
|
52
|
+
next += 1;
|
|
42
53
|
return next;
|
|
43
54
|
}
|
|
44
55
|
/**
|
|
45
|
-
*
|
|
46
|
-
* the
|
|
47
|
-
*
|
|
48
|
-
* is never
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* cards instead of `[工具 X]` text.
|
|
56
|
+
* The step a durable write takes: one past the highest step this turn has
|
|
57
|
+
* opened in the log. The released-log reader admits steps only in order —
|
|
58
|
+
* one past the last, never below it — so the coordinate is decided when the
|
|
59
|
+
* step is written, never reserved ahead (a reservation filled later lands
|
|
60
|
+
* below a higher step already written: a gap, then a step out of order).
|
|
61
|
+
* Stream and tool reservations only key the live preview.
|
|
52
62
|
*/
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
export function nextWrittenKimiStep(childSession, turn) {
|
|
64
|
+
let last = 0;
|
|
65
|
+
for (const event of childSession.snapshotEvents()) {
|
|
66
|
+
if (event.type !== 'step/start')
|
|
67
|
+
continue;
|
|
68
|
+
const data = event.data;
|
|
69
|
+
if (data.turn === turn && typeof data.step === 'number' && data.step > last)
|
|
70
|
+
last = data.step;
|
|
56
71
|
}
|
|
57
|
-
return
|
|
72
|
+
return last + 1;
|
|
58
73
|
}
|
|
59
74
|
/** The run-progress delta text for one transcript line (any kind). */
|
|
60
75
|
export function kimiLineProgressText(line) {
|
|
@@ -64,10 +79,14 @@ export function kimiLineProgressText(line) {
|
|
|
64
79
|
return `${call}${line.result !== undefined ? ` → ${line.result}` : ''}`;
|
|
65
80
|
}
|
|
66
81
|
/**
|
|
67
|
-
* Mirror one kimi session's transcript into
|
|
68
|
-
* user prompt becomes a `user/message`, each
|
|
69
|
-
*
|
|
70
|
-
*
|
|
82
|
+
* Mirror one kimi session's transcript into a dsh subagent session: each
|
|
83
|
+
* user prompt becomes a `user/message`, and each WIRE STEP (one LLM request:
|
|
84
|
+
* its thinking, reply text and tool calls share the wire's own step number)
|
|
85
|
+
* folds into one dsh step whose `assistant/message` carries the reasoning,
|
|
86
|
+
* text and tool-call blocks together — the host session's own shape, so the
|
|
87
|
+
* standard surfaces (conversation, Context Lens) read one request per LLM
|
|
88
|
+
* call. Then the events are appended to persistence so the child session is
|
|
89
|
+
* viewable.
|
|
71
90
|
*
|
|
72
91
|
* A resumed round (or a live poll) passes the already-mirrored
|
|
73
92
|
* transcript-line count so only the delta is appended — re-mirroring earlier
|
|
@@ -77,22 +96,29 @@ export function kimiLineProgressText(line) {
|
|
|
77
96
|
* already in the child session, so a round mirrored across several live polls
|
|
78
97
|
* does not restart step 1 per poll.
|
|
79
98
|
*
|
|
80
|
-
* Every folded step is
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* sequential fold never takes a
|
|
99
|
+
* Every folded step is one ATOMIC `step/start`–`step/end` triple, written
|
|
100
|
+
* only when its content lands: the released-log reader admits only dense,
|
|
101
|
+
* strictly sequential steps (one open at a time — a second `step/start`
|
|
102
|
+
* while another step is open fails the session's reload validation), and the
|
|
103
|
+
* live conversation assembler needs the boundary to materialize the step at
|
|
104
|
+
* all. A fold paired with in-flight streams lands at the earliest consumed
|
|
105
|
+
* reservation (see {@link KimiMirrorStreams}), after `finalizeBefore` has
|
|
106
|
+
* closed every stream reservation below it; a sequential fold never takes a
|
|
107
|
+
* step a stream reserved. Mid-round, a wire step the wire has not CLOSED yet
|
|
108
|
+
* (no `step.end`) holds the pass — its parts may still arrive — and so does
|
|
109
|
+
* an unresulted tool call (the reader resolves every advertised call before
|
|
110
|
+
* the step closes; the result merges into the call's line in place, so the
|
|
111
|
+
* next pass sees it). The settle pass folds the remainder with a synthesized
|
|
112
|
+
* interrupted result. Lines the wire never step-tagged (older kimi) fold one
|
|
113
|
+
* by one, immediately, as before.
|
|
88
114
|
*
|
|
89
115
|
* Usage accounting is exactly-once ACROSS passes: each `usage.record` is
|
|
90
|
-
* tagged with the transcript position it occurred at,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
116
|
+
* tagged with the transcript position it occurred at, a pass owns the records
|
|
117
|
+
* between the old offset and its own consumed end, and inside the window each
|
|
118
|
+
* record rides the unit whose line span encloses it — one record per wire
|
|
119
|
+
* step, landing on its own step's message. (A record raced PAST its content
|
|
120
|
+
* encloses into the next unit instead: attribution off by one in a flush
|
|
121
|
+
* race, totals still exact.)
|
|
96
122
|
* @param ctx - host context carrying the session persistence service.
|
|
97
123
|
* @param childSession - the dsh subagent session created for this delegation.
|
|
98
124
|
* @param homeDir - the `kimi` harness's scoped home.
|
|
@@ -100,7 +126,9 @@ export function kimiLineProgressText(line) {
|
|
|
100
126
|
* recent session (a one-shot `kimi -p` run always creates a fresh one).
|
|
101
127
|
* @param fromLines - transcript lines already mirrored into the child session
|
|
102
128
|
* (0 for the first round); only lines after this offset are appended.
|
|
103
|
-
* @returns the new
|
|
129
|
+
* @returns the new mirror offset and the newly mirrored lines' texts. A held
|
|
130
|
+
* unit (open wire step or unresulted tool) stops the offset before itself;
|
|
131
|
+
* the next pass retries from there.
|
|
104
132
|
*/
|
|
105
133
|
export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, fromLines = 0, options) {
|
|
106
134
|
let workspaces;
|
|
@@ -164,7 +192,6 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
164
192
|
}
|
|
165
193
|
if (transcript === undefined || transcript.lines.length === 0)
|
|
166
194
|
return { total: 0, texts: [] };
|
|
167
|
-
const newTotal = transcript.lines.length;
|
|
168
195
|
const delta = transcript.lines.slice(fromLines);
|
|
169
196
|
const observedModel = options?.turn === undefined || transcript.modelTurn === options.turn ? transcript.model : undefined;
|
|
170
197
|
// The ROUND's tool calls, counted over the transcript lines carrying this
|
|
@@ -172,146 +199,212 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
172
199
|
// whose delta a live poll already drained still owes the round its real
|
|
173
200
|
// count, and a resume round must not inherit the earlier rounds' calls.
|
|
174
201
|
const toolCalls = roundToolCalls(transcript.lines, options?.turn);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (line !== undefined && line.kind !== 'user' && line.kind !== 'tool')
|
|
190
|
-
lastAssistant = index;
|
|
202
|
+
const units = [];
|
|
203
|
+
for (const line of delta) {
|
|
204
|
+
if (line === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
const last = units.at(-1);
|
|
207
|
+
const head = last?.[0];
|
|
208
|
+
if (last !== undefined && head !== undefined
|
|
209
|
+
&& line.kind !== 'user' && head.kind !== 'user'
|
|
210
|
+
&& line.wireStep !== undefined && line.wireStep === head.wireStep
|
|
211
|
+
&& line.turn === head.turn) {
|
|
212
|
+
last.push(line);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
units.push([line]);
|
|
191
216
|
}
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
217
|
+
// Mid-round, a unit that may still grow HOLDS the pass: a wire-step group
|
|
218
|
+
// whose step the wire has not closed yet, or a unit whose tool call lacks
|
|
219
|
+
// its result (the released-log contract resolves every advertised call
|
|
220
|
+
// before its step closes; the result merges into the call's line in place,
|
|
221
|
+
// so the next pass sees it). The offset stops before the first such unit
|
|
222
|
+
// and the fold retries once the wire completes it. The settle pass never
|
|
223
|
+
// holds — a result that will never arrive folds as a synthesized
|
|
224
|
+
// interrupted one.
|
|
225
|
+
let holdAt = -1;
|
|
226
|
+
if (options?.settle !== true) {
|
|
227
|
+
for (let index = 0; index < units.length; index += 1) {
|
|
228
|
+
const unit = units[index];
|
|
229
|
+
const head = unit[0];
|
|
230
|
+
const openGroup = head.kind !== 'user'
|
|
231
|
+
&& head.wireStep !== undefined
|
|
232
|
+
&& !transcript.closedSteps.has(`${head.turn}:${head.wireStep}`);
|
|
233
|
+
if (openGroup || unit.some(line => line.kind === 'tool' && line.result === undefined)) {
|
|
234
|
+
holdAt = index;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const foldedUnits = holdAt < 0 ? units : units.slice(0, holdAt);
|
|
240
|
+
const consumedTotal = fromLines + foldedUnits.reduce((n, unit) => n + unit.length, 0);
|
|
241
|
+
// Usage is exactly-once across passes: a pass owns the records at positions
|
|
242
|
+
// [fromLines, consumedTotal] (the boundary included — a record flushed right
|
|
243
|
+
// after its content otherwise falls out of every span). kimi's norm is one
|
|
244
|
+
// usage.record per wire step, right BEFORE the step's content, so a
|
|
245
|
+
// step-tagged unit claims its own line span's records; records no tagged
|
|
246
|
+
// unit claims (untagged spans on older wires, or the boundary) sum onto the
|
|
247
|
+
// pass's last folded content unit — the pre-tagging shape.
|
|
248
|
+
const unitUsage = [];
|
|
249
|
+
const claimed = [];
|
|
250
|
+
let unitStart = fromLines;
|
|
251
|
+
for (const unit of foldedUnits) {
|
|
252
|
+
const start = unitStart;
|
|
253
|
+
unitStart += unit.length;
|
|
254
|
+
const head = unit[0];
|
|
255
|
+
if (head.kind !== 'user' && head.wireStep !== undefined) {
|
|
256
|
+
claimed.push({ from: start, to: unitStart });
|
|
257
|
+
unitUsage.push(sumUsageRecords(transcript.usageRecords.filter(record => record.line >= start && record.line < unitStart)));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
unitUsage.push(undefined);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const unclaimedUsage = sumUsageRecords(transcript.usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal
|
|
264
|
+
&& !claimed.some(span => record.line >= span.from && record.line < span.to)));
|
|
265
|
+
const deltaUsage = sumUsageRecords(transcript.usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal));
|
|
266
|
+
// The steps a fold may NOT take: every step already written (the session's
|
|
267
|
+
// own event ledger), consulted per turn. A round mirrored across several
|
|
268
|
+
// live polls keeps its earlier writes, so later polls never reuse them.
|
|
269
|
+
const takenSteps = new Map();
|
|
201
270
|
for (const event of childSession.snapshotEvents()) {
|
|
202
271
|
const data = event.data;
|
|
203
272
|
if (typeof data.turn === 'number' && typeof data.step === 'number') {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const call = event.data;
|
|
208
|
-
openCalls.set(call.callId, { turn: call.turn, step: call.step, seq: event.seq });
|
|
209
|
-
}
|
|
210
|
-
else if (event.type === 'tool/result') {
|
|
211
|
-
// The call id moved with the V4 message shape: rc.1's native tool-role
|
|
212
|
-
// message carries it at the top level, the pre-V4 user-role shape
|
|
213
|
-
// wrapped it in the single content block. Read both — a session
|
|
214
|
-
// mirrored before the upgrade still owes its old-shape results pairing.
|
|
215
|
-
const message = event.data.message;
|
|
216
|
-
const id = message?.toolCallId ?? message?.content?.[0]?.toolCallId;
|
|
217
|
-
if (id !== undefined) {
|
|
218
|
-
openCalls.delete(id);
|
|
219
|
-
settledCalls.add(id);
|
|
220
|
-
}
|
|
273
|
+
const taken = takenSteps.get(data.turn) ?? new Set();
|
|
274
|
+
taken.add(data.step);
|
|
275
|
+
takenSteps.set(data.turn, taken);
|
|
221
276
|
}
|
|
222
277
|
}
|
|
223
278
|
/**
|
|
224
|
-
* The
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* rejects).
|
|
279
|
+
* The fold's coordinate: the smallest step of the turn that nothing holds
|
|
280
|
+
* — neither written nor currently reserved. Filling the smallest free step
|
|
281
|
+
* keeps the stored log dense even when a stream reservation below was
|
|
282
|
+
* released mid-round (its content rode an earlier fold's step).
|
|
229
283
|
*/
|
|
230
284
|
const nextFoldStep = (turn) => {
|
|
231
|
-
|
|
232
|
-
const reserved
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
steps.set(turn, step + 1);
|
|
285
|
+
const taken = new Set(takenSteps.get(turn));
|
|
286
|
+
for (const reserved of options?.streams?.reservedSteps(turn) ?? [])
|
|
287
|
+
taken.add(reserved);
|
|
288
|
+
let step = 1;
|
|
289
|
+
while (taken.has(step))
|
|
290
|
+
step += 1;
|
|
239
291
|
return step;
|
|
240
292
|
};
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
openCalls.delete(line.id);
|
|
256
|
-
settledCalls.add(line.id);
|
|
257
|
-
// No second boundary pair here: the call's fold already opened AND closed
|
|
258
|
-
// this (turn, step), and the live assembler throws on a duplicate start
|
|
259
|
-
// match for the same context. The step stays registered in the location
|
|
260
|
-
// index, so the bare result still resolves to the step location — and the
|
|
261
|
-
// tool definition never consults it anyway.
|
|
262
|
-
childSession.append('tool/result', {
|
|
263
|
-
turn: call.turn,
|
|
264
|
-
step: call.step,
|
|
265
|
-
message: createToolResultMessage({
|
|
266
|
-
callId: line.id,
|
|
267
|
-
content: [{ type: 'text', text: line.result }],
|
|
268
|
-
isError: false,
|
|
269
|
-
}),
|
|
270
|
-
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
|
|
271
|
-
texts.push(kimiLineProgressText(line));
|
|
272
|
-
}
|
|
273
|
-
if (delta.length === 0) {
|
|
274
|
-
// No new lines this pass: only the late-result backfill above could have
|
|
275
|
-
// produced events. Persist those and skip the empty delta loop.
|
|
276
|
-
if (texts.length > 0) {
|
|
277
|
-
await persistChildSession(ctx, childSession);
|
|
293
|
+
/** Record a fold's coordinate chosen by a stream pairing. */
|
|
294
|
+
const markTaken = (turn, step) => {
|
|
295
|
+
const written = takenSteps.get(turn) ?? new Set();
|
|
296
|
+
written.add(step);
|
|
297
|
+
takenSteps.set(turn, written);
|
|
298
|
+
};
|
|
299
|
+
/** The window's accounting that no fold carried (dropped units' sums). */
|
|
300
|
+
let carryUsage;
|
|
301
|
+
// The pass's last content unit carries the records no tagged unit claimed.
|
|
302
|
+
let lastContentUnit = -1;
|
|
303
|
+
for (let index = foldedUnits.length - 1; index >= 0; index -= 1) {
|
|
304
|
+
if (foldedUnits[index][0]?.kind !== 'user') {
|
|
305
|
+
lastContentUnit = index;
|
|
306
|
+
break;
|
|
278
307
|
}
|
|
279
|
-
return {
|
|
280
|
-
total: newTotal,
|
|
281
|
-
texts,
|
|
282
|
-
...deltaUsage !== undefined ? { usage: deltaUsage } : {},
|
|
283
|
-
...usageAttached ? { usageAttached: true } : {},
|
|
284
|
-
...observedModel !== undefined ? { model: observedModel } : {},
|
|
285
|
-
...toolCalls !== undefined ? { toolCalls } : {},
|
|
286
|
-
};
|
|
287
308
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
const texts = [];
|
|
310
|
+
for (let unitIndex = 0; unitIndex < foldedUnits.length; unitIndex += 1) {
|
|
311
|
+
const unit = foldedUnits[unitIndex];
|
|
312
|
+
const head = unit[0];
|
|
313
|
+
// The unit's own records plus whatever a dropped unit handed forward.
|
|
314
|
+
let usage = addTokenUsage(carryUsage, unitUsage[unitIndex]);
|
|
315
|
+
carryUsage = undefined;
|
|
316
|
+
if (unitIndex === lastContentUnit)
|
|
317
|
+
usage = addTokenUsage(usage, unclaimedUsage);
|
|
292
318
|
// The wire's turn number (1-based, matching the provider's turn/start
|
|
293
319
|
// numbering) is the dsh round; no offset is needed because both count the
|
|
294
320
|
// same rounds.
|
|
295
|
-
const turn =
|
|
296
|
-
if (
|
|
321
|
+
const turn = head.turn;
|
|
322
|
+
if (head.kind === 'user') {
|
|
297
323
|
// The live driver appends the round's user/message at turn start (so the
|
|
298
324
|
// question renders before the streamed think/text instead of after it).
|
|
299
325
|
// Skip the wire's copy of the same prompt — scoped to THIS turn so two
|
|
300
326
|
// rounds with identical prompts still fold independently.
|
|
301
|
-
if (userAlreadyAppended(childSession, turn,
|
|
327
|
+
if (userAlreadyAppended(childSession, turn, head.text))
|
|
302
328
|
continue;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
329
|
+
childSession.append('user/message', userEvent(head.text), { surfaceOp: 'append' });
|
|
330
|
+
texts.push(head.text);
|
|
331
|
+
carryUsage = usage;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const toolLines = unit.filter((line) => line.kind === 'tool');
|
|
335
|
+
const reasoning = unit.filter(line => line.kind === 'think').map(line => line.text).join('');
|
|
336
|
+
const text = unit.filter(line => line.kind === 'assistant').map(line => line.text).join('');
|
|
337
|
+
const kinds = [
|
|
338
|
+
...reasoning.trim() === '' ? [] : ['think'],
|
|
339
|
+
...text.trim() === '' ? [] : ['text'],
|
|
340
|
+
];
|
|
341
|
+
// Pair the fold with the round's in-flight streams, consuming the kinds
|
|
342
|
+
// present: the fold lands at the earliest consumed reservation, and a
|
|
343
|
+
// kind already gap-sealed keeps its durable triple — its content stays
|
|
344
|
+
// out of this message.
|
|
345
|
+
const pairing = kinds.length === 0 ? undefined : options?.streams?.completeStreams(turn, kinds);
|
|
346
|
+
const sealed = new Set(pairing?.sealed ?? []);
|
|
347
|
+
const blocks = [];
|
|
348
|
+
if (reasoning.trim() !== '' && !sealed.has('think'))
|
|
349
|
+
blocks.push({ type: 'reasoning', text: reasoning });
|
|
350
|
+
if (text.trim() !== '' && !sealed.has('text'))
|
|
351
|
+
blocks.push({ type: 'text', text });
|
|
352
|
+
for (const line of toolLines) {
|
|
353
|
+
blocks.push({ type: 'tool-call', id: line.id, name: line.name, arguments: line.args ?? '' });
|
|
306
354
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
355
|
+
for (const line of unit)
|
|
356
|
+
texts.push(kimiLineProgressText(line));
|
|
357
|
+
if (blocks.length === 0) {
|
|
358
|
+
// Every content kind already durable via a gap fill and no tool
|
|
359
|
+
// lifecycle to fold: the unit drops; its usage rides the next fold.
|
|
360
|
+
carryUsage = usage;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// A tool reservation only pins chronological order — the fold's step
|
|
364
|
+
// carries the whole lifecycle, so the reserved coordinate frees.
|
|
365
|
+
for (const line of toolLines)
|
|
366
|
+
options?.streams?.releaseTool?.(line);
|
|
367
|
+
let step = pairing?.step ?? nextFoldStep(turn);
|
|
368
|
+
// Close every stream reservation below the fold step first: stored steps
|
|
369
|
+
// must land dense and strictly sequential.
|
|
370
|
+
options?.streams?.finalizeBefore?.(turn, step);
|
|
371
|
+
// Finalization may have WRITTEN a reservation below the provisional
|
|
372
|
+
// coordinate — or RELEASED an empty one without a write. Either way the
|
|
373
|
+
// fold must take the smallest step still free afterwards: writing above
|
|
374
|
+
// a released-but-never-written reservation strands a permanent gap the
|
|
375
|
+
// released-log reader rejects as corruption.
|
|
376
|
+
let sequential = nextKimiSessionStep(childSession, turn);
|
|
377
|
+
const reservedAfter = options?.streams?.reservedSteps(turn) ?? [];
|
|
378
|
+
while (reservedAfter.includes(sequential))
|
|
379
|
+
sequential += 1;
|
|
380
|
+
step = Math.min(step, sequential);
|
|
381
|
+
markTaken(turn, step);
|
|
382
|
+
// The durable coordinate is decided now, in write order (see nextWrittenKimiStep).
|
|
383
|
+
step = nextWrittenKimiStep(childSession, turn);
|
|
384
|
+
// The wire's own request config rides a request/header ahead of the fold
|
|
385
|
+
// (the host session's own shape) — Context Lens reads the child log's
|
|
386
|
+
// config from it instead of showing 未记录. Only a change is logged.
|
|
387
|
+
const requestConfig = unit.map(line => line.kind !== 'user' ? line.requestConfig : undefined).find(config => config !== undefined);
|
|
388
|
+
if (requestConfig?.model !== undefined) {
|
|
389
|
+
appendRequestHeaderIfChanged(childSession, {
|
|
390
|
+
provider: 'kimi-cli',
|
|
391
|
+
model: requestConfig.model,
|
|
392
|
+
...requestConfig.reasoningEffort === undefined ? {} : { reasoningEffort: requestConfig.reasoningEffort },
|
|
393
|
+
...requestConfig.maxTokens === undefined ? {} : { maxTokens: requestConfig.maxTokens },
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const model = [...unit].reverse().find(line => line.model !== undefined)?.model;
|
|
397
|
+
childSession.append('step/start', { turn, step });
|
|
398
|
+
childSession.append('assistant/message', {
|
|
399
|
+
turn,
|
|
400
|
+
step,
|
|
401
|
+
message: assistantEvent(blocks, model),
|
|
402
|
+
stream: [],
|
|
403
|
+
...usage === undefined ? {} : { usage },
|
|
404
|
+
}, { surfaceOp: 'append' });
|
|
405
|
+
// The native tool lifecycle inside the same step: the message advertises
|
|
406
|
+
// every call, then each call resolves before the step closes.
|
|
407
|
+
for (const line of toolLines) {
|
|
315
408
|
const call = childSession.append('tool/call', {
|
|
316
409
|
turn,
|
|
317
410
|
step,
|
|
@@ -319,66 +412,28 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
|
|
|
319
412
|
name: line.name,
|
|
320
413
|
arguments: line.args ?? '',
|
|
321
414
|
});
|
|
322
|
-
|
|
323
|
-
if (line.result !== undefined) {
|
|
324
|
-
childSession.append('tool/result', {
|
|
325
|
-
turn,
|
|
326
|
-
step,
|
|
327
|
-
message: createToolResultMessage({
|
|
328
|
-
callId: line.id,
|
|
329
|
-
content: [{ type: 'text', text: line.result }],
|
|
330
|
-
isError: false,
|
|
331
|
-
}),
|
|
332
|
-
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
|
|
333
|
-
}
|
|
334
|
-
childSession.append('step/end', { turn, step });
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
// A completed think/assistant line pairs with its in-flight stream when
|
|
338
|
-
// one is live (the token granularity's per-kind stream): the fold lands
|
|
339
|
-
// at the stream's reserved step, finalizing the snapshots — the step
|
|
340
|
-
// opens only when no snapshot ever landed (a fast item that stayed
|
|
341
|
-
// under the throttle), and closes here. A (turn, step) never takes a
|
|
342
|
-
// second step/start.
|
|
343
|
-
const completion = options?.streams?.completeStream(line);
|
|
344
|
-
if (completion !== undefined) {
|
|
345
|
-
steps.set(turn, Math.max(steps.get(turn) ?? 1, completion.step + 1));
|
|
346
|
-
if (!completion.opened)
|
|
347
|
-
childSession.append('step/start', { turn, step: completion.step });
|
|
348
|
-
if (index === lastAssistant && deltaUsage !== undefined)
|
|
349
|
-
usageAttached = true;
|
|
350
|
-
childSession.append('assistant/message', {
|
|
351
|
-
turn,
|
|
352
|
-
step: completion.step,
|
|
353
|
-
message: assistantEvent(lineBlocks(line), line.model),
|
|
354
|
-
stream: [],
|
|
355
|
-
...index === lastAssistant && deltaUsage !== undefined ? { usage: deltaUsage } : {},
|
|
356
|
-
}, { surfaceOp: 'append' });
|
|
357
|
-
childSession.append('step/end', { turn, step: completion.step });
|
|
358
|
-
texts.push(kimiLineProgressText(line));
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
const step = nextFoldStep(turn);
|
|
362
|
-
childSession.append('step/start', { turn, step });
|
|
363
|
-
if (index === lastAssistant && deltaUsage !== undefined)
|
|
364
|
-
usageAttached = true;
|
|
365
|
-
childSession.append('assistant/message', {
|
|
415
|
+
childSession.append('tool/result', {
|
|
366
416
|
turn,
|
|
367
417
|
step,
|
|
368
|
-
message:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
418
|
+
message: createToolResultMessage({
|
|
419
|
+
callId: line.id,
|
|
420
|
+
content: [{ type: 'text', text: line.result ?? KIMI_INTERRUPTED_TOOL_RESULT }],
|
|
421
|
+
isError: line.result === undefined,
|
|
422
|
+
}),
|
|
423
|
+
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
|
|
374
424
|
}
|
|
425
|
+
childSession.append('step/end', { turn, step });
|
|
375
426
|
}
|
|
376
427
|
await persistChildSession(ctx, childSession);
|
|
428
|
+
// The window's accounting that folded nowhere: dropped units' carry plus the
|
|
429
|
+
// unclaimed sum when no content unit folded at all.
|
|
430
|
+
const remainder = addTokenUsage(carryUsage, lastContentUnit === -1 ? unclaimedUsage : undefined);
|
|
377
431
|
return {
|
|
378
|
-
total:
|
|
432
|
+
total: consumedTotal,
|
|
379
433
|
texts,
|
|
380
434
|
...deltaUsage !== undefined ? { usage: deltaUsage } : {},
|
|
381
|
-
...
|
|
435
|
+
...deltaUsage !== undefined && remainder === undefined ? { usageAttached: true } : {},
|
|
436
|
+
...remainder !== undefined ? { usageUnattached: remainder } : {},
|
|
382
437
|
...observedModel !== undefined ? { model: observedModel } : {},
|
|
383
438
|
...toolCalls !== undefined ? { toolCalls } : {},
|
|
384
439
|
};
|
|
@@ -405,6 +460,27 @@ export function roundToolCalls(lines, turn) {
|
|
|
405
460
|
}
|
|
406
461
|
return count === 0 ? undefined : { count, byName };
|
|
407
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Write the fold's request/header when the wire's declared config differs from
|
|
465
|
+
* the child log's last one (the host logs changed snapshots, not per-call
|
|
466
|
+
* duplicates): first write is `initial`, a differing one `change`, an
|
|
467
|
+
* identical one skipped.
|
|
468
|
+
*/
|
|
469
|
+
function appendRequestHeaderIfChanged(childSession, config) {
|
|
470
|
+
const serialized = JSON.stringify(config);
|
|
471
|
+
let last;
|
|
472
|
+
for (const event of childSession.snapshotEvents()) {
|
|
473
|
+
if (event.type === 'request/header') {
|
|
474
|
+
last = JSON.stringify(event.data.header?.config);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (last === serialized)
|
|
478
|
+
return;
|
|
479
|
+
childSession.append('request/header', {
|
|
480
|
+
header: { config },
|
|
481
|
+
reason: last === undefined ? 'initial' : 'change',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
408
484
|
/**
|
|
409
485
|
* Whether the turn already carries a user/message with this exact text (the
|
|
410
486
|
* live driver's round-start append). Scoped to the events at or after this
|