@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.
- package/README.en.md +132 -0
- package/README.i18n.yaml +6 -0
- package/README.md +132 -0
- package/cordis.patch.yml +34 -0
- package/lib/client.js +1066 -0
- package/lib/index.js +4716 -0
- package/lib/invariant.js +31 -0
- package/lib/types/client/SettingsCard.d.ts +77 -0
- package/lib/types/client/SettingsCard.js +197 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +89 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/credential-guard.d.ts +29 -0
- package/lib/types/credential-guard.js +107 -0
- package/lib/types/index.d.ts +69 -0
- package/lib/types/index.js +270 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/kimi-cli-provider.d.ts +215 -0
- package/lib/types/kimi-cli-provider.js +855 -0
- package/lib/types/live-driver.d.ts +245 -0
- package/lib/types/live-driver.js +1312 -0
- package/lib/types/live-switch.d.ts +78 -0
- package/lib/types/live-switch.js +112 -0
- package/lib/types/member-bridge-config.d.ts +43 -0
- package/lib/types/member-bridge-config.js +80 -0
- package/lib/types/model-broker.d.ts +97 -0
- package/lib/types/model-broker.js +205 -0
- package/lib/types/model-catalog.d.ts +26 -0
- package/lib/types/model-catalog.js +86 -0
- package/lib/types/model-configuration.d.ts +11 -0
- package/lib/types/model-configuration.js +55 -0
- package/lib/types/preset-tools.d.ts +20 -0
- package/lib/types/preset-tools.js +36 -0
- package/lib/types/provision.d.ts +131 -0
- package/lib/types/provision.js +363 -0
- package/lib/types/records.d.ts +35 -0
- package/lib/types/records.js +85 -0
- package/lib/types/session-configuration.d.ts +5 -0
- package/lib/types/session-configuration.js +26 -0
- package/lib/types/session-mirror.d.ts +180 -0
- package/lib/types/session-mirror.js +446 -0
- package/lib/types/session-view.d.ts +121 -0
- package/lib/types/session-view.js +423 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +107 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings-driven live-driver switch: owns the active driver generation
|
|
3
|
+
* and retires old ones on a settings change, without a plugin reload.
|
|
4
|
+
*
|
|
5
|
+
* Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
|
|
6
|
+
* new rounds are refused (the provider's catch falls back to exec), in-flight
|
|
7
|
+
* rounds finish on their runtime, idle runtimes are reclaimed at once.
|
|
8
|
+
* Toggling ON builds the next generation lazily (no process until the first
|
|
9
|
+
* round). Legacy granularity settings are ignored.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-kimi — internal, unit-tested directly.
|
|
12
|
+
*/
|
|
13
|
+
import type { KimiNativeConfiguration } from './model-catalog.ts';
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import type { SettingsFace } from '@khorsheed/dsh-local-agent';
|
|
16
|
+
import { KimiAcpLiveDriver } from './live-driver.ts';
|
|
17
|
+
import type { KimiLiveMirrorGranularity } from './live-driver.ts';
|
|
18
|
+
/** The resolved live settings (rc.1: volatile row config; 0.1.5: schema defaults ← YAML base ← user layer). */
|
|
19
|
+
export interface KimiLiveSettings {
|
|
20
|
+
live: boolean;
|
|
21
|
+
liveMirrorGranularity: KimiLiveMirrorGranularity;
|
|
22
|
+
/** The configured model, when the card or the YAML base names one. */
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Model identifiers the card has saved before (its input's suggestions). */
|
|
25
|
+
recentModels?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export declare class LiveDriverSwitch {
|
|
28
|
+
private readonly ctx;
|
|
29
|
+
private readonly liveIdleMs;
|
|
30
|
+
/**
|
|
31
|
+
* Per-spawn model resolver, handed to every driver generation. Member-aware:
|
|
32
|
+
* the driver's runtime binds whatever the member's layers resolve to.
|
|
33
|
+
*/
|
|
34
|
+
private readonly model?;
|
|
35
|
+
private liveOn;
|
|
36
|
+
private active;
|
|
37
|
+
/** Retiring generations still draining their in-flight rounds. */
|
|
38
|
+
private readonly draining;
|
|
39
|
+
private readonly unwatch;
|
|
40
|
+
constructor(ctx: Context, face: SettingsFace<KimiLiveSettings>, liveIdleMs: number | undefined,
|
|
41
|
+
/**
|
|
42
|
+
* Per-spawn model resolver, handed to every driver generation. Member-aware:
|
|
43
|
+
* the driver's runtime binds whatever the member's layers resolve to.
|
|
44
|
+
*/
|
|
45
|
+
model?: ((childSessionId: string) => string | undefined) | undefined);
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the live driver for one round's member (the provider's call
|
|
48
|
+
* site). A retiring generation still hosting the member gates the new one —
|
|
49
|
+
* the round falls back to exec rather than double-loading the same kimi
|
|
50
|
+
* session into two processes.
|
|
51
|
+
*/
|
|
52
|
+
readonly resolve: (childSessionId: string) => KimiAcpLiveDriver | undefined;
|
|
53
|
+
/** Mirror the resolved settings into the driver generation. */
|
|
54
|
+
private apply;
|
|
55
|
+
/**
|
|
56
|
+
* Retire one member's runtime on the ACTIVE generation (a composer-driven
|
|
57
|
+
* model switch): the next round respawns onto the new model. A member still
|
|
58
|
+
* hosted by a draining generation is left alone — its rounds already gate
|
|
59
|
+
* to exec, and the drain reclaims the process.
|
|
60
|
+
*/
|
|
61
|
+
retireMemberRuntime(childSessionId: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* The model the member's active-generation runtime is bound to, or
|
|
64
|
+
* undefined when no live runtime serves the member (the broker's
|
|
65
|
+
* same-model no-op check).
|
|
66
|
+
*/
|
|
67
|
+
memberRuntimeModel(childSessionId: string): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Whether the active generation hosts (or is spawning) the member's
|
|
70
|
+
* runtime — the broker's retire decision needs this apart from the bound
|
|
71
|
+
* model, since a runtime that bound NO model reports undefined either way.
|
|
72
|
+
*/
|
|
73
|
+
memberHasRuntime(childSessionId: string): boolean;
|
|
74
|
+
memberRuntimeConfiguration(childSessionId: string): KimiNativeConfiguration | undefined;
|
|
75
|
+
/** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
|
|
76
|
+
dispose(): void;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=live-switch.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings-driven live-driver switch: owns the active driver generation
|
|
3
|
+
* and retires old ones on a settings change, without a plugin reload.
|
|
4
|
+
*
|
|
5
|
+
* Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
|
|
6
|
+
* new rounds are refused (the provider's catch falls back to exec), in-flight
|
|
7
|
+
* rounds finish on their runtime, idle runtimes are reclaimed at once.
|
|
8
|
+
* Toggling ON builds the next generation lazily (no process until the first
|
|
9
|
+
* round). Legacy granularity settings are ignored.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-kimi — internal, unit-tested directly.
|
|
12
|
+
*/
|
|
13
|
+
import { KimiAcpLiveDriver } from './live-driver.js';
|
|
14
|
+
export class LiveDriverSwitch {
|
|
15
|
+
ctx;
|
|
16
|
+
liveIdleMs;
|
|
17
|
+
model;
|
|
18
|
+
liveOn = false;
|
|
19
|
+
active;
|
|
20
|
+
/** Retiring generations still draining their in-flight rounds. */
|
|
21
|
+
draining = new Set();
|
|
22
|
+
unwatch;
|
|
23
|
+
constructor(ctx, face, liveIdleMs,
|
|
24
|
+
/**
|
|
25
|
+
* Per-spawn model resolver, handed to every driver generation. Member-aware:
|
|
26
|
+
* the driver's runtime binds whatever the member's layers resolve to.
|
|
27
|
+
*/
|
|
28
|
+
model) {
|
|
29
|
+
this.ctx = ctx;
|
|
30
|
+
this.liveIdleMs = liveIdleMs;
|
|
31
|
+
this.model = model;
|
|
32
|
+
this.apply(face.get());
|
|
33
|
+
this.unwatch = face.watch((next) => { this.apply(next); });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the live driver for one round's member (the provider's call
|
|
37
|
+
* site). A retiring generation still hosting the member gates the new one —
|
|
38
|
+
* the round falls back to exec rather than double-loading the same kimi
|
|
39
|
+
* session into two processes.
|
|
40
|
+
*/
|
|
41
|
+
resolve = (childSessionId) => {
|
|
42
|
+
if (this.active === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
for (const retiring of this.draining) {
|
|
45
|
+
if (retiring.hasRuntime(childSessionId))
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return this.active;
|
|
49
|
+
};
|
|
50
|
+
/** Mirror the resolved settings into the driver generation. */
|
|
51
|
+
apply(next) {
|
|
52
|
+
if (next.live === this.liveOn) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.liveOn = next.live;
|
|
56
|
+
const retiring = this.active;
|
|
57
|
+
this.active = next.live
|
|
58
|
+
? new KimiAcpLiveDriver(this.ctx, {
|
|
59
|
+
...this.liveIdleMs === undefined ? {} : { liveIdleMs: this.liveIdleMs },
|
|
60
|
+
...this.model === undefined ? {} : { model: this.model },
|
|
61
|
+
})
|
|
62
|
+
: undefined;
|
|
63
|
+
if (retiring !== undefined) {
|
|
64
|
+
this.draining.add(retiring);
|
|
65
|
+
void retiring.drain().then(() => { this.draining.delete(retiring); }, (error) => {
|
|
66
|
+
this.draining.delete(retiring);
|
|
67
|
+
this.ctx.logger.warn(`local-agent-kimi: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Retire one member's runtime on the ACTIVE generation (a composer-driven
|
|
73
|
+
* model switch): the next round respawns onto the new model. A member still
|
|
74
|
+
* hosted by a draining generation is left alone — its rounds already gate
|
|
75
|
+
* to exec, and the drain reclaims the process.
|
|
76
|
+
*/
|
|
77
|
+
async retireMemberRuntime(childSessionId) {
|
|
78
|
+
await this.active?.retireRuntime(childSessionId);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The model the member's active-generation runtime is bound to, or
|
|
82
|
+
* undefined when no live runtime serves the member (the broker's
|
|
83
|
+
* same-model no-op check).
|
|
84
|
+
*/
|
|
85
|
+
memberRuntimeModel(childSessionId) {
|
|
86
|
+
return this.active?.runtimeModel(childSessionId);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether the active generation hosts (or is spawning) the member's
|
|
90
|
+
* runtime — the broker's retire decision needs this apart from the bound
|
|
91
|
+
* model, since a runtime that bound NO model reports undefined either way.
|
|
92
|
+
*/
|
|
93
|
+
memberHasRuntime(childSessionId) {
|
|
94
|
+
return this.active?.hasRuntime(childSessionId) ?? false;
|
|
95
|
+
}
|
|
96
|
+
memberRuntimeConfiguration(childSessionId) {
|
|
97
|
+
for (const driver of [this.active, ...this.draining]) {
|
|
98
|
+
const configuration = driver?.runtimeConfiguration(childSessionId);
|
|
99
|
+
if (configuration !== undefined)
|
|
100
|
+
return configuration;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
|
|
105
|
+
dispose() {
|
|
106
|
+
this.unwatch();
|
|
107
|
+
void this.active?.disposeAll();
|
|
108
|
+
for (const retiring of this.draining)
|
|
109
|
+
void retiring.disposeAll();
|
|
110
|
+
this.draining.clear();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi scoped-home MCP config injection for the member bridge. Kimi declares
|
|
3
|
+
* stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
|
|
4
|
+
* is read once per CLI session start, and edits never interrupt an open
|
|
5
|
+
* session — so the family writes one entry PER RUN (keyed by the run's token
|
|
6
|
+
* suffix, keeping concurrent delegations of the same scoped home race-free),
|
|
7
|
+
* prunes it when the run settles, and drops stale family entries (crashed
|
|
8
|
+
* host) on every write. User-declared servers are preserved verbatim.
|
|
9
|
+
* @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
|
|
10
|
+
*/
|
|
11
|
+
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
12
|
+
export declare const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
|
|
13
|
+
/**
|
|
14
|
+
* The per-run MCP server key. The run's token suffix keeps concurrent runs
|
|
15
|
+
* distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
|
|
16
|
+
* than per-member) naming costs nothing.
|
|
17
|
+
* @param token - the run's member-channel token.
|
|
18
|
+
* @returns the mcp.json server key.
|
|
19
|
+
*/
|
|
20
|
+
export declare function memberBridgeServerKey(token: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Declare the member bridge for one run: set the per-run server entry and
|
|
23
|
+
* prune other family entries (settled or crashed runs — an open session's
|
|
24
|
+
* already-registered server is unaffected, per kimi's mid-session semantics).
|
|
25
|
+
* Synchronous read-modify-write: single-process callers race-free by event
|
|
26
|
+
* loop; the file is small and written once per run.
|
|
27
|
+
* @param homeDir - the kimi scoped home.
|
|
28
|
+
* @param key - this run's server key ({@link memberBridgeServerKey}).
|
|
29
|
+
* @param command - the bridge spawn command (`node` + entry path).
|
|
30
|
+
* @param env - the bridge environment (socket path + per-run token).
|
|
31
|
+
*/
|
|
32
|
+
export declare function injectMemberBridge(homeDir: string, key: string, command: {
|
|
33
|
+
command: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
}, env: Record<string, string>): void;
|
|
36
|
+
/**
|
|
37
|
+
* Remove one run's bridge entry (settle path). Absent entries and a missing
|
|
38
|
+
* config are no-ops.
|
|
39
|
+
* @param homeDir - the kimi scoped home.
|
|
40
|
+
* @param key - the run's server key.
|
|
41
|
+
*/
|
|
42
|
+
export declare function removeMemberBridge(homeDir: string, key: string): void;
|
|
43
|
+
//# sourceMappingURL=member-bridge-config.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi scoped-home MCP config injection for the member bridge. Kimi declares
|
|
3
|
+
* stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
|
|
4
|
+
* is read once per CLI session start, and edits never interrupt an open
|
|
5
|
+
* session — so the family writes one entry PER RUN (keyed by the run's token
|
|
6
|
+
* suffix, keeping concurrent delegations of the same scoped home race-free),
|
|
7
|
+
* prunes it when the run settles, and drops stale family entries (crashed
|
|
8
|
+
* host) on every write. User-declared servers are preserved verbatim.
|
|
9
|
+
* @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
/** Kimi's user-level MCP config filename inside the scoped home. */
|
|
14
|
+
const MCP_CONFIG_FILENAME = 'mcp.json';
|
|
15
|
+
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
16
|
+
export const MEMBER_BRIDGE_SERVER_PREFIX = 'dsh-member-';
|
|
17
|
+
/**
|
|
18
|
+
* The per-run MCP server key. The run's token suffix keeps concurrent runs
|
|
19
|
+
* distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
|
|
20
|
+
* than per-member) naming costs nothing.
|
|
21
|
+
* @param token - the run's member-channel token.
|
|
22
|
+
* @returns the mcp.json server key.
|
|
23
|
+
*/
|
|
24
|
+
export function memberBridgeServerKey(token) {
|
|
25
|
+
return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
|
|
26
|
+
}
|
|
27
|
+
/** Read the scoped mcp.json; a missing or malformed file reads as empty. */
|
|
28
|
+
function readMcpConfig(file) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
37
|
+
return {};
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
/** Write the config back, preserving every non-family key. */
|
|
41
|
+
function writeMcpConfig(file, config) {
|
|
42
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Declare the member bridge for one run: set the per-run server entry and
|
|
46
|
+
* prune other family entries (settled or crashed runs — an open session's
|
|
47
|
+
* already-registered server is unaffected, per kimi's mid-session semantics).
|
|
48
|
+
* Synchronous read-modify-write: single-process callers race-free by event
|
|
49
|
+
* loop; the file is small and written once per run.
|
|
50
|
+
* @param homeDir - the kimi scoped home.
|
|
51
|
+
* @param key - this run's server key ({@link memberBridgeServerKey}).
|
|
52
|
+
* @param command - the bridge spawn command (`node` + entry path).
|
|
53
|
+
* @param env - the bridge environment (socket path + per-run token).
|
|
54
|
+
*/
|
|
55
|
+
export function injectMemberBridge(homeDir, key, command, env) {
|
|
56
|
+
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
57
|
+
const config = readMcpConfig(file);
|
|
58
|
+
const servers = { ...(config.mcpServers ?? {}) };
|
|
59
|
+
for (const name of Object.keys(servers)) {
|
|
60
|
+
if (name.startsWith(MEMBER_BRIDGE_SERVER_PREFIX) && name !== key)
|
|
61
|
+
delete servers[name];
|
|
62
|
+
}
|
|
63
|
+
servers[key] = { command: command.command, args: command.args, env };
|
|
64
|
+
writeMcpConfig(file, { ...config, mcpServers: servers });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove one run's bridge entry (settle path). Absent entries and a missing
|
|
68
|
+
* config are no-ops.
|
|
69
|
+
* @param homeDir - the kimi scoped home.
|
|
70
|
+
* @param key - the run's server key.
|
|
71
|
+
*/
|
|
72
|
+
export function removeMemberBridge(homeDir, key) {
|
|
73
|
+
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
74
|
+
const config = readMcpConfig(file);
|
|
75
|
+
if (config.mcpServers === undefined || !(key in config.mcpServers))
|
|
76
|
+
return;
|
|
77
|
+
const servers = { ...config.mcpServers };
|
|
78
|
+
delete servers[key];
|
|
79
|
+
writeMcpConfig(file, { ...config, mcpServers: servers });
|
|
80
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kimi harness's model broker: the member/harness model surface the
|
|
3
|
+
* family gateway routes to (`harnessModel` for the settings card,
|
|
4
|
+
* `memberModel`/`setMemberModel` for the composer picker). Resolution follows
|
|
5
|
+
* the family's fixed order — the session-level override (the composer's
|
|
6
|
+
* picker write) first, then the delegation's recorded model, then the
|
|
7
|
+
* plugin-config `model` key, then the scoped config's own `default_model`,
|
|
8
|
+
* and last the CLI's built-in default, which names nothing.
|
|
9
|
+
*
|
|
10
|
+
* Per-member state is IN MEMORY by design: the session-level override is a
|
|
11
|
+
* composer gesture that deliberately does not survive a host restart, and the
|
|
12
|
+
* start-model ledger only mirrors what the delegation records already persist
|
|
13
|
+
* (the core hands the recorded value back as `delegationModel` on reads).
|
|
14
|
+
* The spawn side of a switch is lazy: `setMemberModel` retires the member's
|
|
15
|
+
* resident runtime when its bound model no longer matches, and the member's
|
|
16
|
+
* next round respawns onto the new model, `session/load`-ing the same CLI
|
|
17
|
+
* session — the conversation carries over, the process fact changes.
|
|
18
|
+
* @module @khorsheed/dsh-local-agent-kimi/model-broker
|
|
19
|
+
*/
|
|
20
|
+
import type { MemberConfigurationAdapter } from '@khorsheed/dsh-local-agent';
|
|
21
|
+
import type { LocalAgentMemberBinding } from '@khorsheed/dsh-local-agent/types';
|
|
22
|
+
import type { KimiModelCatalog } from './model-catalog.ts';
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import type { LocalAgentModelBroker, LocalAgentModelInfo, LocalAgentModelDirectory } from '@khorsheed/dsh-local-agent/types';
|
|
25
|
+
import type { LiveDriverSwitch } from './live-switch.ts';
|
|
26
|
+
/**
|
|
27
|
+
* The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
|
|
28
|
+
* member-model ledger: the provider records each delegation's start model
|
|
29
|
+
* here and reads the session-level override for its exec-path resolution, so
|
|
30
|
+
* the picker write reaches the very next round on either driver.
|
|
31
|
+
*/
|
|
32
|
+
export declare class KimiModelBroker implements LocalAgentModelBroker {
|
|
33
|
+
private readonly ctx;
|
|
34
|
+
private readonly deps;
|
|
35
|
+
/** Session-level overrides per member (the composer picker's writes). */
|
|
36
|
+
private readonly overrides;
|
|
37
|
+
/** Each member's START model — the model its delegation's first round named. */
|
|
38
|
+
private readonly startModels;
|
|
39
|
+
/**
|
|
40
|
+
* @param ctx - plugin context carrying the family registry (the in-flight
|
|
41
|
+
* read behind `switchable`).
|
|
42
|
+
* @param deps - the harness's live readers: the scoped home, the settings
|
|
43
|
+
* layer, the card's recent-model memory, the live toggle, and the driver
|
|
44
|
+
* switch a switch retires runtimes through.
|
|
45
|
+
*/
|
|
46
|
+
constructor(ctx: Context, deps: {
|
|
47
|
+
/** The default scope's scoped home (cliDefault + discovery reads). */
|
|
48
|
+
homeDir: (childSessionId?: string) => string;
|
|
49
|
+
catalog?: KimiModelCatalog;
|
|
50
|
+
/** The plugin-config `model` key, resolved per read. */
|
|
51
|
+
settingsModel: () => string | undefined;
|
|
52
|
+
/** The settings card's recently saved identifiers (choices memory). */
|
|
53
|
+
recentModels: () => readonly string[];
|
|
54
|
+
/** Whether the live driver is currently on. */
|
|
55
|
+
isLive: () => boolean;
|
|
56
|
+
/** The driver generations a model switch retires a member runtime through. */
|
|
57
|
+
liveSwitch: LiveDriverSwitch;
|
|
58
|
+
});
|
|
59
|
+
configurationAdapter(binding: LocalAgentMemberBinding): MemberConfigurationAdapter;
|
|
60
|
+
modelDirectory(childSessionId?: string, refresh?: boolean): Promise<LocalAgentModelDirectory>;
|
|
61
|
+
followModelDirectory(childSessionId: string | undefined, signal: AbortSignal): AsyncIterable<LocalAgentModelDirectory>;
|
|
62
|
+
/**
|
|
63
|
+
* Record the model a member's delegation STARTED with (the provider's call
|
|
64
|
+
* site, fresh and resume rounds alike). The live driver's spawn resolver
|
|
65
|
+
* ranks it between the override and the settings layer.
|
|
66
|
+
*/
|
|
67
|
+
noteStartModel(childSessionId: string, model: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* The member's session-level override, when the composer picker set one —
|
|
70
|
+
* the exec path's top layer (it outranks even the delegation's recorded
|
|
71
|
+
* model, so the provider folds it into the requested slot).
|
|
72
|
+
*/
|
|
73
|
+
overrideFor(childSessionId: string): string | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The member's spawn-binding resolver (the live driver config): override,
|
|
76
|
+
* then the member's start model, then the settings layer. Undefined writes
|
|
77
|
+
* nothing — the scoped config's own `default_model` decides, as it always
|
|
78
|
+
* did.
|
|
79
|
+
*/
|
|
80
|
+
readonly spawnModel: (childSessionId: string) => string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Read the model surface. With no member this is the harness default (the
|
|
83
|
+
* settings card's "what would a round run with"): override and delegation
|
|
84
|
+
* stay absent by construction.
|
|
85
|
+
*/
|
|
86
|
+
modelInfo(childSessionId?: string, delegationModel?: string): Promise<LocalAgentModelInfo>;
|
|
87
|
+
/**
|
|
88
|
+
* Set (or clear) the member's session-level override. Throws while a round
|
|
89
|
+
* is in flight for the member — the switch retires the resident runtime,
|
|
90
|
+
* and retiring mid-round would kill the run. A same-value set (or clearing
|
|
91
|
+
* an unset override) is a no-op; otherwise a live runtime whose bound model
|
|
92
|
+
* differs from the new effective model is retired so the next round
|
|
93
|
+
* respawns onto it, resuming the same CLI session.
|
|
94
|
+
*/
|
|
95
|
+
setMemberModel(childSessionId: string, model: string | undefined): Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=model-broker.d.ts.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kimi harness's model broker: the member/harness model surface the
|
|
3
|
+
* family gateway routes to (`harnessModel` for the settings card,
|
|
4
|
+
* `memberModel`/`setMemberModel` for the composer picker). Resolution follows
|
|
5
|
+
* the family's fixed order — the session-level override (the composer's
|
|
6
|
+
* picker write) first, then the delegation's recorded model, then the
|
|
7
|
+
* plugin-config `model` key, then the scoped config's own `default_model`,
|
|
8
|
+
* and last the CLI's built-in default, which names nothing.
|
|
9
|
+
*
|
|
10
|
+
* Per-member state is IN MEMORY by design: the session-level override is a
|
|
11
|
+
* composer gesture that deliberately does not survive a host restart, and the
|
|
12
|
+
* start-model ledger only mirrors what the delegation records already persist
|
|
13
|
+
* (the core hands the recorded value back as `delegationModel` on reads).
|
|
14
|
+
* The spawn side of a switch is lazy: `setMemberModel` retires the member's
|
|
15
|
+
* resident runtime when its bound model no longer matches, and the member's
|
|
16
|
+
* next round respawns onto the new model, `session/load`-ing the same CLI
|
|
17
|
+
* session — the conversation carries over, the process fact changes.
|
|
18
|
+
* @module @khorsheed/dsh-local-agent-kimi/model-broker
|
|
19
|
+
*/
|
|
20
|
+
import { extendModelDirectory } from '@khorsheed/dsh-local-agent';
|
|
21
|
+
import { readKimiModelConfiguration } from './model-configuration.js';
|
|
22
|
+
import { listKimiConfigModels, readKimiDefaultModel } from './provision.js';
|
|
23
|
+
/** The refusal a mid-round switch gets (in flight = the runtime must not die). */
|
|
24
|
+
const SWITCH_IN_FLIGHT_REASON = '该成员有进行中的委派轮次,等其完成后再切换模型';
|
|
25
|
+
/** Dedupe a candidate list, dropping blanks and keeping first-seen order. */
|
|
26
|
+
function dedupe(candidates) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
const value = candidate?.trim();
|
|
31
|
+
if (value === undefined || value === '' || seen.has(value))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(value);
|
|
34
|
+
out.push(value);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
|
|
40
|
+
* member-model ledger: the provider records each delegation's start model
|
|
41
|
+
* here and reads the session-level override for its exec-path resolution, so
|
|
42
|
+
* the picker write reaches the very next round on either driver.
|
|
43
|
+
*/
|
|
44
|
+
export class KimiModelBroker {
|
|
45
|
+
ctx;
|
|
46
|
+
deps;
|
|
47
|
+
/** Session-level overrides per member (the composer picker's writes). */
|
|
48
|
+
overrides = new Map();
|
|
49
|
+
/** Each member's START model — the model its delegation's first round named. */
|
|
50
|
+
startModels = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* @param ctx - plugin context carrying the family registry (the in-flight
|
|
53
|
+
* read behind `switchable`).
|
|
54
|
+
* @param deps - the harness's live readers: the scoped home, the settings
|
|
55
|
+
* layer, the card's recent-model memory, the live toggle, and the driver
|
|
56
|
+
* switch a switch retires runtimes through.
|
|
57
|
+
*/
|
|
58
|
+
constructor(ctx, deps) {
|
|
59
|
+
this.ctx = ctx;
|
|
60
|
+
this.deps = deps;
|
|
61
|
+
}
|
|
62
|
+
configurationAdapter(binding) {
|
|
63
|
+
const home = this.ctx.localAgent.homeDir('kimi', binding.scope);
|
|
64
|
+
const resolve = async (selection) => {
|
|
65
|
+
const model = selection.model.mode === 'value' ? selection.model.value
|
|
66
|
+
: (selection.model.mode === 'inherit' ? binding.model : undefined) ?? this.deps.settingsModel() ?? await readKimiDefaultModel(home);
|
|
67
|
+
const configured = await readKimiModelConfiguration(home, model);
|
|
68
|
+
const native = this.deps.liveSwitch.memberRuntimeConfiguration(binding.childSessionId);
|
|
69
|
+
const entry = native?.directory.entries.find(entry => entry.value === model)
|
|
70
|
+
?? configured.entries.find(entry => entry.value === model);
|
|
71
|
+
const effort = (selection.effort.mode === 'value' ? selection.effort.value : selection.effort.mode === 'inherit' ? binding.effort : undefined) ?? configured.effort;
|
|
72
|
+
if (effort !== undefined && entry?.reasoning !== undefined && !entry.reasoning.options.some(option => option.value === effort)) {
|
|
73
|
+
throw new Error('Kimi has not advertised this reasoning effort for the selected model');
|
|
74
|
+
}
|
|
75
|
+
if (!this.deps.isLive() && effort !== undefined && configured.protocol !== 'kimi') {
|
|
76
|
+
throw new Error('This model protocol has no Kimi exec effort override; use ACP live mode');
|
|
77
|
+
}
|
|
78
|
+
return { ...model === undefined ? {} : { model }, ...effort === undefined ? {} : { effort } };
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
validate: async (selection) => { await resolve(selection); }, prepare: resolve,
|
|
82
|
+
apply: async (selection) => { const resolved = await resolve(selection); await this.deps.liveSwitch.retireMemberRuntime(binding.childSessionId); return resolved; },
|
|
83
|
+
reconcile: async (state) => {
|
|
84
|
+
if (this.ctx.localAgent.isDelegationActive(binding.childSessionId))
|
|
85
|
+
return { active: true, matches: 'unknown', resolved: {} };
|
|
86
|
+
await this.deps.liveSwitch.retireMemberRuntime(binding.childSessionId);
|
|
87
|
+
return { active: false, matches: 'current', resolved: await resolve(state.current.selection) };
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async modelDirectory(childSessionId, refresh = false) {
|
|
92
|
+
if (refresh)
|
|
93
|
+
await this.deps.catalog?.refresh(childSessionId);
|
|
94
|
+
const info = await this.modelInfo(childSessionId);
|
|
95
|
+
return info.directory ?? { entries: [], complete: false, customInput: true, status: 'unsupported', refreshing: false, revision: 0 };
|
|
96
|
+
}
|
|
97
|
+
async *followModelDirectory(childSessionId, signal) {
|
|
98
|
+
if (this.deps.catalog === undefined) {
|
|
99
|
+
yield await this.modelDirectory(childSessionId);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for await (const _snapshot of this.deps.catalog.follow(childSessionId, signal))
|
|
103
|
+
yield await this.modelDirectory(childSessionId);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Record the model a member's delegation STARTED with (the provider's call
|
|
107
|
+
* site, fresh and resume rounds alike). The live driver's spawn resolver
|
|
108
|
+
* ranks it between the override and the settings layer.
|
|
109
|
+
*/
|
|
110
|
+
noteStartModel(childSessionId, model) {
|
|
111
|
+
const value = model.trim();
|
|
112
|
+
if (value !== '')
|
|
113
|
+
this.startModels.set(childSessionId, value);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The member's session-level override, when the composer picker set one —
|
|
117
|
+
* the exec path's top layer (it outranks even the delegation's recorded
|
|
118
|
+
* model, so the provider folds it into the requested slot).
|
|
119
|
+
*/
|
|
120
|
+
overrideFor(childSessionId) {
|
|
121
|
+
return this.overrides.get(childSessionId);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The member's spawn-binding resolver (the live driver config): override,
|
|
125
|
+
* then the member's start model, then the settings layer. Undefined writes
|
|
126
|
+
* nothing — the scoped config's own `default_model` decides, as it always
|
|
127
|
+
* did.
|
|
128
|
+
*/
|
|
129
|
+
spawnModel = (childSessionId) => this.overrideFor(childSessionId)
|
|
130
|
+
?? this.startModels.get(childSessionId)
|
|
131
|
+
?? this.deps.settingsModel();
|
|
132
|
+
/**
|
|
133
|
+
* Read the model surface. With no member this is the harness default (the
|
|
134
|
+
* settings card's "what would a round run with"): override and delegation
|
|
135
|
+
* stay absent by construction.
|
|
136
|
+
*/
|
|
137
|
+
async modelInfo(childSessionId, delegationModel) {
|
|
138
|
+
const override = childSessionId === undefined ? undefined : this.overrideFor(childSessionId);
|
|
139
|
+
const delegation = childSessionId === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: delegationModel ?? this.startModels.get(childSessionId);
|
|
142
|
+
const settings = this.deps.settingsModel();
|
|
143
|
+
const homeDir = this.deps.homeDir(childSessionId);
|
|
144
|
+
const native = this.deps.catalog?.read(childSessionId);
|
|
145
|
+
const [cliDefault, discovered] = await Promise.all([
|
|
146
|
+
readKimiDefaultModel(homeDir).catch(() => undefined),
|
|
147
|
+
listKimiConfigModels(homeDir).catch(() => []),
|
|
148
|
+
]);
|
|
149
|
+
const choices = dedupe([settings, cliDefault, ...discovered, ...(native?.entries.filter(entry => !entry.hidden).map(entry => entry.value) ?? []), ...this.deps.recentModels()]);
|
|
150
|
+
const [effective, source] = override !== undefined ? [override, 'override']
|
|
151
|
+
: delegation !== undefined ? [delegation, 'delegation']
|
|
152
|
+
: settings !== undefined ? [settings, 'settings']
|
|
153
|
+
: cliDefault !== undefined ? [cliDefault, 'cli-config']
|
|
154
|
+
: [undefined, 'cli-builtin'];
|
|
155
|
+
const inFlight = childSessionId !== undefined
|
|
156
|
+
&& this.ctx.localAgent.activeDelegations().includes(childSessionId);
|
|
157
|
+
return {
|
|
158
|
+
...effective === undefined ? {} : { effective },
|
|
159
|
+
source,
|
|
160
|
+
...override === undefined ? {} : { override },
|
|
161
|
+
...delegation === undefined ? {} : { delegation },
|
|
162
|
+
...settings === undefined ? {} : { settings },
|
|
163
|
+
...cliDefault === undefined ? {} : { cliDefault },
|
|
164
|
+
choices,
|
|
165
|
+
...native === undefined ? {} : { directory: extendModelDirectory(native, [settings, cliDefault, ...discovered], this.deps.recentModels()) },
|
|
166
|
+
live: this.deps.isLive(),
|
|
167
|
+
switchable: !inFlight,
|
|
168
|
+
...inFlight ? { reason: SWITCH_IN_FLIGHT_REASON } : {},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Set (or clear) the member's session-level override. Throws while a round
|
|
173
|
+
* is in flight for the member — the switch retires the resident runtime,
|
|
174
|
+
* and retiring mid-round would kill the run. A same-value set (or clearing
|
|
175
|
+
* an unset override) is a no-op; otherwise a live runtime whose bound model
|
|
176
|
+
* differs from the new effective model is retired so the next round
|
|
177
|
+
* respawns onto it, resuming the same CLI session.
|
|
178
|
+
*/
|
|
179
|
+
async setMemberModel(childSessionId, model) {
|
|
180
|
+
if (typeof this.ctx.localAgent.setMemberModel === 'function')
|
|
181
|
+
return this.ctx.localAgent.setMemberModel(childSessionId, model);
|
|
182
|
+
if (this.ctx.localAgent.activeDelegations().includes(childSessionId)) {
|
|
183
|
+
throw new Error(SWITCH_IN_FLIGHT_REASON);
|
|
184
|
+
}
|
|
185
|
+
const value = model?.trim();
|
|
186
|
+
const previous = this.overrides.get(childSessionId);
|
|
187
|
+
if (value === undefined || value === '') {
|
|
188
|
+
if (previous === undefined)
|
|
189
|
+
return;
|
|
190
|
+
this.overrides.delete(childSessionId);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
if (previous === value)
|
|
194
|
+
return;
|
|
195
|
+
this.overrides.set(childSessionId, value);
|
|
196
|
+
}
|
|
197
|
+
// Retire only when the runtime's bound model no longer matches what the
|
|
198
|
+
// member's next round resolves to — a same-model switch keeps the process.
|
|
199
|
+
const effective = (await this.modelInfo(childSessionId)).effective;
|
|
200
|
+
if (this.deps.liveSwitch.memberHasRuntime(childSessionId)
|
|
201
|
+
&& this.deps.liveSwitch.memberRuntimeModel(childSessionId) !== effective) {
|
|
202
|
+
await this.deps.liveSwitch.retireMemberRuntime(childSessionId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LocalAgentModelDirectory, LocalAgentModelDirectoryData } from '@khorsheed/dsh-local-agent/types';
|
|
2
|
+
export interface KimiNativeConfiguration {
|
|
3
|
+
directory: LocalAgentModelDirectoryData;
|
|
4
|
+
modelConfigId?: string;
|
|
5
|
+
effortConfigId?: string;
|
|
6
|
+
currentModel?: string;
|
|
7
|
+
currentEffort?: string;
|
|
8
|
+
}
|
|
9
|
+
/** ACP v1 configOptions are authoritative; old models metadata is a read-only fallback. */
|
|
10
|
+
export declare function kimiNativeConfiguration(value: unknown): KimiNativeConfiguration | undefined;
|
|
11
|
+
/** Reads configuration before a session exists; never starts a disposable ACP session. */
|
|
12
|
+
export declare class KimiModelCatalog {
|
|
13
|
+
private readonly context;
|
|
14
|
+
private readonly cache;
|
|
15
|
+
constructor(context: (childSessionId?: string) => {
|
|
16
|
+
homeDir: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
native?: KimiNativeConfiguration;
|
|
19
|
+
});
|
|
20
|
+
private key;
|
|
21
|
+
read(childSessionId?: string): LocalAgentModelDirectory;
|
|
22
|
+
refresh(childSessionId?: string): Promise<LocalAgentModelDirectory>;
|
|
23
|
+
follow(childSessionId: string | undefined, signal: AbortSignal): AsyncIterable<LocalAgentModelDirectory>;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=model-catalog.d.ts.map
|