@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.
@@ -35,6 +35,42 @@ function dedupe(candidates) {
35
35
  }
36
36
  return out;
37
37
  }
38
+ /**
39
+ * Scope the served directory to the surface asking for it (the same helper
40
+ * lives in each provider's broker — fold into the family core when the
41
+ * directory faces next evolve):
42
+ *
43
+ * - memberless (the card's add-model dialog, the invite picker): the
44
+ * endpoint's served catalog only — the configuration/history extras the
45
+ * broker's info extension adds for STATUS display (a config-defined model
46
+ * the endpoint does not serve) must never become add candidates;
47
+ * - a member whose harness has a curated list: the list IS the vocabulary,
48
+ * in the list's own order (the same rule `choices` already follows) — a
49
+ * curated value the endpoint no longer serves stays as a bare row (the
50
+ * card shows it the same way), and the member's current effective model is
51
+ * kept visible as a bare entry when it sits outside the list, so a pre-list
52
+ * binding never renders blank;
53
+ * - a member without a curated list: the legacy extended vocabulary.
54
+ */
55
+ function scopeServedDirectory(directory, opts) {
56
+ if (opts.member && opts.curated.length === 0)
57
+ return directory;
58
+ const served = directory.entries.filter(entry => entry.source === 'native');
59
+ if (!opts.member)
60
+ return { ...directory, entries: served };
61
+ const byValue = new Map(served.flatMap(entry => entry.resolvedModel === undefined
62
+ ? [[entry.value, entry]]
63
+ : [[entry.value, entry], [entry.resolvedModel, entry]]));
64
+ // Curated ORDER leads (the card's first row is the default); a curated value
65
+ // the endpoint no longer serves stays listed as a bare row, exactly like the
66
+ // card shows it.
67
+ const entries = opts.curated.map(value => byValue.get(value) ?? { value, label: value, source: 'configuration' });
68
+ const current = opts.current?.trim();
69
+ if (current !== undefined && current !== '' && !entries.some(entry => entry.value === current || entry.resolvedModel === current)) {
70
+ entries.push({ value: current, label: current, source: 'configuration' });
71
+ }
72
+ return { ...directory, entries };
73
+ }
38
74
  /**
39
75
  * The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
40
76
  * member-model ledger: the provider records each delegation's start model
@@ -92,7 +128,12 @@ export class KimiModelBroker {
92
128
  if (refresh)
93
129
  await this.deps.catalog?.refresh(childSessionId);
94
130
  const info = await this.modelInfo(childSessionId);
95
- return info.directory ?? { entries: [], complete: false, customInput: true, status: 'unsupported', refreshing: false, revision: 0 };
131
+ const directory = info.directory ?? { entries: [], complete: false, customInput: true, status: 'unsupported', refreshing: false, revision: 0 };
132
+ return scopeServedDirectory(directory, {
133
+ member: childSessionId !== undefined,
134
+ curated: this.deps.modelList?.(this.deps.homeDir(childSessionId)) ?? [],
135
+ current: info.effective,
136
+ });
96
137
  }
97
138
  async *followModelDirectory(childSessionId, signal) {
98
139
  if (this.deps.catalog === undefined) {
@@ -11,24 +11,45 @@ import type { ContentBlock, TokenUsage } from '@deepseek-ai/dsh-llm';
11
11
  import type { Session } from '@deepseek-ai/dsh-session';
12
12
  import type { LocalAgentToolCalls } from '@khorsheed/dsh-local-agent/types';
13
13
  import { type KimiTranscriptLine } from './session-view.ts';
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 declare const KIMI_INTERRUPTED_TOOL_RESULT = "\uFF08\u5DE5\u5177\u8C03\u7528\u88AB\u4E2D\u65AD\uFF0C\u672A\u80FD\u8BB0\u5F55\u8FD4\u56DE\u7ED3\u679C\uFF09";
14
21
  /** One assistant-role message event, attributed to the kimi route. */
15
22
  export declare function assistantEvent(blocks: readonly ContentBlock[], model?: string): import("@deepseek-ai/dsh-llm").AssistantMessage;
16
23
  /**
17
- * The step the next fold pass would assign in one turn (the max existing
18
- * step + 1). The token-granularity live
19
- * driver reserves its stream merge key here LAZILY at the first delta, so
20
- * tool cards folded before the stream started keep their chronological
21
- * place below it instead of the stream squatting on step 1.
24
+ * The step the next fold pass would assign in one turn: the smallest step
25
+ * nothing holds (never written, never reserved at call time — callers skip
26
+ * their own reservations on top). Filling the smallest free step keeps the
27
+ * stored log dense: the released-log reader admits only dense, strictly
28
+ * sequential steps, and a reservation released mid-round (its content rode an
29
+ * earlier fold) leaves a gap only the next first-free write fills.
22
30
  * @param childSession - the run's child session.
23
31
  * @param turn - the round's turn number.
24
- * @returns the next free step in the turn.
32
+ * @returns the smallest free step in the turn.
25
33
  */
26
34
  export declare function nextKimiSessionStep(childSession: Session, turn: number): number;
35
+ /**
36
+ * The step a durable write takes: one past the highest step this turn has
37
+ * opened in the log. The released-log reader admits steps only in order —
38
+ * one past the last, never below it — so the coordinate is decided when the
39
+ * step is written, never reserved ahead (a reservation filled later lands
40
+ * below a higher step already written: a gap, then a step out of order).
41
+ * Stream and tool reservations only key the live preview.
42
+ */
43
+ export declare function nextWrittenKimiStep(childSession: Session, turn: number): number;
27
44
  /** The run-progress delta text for one transcript line (any kind). */
28
45
  export declare function kimiLineProgressText(line: KimiTranscriptLine): string;
29
46
  /** The result of one mirror pass: the new offset plus what was newly mirrored. */
30
47
  export interface KimiMirrorDelta {
31
- /** The new total transcript-line count mirrored into the child session. */
48
+ /**
49
+ * The new mirror offset (transcript lines folded so far). An unresulted
50
+ * tool line (or a wire step the wire has not closed yet) HOLDS a mid-round
51
+ * pass: the offset stops before it and the next pass retries from there.
52
+ */
32
53
  total: number;
33
54
  /**
34
55
  * The folded text of each transcript line THIS pass newly mirrored, in
@@ -38,17 +59,24 @@ export interface KimiMirrorDelta {
38
59
  */
39
60
  texts: string[];
40
61
  /**
41
- * The usage this pass computed for its window. Attached to the window's
42
- * last folded assistant message when one exists (see `usageAttached`);
43
- * otherwise returned unattached, so the caller (the token-granularity live
44
- * driver) can carry it on the stream's final snapshot instead.
62
+ * The usage this pass computed for its window. Attached per wire step: the
63
+ * records whose line position falls inside a folded unit's span ride THAT
64
+ * unit's message (one usage.record per LLM request, matching the host
65
+ * session's own shape); anything no fold carried comes back as
66
+ * `usageUnattached` for the caller to carry.
45
67
  */
46
68
  usage?: TokenUsage;
47
69
  /**
48
- * True when this pass's usage rode a folded assistant message — the caller
70
+ * True when this pass's usage rode folded assistant messages — the caller
49
71
  * must never attach that accounting a second time.
50
72
  */
51
73
  usageAttached?: boolean;
74
+ /**
75
+ * The window's accounting that NO fold carried (a pass with only held or
76
+ * fully-sealed folds): the token-granularity live driver sums it into the
77
+ * round's uncarried remainder, which the settle's final snapshot carries.
78
+ */
79
+ usageUnattached?: TokenUsage;
52
80
  /**
53
81
  * The model identifier the wire named (a `usage.record` or `llm.request`
54
82
  * `model` field, last one seen) — the delegation's observed-model source.
@@ -66,35 +94,51 @@ export interface KimiMirrorDelta {
66
94
  /**
67
95
  * The live stream ledger a fold pass coordinates with.
68
96
  * Absent on the exec path, where no streams reserve steps.
97
+ *
98
+ * The durable contract this enforces: the released-log reader admits only
99
+ * dense, strictly sequential steps (one open at a time) — so the fold writes
100
+ * every step as one atomic step/start→…→step/end triple, and a fold landing
101
+ * at a reserved step first finalizes every stream reservation below it.
69
102
  */
70
103
  export interface KimiMirrorStreams {
71
104
  /**
72
- * Steps in-flight streams reserved for this turn (their snapshot messages
73
- * append there). Sequential folds must skip them, so a tool card folded
74
- * mid-stream lands ABOVE the stream's step and the projection renders the
75
- * round's chronological order.
105
+ * Steps in-flight streams reserved for this turn (their live updates ride
106
+ * `local-agent/stream` events there). Sequential folds skip them, so a
107
+ * tool card folded mid-stream lands ABOVE the stream's step and the
108
+ * projection renders the round's chronological order.
76
109
  */
77
110
  reservedSteps(turn: number): readonly number[];
78
- /** Reserve chronological placement from the ACP tool notification before its file fold. */
79
- completeTool?(line: KimiTranscriptLine & {
111
+ /**
112
+ * Finalize every in-flight stream reserved below `step`, in ascending
113
+ * order, writing each one's atomic step triple with its accumulated text.
114
+ * Called before any fold writes at `step`. Stream reservations only: an
115
+ * unresulted tool line HOLDS the pass instead, so a tool reservation
116
+ * never sits inside the gap.
117
+ */
118
+ finalizeBefore?(turn: number, step: number): void;
119
+ /**
120
+ * Release the ACP tool notification's chronological reservation for this
121
+ * wire line WITHOUT writing: the line's fold carries the whole native
122
+ * lifecycle inside its step, so the reserved coordinate frees for a later
123
+ * fold. (A reservation kimi never logs to the wire stays reserved and
124
+ * finalizes as the driver's settle-time orphan fold.)
125
+ */
126
+ releaseTool?(line: KimiTranscriptLine & {
80
127
  kind: 'tool';
81
- }): {
82
- readonly step: number;
83
- readonly opened: boolean;
84
- } | undefined;
128
+ }): void;
85
129
  /**
86
- * Pair one completed think/assistant transcript line with its in-flight
87
- * stream. Returns the reservation — consuming it — when this line finalizes
88
- * a stream: the caller folds the line's message at the reserved step,
89
- * opening the step boundary only when `opened` is false (no snapshot ever
90
- * landed), and closes it. Undefined folds the line sequentially. A
91
- * (turn, step) NEVER takes a second step/start.
130
+ * Consume the round's in-flight content streams of the given kinds, pairing
131
+ * them with one fold: `step` is the fold coordinate (the earliest consumed
132
+ * reservation still needing one — absent when every consumed kind was
133
+ * already sealed, so the fold goes sequential), and `sealed` lists the
134
+ * kinds already durable (a gap fill wrote their triples — the fold must
135
+ * EXCLUDE their content). Reservations whose content rides this fold are
136
+ * released, so a later sequential fold may take their steps. Undefined when
137
+ * no stream pairs — the fold goes sequential.
92
138
  */
93
- completeStream(line: KimiTranscriptLine & {
94
- kind: 'assistant' | 'think';
95
- }): {
96
- readonly step: number;
97
- readonly opened: boolean;
139
+ completeStreams(turn: number, kinds: readonly ('think' | 'text')[]): {
140
+ readonly step?: number;
141
+ readonly sealed: readonly ('think' | 'text')[];
98
142
  } | undefined;
99
143
  }
100
144
  /** Mirror behavior switches shared by the exec and live paths. */
@@ -113,12 +157,22 @@ export interface KimiMirrorOptions {
113
157
  * think/assistant lines with their streams and skip reserved steps.
114
158
  */
115
159
  streams?: KimiMirrorStreams;
160
+ /**
161
+ * Settle pass: a tool line whose result the wire never recorded folds with
162
+ * a synthesized interrupted result instead of holding the pass (the round
163
+ * is over — the result will never arrive). Mid-round passes hold there.
164
+ */
165
+ settle?: boolean;
116
166
  }
117
167
  /**
118
- * Mirror one kimi session's transcript into the dsh subagent session: each
119
- * user prompt becomes a `user/message`, each assistant reply (including
120
- * folded thinking and tool activity) an `assistant/message`, then the events
121
- * are appended to persistence so the child session is viewable.
168
+ * Mirror one kimi session's transcript into a dsh subagent session: each
169
+ * user prompt becomes a `user/message`, and each WIRE STEP (one LLM request:
170
+ * its thinking, reply text and tool calls share the wire's own step number)
171
+ * folds into one dsh step whose `assistant/message` carries the reasoning,
172
+ * text and tool-call blocks together — the host session's own shape, so the
173
+ * standard surfaces (conversation, Context Lens) read one request per LLM
174
+ * call. Then the events are appended to persistence so the child session is
175
+ * viewable.
122
176
  *
123
177
  * A resumed round (or a live poll) passes the already-mirrored
124
178
  * transcript-line count so only the delta is appended — re-mirroring earlier
@@ -128,22 +182,29 @@ export interface KimiMirrorOptions {
128
182
  * already in the child session, so a round mirrored across several live polls
129
183
  * does not restart step 1 per poll.
130
184
  *
131
- * Every folded step is wrapped in the `step/start`–`step/end` boundary pair
132
- * the live conversation assembler requires: without a boundary the step never
133
- * enters the location index, the assistant message resolves to a turn-level
134
- * location, and the real-time view renders nothing (only a full rebuild,
135
- * which mints step drafts from the explicit coordinates, recovers it). A
136
- * think/assistant line that completes an in-flight stream folds at the
137
- * stream's reserved step instead (see {@link KimiMirrorStreams}), and a
138
- * sequential fold never takes a step a stream reserved.
185
+ * Every folded step is one ATOMIC `step/start`–`step/end` triple, written
186
+ * only when its content lands: the released-log reader admits only dense,
187
+ * strictly sequential steps (one open at a time — a second `step/start`
188
+ * while another step is open fails the session's reload validation), and the
189
+ * live conversation assembler needs the boundary to materialize the step at
190
+ * all. A fold paired with in-flight streams lands at the earliest consumed
191
+ * reservation (see {@link KimiMirrorStreams}), after `finalizeBefore` has
192
+ * closed every stream reservation below it; a sequential fold never takes a
193
+ * step a stream reserved. Mid-round, a wire step the wire has not CLOSED yet
194
+ * (no `step.end`) holds the pass — its parts may still arrive — and so does
195
+ * an unresulted tool call (the reader resolves every advertised call before
196
+ * the step closes; the result merges into the call's line in place, so the
197
+ * next pass sees it). The settle pass folds the remainder with a synthesized
198
+ * interrupted result. Lines the wire never step-tagged (older kimi) fold one
199
+ * by one, immediately, as before.
139
200
  *
140
201
  * Usage accounting is exactly-once ACROSS passes: each `usage.record` is
141
- * tagged with the transcript position it occurred at, and a pass attaches the
142
- * records in the half-open range `(fromLines, newTotal]` — the records that
143
- * trail THIS pass's lines — summed onto the delta's final assistant message.
144
- * (A record flushed only after its line was already mirrored belongs to no
145
- * later pass and is dropped; kimi flushes content and usage together per
146
- * request, so this window is a flush race, not the norm.)
202
+ * tagged with the transcript position it occurred at, a pass owns the records
203
+ * between the old offset and its own consumed end, and inside the window each
204
+ * record rides the unit whose line span encloses it — one record per wire
205
+ * step, landing on its own step's message. (A record raced PAST its content
206
+ * encloses into the next unit instead: attribution off by one in a flush
207
+ * race, totals still exact.)
147
208
  * @param ctx - host context carrying the session persistence service.
148
209
  * @param childSession - the dsh subagent session created for this delegation.
149
210
  * @param homeDir - the `kimi` harness's scoped home.
@@ -151,7 +212,9 @@ export interface KimiMirrorOptions {
151
212
  * recent session (a one-shot `kimi -p` run always creates a fresh one).
152
213
  * @param fromLines - transcript lines already mirrored into the child session
153
214
  * (0 for the first round); only lines after this offset are appended.
154
- * @returns the new total and the newly mirrored lines' texts.
215
+ * @returns the new mirror offset and the newly mirrored lines' texts. A held
216
+ * unit (open wire step or unresulted tool) stops the offset before itself;
217
+ * the next pass retries from there.
155
218
  */
156
219
  export declare function mirrorKimiSessionDelta(ctx: Context, childSession: Session, homeDir: string, kimiSessionId?: string, fromLines?: number, options?: KimiMirrorOptions): Promise<KimiMirrorDelta>;
157
220
  /**