@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
package/lib/types/live-driver.js
CHANGED
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
* throttled `mirrorKimiDelta` passes and the settle pass stays authoritative
|
|
17
17
|
* (one fold, one offset, zero divergence between the two drivers). Every
|
|
18
18
|
* transcript item folds 1:1 into the child session log.
|
|
19
|
-
* Live output
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Live output rides the `local-agent/stream` channel: chunk deltas
|
|
20
|
+
* accumulate per streaming item and publish throttled updates at the item's
|
|
21
|
+
* reserved (turn, step) — the member live node renders them as-is — while
|
|
22
|
+
* the item's durable content lands as ONE atomic step/start→…→step/end
|
|
23
|
+
* triple at that step, written by the completing wire line's fold or at
|
|
24
|
+
* settle. Durable steps stay dense and strictly sequential (the released-log
|
|
25
|
+
* reader validates exactly that on every cold open); snapshots never hold a
|
|
26
|
+
* step open across appends.
|
|
24
27
|
*
|
|
25
28
|
* Lifecycle mirrors the dsh/codex live drivers' discipline: lazy spawn with an
|
|
26
29
|
* initialize handshake (loadSession capability required — crash recovery is
|
|
@@ -37,7 +40,7 @@ import { configureKimiSession } from './session-configuration.js';
|
|
|
37
40
|
import { kimiNativeConfiguration } from './model-catalog.js';
|
|
38
41
|
import { StringDecoder } from 'node:string_decoder';
|
|
39
42
|
import { randomUUID } from 'node:crypto';
|
|
40
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
43
|
+
import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
41
44
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
42
45
|
import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
43
46
|
import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIVE_FLUSH_INTERVAL_MS } from '@khorsheed/dsh-local-agent';
|
|
@@ -45,7 +48,7 @@ import { endpointAuthEnv, hasEndpointCredential } from './endpoint-auth.js';
|
|
|
45
48
|
import { ensureKimiEndpointProvider, KIMI_MANAGED_BASE_URL } from './provision.js';
|
|
46
49
|
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
|
|
47
50
|
import { DEFAULT_DISPOSE_GRACE_MS, mirrorKimiDelta, textTask, } from './kimi-cli-provider.js';
|
|
48
|
-
import { assistantEvent, nextKimiSessionStep } from './session-mirror.js';
|
|
51
|
+
import { assistantEvent, KIMI_INTERRUPTED_TOOL_RESULT, nextKimiSessionStep, nextWrittenKimiStep } from './session-mirror.js';
|
|
49
52
|
import { addTokenUsage } from './session-view.js';
|
|
50
53
|
import { writeKimiDefaultModel } from './provision.js';
|
|
51
54
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
@@ -793,25 +796,17 @@ export class KimiAcpLiveDriver {
|
|
|
793
796
|
/** The round's accumulated assistant text (the run output — chunks are the only source). */
|
|
794
797
|
let roundText = '';
|
|
795
798
|
/**
|
|
796
|
-
* The step ledger:
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
799
|
+
* The step ledger: the steps in-flight reservations currently hold this
|
|
800
|
+
* round. A reservation's durable content lands at the fold that consumes
|
|
801
|
+
* it (possibly carrying several reservations' kinds in one message — the
|
|
802
|
+
* wire step's fold), which RELEASES the other consumed coordinates for
|
|
803
|
+
* later folds; only a reservation kimi never completes (a plan stream, an
|
|
804
|
+
* unwired tool notification) stays held until its settle-time finalize.
|
|
805
|
+
* The file fold skips held steps for its sequential folds, so a tool card
|
|
806
|
+
* folded mid-stream lands ABOVE the stream's step and the projection
|
|
807
|
+
* renders the round's chronological order.
|
|
802
808
|
*/
|
|
803
809
|
const reservedSteps = [];
|
|
804
|
-
/**
|
|
805
|
-
* The streaming items seen this round . kimi's ACP
|
|
806
|
-
* chunks carry no item id, so the stream key is synthetic per kind per
|
|
807
|
-
* round (the codex live driver's fallback shape): thought chunks share
|
|
808
|
-
* one stream, message chunks another. Deltas accumulate into throttled
|
|
809
|
-
* snapshot assistant/messages appended at the stream's reserved
|
|
810
|
-
* (turn, step) — the host folds repeated settles at one coordinate into
|
|
811
|
-
* one live-updating chat node. The wire line that completes the stream's
|
|
812
|
-
* kind folds at the same step and finalizes it; an entry whose completion
|
|
813
|
-
* never folds is force-finalized at settle.
|
|
814
|
-
*/
|
|
815
810
|
const streams = new Map();
|
|
816
811
|
/** The stream the latest delta accumulated into (a kind switch force-snapshots it). */
|
|
817
812
|
let activeStream;
|
|
@@ -839,7 +834,23 @@ export class KimiAcpLiveDriver {
|
|
|
839
834
|
};
|
|
840
835
|
/** A stream's synthetic key: kimi's ACP deltas carry no item id, so kind + turn pairs them. */
|
|
841
836
|
let generation = 0;
|
|
837
|
+
/**
|
|
838
|
+
* One step reservation per generation, shared by the think and text
|
|
839
|
+
* streams of the same response segment: the host's own live shape is
|
|
840
|
+
* reasoning + text on ONE step, and a per-kind step would leave the
|
|
841
|
+
* consumed reservation's stream frames pointing at a step that never
|
|
842
|
+
* materializes (an empty trailing step breaks the host's answer
|
|
843
|
+
* detection, folding the reply into the collapsed process block).
|
|
844
|
+
*/
|
|
845
|
+
const generationSteps = new Map();
|
|
842
846
|
const seenToolCalls = new Set();
|
|
847
|
+
/**
|
|
848
|
+
* ACP tool_call notifications' chronological reservations (call id →
|
|
849
|
+
* step, plus the notification's display title). No durable event lands at
|
|
850
|
+
* notification time: the native lifecycle (advertisement, call, result in
|
|
851
|
+
* one closed step) is written by the wire line's fold, or at settle with
|
|
852
|
+
* an interrupted placeholder when kimi never logged the call.
|
|
853
|
+
*/
|
|
843
854
|
const toolSteps = new Map();
|
|
844
855
|
const streamKey = (kind) => `kimi-stream-${kind}-${turn}-${generation}`;
|
|
845
856
|
/** The plan stream's key: a snapshot stream beside the think/text delta streams. */
|
|
@@ -857,7 +868,7 @@ export class KimiAcpLiveDriver {
|
|
|
857
868
|
const stream = streams.get(key);
|
|
858
869
|
if (stream !== undefined) {
|
|
859
870
|
stream.text += text;
|
|
860
|
-
|
|
871
|
+
publishStreamSnapshot(stream, false);
|
|
861
872
|
}
|
|
862
873
|
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text });
|
|
863
874
|
};
|
|
@@ -881,133 +892,304 @@ export class KimiAcpLiveDriver {
|
|
|
881
892
|
if (plan === '')
|
|
882
893
|
return;
|
|
883
894
|
// The plan's own snapshot stream (think-styled): every update replaces
|
|
884
|
-
// the text and
|
|
885
|
-
//
|
|
886
|
-
//
|
|
895
|
+
// the text and publishes at the reserved (turn, step); the stream
|
|
896
|
+
// finalizes at settle (the wire carries no plan line that could
|
|
897
|
+
// complete it) — or early, when a fold needs its step closed.
|
|
887
898
|
reserveStream(planKey, 'think');
|
|
888
899
|
const stream = streams.get(planKey);
|
|
889
900
|
if (stream !== undefined && stream.text !== plan) {
|
|
890
901
|
stream.text = plan;
|
|
891
|
-
|
|
902
|
+
publishStreamSnapshot(stream, true);
|
|
892
903
|
}
|
|
893
904
|
};
|
|
894
905
|
/**
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
* legitimately) and, when `withUsage`, the round's uncarried usage. An
|
|
899
|
-
* empty stream leaves no boundary at all.
|
|
906
|
+
* The live-output channel. Snapshots are browser-only previews (never
|
|
907
|
+
* `local-agent/stream` log events) — durable step content lands once per
|
|
908
|
+
* stream, at finalization, as the fold's atomic triple.
|
|
900
909
|
*/
|
|
901
910
|
const streamPublisher = localAgent?.liveStreams === undefined ? undefined
|
|
902
911
|
: new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, error => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
|
|
903
912
|
const liveFlush = new LiveFlush(error => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
904
913
|
const pendingStreamArrival = new Map();
|
|
905
|
-
|
|
906
|
-
|
|
914
|
+
/**
|
|
915
|
+
* Publish one snapshot of a streaming item at its reserved (turn, step)
|
|
916
|
+
* through the live channel. Batched per stream on a bounded deadline
|
|
917
|
+
* unless `force`. Never writes durable content.
|
|
918
|
+
*/
|
|
919
|
+
const publishStreamSnapshot = (stream, force) => {
|
|
920
|
+
if (stream.text.trim() === '' || stream.sealed === true)
|
|
907
921
|
return;
|
|
908
922
|
const now = Date.now();
|
|
909
923
|
if (!force) {
|
|
910
924
|
if (!pendingStreamArrival.has(stream.step))
|
|
911
925
|
pendingStreamArrival.set(stream.step, now);
|
|
912
|
-
liveFlush.schedule(stream.step, () =>
|
|
926
|
+
liveFlush.schedule(stream.step, () => publishStreamSnapshot(stream, true));
|
|
913
927
|
return;
|
|
914
928
|
}
|
|
915
929
|
liveFlush.cancel(stream.step);
|
|
916
930
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
917
931
|
pendingStreamArrival.delete(stream.step);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
932
|
+
// The kind rides the item id so two kinds sharing one step never collide.
|
|
933
|
+
// Browser-only (preview): the step is reserved, not yet open in the log,
|
|
934
|
+
// and a durable checkpoint there would land before its step/start — a
|
|
935
|
+
// replayed log then orders that step first and loses the turn's final
|
|
936
|
+
// answer under the fold. The durable content is the fold's atomic triple.
|
|
937
|
+
streamPublisher?.preview(stream.step, stream.kind, stream.text, { receivedAt, itemId: stream.kind });
|
|
938
|
+
stream.lastSnapshotLen = stream.text.length;
|
|
939
|
+
};
|
|
940
|
+
/**
|
|
941
|
+
* Write the stream's durable content as ONE atomic
|
|
942
|
+
* step/start→assistant/message→step/end triple at its reserved step and
|
|
943
|
+
* seal the reservation (its completing wire line, when one exists, pairs
|
|
944
|
+
* and drops). `interrupted` marks a non-completed round's partial (a
|
|
945
|
+
* cancelled turn reads 已停止 legitimately); `withUsage` attaches the
|
|
946
|
+
* round's uncarried usage.
|
|
947
|
+
*/
|
|
948
|
+
/**
|
|
949
|
+
* Write the durable content of every stream sharing one step as ONE atomic
|
|
950
|
+
* step/start→assistant/message→step/end triple (a generation's think and
|
|
951
|
+
* text merge into a single message — the host's own shape) and seal the
|
|
952
|
+
* reservations. `interrupted` marks a non-completed round's partial (a
|
|
953
|
+
* cancelled turn reads 已停止 legitimately); `withUsage` attaches the
|
|
954
|
+
* round's uncarried usage.
|
|
955
|
+
*/
|
|
956
|
+
const finalizeStep = (step, entries, interrupted, withUsage) => {
|
|
957
|
+
liveFlush.cancel(step);
|
|
958
|
+
pendingStreamArrival.delete(step);
|
|
959
|
+
const reasoning = entries.filter(([, stream]) => stream.kind === 'think').map(([, stream]) => stream.text).join('');
|
|
960
|
+
const text = entries.filter(([, stream]) => stream.kind === 'text').map(([, stream]) => stream.text).join('');
|
|
961
|
+
const blocks = [];
|
|
962
|
+
if (reasoning.trim() !== '')
|
|
963
|
+
blocks.push({ type: 'reasoning', text: reasoning });
|
|
964
|
+
if (text.trim() !== '')
|
|
965
|
+
blocks.push({ type: 'text', text });
|
|
966
|
+
if (blocks.length === 0) {
|
|
967
|
+
// Nothing durable to write: release the coordinate instead of pinning
|
|
968
|
+
// it forever — a later fold landing above would otherwise strand a
|
|
969
|
+
// permanent step gap the released-log reader rejects as corruption.
|
|
970
|
+
streamPublisher?.finish(step);
|
|
971
|
+
const keys = new Set(entries.map(([key]) => key));
|
|
972
|
+
const held = new Set([...streams.entries()]
|
|
973
|
+
.filter(([key, stream]) => !keys.has(key) && stream.sealed !== true)
|
|
974
|
+
.map(([, stream]) => stream.step));
|
|
975
|
+
if (!held.has(step)) {
|
|
976
|
+
const index = reservedSteps.indexOf(step);
|
|
977
|
+
if (index >= 0)
|
|
978
|
+
reservedSteps.splice(index, 1);
|
|
979
|
+
}
|
|
980
|
+
for (const [key, stream] of entries) {
|
|
981
|
+
stream.sealed = true;
|
|
982
|
+
generationSteps.delete(stream.generation);
|
|
983
|
+
if (activeStream === key)
|
|
984
|
+
activeStream = undefined;
|
|
922
985
|
}
|
|
923
|
-
streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
|
|
924
|
-
stream.lastSnapshotAt = now;
|
|
925
|
-
stream.lastSnapshotLen = stream.text.length;
|
|
926
986
|
return;
|
|
927
987
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
988
|
+
// The reservation keyed the preview; the log takes the next step in write order.
|
|
989
|
+
const durable = nextWrittenKimiStep(childSession, turn);
|
|
990
|
+
childSession.append('step/start', { turn, step: durable });
|
|
932
991
|
childSession.append('assistant/message', {
|
|
933
992
|
turn,
|
|
934
|
-
step:
|
|
935
|
-
message: assistantEvent(
|
|
936
|
-
stream.kind === 'think'
|
|
937
|
-
? { type: 'reasoning', text: stream.text }
|
|
938
|
-
: { type: 'text', text: stream.text },
|
|
939
|
-
], runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
993
|
+
step: durable,
|
|
994
|
+
message: assistantEvent(blocks, runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
940
995
|
stream: [],
|
|
941
996
|
...withUsage && roundUsage !== undefined ? { usage: roundUsage } : {},
|
|
942
997
|
...interrupted ? { interrupted: true } : {},
|
|
943
998
|
}, { surfaceOp: 'append' });
|
|
944
999
|
if (withUsage)
|
|
945
1000
|
roundUsage = undefined;
|
|
946
|
-
|
|
947
|
-
|
|
1001
|
+
streamPublisher?.finish(step);
|
|
1002
|
+
childSession.append('step/end', { turn, step: durable });
|
|
1003
|
+
for (const [key, stream] of entries) {
|
|
1004
|
+
stream.sealed = true;
|
|
1005
|
+
generationSteps.delete(stream.generation);
|
|
1006
|
+
if (activeStream === key)
|
|
1007
|
+
activeStream = undefined;
|
|
1008
|
+
}
|
|
948
1009
|
persist();
|
|
949
1010
|
};
|
|
1011
|
+
/**
|
|
1012
|
+
* Write the durable step for an ACP tool call kimi never logged to the
|
|
1013
|
+
* wire (a crash between the notification and the wire flush): the
|
|
1014
|
+
* notification's title stands in for the name and the result is the
|
|
1015
|
+
* shared interrupted placeholder — the native lifecycle in one step.
|
|
1016
|
+
*/
|
|
1017
|
+
/** Whether this turn's log already carries the tool call (folded from the wire). */
|
|
1018
|
+
const toolLogged = (id) => childSession.snapshotEvents().some(event => event.type === 'tool/call' && event.data.turn === turn
|
|
1019
|
+
&& event.data.callId === id);
|
|
1020
|
+
const finalizeOrphanTool = (id, entry) => {
|
|
1021
|
+
const name = entry.title ?? 'tool';
|
|
1022
|
+
// The reservation keyed the preview; the log takes the next step in write order.
|
|
1023
|
+
const step = nextWrittenKimiStep(childSession, turn);
|
|
1024
|
+
childSession.append('step/start', { turn, step });
|
|
1025
|
+
childSession.append('assistant/message', {
|
|
1026
|
+
turn,
|
|
1027
|
+
step,
|
|
1028
|
+
message: assistantEvent([{ type: 'tool-call', id: id, name, arguments: '' }], runtime?.modelConfiguration?.currentModel ?? roundModel),
|
|
1029
|
+
stream: [],
|
|
1030
|
+
}, { surfaceOp: 'append' });
|
|
1031
|
+
const call = childSession.append('tool/call', {
|
|
1032
|
+
turn,
|
|
1033
|
+
step,
|
|
1034
|
+
callId: id,
|
|
1035
|
+
name,
|
|
1036
|
+
arguments: '',
|
|
1037
|
+
});
|
|
1038
|
+
childSession.append('tool/result', {
|
|
1039
|
+
turn,
|
|
1040
|
+
step,
|
|
1041
|
+
message: createToolResultMessage({
|
|
1042
|
+
callId: id,
|
|
1043
|
+
content: [{ type: 'text', text: KIMI_INTERRUPTED_TOOL_RESULT }],
|
|
1044
|
+
isError: true,
|
|
1045
|
+
}),
|
|
1046
|
+
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
|
|
1047
|
+
childSession.append('step/end', { turn, step });
|
|
1048
|
+
};
|
|
1049
|
+
/**
|
|
1050
|
+
* The fold's gap fill: a fold about to write at `step` first finalizes
|
|
1051
|
+
* every live stream reserved below it (ascending), so the stored log's
|
|
1052
|
+
* steps land dense and strictly sequential. Only streams can sit in the
|
|
1053
|
+
* gap: an unresulted tool line holds its fold instead (session-mirror).
|
|
1054
|
+
*/
|
|
1055
|
+
const finalizeBefore = (targetTurn, step) => {
|
|
1056
|
+
if (targetTurn !== turn)
|
|
1057
|
+
return;
|
|
1058
|
+
const pending = [...streams.entries()]
|
|
1059
|
+
.filter(([, stream]) => stream.sealed !== true && stream.step < step)
|
|
1060
|
+
.sort((a, b) => a[1].step - b[1].step);
|
|
1061
|
+
const byStep = new Map();
|
|
1062
|
+
for (const entry of pending) {
|
|
1063
|
+
const group = byStep.get(entry[1].step) ?? [];
|
|
1064
|
+
group.push(entry);
|
|
1065
|
+
byStep.set(entry[1].step, group);
|
|
1066
|
+
}
|
|
1067
|
+
for (const [at, entries] of byStep)
|
|
1068
|
+
finalizeStep(at, entries, false, false);
|
|
1069
|
+
};
|
|
950
1070
|
/**
|
|
951
1071
|
* Reserve the step one streaming item will occupy — past every folded
|
|
952
1072
|
* line (the session's own event ledger: the file fold writes events
|
|
953
|
-
* synchronously) and every earlier reservation. A kind switch
|
|
954
|
-
* freshness snapshot of the current stream (skipped when its text has
|
|
955
|
-
* grown since the last one); its
|
|
956
|
-
* reserved step when the wire line
|
|
1073
|
+
* synchronously) and every earlier reservation. A kind switch publishes
|
|
1074
|
+
* one freshness snapshot of the current stream (skipped when its text has
|
|
1075
|
+
* not grown since the last one); its durable triple still lands at its
|
|
1076
|
+
* own reserved step when the wire line folds or the round settles.
|
|
957
1077
|
*/
|
|
958
1078
|
const reserveStream = (key, kind) => {
|
|
959
1079
|
if (activeStream !== undefined && activeStream !== key) {
|
|
960
1080
|
const previous = streams.get(activeStream);
|
|
961
1081
|
if (previous !== undefined && previous.text.length !== previous.lastSnapshotLen) {
|
|
962
|
-
|
|
1082
|
+
publishStreamSnapshot(previous, true);
|
|
963
1083
|
}
|
|
964
1084
|
activeStream = undefined;
|
|
965
1085
|
}
|
|
966
1086
|
let stream = streams.get(key);
|
|
1087
|
+
// A sealed entry's durable triple already landed: later deltas (a plan
|
|
1088
|
+
// rewriting itself after a gap fill, a same-generation think resuming)
|
|
1089
|
+
// open a FRESH reservation instead of growing a closed step.
|
|
1090
|
+
if (stream?.sealed === true) {
|
|
1091
|
+
streams.delete(key);
|
|
1092
|
+
stream = undefined;
|
|
1093
|
+
}
|
|
967
1094
|
if (stream === undefined) {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1095
|
+
// The think and text streams of one generation share ONE reserved
|
|
1096
|
+
// step; the plan stream keeps its own.
|
|
1097
|
+
let step = key === planKey ? undefined : generationSteps.get(generation);
|
|
1098
|
+
if (step === undefined) {
|
|
1099
|
+
step = nextKimiSessionStep(childSession, turn);
|
|
1100
|
+
while (reservedSteps.includes(step))
|
|
1101
|
+
step += 1;
|
|
1102
|
+
reservedSteps.push(step);
|
|
1103
|
+
if (key !== planKey)
|
|
1104
|
+
generationSteps.set(generation, step);
|
|
1105
|
+
}
|
|
1106
|
+
stream = { kind, step, generation: key === planKey ? -1 : generation, text: '', lastSnapshotLen: 0 };
|
|
973
1107
|
streams.set(key, stream);
|
|
974
1108
|
}
|
|
975
1109
|
activeStream = key;
|
|
976
1110
|
};
|
|
977
1111
|
/**
|
|
978
|
-
* The fold-pass coordination surface
|
|
979
|
-
* fold skips reserved steps for sequential folds
|
|
980
|
-
*
|
|
981
|
-
*
|
|
1112
|
+
* The fold-pass coordination surface: the file
|
|
1113
|
+
* fold skips reserved steps for sequential folds, pairs one fold with the
|
|
1114
|
+
* round's in-flight streams of the fold's kinds (consuming both — a wire
|
|
1115
|
+
* step's think and text ride ONE durable step), and closes every
|
|
1116
|
+
* reservation below each fold step first (finalizeBefore) — scoped to
|
|
1117
|
+
* THIS round's turn, so an earlier round's leftover lines never consume
|
|
1118
|
+
* this round's stream.
|
|
982
1119
|
*/
|
|
983
1120
|
const mirrorStreams = {
|
|
984
1121
|
reservedSteps: t => t === turn ? reservedSteps : [],
|
|
985
|
-
|
|
1122
|
+
finalizeBefore,
|
|
1123
|
+
releaseTool: (line) => {
|
|
986
1124
|
if (line.turn !== turn)
|
|
987
|
-
return
|
|
988
|
-
const
|
|
989
|
-
if (
|
|
990
|
-
return
|
|
1125
|
+
return;
|
|
1126
|
+
const entry = toolSteps.get(line.id);
|
|
1127
|
+
if (entry === undefined)
|
|
1128
|
+
return;
|
|
991
1129
|
toolSteps.delete(line.id);
|
|
992
|
-
|
|
1130
|
+
// The fold's step carries the whole lifecycle; the coordinate frees.
|
|
1131
|
+
const index = reservedSteps.indexOf(entry.step);
|
|
1132
|
+
if (index >= 0)
|
|
1133
|
+
reservedSteps.splice(index, 1);
|
|
993
1134
|
},
|
|
994
|
-
|
|
995
|
-
if (
|
|
1135
|
+
completeStreams: (t, kinds) => {
|
|
1136
|
+
if (t !== turn)
|
|
996
1137
|
return undefined;
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1138
|
+
const picked = [];
|
|
1139
|
+
for (const kind of kinds) {
|
|
1140
|
+
const entry = [...streams.entries()].find(([key, stream]) => key !== planKey && stream.kind === kind);
|
|
1141
|
+
if (entry !== undefined)
|
|
1142
|
+
picked.push(entry);
|
|
1143
|
+
}
|
|
1144
|
+
if (picked.length === 0)
|
|
1000
1145
|
return undefined;
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1146
|
+
const sealed = [];
|
|
1147
|
+
for (const [key, stream] of picked) {
|
|
1148
|
+
streams.delete(key);
|
|
1149
|
+
generationSteps.delete(stream.generation);
|
|
1150
|
+
if (activeStream === key)
|
|
1151
|
+
activeStream = undefined;
|
|
1152
|
+
// A sealed entry's durable triple already landed: its content stays
|
|
1153
|
+
// out of the fold, and its coordinate keeps its (durable) place.
|
|
1154
|
+
if (stream.sealed === true) {
|
|
1155
|
+
sealed.push(stream.kind);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
liveFlush.cancel(stream.step);
|
|
1159
|
+
}
|
|
1160
|
+
const open = picked.filter(([, stream]) => stream.sealed !== true);
|
|
1161
|
+
if (open.length === 0)
|
|
1162
|
+
return { sealed };
|
|
1163
|
+
// The reservations whose content rides this fold free their own —
|
|
1164
|
+
// unless another open stream still holds the same (shared) step.
|
|
1165
|
+
const held = new Set([...streams.values()].filter(stream => stream.sealed !== true).map(stream => stream.step));
|
|
1166
|
+
for (const [, stream] of open) {
|
|
1167
|
+
if (held.has(stream.step))
|
|
1168
|
+
continue;
|
|
1169
|
+
const index = reservedSteps.indexOf(stream.step);
|
|
1170
|
+
if (index >= 0)
|
|
1171
|
+
reservedSteps.splice(index, 1);
|
|
1172
|
+
}
|
|
1173
|
+
for (const step of new Set(open.map(([, stream]) => stream.step)))
|
|
1174
|
+
streamPublisher?.finish(step);
|
|
1175
|
+
// The fold's coordinate: the earliest consumed reservation — or a
|
|
1176
|
+
// lower step an earlier release freed, so the stored log stays dense.
|
|
1177
|
+
// A step whose triple an earlier fold already wrote takes nothing.
|
|
1178
|
+
const written = new Set();
|
|
1179
|
+
for (const event of childSession.snapshotEvents()) {
|
|
1180
|
+
const data = event.data;
|
|
1181
|
+
if (event.type === 'step/start' && data.turn === turn && typeof data.step === 'number')
|
|
1182
|
+
written.add(data.step);
|
|
1183
|
+
}
|
|
1184
|
+
let free = nextKimiSessionStep(childSession, turn);
|
|
1185
|
+
while (reservedSteps.includes(free))
|
|
1186
|
+
free += 1;
|
|
1187
|
+
const candidates = [...new Set(open.map(([, stream]) => stream.step))].filter(step => !written.has(step));
|
|
1188
|
+
const step = Math.min(free, ...candidates);
|
|
1189
|
+
return { step, sealed };
|
|
1008
1190
|
},
|
|
1009
1191
|
};
|
|
1010
|
-
const mirrorOptions = () => ({ streams: mirrorStreams });
|
|
1192
|
+
const mirrorOptions = (settle = false) => ({ streams: mirrorStreams, ...settle ? { settle: true } : {} });
|
|
1011
1193
|
/**
|
|
1012
1194
|
* Observation accounting across mirror passes: a pass whose window
|
|
1013
1195
|
* attached its usage to a folded message is already carried; anything else
|
|
@@ -1016,8 +1198,8 @@ export class KimiAcpLiveDriver {
|
|
|
1016
1198
|
* the latest.
|
|
1017
1199
|
*/
|
|
1018
1200
|
const noteMirrorUsage = (delta) => {
|
|
1019
|
-
if (delta.
|
|
1020
|
-
roundUsage = addTokenUsage(roundUsage, delta.
|
|
1201
|
+
if (delta.usageUnattached !== undefined) {
|
|
1202
|
+
roundUsage = addTokenUsage(roundUsage, delta.usageUnattached);
|
|
1021
1203
|
}
|
|
1022
1204
|
if (delta.model !== undefined)
|
|
1023
1205
|
roundModel = delta.model;
|
|
@@ -1078,22 +1260,32 @@ export class KimiAcpLiveDriver {
|
|
|
1078
1260
|
const stream = streams.get(key);
|
|
1079
1261
|
if (stream !== undefined) {
|
|
1080
1262
|
stream.text += text;
|
|
1081
|
-
|
|
1263
|
+
publishStreamSnapshot(stream, false);
|
|
1082
1264
|
}
|
|
1083
1265
|
}
|
|
1084
1266
|
}
|
|
1085
1267
|
else if (kind === 'tool_call') {
|
|
1086
1268
|
const id = typeof update['toolCallId'] === 'string' ? update['toolCallId'] : undefined;
|
|
1087
1269
|
if (id === undefined || !seenToolCalls.has(id)) {
|
|
1088
|
-
|
|
1270
|
+
// The wire flush can win the race: a call already folded into the
|
|
1271
|
+
// log (its notification arriving late) needs no reservation — one
|
|
1272
|
+
// would never be released and settle would write the call twice.
|
|
1273
|
+
if (id !== undefined && toolLogged(id))
|
|
1274
|
+
seenToolCalls.add(id);
|
|
1275
|
+
else if (id !== undefined) {
|
|
1089
1276
|
seenToolCalls.add(id);
|
|
1277
|
+
// Reserve the chronological step only — nothing durable lands at
|
|
1278
|
+
// notification time. The wire line's fold writes the whole native
|
|
1279
|
+
// lifecycle in one step; a call kimi never logs finalizes as an
|
|
1280
|
+
// interrupted placeholder at settle.
|
|
1090
1281
|
let step = nextKimiSessionStep(childSession, turn);
|
|
1091
|
-
|
|
1092
|
-
step
|
|
1282
|
+
while (reservedSteps.includes(step))
|
|
1283
|
+
step += 1;
|
|
1093
1284
|
reservedSteps.push(step);
|
|
1094
|
-
toolSteps.set(id,
|
|
1095
|
-
|
|
1096
|
-
|
|
1285
|
+
toolSteps.set(id, {
|
|
1286
|
+
step,
|
|
1287
|
+
...typeof update['title'] === 'string' ? { title: update['title'] } : {},
|
|
1288
|
+
});
|
|
1097
1289
|
}
|
|
1098
1290
|
// ACP has no text-item ids. A new tool call separates generation
|
|
1099
1291
|
// segments; late file folds consume same-kind segments in order.
|
|
@@ -1191,7 +1383,7 @@ export class KimiAcpLiveDriver {
|
|
|
1191
1383
|
let stableReads = 0;
|
|
1192
1384
|
const deadline = Date.now() + SETTLE_MIRROR_QUIESCE_MS;
|
|
1193
1385
|
for (;;) {
|
|
1194
|
-
const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions());
|
|
1386
|
+
const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions(true));
|
|
1195
1387
|
noteMirrorUsage(delta);
|
|
1196
1388
|
localAgent?.reportRunProgress(childSession.id, { kind: 'mirror', mirroredLines: delta.total });
|
|
1197
1389
|
stableReads = delta.total === lastTotal ? stableReads + 1 : 0;
|
|
@@ -1222,33 +1414,46 @@ export class KimiAcpLiveDriver {
|
|
|
1222
1414
|
if (turnOpened) {
|
|
1223
1415
|
// Reconcile the file fold INSIDE the turn window
|
|
1224
1416
|
// (bounded quiescence — the price of the wire flush race; a mirror
|
|
1225
|
-
// failure never fails the round),
|
|
1226
|
-
// fold
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1417
|
+
// failure never fails the round), then finalize every reservation
|
|
1418
|
+
// the fold left behind in ASCENDING step order, so the stored log's
|
|
1419
|
+
// steps stay dense and strictly sequential — the contract the
|
|
1420
|
+
// released-log reader validates on every cold open:
|
|
1421
|
+
// - a stream whose completing wire line never folded (abort, or a
|
|
1422
|
+
// flush outlasting the quiescence window) finalizes from its
|
|
1423
|
+
// accumulated text — interrupted on a non-completed round, the
|
|
1424
|
+
// LAST stream carrying the round's uncarried usage;
|
|
1425
|
+
// - a tool reservation kimi never logged to the wire finalizes as
|
|
1426
|
+
// an interrupted placeholder step.
|
|
1231
1427
|
try {
|
|
1232
1428
|
await settleMirror();
|
|
1233
1429
|
}
|
|
1234
1430
|
catch (error) {
|
|
1235
1431
|
this.ctx.logger.warn(`subagent-kimi: live settle mirror failed: ${thrown(error).message}`);
|
|
1236
1432
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1433
|
+
const pending = [
|
|
1434
|
+
...[...[...streams.entries()]
|
|
1435
|
+
.filter(([, stream]) => stream.sealed !== true)
|
|
1436
|
+
.reduce((byStep, entry) => {
|
|
1437
|
+
const group = byStep.get(entry[1].step) ?? [];
|
|
1438
|
+
group.push(entry);
|
|
1439
|
+
byStep.set(entry[1].step, group);
|
|
1440
|
+
return byStep;
|
|
1441
|
+
}, new Map())
|
|
1442
|
+
.entries()].map(([step, entries]) => ({ step, entries })),
|
|
1443
|
+
...[...toolSteps.entries()].map(([toolId, tool]) => ({ step: tool.step, toolId, tool })),
|
|
1444
|
+
].sort((a, b) => a.step - b.step);
|
|
1445
|
+
const lastStream = pending.map((entry, index) => 'entries' in entry ? index : -1).filter(index => index >= 0).at(-1);
|
|
1446
|
+
pending.forEach((entry, index) => {
|
|
1447
|
+
if ('entries' in entry)
|
|
1448
|
+
finalizeStep(entry.step, entry.entries, settled.stopReason !== 'completed', index === lastStream);
|
|
1449
|
+
else if (!toolLogged(entry.toolId))
|
|
1450
|
+
finalizeOrphanTool(entry.toolId, entry.tool);
|
|
1451
|
+
});
|
|
1452
|
+
streams.clear();
|
|
1251
1453
|
toolSteps.clear();
|
|
1454
|
+
activeStream = undefined;
|
|
1455
|
+
if (pending.length > 0)
|
|
1456
|
+
persist();
|
|
1252
1457
|
if (settled.stopReason === 'completed') {
|
|
1253
1458
|
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
1254
1459
|
}
|