@klarkxy/dsh-model-hub 0.1.2 → 0.1.4

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/lib/index.js CHANGED
@@ -1,13 +1,45 @@
1
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import Schema from "@deepseek-ai/schemastery";
4
4
  import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
5
5
  import { modelRouteRoleSchemastery, modelRouteSchemastery } from "@klarkxy/dsh-model-route/schemastery";
6
6
  import { detectModelFields, modelRouteOverride, normalizeModelRoute } from "@klarkxy/dsh-model-route";
7
- import { MODEL_SETTINGS_CHANGED_EVENT, collectModelSettings, notifyModelSettingsChanged } from "@klarkxy/dsh-plugin-kit/model-settings";
8
- import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
- import { MODEL_SETTINGS_CHANGES_REMOTE } from "@klarkxy/dsh-plugin-kit/contracts";
10
- import { parseModelSettingsChangeFrame } from "@klarkxy/dsh-plugin-kit/model-settings-client";
7
+ import { collectModelSettings, notifyModelSettingsChanged } from "@klarkxy/dsh-plugin-kit/model-settings";
8
+ import { isSubagentSession, modelSettingsChangesIdentity } from "@klarkxy/dsh-plugin-kit/contracts";
9
+ import { createModelSettingsChanges } from "@klarkxy/dsh-plugin-kit/model-settings-changes";
10
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
11
+ //#region src/diagnostics.ts
12
+ /** Invalidation is constant-time. Only startup and explicit reads request a
13
+ * diagnostic; a burst shares one deferred run. Work never reschedules itself. */
14
+ function createProbeDiagnostics(run, onError) {
15
+ let dirty = true;
16
+ let stopped = false;
17
+ let timer;
18
+ let running;
19
+ return {
20
+ invalidate() {
21
+ if (!stopped) dirty = true;
22
+ },
23
+ request() {
24
+ if (stopped || !dirty || timer !== void 0 || running) return;
25
+ timer = setTimeout(() => {
26
+ timer = void 0;
27
+ if (stopped || !dirty) return;
28
+ dirty = false;
29
+ running = Promise.resolve().then(run).catch(onError).finally(() => {
30
+ running = void 0;
31
+ });
32
+ }, 0);
33
+ },
34
+ async dispose() {
35
+ stopped = true;
36
+ if (timer !== void 0) clearTimeout(timer);
37
+ timer = void 0;
38
+ await running;
39
+ }
40
+ };
41
+ }
42
+ //#endregion
11
43
  //#region src/probe.ts
12
44
  /**
13
45
  * Pure probe-report builder for the model hub. Takes the host's
@@ -209,170 +241,10 @@ function createHubSettings(events, settings, ownNamespace = "@klarkxy/dsh-model-
209
241
  }
210
242
  //#endregion
211
243
  //#region src/changes.ts
212
- var __create = Object.create;
213
- var __defProp = Object.defineProperty;
214
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
215
- var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
216
- var __typeError = (msg) => {
217
- throw TypeError(msg);
218
- };
219
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
220
- enumerable: true,
221
- configurable: true,
222
- writable: true,
223
- value
224
- }) : obj[key] = value;
225
- var __name = (target, value) => __defProp(target, "name", {
226
- value,
227
- configurable: true
228
- });
229
- var __decoratorStart = (base) => [
230
- ,
231
- ,
232
- ,
233
- __create(base?.[__knownSymbol("metadata")] ?? null)
234
- ];
235
- var __decoratorStrings = [
236
- "class",
237
- "method",
238
- "getter",
239
- "setter",
240
- "accessor",
241
- "field",
242
- "value",
243
- "get",
244
- "set"
245
- ];
246
- var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
247
- var __decoratorContext = (kind, name, done, metadata, fns) => ({
248
- kind: __decoratorStrings[kind],
249
- name,
250
- metadata,
251
- addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
252
- });
253
- var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
254
- var __runInitializers = (array, flags, self, value) => {
255
- for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
256
- return value;
257
- };
258
- var __decorateElement = (array, flags, name, decorators, target, extra) => {
259
- var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
260
- var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
261
- var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
262
- var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
263
- get [name]() {
264
- return __privateGet(this, extra);
265
- },
266
- set [name](x) {
267
- return __privateSet(this, extra, x);
268
- }
269
- }, name));
270
- k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
271
- for (var i = decorators.length - 1; i >= 0; i--) {
272
- ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
273
- if (k) {
274
- ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
275
- if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
276
- if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
277
- }
278
- it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : {
279
- get: desc.get,
280
- set: desc.set
281
- } : target, ctx), done._ = 1;
282
- if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
283
- else if (typeof it !== "object" || it === null) __typeError("Object expected");
284
- else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
285
- }
286
- return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
287
- };
288
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
289
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
290
- var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError("Cannot use the \"in\" operator on this value") : member.has(obj);
291
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
292
- var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
293
- var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
294
- var _watch_dec;
295
- var _a;
296
- var _init;
297
- async function* watchModelSettingsChanges(events, signal) {
298
- signal.throwIfAborted();
299
- const pending = /* @__PURE__ */ new Map();
300
- let full = false;
301
- let wake;
302
- const changed = (ns, revision) => {
303
- let frame;
304
- try {
305
- frame = parseModelSettingsChangeFrame({
306
- kind: "changed",
307
- ns,
308
- revision
309
- });
310
- } catch {
311
- return;
312
- }
313
- if (!full) {
314
- if (!pending.has(ns) && pending.size >= 256) {
315
- pending.clear();
316
- full = true;
317
- } else pending.set(ns, frame);
318
- }
319
- wake?.();
320
- };
321
- const disposers = [];
322
- const cleanup = () => {
323
- for (const off of disposers.splice(0).reverse()) off();
324
- pending.clear();
325
- };
326
- const aborted = () => {
327
- cleanup();
328
- wake?.();
329
- };
330
- signal.addEventListener("abort", aborted, { once: true });
331
- try {
332
- disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)));
333
- disposers.push(events.on("settings/document-updated", changed));
334
- signal.throwIfAborted();
335
- yield { kind: "ready" };
336
- while (true) {
337
- signal.throwIfAborted();
338
- if (full) {
339
- full = false;
340
- yield { kind: "ready" };
341
- continue;
342
- }
343
- if (pending.size) {
344
- const [ns, frame] = pending.entries().next().value;
345
- pending.delete(ns);
346
- yield frame;
347
- continue;
348
- }
349
- await new Promise((resolve) => {
350
- wake = resolve;
351
- if (signal.aborted || full || pending.size) resolve();
352
- });
353
- wake = void 0;
354
- }
355
- } finally {
356
- signal.removeEventListener("abort", aborted);
357
- cleanup();
358
- }
244
+ /** Model Hub forwards every settings namespace; feature plugins filter their own. */
245
+ function registerHubModelSettingsChanges(ctx) {
246
+ createModelSettingsChanges(ctx, modelSettingsChangesIdentity("@klarkxy/dsh-model-hub"));
359
247
  }
360
- var ModelSettingsChangesController = class extends (_a = TypertRemoteService, _watch_dec = [Remote({ mode: "stream" })], _a) {
361
- constructor(ctx) {
362
- super(ctx, MODEL_SETTINGS_CHANGES_REMOTE.descriptors[0].service, { namespace: MODEL_SETTINGS_CHANGES_REMOTE.descriptors[0].namespace });
363
- __runInitializers(_init, 5, this);
364
- __publicField(this, "lifetime", new AbortController());
365
- ctx.effect(() => () => {
366
- this.lifetime.abort(/* @__PURE__ */ new Error("Model settings sync owner unloaded"));
367
- });
368
- }
369
- watch(signal) {
370
- return watchModelSettingsChanges(this.ctx, AbortSignal.any([this.lifetime.signal, signal]));
371
- }
372
- };
373
- _init = __decoratorStart(_a);
374
- __decorateElement(_init, 1, "watch", _watch_dec, ModelSettingsChangesController);
375
- __decoratorMetadata(_init, ModelSettingsChangesController);
376
248
  //#endregion
377
249
  //#region src/cast.ts
378
250
  /**
@@ -457,9 +329,15 @@ function validateIndex(value) {
457
329
  */
458
330
  function buildCastPrompt(enabled, routes) {
459
331
  if (!enabled || !routes.length) return "";
460
- const annotated = routes.filter((route) => typeof route.tendency === "string" && route.tendency);
461
- if (!annotated.length) return "";
462
- return ["Subagent cast: the user has annotated the models available for subagent delegation with task tendencies. When delegating to a subagent, prefer a model whose tendency matches the task at hand; treat models without a tendency as general-purpose. These tendencies are advisory — the final model choice is always yours.", ...annotated.map((route) => `- ${route.provider}/${route.model}${route.reasoningEffort ? ` (reasoning effort: ${route.reasoningEffort})` : ""}: ${route.tendency}`)].join("\n");
332
+ const annotated = routes.filter((route) => typeof route.tendency === "string" && route.tendency.trim());
333
+ const guidance = [
334
+ "Subagent model selection:",
335
+ "When delegating an independent task, actively choose a suitable model instead of silently inheriting your own. If list_subagent_models and a delegation tool with provider/model fields are available, inspect the available routes before the first model-selectable delegation in this task, reuse those results for later delegations, and supply provider and model together explicitly.",
336
+ "For independent reviews, alternative approaches, or disputed conclusions, prefer a model different from your own when a suitable candidate is available. Choose for task fit and the user's constraints; using the same model is appropriate when it fits better or no suitable alternative is available. Do not create extra subtasks solely for model diversity.",
337
+ "The current Session's discovery results and tool schema are authoritative. Configured model notes may refer to routes unavailable in this Session. If discovery or model-selection fields are unavailable, use the supported delegation behavior; do not invent routes or pass unsupported fields to tools such as subagent_fork. When changing models, omit reasoning_effort unless you have verified a supported value."
338
+ ];
339
+ if (annotated.length) guidance.push("User-provided task tendencies (advisory): prefer a matching tendency among available models. Models without a tendency remain general-purpose candidates; no model-specific strengths are assumed.", ...annotated.map((route) => `- ${route.provider}/${route.model}${route.reasoningEffort ? ` (reasoning effort: ${route.reasoningEffort})` : ""}: ${route.tendency}`));
340
+ return guidance.join("\n");
463
341
  }
464
342
  function createCast(deps) {
465
343
  const castFile = () => {
@@ -683,7 +561,7 @@ function createCast(deps) {
683
561
  return buildCastPrompt(snapshot.enabled, mergeTendencies(snapshot.routes, readTendencies().tendencies));
684
562
  } catch (error) {
685
563
  deps.logger?.warn("model-hub: cast tendencies unavailable: %s", error.message);
686
- return "";
564
+ return buildCastPrompt(snapshot.enabled, snapshot.routes);
687
565
  }
688
566
  };
689
567
  return {
@@ -697,6 +575,128 @@ function createCast(deps) {
697
575
  };
698
576
  }
699
577
  //#endregion
578
+ //#region src/running.ts
579
+ /**
580
+ * dsh-model-hub — running subagent routes (host half).
581
+ *
582
+ * Read-only live view answering "which model and reasoning effort is each
583
+ * running subagent actually using". `subagent/start` and `subagent/end`
584
+ * maintain the live set; each child's effective route is read from its
585
+ * session epoch header (`session.requestHeader()`) and kept current through
586
+ * the session's own `request/header` events (reason `change` covers mid-run
587
+ * model switches). Children already running when this plugin (re)loads are
588
+ * recovered from the live agent registry via `isSubagentSession`.
589
+ *
590
+ * The effective route is session data recorded by the host; this tracker
591
+ * never writes to sessions or settings. Display-only invalidations reuse the
592
+ * model-settings change channel with RUNNING_NAMESPACE so open pages refetch.
593
+ */
594
+ /** Invalidation namespace for the running-subagent view; carries no settings. */
595
+ const RUNNING_NAMESPACE = "@klarkxy/dsh-model-hub/running-subagents";
596
+ function text(value) {
597
+ return typeof value === "string" ? value.trim() : "";
598
+ }
599
+ /** Normalize one epoch header into the display route plus the adapter-default flag. */
600
+ function routeFromHeader(header) {
601
+ const config = header?.config;
602
+ const provider = text(config?.provider);
603
+ const model = text(config?.model);
604
+ if (!provider || !model) return {};
605
+ const effort = text(config?.reasoningEffort);
606
+ return {
607
+ route: effort ? {
608
+ provider,
609
+ model,
610
+ reasoningEffort: effort
611
+ } : {
612
+ provider,
613
+ model
614
+ },
615
+ ...effort && header?.adapterDefaults?.reasoningEffort === true ? { effortIsAdapterDefault: true } : {}
616
+ };
617
+ }
618
+ function sameRoute(left, right) {
619
+ return left.route?.provider === right.route?.provider && left.route?.model === right.route?.model && left.route?.reasoningEffort === right.route?.reasoningEffort && left.effortIsAdapterDefault === right.effortIsAdapterDefault;
620
+ }
621
+ function createRunningTracker(deps) {
622
+ const entries = /* @__PURE__ */ new Map();
623
+ let revision = 0;
624
+ const bump = () => deps.notify(++revision);
625
+ const add = (id, info) => {
626
+ const session = (deps.agents?.get(id))?.session;
627
+ const next = routeFromHeader(session?.requestHeader?.());
628
+ const existing = entries.get(id);
629
+ if (existing) {
630
+ const providerName = info?.provider ?? existing.providerName;
631
+ const local = info?.local ?? existing.local;
632
+ const changed = existing.providerName !== providerName || existing.local !== local || !sameRoute(existing, next);
633
+ existing.providerName = providerName;
634
+ existing.local = local;
635
+ existing.route = next.route;
636
+ existing.effortIsAdapterDefault = next.effortIsAdapterDefault;
637
+ if (changed) bump();
638
+ return;
639
+ }
640
+ entries.set(id, {
641
+ id,
642
+ local: info?.local !== false,
643
+ ...info?.provider ? { providerName: info.provider } : {},
644
+ ...typeof session?.header?.parentSession === "string" && session.header.parentSession ? { parentSessionId: session.header.parentSession } : {},
645
+ ...typeof session?.header?.createdAt === "number" ? { startedAt: session.header.createdAt } : {},
646
+ route: next.route,
647
+ effortIsAdapterDefault: next.effortIsAdapterDefault
648
+ });
649
+ bump();
650
+ };
651
+ const remove = (id) => {
652
+ if (entries.delete(id)) bump();
653
+ };
654
+ try {
655
+ for (const agent of deps.agents?.list() ?? []) if (agent?.id && isSubagentSession(agent.session)) add(agent.id);
656
+ } catch {}
657
+ deps.events.on("subagent/start", (info) => {
658
+ if (typeof info?.id === "string" && info.id) add(info.id, info);
659
+ });
660
+ deps.events.on("subagent/end", (info) => {
661
+ if (typeof info?.id === "string") remove(info.id);
662
+ });
663
+ deps.events.on("agent/disposed", (payload) => {
664
+ if (typeof payload?.agent?.id === "string") remove(payload.agent.id);
665
+ });
666
+ deps.events.on("session/event", (session, event) => {
667
+ if (event?.type !== "request/header") return;
668
+ const id = typeof session?.id === "string" ? session.id : void 0;
669
+ const entry = id ? entries.get(id) : void 0;
670
+ if (!entry) return;
671
+ const next = routeFromHeader(event.data?.header);
672
+ if (sameRoute(entry, next)) return;
673
+ entry.route = next.route;
674
+ entry.effortIsAdapterDefault = next.effortIsAdapterDefault;
675
+ bump();
676
+ });
677
+ return { async list(signal) {
678
+ const pendingParents = /* @__PURE__ */ new Set();
679
+ for (const entry of entries.values()) if (entry.label === void 0 && entry.parentSessionId) pendingParents.add(entry.parentSessionId);
680
+ if (deps.subagents && pendingParents.size) await Promise.allSettled([...pendingParents].map(async (parent) => {
681
+ const children = await deps.subagents.listChildren(parent, signal);
682
+ const labels = new Map(children.map((child) => [child.id, child.label]));
683
+ for (const entry of entries.values()) {
684
+ if (entry.parentSessionId !== parent || entry.label !== void 0) continue;
685
+ const label = labels.get(entry.id);
686
+ if (typeof label === "string" && label.trim()) entry.label = label.trim();
687
+ }
688
+ }));
689
+ const snapshot = [...entries.values()].map((entry) => ({
690
+ ...entry,
691
+ route: entry.route ? { ...entry.route } : void 0
692
+ })).sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0) || a.id.localeCompare(b.id));
693
+ return {
694
+ revision,
695
+ entries: snapshot
696
+ };
697
+ } };
698
+ }
699
+ //#endregion
700
700
  //#region src/index.ts
701
701
  /**
702
702
  * dsh-model-hub — cross-plugin model settings hub.
@@ -735,7 +735,7 @@ const Config = Schema.object({
735
735
  }).volatile()
736
736
  });
737
737
  function apply(ctx) {
738
- new ModelSettingsChangesController(ctx);
738
+ registerHubModelSettingsChanges(ctx);
739
739
  const profile = ctx.get("profileContext");
740
740
  const settings = ctx.settings;
741
741
  const describe = () => settings.describe({ redactSecrets: true });
@@ -751,13 +751,20 @@ function apply(ctx) {
751
751
  logger: ctx.logger,
752
752
  notifyTendenciesChanged: (revision) => notifyModelSettingsChanged(ctx, CAST_TENDENCIES_NAMESPACE, revision)
753
753
  });
754
+ const running = createRunningTracker({
755
+ events: ctx,
756
+ agents: ctx.get("agents"),
757
+ subagents: ctx.get("subagents"),
758
+ notify: (revision) => notifyModelSettingsChanged(ctx, RUNNING_NAMESPACE, revision)
759
+ });
754
760
  ctx.systemPrompt.section({
755
761
  name: "model-hub:subagent-cast",
756
762
  order: ctx.systemPrompt.getSectionOrder("TOOL_SUBAGENT") + 1,
757
763
  interpolate: false,
758
764
  text: () => cast.promptText(() => ctx.get("subagentModelSelection")?.current())
759
765
  });
760
- const run = (reason) => {
766
+ const run = async () => {
767
+ const reason = "requested";
761
768
  let report;
762
769
  try {
763
770
  report = buildProbeReport(describe());
@@ -768,20 +775,33 @@ function apply(ctx) {
768
775
  ctx.logger.info("model-hub: probe (%s): %d namespaces, %d with model fields; self-test extra-key=%s role=%s", reason, report.totalNamespaces, report.namespacesWithModelFields, report.selfTest.extraKey, report.selfTest.role);
769
776
  if (profile?.dir) try {
770
777
  const dir = join(profile.dir, "data", "model-hub");
771
- mkdirSync(dir, { recursive: true });
772
- writeFileSync(join(dir, "probe-report.json"), JSON.stringify(report, null, 2));
778
+ await mkdir(dir, { recursive: true });
779
+ await writeFile(join(dir, "probe-report.json"), JSON.stringify(report, null, 2));
773
780
  } catch (error) {
774
781
  ctx.logger.warn("model-hub: could not write probe report: %s", error instanceof Error ? error.message : String(error));
775
782
  }
776
783
  };
777
- run("boot");
778
- ctx.on("settings/document-updated", () => run("document-updated"));
784
+ const diagnostics = createProbeDiagnostics(run, (error) => {
785
+ ctx.logger.warn("model-hub: diagnostics failed: %s", String(error));
786
+ });
787
+ ctx.effect(() => {
788
+ const off = ctx.on("settings/document-updated", () => diagnostics.invalidate());
789
+ diagnostics.request();
790
+ return async () => {
791
+ off();
792
+ await diagnostics.dispose();
793
+ };
794
+ });
779
795
  ctx.effect(() => registerHostRpc(ctx, CHANNEL, async (endpoint, payload, signal) => {
780
796
  try {
781
- if (endpoint === "fields.list") return {
782
- ok: true,
783
- value: await hub.list(signal)
784
- };
797
+ if (endpoint === "fields.list") {
798
+ const value = await hub.list(signal);
799
+ diagnostics.request();
800
+ return {
801
+ ok: true,
802
+ value
803
+ };
804
+ }
785
805
  if (endpoint === "fields.set") return {
786
806
  ok: true,
787
807
  value: await hub.set(payload, signal)
@@ -812,6 +832,10 @@ function apply(ctx) {
812
832
  ok: true,
813
833
  value: cast.setTendency(row.provider, row.model, row.tendency, row.expectedRevision, signal)
814
834
  };
835
+ if (endpoint === "cast.running") return {
836
+ ok: true,
837
+ value: await running.list(signal)
838
+ };
815
839
  }
816
840
  throw new Error(`Unknown endpoint: ${endpoint}`);
817
841
  } catch (error) {