@khorsheed/dsh-local-agent-dsh 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 +150 -0
- package/README.i18n.yaml +6 -0
- package/README.md +151 -0
- package/cordis.patch.yml +24 -0
- package/lib/client.js +1115 -0
- package/lib/dsh-cli-provider-UhWvivte.js +1701 -0
- package/lib/dsh-cli-provider.js +3 -0
- package/lib/index.js +624 -0
- package/lib/invariant.js +33 -0
- package/lib/provision.js +602 -0
- package/lib/records.js +109 -0
- package/lib/session-log-DOsSz56-.js +88 -0
- package/lib/types/client/SettingsCard.d.ts +78 -0
- package/lib/types/client/SettingsCard.js +200 -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 +45 -0
- package/lib/types/client/index.js +88 -0
- package/lib/types/client/locales.d.ts +76 -0
- package/lib/types/client/locales.js +67 -0
- package/lib/types/dsh-cli-provider.d.ts +213 -0
- package/lib/types/dsh-cli-provider.js +733 -0
- package/lib/types/index.d.ts +124 -0
- package/lib/types/index.js +347 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +37 -0
- package/lib/types/live-driver.d.ts +180 -0
- package/lib/types/live-driver.js +887 -0
- package/lib/types/live-switch.d.ts +76 -0
- package/lib/types/live-switch.js +100 -0
- package/lib/types/model-broker.d.ts +86 -0
- package/lib/types/model-broker.js +149 -0
- package/lib/types/model-catalog.d.ts +27 -0
- package/lib/types/model-catalog.js +71 -0
- package/lib/types/provision.d.ts +382 -0
- package/lib/types/provision.js +656 -0
- package/lib/types/records.d.ts +41 -0
- package/lib/types/records.js +125 -0
- package/lib/types/session-log.d.ts +58 -0
- package/lib/types/session-log.js +83 -0
- package/lib/types/session-mirror.d.ts +155 -0
- package/lib/types/session-mirror.js +451 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +121 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import "./provision.js";
|
|
2
|
+
import { a as MEMBER_BRIDGE_ENTRY_ENV, c as dshTextTask, d as startDshCliRun, i as DshCliProvider, l as registerMemberRun, n as DEFAULT_DISPOSE_GRACE_MS, o as dshCliVersion, r as DEFAULT_LIVE_MIRROR_INTERVAL_MS, s as dshLaunchArgv, t as CONTAINER_NODE_OPTIONS, u as resolveApiKey } from "./dsh-cli-provider-UhWvivte.js";
|
|
3
|
+
export { CONTAINER_NODE_OPTIONS, DEFAULT_DISPOSE_GRACE_MS, DEFAULT_LIVE_MIRROR_INTERVAL_MS, DshCliProvider, MEMBER_BRIDGE_ENTRY_ENV, dshCliVersion, dshLaunchArgv, dshTextTask, registerMemberRun, resolveApiKey, startDshCliRun };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { DEFAULT_SUB_PROFILE_NAME, USER_PRESET_DIR, defaultPresetRoot, provisionDshScope } from "./provision.js";
|
|
2
|
+
import { f as DEFAULT_LIVE_IDLE_MS, i as DshCliProvider, o as dshCliVersion, p as DshLiveDriver, t as CONTAINER_NODE_OPTIONS } from "./dsh-cli-provider-UhWvivte.js";
|
|
3
|
+
import { listDshSessions } from "./records.js";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
|
+
import { ModelDirectoryCache, extendModelDirectory, settingsFace, vol } from "@khorsheed/dsh-local-agent";
|
|
8
|
+
import * as toolModule from "@khorsheed/dsh-local-agent-tool-subagent";
|
|
9
|
+
//#region lib/types/live-switch.js
|
|
10
|
+
/**
|
|
11
|
+
* The settings-driven live-driver switch: owns the active driver generation
|
|
12
|
+
* and retires old ones on a settings change, without a plugin reload.
|
|
13
|
+
*
|
|
14
|
+
* Toggle OFF (or the enabled generation unloading): the retiring generation
|
|
15
|
+
* is DRAINED, not killed — new rounds are refused (the provider's catch falls
|
|
16
|
+
* back to exec), in-flight rounds finish on their runtime, idle runtimes are
|
|
17
|
+
* reclaimed at once. Toggling ON builds the next generation lazily (no
|
|
18
|
+
* process until the first round). Legacy granularity settings are ignored.
|
|
19
|
+
*
|
|
20
|
+
* Composition with the `enabled` toggle: the switch lives INSIDE one enabled
|
|
21
|
+
* generation (index.ts constructs it only while the harness/provider/tool are
|
|
22
|
+
* registered, and disposes it with them). `enabled` gates EXISTENCE — its
|
|
23
|
+
* teardown keeps the historical hard semantics (`dispose()` → disposeAll,
|
|
24
|
+
* which may interrupt); `live` gates the MODE of an enabled generation and
|
|
25
|
+
* never interrupts (drain). While `enabled` is off the live value is inert
|
|
26
|
+
* and applies when the toggle turns on again.
|
|
27
|
+
*
|
|
28
|
+
* @module @khorsheed/dsh-local-agent-dsh — internal, unit-tested directly.
|
|
29
|
+
*/
|
|
30
|
+
var LiveDriverSwitch = class {
|
|
31
|
+
ctx;
|
|
32
|
+
config;
|
|
33
|
+
extras;
|
|
34
|
+
liveOn = false;
|
|
35
|
+
active;
|
|
36
|
+
/** Retiring generations still draining their in-flight rounds. */
|
|
37
|
+
draining = /* @__PURE__ */ new Set();
|
|
38
|
+
unwatch;
|
|
39
|
+
constructor(ctx, face, config, extras = {}) {
|
|
40
|
+
this.ctx = ctx;
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.extras = extras;
|
|
43
|
+
this.apply(face.get());
|
|
44
|
+
this.unwatch = face.watch((next) => {
|
|
45
|
+
this.apply(next);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the live driver for one round's member (the provider's call
|
|
50
|
+
* site). A retiring generation still hosting the member gates the new one —
|
|
51
|
+
* the round falls back to exec rather than double-loading the same sub-dsh
|
|
52
|
+
* session into two processes.
|
|
53
|
+
*/
|
|
54
|
+
resolve = (childSessionId) => {
|
|
55
|
+
if (this.active === void 0) return void 0;
|
|
56
|
+
for (const retiring of this.draining) if (retiring.hasRuntime(childSessionId)) return void 0;
|
|
57
|
+
return this.active;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The model the member's live runtime bound at spawn (the broker's switch
|
|
61
|
+
* check): the identifier, undefined for "bound to no model", null for "no
|
|
62
|
+
* live runtime". Consults only the ACTIVE generation — a draining one is on
|
|
63
|
+
* its way out anyway.
|
|
64
|
+
*/
|
|
65
|
+
boundModel(childSessionId) {
|
|
66
|
+
return this.active?.boundModelOf(childSessionId) ?? null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Retire the member's resident runtime in the active generation so the next
|
|
70
|
+
* round respawns onto the member's new effective model (the composer's
|
|
71
|
+
* model switch path; the sub-dsh session itself carries over via the
|
|
72
|
+
* on-disk resume).
|
|
73
|
+
*/
|
|
74
|
+
async retireRuntime(childSessionId) {
|
|
75
|
+
await this.active?.retireRuntime(childSessionId);
|
|
76
|
+
}
|
|
77
|
+
/** Mirror the resolved settings into the driver generation. */
|
|
78
|
+
apply(next) {
|
|
79
|
+
if (next.live === this.liveOn) return;
|
|
80
|
+
this.liveOn = next.live;
|
|
81
|
+
const retiring = this.active;
|
|
82
|
+
this.active = next.live ? new DshLiveDriver(this.ctx, {
|
|
83
|
+
...this.config,
|
|
84
|
+
...this.extras.modelFor === void 0 ? {} : { modelFor: this.extras.modelFor }
|
|
85
|
+
}) : void 0;
|
|
86
|
+
if (retiring !== void 0) {
|
|
87
|
+
this.draining.add(retiring);
|
|
88
|
+
retiring.drain().then(() => {
|
|
89
|
+
this.draining.delete(retiring);
|
|
90
|
+
}, (error) => {
|
|
91
|
+
this.draining.delete(retiring);
|
|
92
|
+
this.ctx.logger.warn(`local-agent-dsh: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Enabled-generation unload: interrupt whatever survives (disposeAll, not drain). */
|
|
97
|
+
dispose() {
|
|
98
|
+
this.unwatch();
|
|
99
|
+
this.active?.disposeAll();
|
|
100
|
+
for (const retiring of this.draining) retiring.disposeAll();
|
|
101
|
+
this.draining.clear();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region lib/types/model-catalog.js
|
|
106
|
+
/** The public host adapter catalog used by sub-DSH provisioning. No generation request. */
|
|
107
|
+
var DshModelCatalog = class {
|
|
108
|
+
deps;
|
|
109
|
+
cache;
|
|
110
|
+
constructor(deps) {
|
|
111
|
+
this.deps = deps;
|
|
112
|
+
this.cache = new ModelDirectoryCache({ load: async (_key, signal) => {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const abort = () => controller.abort();
|
|
115
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
116
|
+
if (signal.aborted) abort();
|
|
117
|
+
let timer;
|
|
118
|
+
try {
|
|
119
|
+
return await Promise.race([this.discover(controller.signal), new Promise((_, reject) => {
|
|
120
|
+
timer = setTimeout(() => {
|
|
121
|
+
controller.abort();
|
|
122
|
+
reject(/* @__PURE__ */ new Error("DSH model directory timed out"));
|
|
123
|
+
}, deps.timeoutMs ?? 5e3);
|
|
124
|
+
timer.unref();
|
|
125
|
+
})]);
|
|
126
|
+
} finally {
|
|
127
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
128
|
+
signal.removeEventListener("abort", abort);
|
|
129
|
+
}
|
|
130
|
+
} });
|
|
131
|
+
}
|
|
132
|
+
key() {
|
|
133
|
+
return JSON.stringify({ defaultModel: this.deps.defaultModel() });
|
|
134
|
+
}
|
|
135
|
+
read() {
|
|
136
|
+
return this.cache.read(this.key());
|
|
137
|
+
}
|
|
138
|
+
refresh() {
|
|
139
|
+
return this.cache.refresh(this.key());
|
|
140
|
+
}
|
|
141
|
+
follow(signal) {
|
|
142
|
+
return this.cache.follow(() => this.key(), signal);
|
|
143
|
+
}
|
|
144
|
+
invalidate() {
|
|
145
|
+
this.cache.invalidate(this.key());
|
|
146
|
+
}
|
|
147
|
+
dispose() {
|
|
148
|
+
this.cache.dispose();
|
|
149
|
+
}
|
|
150
|
+
async discover(signal) {
|
|
151
|
+
const llm = this.deps.llm();
|
|
152
|
+
if (llm === void 0 || typeof llm.listProviders !== "function" || typeof llm.listModels !== "function") return {
|
|
153
|
+
entries: [],
|
|
154
|
+
complete: false,
|
|
155
|
+
customInput: true,
|
|
156
|
+
unsupported: true,
|
|
157
|
+
reason: "The host exposes no adapter model directory"
|
|
158
|
+
};
|
|
159
|
+
const providers = llm.listProviders();
|
|
160
|
+
const groups = await Promise.allSettled(providers.map(async (provider) => Promise.all((await llm.listModels(provider.id)).map(async (model) => {
|
|
161
|
+
let resolved;
|
|
162
|
+
try {
|
|
163
|
+
resolved = await llm.resolveModelInfo?.(provider.id, model.id, signal);
|
|
164
|
+
} catch {}
|
|
165
|
+
return {
|
|
166
|
+
value: `${provider.id}/${model.id}`,
|
|
167
|
+
label: model.name ?? model.id,
|
|
168
|
+
source: "native",
|
|
169
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
170
|
+
...resolved === void 0 ? {} : { resolvedModel: `${resolved.provider}/${resolved.id}` },
|
|
171
|
+
...resolved?.reasoning === void 0 ? {} : { reasoning: {
|
|
172
|
+
options: resolved.reasoning.efforts.map((effort) => ({
|
|
173
|
+
value: String(effort.id),
|
|
174
|
+
label: effort.name,
|
|
175
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
176
|
+
})),
|
|
177
|
+
...resolved.reasoning.defaultEffort === void 0 ? {} : { default: String(resolved.reasoning.defaultEffort) }
|
|
178
|
+
} }
|
|
179
|
+
};
|
|
180
|
+
}))));
|
|
181
|
+
const failed = groups.filter((group) => group.status === "rejected").length;
|
|
182
|
+
if (failed > 0 && failed === groups.length) throw new Error("DSH adapters could not list models");
|
|
183
|
+
const entries = groups.flatMap((group) => group.status === "fulfilled" ? group.value : []);
|
|
184
|
+
const defaultModel = this.deps.defaultModel();
|
|
185
|
+
return {
|
|
186
|
+
entries,
|
|
187
|
+
complete: failed === 0,
|
|
188
|
+
customInput: true,
|
|
189
|
+
...defaultModel === void 0 ? {} : { defaultModel },
|
|
190
|
+
...failed === 0 ? {} : { reason: `${failed} adapter model list(s) unavailable` }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region lib/types/model-broker.js
|
|
196
|
+
/**
|
|
197
|
+
* The dsh harness's model broker: the member-aware model surface the core
|
|
198
|
+
* gateway routes to (`harnessModel` for the settings card, `memberModel` /
|
|
199
|
+
* `setMemberModel` for the composer picker). It owns the session-level
|
|
200
|
+
* override map — shared with the exec provider (which consults it per round)
|
|
201
|
+
* and the live driver (which binds it at runtime spawn) — and retires a
|
|
202
|
+
* member's resident serve process when a switch leaves it bound to a
|
|
203
|
+
* different model, so the NEXT round respawns onto the new one while the
|
|
204
|
+
* sub-dsh session itself carries over via the on-disk resume.
|
|
205
|
+
*
|
|
206
|
+
* Resolution order is the family's fixed one (first match wins): override →
|
|
207
|
+
* the delegation's recorded model → the plugin-config `model` (settings) →
|
|
208
|
+
* the host instance's default model selection spelled `provider/model`
|
|
209
|
+
* (cliDefault — the selection a sub-dsh with no `--model` inherits) → the
|
|
210
|
+
* CLI's built-in default, which names nothing. The pickable vocabulary is
|
|
211
|
+
* settings + cliDefault + the host's own adapter enumeration (the public
|
|
212
|
+
* `ctx.llm` surface — `listProviders` × `listModels`, spelled `provider/model`
|
|
213
|
+
* exactly as the sub-dsh's `--model` expects; the same trio the host's model
|
|
214
|
+
* picker is built on) + the card's recent-model memory.
|
|
215
|
+
* @module @khorsheed/dsh-local-agent-dsh/model-broker
|
|
216
|
+
*/
|
|
217
|
+
/** Dedupe the choice vocabulary, dropping unset layers and blanks. */
|
|
218
|
+
function dedupeChoices(layers) {
|
|
219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
220
|
+
const choices = [];
|
|
221
|
+
for (const layer of layers) {
|
|
222
|
+
const value = layer?.trim();
|
|
223
|
+
if (value === void 0 || value === "" || seen.has(value)) continue;
|
|
224
|
+
seen.add(value);
|
|
225
|
+
choices.push(value);
|
|
226
|
+
}
|
|
227
|
+
return choices;
|
|
228
|
+
}
|
|
229
|
+
var DshModelBroker = class {
|
|
230
|
+
deps;
|
|
231
|
+
constructor(deps) {
|
|
232
|
+
this.deps = deps;
|
|
233
|
+
}
|
|
234
|
+
configurationAdapter(binding) {
|
|
235
|
+
const resolve = async (selection) => {
|
|
236
|
+
let directory = this.deps.catalog?.read();
|
|
237
|
+
if (directory?.status === "loading") directory = await this.deps.catalog?.refresh();
|
|
238
|
+
const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel() ?? this.deps.cliDefault();
|
|
239
|
+
const entry = directory?.entries.find((entry) => entry.value === model);
|
|
240
|
+
const effort = (selection.effort.mode === "value" ? selection.effort.value : selection.effort.mode === "inherit" ? binding.effort : void 0) ?? this.deps.defaultEffort?.() ?? entry?.reasoning?.default;
|
|
241
|
+
if (effort !== void 0 && !entry?.reasoning?.options.some((option) => option.value === effort)) throw new Error("DSH has not advertised this reasoning effort for the selected model");
|
|
242
|
+
return {
|
|
243
|
+
...model === void 0 ? {} : { model },
|
|
244
|
+
...effort === void 0 ? {} : { effort }
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
validate: async (selection) => {
|
|
249
|
+
await resolve(selection);
|
|
250
|
+
},
|
|
251
|
+
prepare: resolve,
|
|
252
|
+
apply: async (selection) => {
|
|
253
|
+
const resolved = await resolve(selection);
|
|
254
|
+
await this.deps.retireRuntime(binding.childSessionId);
|
|
255
|
+
return resolved;
|
|
256
|
+
},
|
|
257
|
+
reconcile: async (state) => {
|
|
258
|
+
if (this.activeDelegations().includes(binding.childSessionId)) return {
|
|
259
|
+
active: true,
|
|
260
|
+
matches: "unknown",
|
|
261
|
+
resolved: {}
|
|
262
|
+
};
|
|
263
|
+
await this.deps.retireRuntime(binding.childSessionId);
|
|
264
|
+
return {
|
|
265
|
+
active: false,
|
|
266
|
+
matches: "current",
|
|
267
|
+
resolved: await resolve(state.current.selection)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async modelDirectory(childSessionId, refresh = false) {
|
|
273
|
+
if (refresh) await this.deps.catalog?.refresh();
|
|
274
|
+
return this.modelInfo(childSessionId).directory ?? {
|
|
275
|
+
entries: [],
|
|
276
|
+
complete: false,
|
|
277
|
+
customInput: true,
|
|
278
|
+
status: "unsupported",
|
|
279
|
+
refreshing: false,
|
|
280
|
+
revision: 0
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async *followModelDirectory(childSessionId, signal) {
|
|
284
|
+
if (this.deps.catalog === void 0) {
|
|
285
|
+
yield await this.modelDirectory(childSessionId);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
for await (const _snapshot of this.deps.catalog.follow(signal)) yield await this.modelDirectory(childSessionId);
|
|
289
|
+
}
|
|
290
|
+
/** The member's in-flight rounds (an empty answer on a core that predates the read). */
|
|
291
|
+
activeDelegations() {
|
|
292
|
+
const registry = this.deps.ctx.localAgent;
|
|
293
|
+
return typeof registry.activeDelegations === "function" ? registry.activeDelegations() : [];
|
|
294
|
+
}
|
|
295
|
+
/** The model the member's NEXT spawn binds explicitly: override → recorded → settings. */
|
|
296
|
+
boundLayer(childSessionId) {
|
|
297
|
+
const override = this.deps.overrides.get(childSessionId);
|
|
298
|
+
if (override !== void 0) return override;
|
|
299
|
+
const recorded = this.deps.ctx.localAgent.getDelegation(childSessionId)?.model?.trim();
|
|
300
|
+
if (recorded !== void 0 && recorded !== "") return recorded;
|
|
301
|
+
return this.deps.settingsModel();
|
|
302
|
+
}
|
|
303
|
+
/** Read the model surface; every layer reports its own value so a UI can explain the effective one. */
|
|
304
|
+
modelInfo(childSessionId, delegationModel) {
|
|
305
|
+
const settings = this.deps.settingsModel();
|
|
306
|
+
const cliDefault = this.deps.cliDefault();
|
|
307
|
+
const directory = this.deps.catalog?.read();
|
|
308
|
+
const override = childSessionId === void 0 ? void 0 : this.deps.overrides.get(childSessionId);
|
|
309
|
+
const delegation = delegationModel?.trim() === "" ? void 0 : delegationModel;
|
|
310
|
+
const effective = override ?? delegation ?? settings ?? cliDefault;
|
|
311
|
+
const source = override !== void 0 ? "override" : delegation !== void 0 ? "delegation" : settings !== void 0 ? "settings" : cliDefault !== void 0 ? "cli-config" : "cli-builtin";
|
|
312
|
+
const inFlight = childSessionId !== void 0 && this.activeDelegations().includes(childSessionId);
|
|
313
|
+
return {
|
|
314
|
+
...effective === void 0 ? {} : { effective },
|
|
315
|
+
source,
|
|
316
|
+
...override === void 0 ? {} : { override },
|
|
317
|
+
...delegation === void 0 ? {} : { delegation },
|
|
318
|
+
...settings === void 0 ? {} : { settings },
|
|
319
|
+
...cliDefault === void 0 ? {} : { cliDefault },
|
|
320
|
+
...directory === void 0 ? {} : { directory: extendModelDirectory(directory, [settings, cliDefault], this.deps.recentModels()) },
|
|
321
|
+
choices: dedupeChoices([
|
|
322
|
+
settings,
|
|
323
|
+
cliDefault,
|
|
324
|
+
...this.deps.discovered(),
|
|
325
|
+
...this.deps.recentModels()
|
|
326
|
+
]),
|
|
327
|
+
live: this.deps.live(),
|
|
328
|
+
switchable: !inFlight,
|
|
329
|
+
...inFlight ? { reason: "成员有进行中的委派轮次,等其完成后再切换模型" } : {}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Set (or clear) the member's session-level override. Same-model is a
|
|
334
|
+
* no-op; a different model with a live runtime bound elsewhere retires the
|
|
335
|
+
* runtime so the next round respawns (the sub-dsh session resumes from
|
|
336
|
+
* disk). Throws while a round is in flight — retiring mid-round would kill
|
|
337
|
+
* the run.
|
|
338
|
+
*/
|
|
339
|
+
async setMemberModel(childSessionId, model) {
|
|
340
|
+
if (typeof this.deps.ctx.localAgent.setMemberModel === "function") return this.deps.ctx.localAgent.setMemberModel(childSessionId, model);
|
|
341
|
+
if (this.activeDelegations().includes(childSessionId)) throw new Error(`subagent-dsh: 成员有进行中的委派轮次,等其完成后再切换模型 (child session ${childSessionId})`);
|
|
342
|
+
const trimmed = model?.trim();
|
|
343
|
+
const next = trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
344
|
+
if (this.deps.overrides.get(childSessionId) === next) return;
|
|
345
|
+
if (next === void 0) this.deps.overrides.delete(childSessionId);
|
|
346
|
+
else this.deps.overrides.set(childSessionId, next);
|
|
347
|
+
const bound = this.deps.liveBoundModel(childSessionId);
|
|
348
|
+
if (bound !== null && bound !== this.boundLayer(childSessionId)) await this.deps.retireRuntime(childSessionId);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region lib/types/index.js
|
|
353
|
+
/**
|
|
354
|
+
* dsh harness instantiation of the local-agent family, plus the DeepSeek
|
|
355
|
+
* settings toggle that gates it. Registers the `local-agent-dsh` settings
|
|
356
|
+
* namespace (default off); while ON, the controller registers the `dsh`
|
|
357
|
+
* harness into the core registry (scoped home under the shared homes root,
|
|
358
|
+
* `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
|
|
359
|
+
* the parent's resolved DeepSeek key), and the family delegation tool for it.
|
|
360
|
+
* While OFF — the default — nothing model-visible mounts, so the instance
|
|
361
|
+
* keeps exactly the current behavior (official in-process subagent tools) and
|
|
362
|
+
* the model never sees two overlapping delegation tools at once.
|
|
363
|
+
*
|
|
364
|
+
* The same namespace carries the resident-mode preferences (`live`,
|
|
365
|
+
* `liveMirrorGranularity`): the YAML config is the composition base, the
|
|
366
|
+
* settings card's writes are user-layer overrides, and a LiveDriverSwitch
|
|
367
|
+
* inside each enabled generation hot-swaps driver generations on a change —
|
|
368
|
+
* no reload, in-flight rounds never interrupted.
|
|
369
|
+
*
|
|
370
|
+
* The sub-dsh half lives in the sibling bundle
|
|
371
|
+
* `@khorsheed/dsh-local-agent-dsh-headless`: a headless profile under the
|
|
372
|
+
* scoped home that the provider spawns with `--session-id <uuid>` (fresh) or
|
|
373
|
+
* `--resume <uuid>` (continuation).
|
|
374
|
+
*
|
|
375
|
+
* @module @khorsheed/dsh-local-agent-dsh
|
|
376
|
+
*/
|
|
377
|
+
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
378
|
+
const name = "local-agent-dsh";
|
|
379
|
+
/** Services required before the toggle controller can mount. */
|
|
380
|
+
const inject = [
|
|
381
|
+
"localAgent",
|
|
382
|
+
"subagents",
|
|
383
|
+
"subprocess",
|
|
384
|
+
"credentials",
|
|
385
|
+
"settings"
|
|
386
|
+
];
|
|
387
|
+
/**
|
|
388
|
+
* Runtime schema so the Loader always passes an object, never undefined.
|
|
389
|
+
* The settings fields (`enabled`, `live`, `liveMirrorGranularity`, `model`,
|
|
390
|
+
* `recentModels`) ride the same schema, marked `.volatile()` where the host's
|
|
391
|
+
* schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed): there
|
|
392
|
+
* the SettingsForms writes hot-track the running fiber's Volatile references
|
|
393
|
+
* without a remount, and the official one-shot import moves an old
|
|
394
|
+
* settings.yaml section into this row's config. On 0.1.5 the call is absent,
|
|
395
|
+
* the fields stay plain config keys (a semantic superset — the profile row
|
|
396
|
+
* can now preset them too), and the legacy settings namespace below carries
|
|
397
|
+
* the user layer exactly as before.
|
|
398
|
+
*
|
|
399
|
+
* Bare `z` annotation: `z<LocalAgentDshConfig>` fails schemastery 3.18.4's
|
|
400
|
+
* variance under exactOptionalPropertyTypes (TS2375) and dropping the
|
|
401
|
+
* annotation trips TS2742; the interface stays the apply signature's contract.
|
|
402
|
+
*/
|
|
403
|
+
const SETTINGS_FIELDS = {
|
|
404
|
+
enabled: z.boolean().default(false),
|
|
405
|
+
live: z.boolean().default(false),
|
|
406
|
+
liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
|
|
407
|
+
model: z.string(),
|
|
408
|
+
recentModels: z.array(z.string()).default([])
|
|
409
|
+
};
|
|
410
|
+
/** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
|
|
411
|
+
function volatilize(field) {
|
|
412
|
+
return typeof field.volatile === "function" ? field.volatile() : field;
|
|
413
|
+
}
|
|
414
|
+
const Config = z.object({
|
|
415
|
+
profileName: z.string().default(DEFAULT_SUB_PROFILE_NAME),
|
|
416
|
+
apiKeyRef: z.string().default("DEEPSEEK_API_KEY"),
|
|
417
|
+
cliLaunch: z.array(z.string()),
|
|
418
|
+
headlessBundleDir: z.string(),
|
|
419
|
+
permissions: z.union([
|
|
420
|
+
z.const("read-only"),
|
|
421
|
+
z.const("workspace-write"),
|
|
422
|
+
z.const("danger-full-access")
|
|
423
|
+
]),
|
|
424
|
+
model: volatilize(SETTINGS_FIELDS.model),
|
|
425
|
+
live: volatilize(SETTINGS_FIELDS.live),
|
|
426
|
+
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
427
|
+
liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
|
|
428
|
+
enabled: volatilize(SETTINGS_FIELDS.enabled),
|
|
429
|
+
recentModels: volatilize(SETTINGS_FIELDS.recentModels)
|
|
430
|
+
});
|
|
431
|
+
/**
|
|
432
|
+
* Settings namespace owning the DeepSeek toggle and the live preferences. On
|
|
433
|
+
* 0.1.5 the Cordis config feeds the namespace's composition `base` layer, so
|
|
434
|
+
* a field absent from the user layer inherits the YAML value — the settings
|
|
435
|
+
* card only ever stores deliberate overrides. On rc.1 the namespace is the
|
|
436
|
+
* plugin row id itself: the row config IS the document, and the settings
|
|
437
|
+
* form's user layer rides the profile patch.
|
|
438
|
+
*/
|
|
439
|
+
const DSH_SETTINGS_NAMESPACE = "local-agent-dsh";
|
|
440
|
+
/**
|
|
441
|
+
* The card's 0.1.5 schema: `enabled` OFF (default) means the official
|
|
442
|
+
* in-process subagent stays the only delegation path; the live fields mirror
|
|
443
|
+
* the YAML config's deployment defaults through the composition base.
|
|
444
|
+
*/
|
|
445
|
+
const DSH_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
446
|
+
/** The delegation tool the toggle mounts while ON. */
|
|
447
|
+
const DSH_TOOL_NAME = "subagent_dsh";
|
|
448
|
+
/** Resolve the sub-dsh credential or report unconfigured. */
|
|
449
|
+
async function resolveApiKey(ctx, config) {
|
|
450
|
+
return ctx.credentials.resolve(credentialRef(config.apiKeyRef ?? "DEEPSEEK_API_KEY"));
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Mount the toggle controller: register the settings namespace, then mirror
|
|
454
|
+
* its value into harness/provider/tool registrations, live.
|
|
455
|
+
* @param ctx - plugin context carrying the family registry, subagents,
|
|
456
|
+
* subprocess, credentials, and settings services.
|
|
457
|
+
* @param config - validated plugin config.
|
|
458
|
+
*/
|
|
459
|
+
function apply(ctx, config) {
|
|
460
|
+
ctx.effect(() => {
|
|
461
|
+
const readSettings = () => ({
|
|
462
|
+
enabled: vol(config.enabled) ?? false,
|
|
463
|
+
live: vol(config.live) ?? false,
|
|
464
|
+
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
|
|
465
|
+
model: vol(config.model),
|
|
466
|
+
recentModels: vol(config.recentModels) ?? []
|
|
467
|
+
});
|
|
468
|
+
const legacyBase = {};
|
|
469
|
+
for (const field of [
|
|
470
|
+
"enabled",
|
|
471
|
+
"live",
|
|
472
|
+
"liveMirrorGranularity",
|
|
473
|
+
"model"
|
|
474
|
+
]) {
|
|
475
|
+
const value = vol(config[field]);
|
|
476
|
+
if (value !== void 0) legacyBase[field] = value;
|
|
477
|
+
}
|
|
478
|
+
const face = settingsFace(ctx, {
|
|
479
|
+
namespace: DSH_SETTINGS_NAMESPACE,
|
|
480
|
+
legacySchema: DSH_SETTINGS_SCHEMA,
|
|
481
|
+
legacyBase,
|
|
482
|
+
read: readSettings
|
|
483
|
+
});
|
|
484
|
+
const resolveModel = () => {
|
|
485
|
+
const model = face.get().model?.trim();
|
|
486
|
+
return model === void 0 || model === "" ? void 0 : model;
|
|
487
|
+
};
|
|
488
|
+
const hostDefaultModel = () => {
|
|
489
|
+
try {
|
|
490
|
+
const selection = ctx.get("agentDefaultModel")?.currentSelection?.();
|
|
491
|
+
if (typeof selection?.model === "string" && selection.model !== "") return typeof selection.provider === "string" && selection.provider !== "" ? `${selection.provider}/${selection.model}` : selection.model;
|
|
492
|
+
} catch {}
|
|
493
|
+
};
|
|
494
|
+
const modelCatalog = new DshModelCatalog({
|
|
495
|
+
llm: () => ctx.get("llm"),
|
|
496
|
+
defaultModel: hostDefaultModel
|
|
497
|
+
});
|
|
498
|
+
modelCatalog.read();
|
|
499
|
+
ctx.on("llm/adapters-updated", () => {
|
|
500
|
+
modelCatalog.invalidate();
|
|
501
|
+
});
|
|
502
|
+
const memberModelOverrides = /* @__PURE__ */ new Map();
|
|
503
|
+
let currentLiveSwitch;
|
|
504
|
+
const modelBroker = new DshModelBroker({
|
|
505
|
+
ctx,
|
|
506
|
+
settingsModel: resolveModel,
|
|
507
|
+
cliDefault: hostDefaultModel,
|
|
508
|
+
defaultEffort: () => ctx.get("agentDefaultModel")?.currentSelection().reasoningEffort,
|
|
509
|
+
discovered: () => modelCatalog.read().entries.filter((entry) => !entry.hidden).map((entry) => entry.value),
|
|
510
|
+
catalog: modelCatalog,
|
|
511
|
+
recentModels: () => face.get().recentModels ?? [],
|
|
512
|
+
live: () => face.get().live,
|
|
513
|
+
overrides: memberModelOverrides,
|
|
514
|
+
liveBoundModel: (childSessionId) => currentLiveSwitch?.boundModel(childSessionId) ?? null,
|
|
515
|
+
retireRuntime: (childSessionId) => currentLiveSwitch?.retireRuntime(childSessionId) ?? Promise.resolve()
|
|
516
|
+
});
|
|
517
|
+
/**
|
|
518
|
+
* This deployment's preset root — where a person or a pack installed the
|
|
519
|
+
* presets this instance offers, and the SOURCE every scope's own copy is
|
|
520
|
+
* taken from. Derived the way the roster derives it, from the settings
|
|
521
|
+
* service's harness home when there is one.
|
|
522
|
+
*/
|
|
523
|
+
const instancePresetRoot = () => {
|
|
524
|
+
const home = ctx.get?.("settings")?.home;
|
|
525
|
+
return home === void 0 ? defaultPresetRoot() : join(home, USER_PRESET_DIR);
|
|
526
|
+
};
|
|
527
|
+
const harness = {
|
|
528
|
+
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
529
|
+
const driver = currentLiveSwitch?.resolve(binding.childSessionId);
|
|
530
|
+
if (driver === void 0 || driver.disabled) throw new Error("Enable the native live harness before preparing a coordinator");
|
|
531
|
+
await driver.prepare({
|
|
532
|
+
cwd: binding.cwd,
|
|
533
|
+
homeDir: ctx.localAgent.homeDir("dsh", binding.scope),
|
|
534
|
+
childSession,
|
|
535
|
+
parentSessionId: binding.parentSessionId,
|
|
536
|
+
configuration: configuration.resolved,
|
|
537
|
+
sessionId: binding.childSessionId
|
|
538
|
+
}, signal);
|
|
539
|
+
},
|
|
540
|
+
name: "dsh",
|
|
541
|
+
displayName: "dsh",
|
|
542
|
+
homeEnvVar: "DSH_HOME",
|
|
543
|
+
delegationProvider: "dsh-cli",
|
|
544
|
+
modelBroker,
|
|
545
|
+
records: { listSessions: (homeDir) => listDshSessions(homeDir) },
|
|
546
|
+
provision: (scopedHome, options) => {
|
|
547
|
+
const provisioned = provisionDshScope(scopedHome, config, {
|
|
548
|
+
...options?.preset === void 0 ? {} : { preset: options.preset },
|
|
549
|
+
presetRoot: instancePresetRoot()
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
...provisioned.preset === void 0 ? {} : { preset: provisioned.preset },
|
|
553
|
+
...provisioned.presetSnapshot === void 0 ? {} : { presetSnapshot: provisioned.presetSnapshot }
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
isAuthenticated: async () => {
|
|
557
|
+
try {
|
|
558
|
+
return await resolveApiKey(ctx, config) !== void 0;
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
effectiveSettings: async (scopedHome) => {
|
|
564
|
+
const cliVersion = await dshCliVersion(ctx, config, scopedHome).catch(() => void 0);
|
|
565
|
+
const model = resolveModel() ?? hostDefaultModel();
|
|
566
|
+
return {
|
|
567
|
+
drive: face.get().live ? "live" : "exec",
|
|
568
|
+
baseUrlSet: false,
|
|
569
|
+
containerNodeOptions: CONTAINER_NODE_OPTIONS,
|
|
570
|
+
...config.permissions !== void 0 ? { sandbox: config.permissions } : {},
|
|
571
|
+
...model !== void 0 ? { model } : {},
|
|
572
|
+
...cliVersion !== void 0 ? { cliVersion } : {}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const homeDir = ctx.localAgent.homeDir("dsh");
|
|
577
|
+
let generation = 0;
|
|
578
|
+
let currentEnabled;
|
|
579
|
+
const disposers = [];
|
|
580
|
+
const sync = (enabled) => {
|
|
581
|
+
if (enabled === currentEnabled) return;
|
|
582
|
+
currentEnabled = enabled;
|
|
583
|
+
generation += 1;
|
|
584
|
+
const gen = generation;
|
|
585
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
586
|
+
if (!enabled) return;
|
|
587
|
+
provisionDshScope(homeDir, config, { presetRoot: instancePresetRoot() });
|
|
588
|
+
disposers.push(ctx.localAgent.register(harness));
|
|
589
|
+
const liveSwitch = new LiveDriverSwitch(ctx, face, config, { modelFor: (childSessionId) => memberModelOverrides.get(childSessionId) ?? resolveModel() });
|
|
590
|
+
currentLiveSwitch = liveSwitch;
|
|
591
|
+
disposers.push(ctx.subagents.registerProvider(new DshCliProvider(ctx, config, liveSwitch.resolve, resolveModel, (childSessionId) => memberModelOverrides.get(childSessionId))));
|
|
592
|
+
disposers.push(() => {
|
|
593
|
+
if (currentLiveSwitch === liveSwitch) currentLiveSwitch = void 0;
|
|
594
|
+
liveSwitch.dispose();
|
|
595
|
+
});
|
|
596
|
+
ctx.plugin(toolModule, {
|
|
597
|
+
provider: "dsh-cli",
|
|
598
|
+
toolName: DSH_TOOL_NAME
|
|
599
|
+
}).then((fiber) => {
|
|
600
|
+
if (gen !== generation) {
|
|
601
|
+
fiber.dispose();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
disposers.push(() => {
|
|
605
|
+
fiber.dispose();
|
|
606
|
+
});
|
|
607
|
+
}, (error) => {
|
|
608
|
+
ctx.logger.warn(`local-agent-dsh: delegation tool mount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
sync(face.get().enabled);
|
|
612
|
+
const unwatch = face.watch((next) => {
|
|
613
|
+
sync(next.enabled);
|
|
614
|
+
});
|
|
615
|
+
return () => {
|
|
616
|
+
modelCatalog.dispose();
|
|
617
|
+
unwatch();
|
|
618
|
+
generation += 1;
|
|
619
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
620
|
+
};
|
|
621
|
+
}, "local-agent-dsh: toggle controller");
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
export { Config, DSH_SETTINGS_NAMESPACE, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh`.
|
|
4
|
+
* @module @khorsheed/dsh-local-agent-dsh/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@khorsheed/dsh-local-agent-dsh";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "local-agent-dsh-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* The dsh harness definition is this bundle's contract with the core
|
|
13
|
+
* registry: whenever a `dsh` harness registers, its scoped-home variable must
|
|
14
|
+
* be the one this bundle pins, and it must delegate through the `dsh-cli`
|
|
15
|
+
* provider this bundle mounts.
|
|
16
|
+
*/
|
|
17
|
+
const install = (ctx, fail) => {
|
|
18
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
19
|
+
if (eventName !== "localAgent/harness-added") return;
|
|
20
|
+
const harness = args[0];
|
|
21
|
+
if (harness.name !== "dsh") return;
|
|
22
|
+
if (harness.homeEnvVar !== "DSH_HOME") fail("dsh harness registered with a home env other than the local-agent-dsh contract");
|
|
23
|
+
if (harness.delegationProvider !== "dsh-cli") fail("dsh harness registered with a delegation provider other than the local-agent-dsh contract");
|
|
24
|
+
}, { global: true });
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Register this package's invariant companion.
|
|
28
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
29
|
+
* @returns the installed registration's disposer.
|
|
30
|
+
*/
|
|
31
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
32
|
+
//#endregion
|
|
33
|
+
export { apply, inject, name };
|