dsh-model-switch 0.4.20 → 0.4.22

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.
@@ -0,0 +1,205 @@
1
+ //#region lib/types/compaction.js
2
+ /** Official compaction row this module supersedes. */
3
+ const OFFICIAL_COMPACTION_ROW = "compaction-basic";
4
+ /** Official compaction package this module mounts in its place. */
5
+ const OFFICIAL_COMPACTION_PACKAGE = "@deepseek-ai/dsh-compaction-basic";
6
+ /** Cordis plugin identity; matches the cordis.patch.yml insert row id. */
7
+ const name = "model-switch-compaction";
8
+ /** The loader is awaited at row activation; settings and the official row are still read structurally so stock compaction survives a disabled Model Switch row. */
9
+ const inject = ["loader"];
10
+ /**
11
+ * Resolve the configurable summarization route from live settings.
12
+ * @param settings - Model Switch settings carrying the optional compaction pair.
13
+ * @returns Both identifiers trimmed and non-empty, or undefined (official default).
14
+ */
15
+ function compactionRouteOf(settings) {
16
+ const provider = settings?.compactProvider?.trim();
17
+ const model = settings?.compactModel?.trim();
18
+ if (provider === void 0 || provider === "" || model === void 0 || model === "") return void 0;
19
+ return {
20
+ provider,
21
+ model
22
+ };
23
+ }
24
+ /**
25
+ * Merge the resolved route over the official row's own config.
26
+ * @param forwarded - config the official compaction-basic row was configured with.
27
+ * @param route - resolved summarization route; undefined keeps official behavior.
28
+ * @returns A detached config safe to hand to a fresh engine construction.
29
+ */
30
+ function composeCompactionConfig(forwarded, route) {
31
+ if (!route) return { ...forwarded };
32
+ return {
33
+ ...forwarded,
34
+ summarizationProvider: route.provider,
35
+ summarizationModel: route.model
36
+ };
37
+ }
38
+ /**
39
+ * Locate the official compaction row in the loader tree.
40
+ * Disabled entries stay in the tree, so a name-matched disable still matches.
41
+ * @param loader - the host loader, when one is reachable.
42
+ * @returns The official entry, or undefined when this profile has no such row.
43
+ */
44
+ function officialCompactionEntry(loader) {
45
+ if (!loader) return void 0;
46
+ for (const entry of loader.entries()) {
47
+ if (entry.id === OFFICIAL_COMPACTION_ROW) return entry;
48
+ if (entry.options?.name === OFFICIAL_COMPACTION_PACKAGE) return entry;
49
+ }
50
+ }
51
+ /**
52
+ * Copy the official row's stored config, ignoring non-object payloads.
53
+ * @param entry - the official compaction entry.
54
+ * @returns A plain object spread; missing or invalid configs become `{}`.
55
+ */
56
+ function forwardedConfig(entry) {
57
+ const raw = entry.options?.config;
58
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
59
+ return {};
60
+ }
61
+ /**
62
+ * Read the live summarization route from the Model Switch host.
63
+ *
64
+ * The host is read structurally instead of injected so this row keeps mounting
65
+ * the official engine (on official defaults) when the `model-switch` row is
66
+ * disabled.
67
+ * @param ctx - this row's context.
68
+ * @returns The configured route, or undefined when unset or host-unavailable.
69
+ */
70
+ function currentCompactionRoute(ctx) {
71
+ let settings;
72
+ try {
73
+ settings = ctx.get("modelSwitch", false)?.currentSettings();
74
+ } catch {
75
+ settings = void 0;
76
+ }
77
+ if (!settings) {
78
+ ctx.logger.info("model-switch: host settings unavailable; compaction uses the official summarization default");
79
+ return;
80
+ }
81
+ return compactionRouteOf(settings);
82
+ }
83
+ /**
84
+ * Load and mount the official basic compaction engine under `config`.
85
+ * @param ctx - this row's context; the engine mounts unisolated so `/compact` sees it.
86
+ * @param config - composed engine config.
87
+ * @param label - which config attempt failed, for the error message.
88
+ * @returns The mounted fiber, or undefined with an explicit error log.
89
+ */
90
+ async function mountEngine(ctx, config, label) {
91
+ let loaded;
92
+ try {
93
+ loaded = await import("@deepseek-ai/dsh-compaction-basic");
94
+ } catch (cause) {
95
+ ctx.logger.error("model-switch: failed to load the official compaction engine package");
96
+ ctx.logger.error(cause);
97
+ return;
98
+ }
99
+ let fiber;
100
+ try {
101
+ fiber = ctx.plugin(loaded.default, config);
102
+ await fiber;
103
+ return fiber;
104
+ } catch (cause) {
105
+ if (fiber) try {
106
+ await fiber.dispose();
107
+ } catch {}
108
+ ctx.logger.error(`model-switch: failed to mount the compaction engine with ${label}`);
109
+ ctx.logger.error(cause);
110
+ return;
111
+ }
112
+ }
113
+ /**
114
+ * Dispose the live engine, keeping the ledger clean for the next mount.
115
+ * @param ctx - this row's context.
116
+ * @param state - the row's reconcile ledger.
117
+ */
118
+ async function disposeEngine(ctx, state) {
119
+ const previous = state.engine;
120
+ state.engine = void 0;
121
+ if (!previous) return;
122
+ try {
123
+ await previous.dispose();
124
+ } catch (cause) {
125
+ ctx.logger.error("model-switch: failed to dispose the previous compaction engine");
126
+ ctx.logger.error(cause);
127
+ }
128
+ }
129
+ /**
130
+ * Reconcile one compaction mount: remount only when the composed config changed.
131
+ *
132
+ * Failure ladder: configured config → official defaults (`{}`) → unmounted with
133
+ * explicit errors, so the next settings event retries the whole ladder.
134
+ * @param ctx - this row's context.
135
+ * @param state - the row's reconcile ledger.
136
+ */
137
+ async function runReconcile(ctx, state) {
138
+ const entry = officialCompactionEntry(ctx.get("loader", false));
139
+ if (!entry) {
140
+ if (!state.missingRow) {
141
+ state.missingRow = true;
142
+ ctx.logger.info("model-switch: no official compaction-basic row in this profile; the compaction engine stays unmounted");
143
+ }
144
+ return;
145
+ }
146
+ state.missingRow = false;
147
+ const config = composeCompactionConfig(forwardedConfig(entry), currentCompactionRoute(ctx));
148
+ const signature = JSON.stringify(config);
149
+ if (state.signature === signature && state.engine) return;
150
+ await disposeEngine(ctx, state);
151
+ state.signature = signature;
152
+ const fiber = await mountEngine(ctx, config, "the configured summarization route");
153
+ if (fiber) {
154
+ state.engine = fiber;
155
+ return;
156
+ }
157
+ ctx.logger.error("model-switch: retrying the compaction engine on the official defaults");
158
+ const fallback = await mountEngine(ctx, {}, "the official defaults");
159
+ if (fallback) {
160
+ state.engine = fallback;
161
+ return;
162
+ }
163
+ ctx.logger.error("model-switch: compaction engine is unmounted; /compact and automatic compaction are unavailable until settings change");
164
+ }
165
+ /**
166
+ * Queue a reconcile behind the row's serialized chain, absorbing failures.
167
+ * @param ctx - this row's context.
168
+ * @param state - the row's reconcile ledger.
169
+ * @returns The chain tail; never rejects.
170
+ */
171
+ function scheduleReconcile(ctx, state) {
172
+ state.chain = state.chain.then(() => runReconcile(ctx, state)).catch((cause) => {
173
+ ctx.logger.error("model-switch: compaction engine reconcile failed");
174
+ ctx.logger.error(cause);
175
+ });
176
+ return state.chain;
177
+ }
178
+ /**
179
+ * Mount the Model Switch compaction engine for this profile.
180
+ *
181
+ * Installs a sibling row that replaces the official compaction-basic engine
182
+ * with the same package under the configurable summarization route. Host
183
+ * settings and the loader are read structurally so a disabled `model-switch`
184
+ * row leaves official compaction untouched. Reconciles run on
185
+ * `model-switch/settings-update` (emitted by the host on volatile commits and
186
+ * ordinary saves); the listener is global so instances of this row inside
187
+ * agent-preset realms still hear the host row's emission. The initial
188
+ * reconcile is awaited so `/compact` resolves the service as soon as this row
189
+ * completes.
190
+ * @param ctx - this row's context.
191
+ */
192
+ async function apply(ctx) {
193
+ const state = {
194
+ chain: Promise.resolve(),
195
+ signature: null,
196
+ engine: void 0,
197
+ missingRow: false
198
+ };
199
+ ctx.on("model-switch/settings-update", () => {
200
+ scheduleReconcile(ctx, state);
201
+ }, { global: true });
202
+ await scheduleReconcile(ctx, state);
203
+ }
204
+ //#endregion
205
+ export { apply, compactionRouteOf, composeCompactionConfig, forwardedConfig, inject, name, officialCompactionEntry };
package/lib/index.js CHANGED
@@ -12,7 +12,83 @@ import { dirname, join } from "node:path";
12
12
  import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
13
13
  import { boundContextSummary, createMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
14
14
  import { canonicalHeader } from "@deepseek-ai/dsh-session";
15
+ //#region lib/types/provider-group.js
16
+ /**
17
+ * Provider configuration groups: per-provider presets for the compaction,
18
+ * subagent, and auto-review routes.
19
+ *
20
+ * A group stores the exact flat settings fields it will write, so applying a
21
+ * group is a plain settings-layer write: every capability consumer keeps
22
+ * reading the live values unchanged. All functions here are pure and never
23
+ * throw on malformed storage.
24
+ */
25
+ const ROUTE_FIELDS = Object.freeze([
26
+ "compactProvider",
27
+ "compactModel",
28
+ "autoReviewProvider",
29
+ "autoReviewModel",
30
+ "subagentProvider",
31
+ "subagentModel",
32
+ "subagentReasoningEffort"
33
+ ]);
34
+ /** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
35
+ function setKey(target, key, value) {
36
+ Object.defineProperty(target, key, {
37
+ value,
38
+ enumerable: true,
39
+ writable: true,
40
+ configurable: true
41
+ });
42
+ }
43
+ /** A non-blank string. Blank values behave exactly like an absent field. */
44
+ function present(value) {
45
+ return typeof value === "string" && value.trim() !== "";
46
+ }
47
+ /** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
48
+ function normalizeEntry(value) {
49
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
50
+ const source = value;
51
+ const group = {};
52
+ for (const field of ROUTE_FIELDS) {
53
+ const item = source[field];
54
+ if (present(item)) group[field] = item;
55
+ }
56
+ return {
57
+ provider: present(source.provider) ? source.provider : "",
58
+ group
59
+ };
60
+ }
61
+ /**
62
+ * Strip a raw stored value to well-formed groups; never throws. Both storage
63
+ * shapes are accepted: the stored entry array (keys live in each entry's
64
+ * `provider` field) and a hand-written provider → routes map (keys are the
65
+ * providers). All-empty entries stay: they pin a provider to every default.
66
+ */
67
+ function normalizeProviderGroups(raw) {
68
+ const groups = {};
69
+ if (Array.isArray(raw)) {
70
+ for (const entry of raw) {
71
+ const view = normalizeEntry(entry);
72
+ if (view === void 0 || view.provider === "") continue;
73
+ setKey(groups, view.provider, view.group);
74
+ }
75
+ return groups;
76
+ }
77
+ if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
78
+ if (key.trim() === "") continue;
79
+ const view = normalizeEntry(value);
80
+ if (view === void 0) continue;
81
+ setKey(groups, key, view.group);
82
+ }
83
+ return groups;
84
+ }
85
+ //#endregion
15
86
  //#region lib/types/host-settings.js
87
+ /**
88
+ * Annotated rather than inferred: the nested entry schema's inferred type
89
+ * expands to cosmokit internals (`Dict`) that cannot be named in the emitted
90
+ * declaration, and the annotation pins the output shape to this interface.
91
+ */
16
92
  const Config = z.object({
17
93
  subagentMode: z.union(["follow-main", "fixed"]).default("follow-main").volatile(),
18
94
  subagentProvider: z.string().volatile(),
@@ -25,7 +101,18 @@ const Config = z.object({
25
101
  compactOnSwitch: z.boolean().default(true).volatile(),
26
102
  autoReviewProvider: z.string().volatile(),
27
103
  autoReviewModel: z.string().volatile(),
28
- planCompactThreshold: z.number().default(2e5).volatile()
104
+ compactProvider: z.string().volatile(),
105
+ compactModel: z.string().volatile(),
106
+ providerGroups: z.array(z.object({
107
+ provider: z.string().default(""),
108
+ compactProvider: z.string().default(""),
109
+ compactModel: z.string().default(""),
110
+ autoReviewProvider: z.string().default(""),
111
+ autoReviewModel: z.string().default(""),
112
+ subagentProvider: z.string().default(""),
113
+ subagentModel: z.string().default(""),
114
+ subagentReasoningEffort: z.string().default("")
115
+ })).default([]).volatile()
29
116
  });
30
117
  function readConfig(config) {
31
118
  const subagentProvider = config.subagentProvider.get();
@@ -37,6 +124,9 @@ function readConfig(config) {
37
124
  const imageModel = config.imageModel.get();
38
125
  const autoReviewProvider = config.autoReviewProvider.get();
39
126
  const autoReviewModel = config.autoReviewModel.get();
127
+ const compactProvider = config.compactProvider.get();
128
+ const compactModel = config.compactModel.get();
129
+ const providerGroups = normalizeProviderGroups(config.providerGroups.get());
40
130
  return {
41
131
  subagentMode: config.subagentMode.get(),
42
132
  ...subagentProvider === void 0 ? {} : { subagentProvider },
@@ -49,7 +139,9 @@ function readConfig(config) {
49
139
  compactOnSwitch: config.compactOnSwitch.get(),
50
140
  ...autoReviewProvider === void 0 || autoReviewProvider === "" ? {} : { autoReviewProvider },
51
141
  ...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
52
- planCompactThreshold: config.planCompactThreshold.get()
142
+ ...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
143
+ ...compactModel === void 0 || compactModel === "" ? {} : { compactModel },
144
+ ...Object.keys(providerGroups).length === 0 ? {} : { providerGroups }
53
145
  };
54
146
  }
55
147
  //#endregion
@@ -347,28 +439,8 @@ async function handleFetchRpc(request, handler, operator) {
347
439
  return statusResponse(500);
348
440
  }
349
441
  }
350
- /** Validate one plan-compact payload; returns the request or an error string. */
351
- function planCompactRequest(payload) {
352
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return {
353
- ok: false,
354
- message: "Expected an object payload"
355
- };
356
- const { sessionId, provider, model } = payload;
357
- if (typeof sessionId !== "string" || sessionId.length === 0 || typeof provider !== "string" || provider.length === 0 || typeof model !== "string" || model.length === 0) return {
358
- ok: false,
359
- message: "Expected sessionId, provider, and model strings"
360
- };
361
- return {
362
- ok: true,
363
- request: {
364
- sessionId,
365
- provider,
366
- model
367
- }
368
- };
369
- }
370
442
  /** Bounded long-poll on the existing registry; each caller owns and disposes its subscription. */
371
- function capabilitiesRpc(registry, snapshot, lifetime, planCompact) {
443
+ function capabilitiesRpc(registry, snapshot, lifetime) {
372
444
  let revision = 0;
373
445
  let previous;
374
446
  const read = () => {
@@ -382,39 +454,6 @@ function capabilitiesRpc(registry, snapshot, lifetime, planCompact) {
382
454
  };
383
455
  };
384
456
  return async (endpoint, payload, signal) => {
385
- if (endpoint === "plan-compact") {
386
- if (planCompact === void 0) return {
387
- ok: false,
388
- error: {
389
- details: {},
390
- code: "unavailable",
391
- message: "Plan compaction is not mounted on this Host"
392
- }
393
- };
394
- const request = planCompactRequest(payload);
395
- if (!request.ok) return {
396
- ok: false,
397
- error: {
398
- details: {},
399
- code: "invalid-request",
400
- message: request.message
401
- }
402
- };
403
- try {
404
- return {
405
- ok: true,
406
- value: await planCompact(request.request, signal)
407
- };
408
- } catch (cause) {
409
- return {
410
- ok: true,
411
- value: {
412
- status: "error",
413
- message: cause instanceof Error ? cause.message : String(cause)
414
- }
415
- };
416
- }
417
- }
418
457
  if (endpoint !== "capabilities") return {
419
458
  ok: false,
420
459
  error: {
@@ -467,10 +506,10 @@ function capabilitiesRpc(registry, snapshot, lifetime, planCompact) {
467
506
  };
468
507
  };
469
508
  }
470
- function installCapabilitiesRpc(ctx, registry, snapshot, planCompact) {
509
+ function installCapabilitiesRpc(ctx, registry, snapshot) {
471
510
  ctx.inject(["connection"], (scope) => scope.effect(() => {
472
511
  const lifetime = new AbortController();
473
- const handler = capabilitiesRpc(registry, snapshot, lifetime.signal, planCompact);
512
+ const handler = capabilitiesRpc(registry, snapshot, lifetime.signal);
474
513
  const dispose = scope.connection.fetch.register({
475
514
  path: "/api/plugin-rpc/model-switch",
476
515
  methods: ["POST"],
@@ -925,189 +964,6 @@ async function compactWithPrevious(ctx, agent, previous, signal) {
925
964
  }
926
965
  }
927
966
  //#endregion
928
- //#region lib/types/picker/plan-compact.js
929
- /**
930
- * Plan-exit compaction vocabulary shared by the card and the Host RPC.
931
- *
932
- * The card decides *when* to ask (only an approval that switches the execution
933
- * model); the Host owns measurement, threshold policy, and the compaction
934
- * itself. Failures are explicit: the Plan stays pending and can be retried,
935
- * while an empty history is a completed attempt that may proceed.
936
- *
937
- * @module dsh-model-switch/picker/plan-compact
938
- */
939
- /** Whether the user's threshold arms the plan-exit mechanism at all. */
940
- function planCompactEnabled(threshold) {
941
- return Number.isFinite(threshold) && threshold > 0;
942
- }
943
- /**
944
- * Decide whether a plan exit must compact before it switches models.
945
- * @param threshold — user-configured token threshold; 0 disables the mechanism.
946
- * @param estimate — measured next-request input tokens.
947
- * @returns `disabled` when off, `compact` only above the threshold, else `skip`.
948
- */
949
- function planCompactDecision(threshold, estimate) {
950
- if (!planCompactEnabled(threshold)) return "disabled";
951
- if (!Number.isFinite(estimate) || estimate < 0) return "skip";
952
- return estimate > threshold ? "compact" : "skip";
953
- }
954
- //#endregion
955
- //#region lib/types/plan-compact.js
956
- /**
957
- * Plan-exit compaction on the Host: measure, threshold, then summarize the
958
- * history with the switched-to model before the approval resumes the session.
959
- *
960
- * The approval answer lands inside a still-open `exit_plan_mode` tool call, so
961
- * the agent phase is `running` and the idle-only `compactNow` seam is off
962
- * limits. This module therefore uses `compactIfNeeded(agent, 'context-overflow')`
963
- * — the same turn-safe, threshold-free operation official overflow recovery
964
- * uses — under an isolated `compaction-basic` engine configured to summarize
965
- * with the route the human just chose. The user's threshold decides *whether*
966
- * to call it; the engine's overflow policy guarantees one useful balanced
967
- * reduction whenever a safe range exists.
968
- *
969
- * @module dsh-model-switch/plan-compact
970
- */
971
- /**
972
- * Estimate the next request's input tokens under both envelopes.
973
- *
974
- * Takes the larger of the target-route heuristic price and the previous
975
- * envelope's anchored price — the same conservative stance the send-time
976
- * switch guard takes, so dense histories cannot under-trigger the threshold.
977
- *
978
- * @param meter — replay token meter.
979
- * @param session — session about to resume.
980
- * @param route — the switched-to provider/model.
981
- * @returns Estimated tokens, or undefined when no prior request exists to price.
982
- */
983
- function estimatePlanExitTokens(meter, session, route) {
984
- const previousHeader = session.requestHeader();
985
- if (previousHeader === void 0) return void 0;
986
- try {
987
- const targetHeader = canonicalHeader({ config: {
988
- provider: route.provider,
989
- model: route.model
990
- } });
991
- return Math.max(meter.measure(session, targetHeader).totalTokens, meter.measure(session, previousHeader).totalTokens);
992
- } catch {
993
- return;
994
- }
995
- }
996
- /**
997
- * Drive one compaction through an already-mounted isolated engine.
998
- * @param engine — engine whose overflow policy selects the range.
999
- * @param agent — live agent owning the session.
1000
- * @param signal — cancellation scoped to this request.
1001
- * @returns `empty` when no safe range exists, `compacted` after a summary, `error` on failure.
1002
- */
1003
- async function planCompactWithEngine(engine, agent, signal) {
1004
- try {
1005
- const result = await engine.compactIfNeeded(agent, "context-overflow", signal);
1006
- return result === null || result === void 0 ? { status: "empty" } : { status: "compacted" };
1007
- } catch (cause) {
1008
- if (signal.aborted) return {
1009
- status: "error",
1010
- message: "compaction was cancelled"
1011
- };
1012
- return {
1013
- status: "error",
1014
- message: cause instanceof Error ? cause.message : String(cause)
1015
- };
1016
- }
1017
- }
1018
- /**
1019
- * Mount an isolated compaction-basic engine pinned to the switched-to route
1020
- * and run one overflow-policy reduction over the agent's session.
1021
- * @param ctx — host context that owns token metering and session state.
1022
- * @param agent — live agent about to resume the approved plan.
1023
- * @param route — provider/model whose model must write the summary.
1024
- * @param signal — cancellation scoped to this request.
1025
- * @param loadEngine — dynamic import seam (tests inject the fake module).
1026
- * @returns The compaction outcome for this request.
1027
- */
1028
- async function runPlanCompaction(ctx, agent, route, signal, loadEngine) {
1029
- const load = loadEngine ?? (async () => import("@deepseek-ai/dsh-compaction-basic"));
1030
- let loaded;
1031
- try {
1032
- loaded = await load();
1033
- } catch (cause) {
1034
- return {
1035
- status: "error",
1036
- message: cause instanceof Error ? cause.message : String(cause)
1037
- };
1038
- }
1039
- const isolated = ctx.isolate("plan-compaction");
1040
- const fiber = isolated.plugin(loaded.default, {
1041
- auto: false,
1042
- summarizationProvider: route.provider,
1043
- summarizationModel: route.model,
1044
- compactionRetries: 0
1045
- });
1046
- try {
1047
- await fiber;
1048
- const engine = isolated.get("compaction", false);
1049
- if (engine === void 0 || typeof engine.compactIfNeeded !== "function") return {
1050
- status: "error",
1051
- message: "private compaction engine is unavailable"
1052
- };
1053
- return await planCompactWithEngine(engine, agent, signal);
1054
- } finally {
1055
- await fiber.dispose();
1056
- }
1057
- }
1058
- /**
1059
- * Build the `plan-compact` RPC runner bound to this host's live settings.
1060
- * @param ctx — host context resolving the live agent registry.
1061
- * @param settings — live Model Switch settings (`planCompactThreshold`).
1062
- * @param loadEngine — dynamic import seam (tests inject the fake module).
1063
- * @returns The runner the connection RPC dispatches to.
1064
- */
1065
- function planCompactRunner(ctx, settings, loadEngine) {
1066
- return async (request, signal) => {
1067
- const threshold = settings().planCompactThreshold;
1068
- if (!planCompactEnabled(threshold)) return {
1069
- status: "disabled",
1070
- threshold
1071
- };
1072
- const provider = request.provider.trim();
1073
- const model = request.model.trim();
1074
- if (provider === "" || model === "") return {
1075
- status: "error",
1076
- message: "the execution route is incomplete"
1077
- };
1078
- const route = {
1079
- provider,
1080
- model
1081
- };
1082
- const meter = ctx.get("tokenMeter", false);
1083
- const agent = ctx.get("agents", false)?.get(request.sessionId);
1084
- if (agent === void 0) return {
1085
- status: "error",
1086
- message: "the session has no live agent"
1087
- };
1088
- if (meter === void 0) return {
1089
- status: "error",
1090
- message: "token metering is unavailable"
1091
- };
1092
- const estimate = estimatePlanExitTokens(meter, agent.session, route);
1093
- const decision = planCompactDecision(threshold, estimate ?? NaN);
1094
- if (decision === "disabled") return {
1095
- status: "disabled",
1096
- threshold
1097
- };
1098
- if (decision === "skip") return {
1099
- status: "skipped",
1100
- threshold,
1101
- ...estimate === void 0 ? {} : { estimate }
1102
- };
1103
- return {
1104
- ...await runPlanCompaction(ctx, agent, route, signal, loadEngine),
1105
- threshold,
1106
- ...estimate === void 0 ? {} : { estimate }
1107
- };
1108
- };
1109
- }
1110
- //#endregion
1111
967
  //#region lib/types/host-runtime.js
1112
968
  /** Host owner for Model Switch settings and the released Main-default adapter. */
1113
969
  var ModelSwitchRuntime = class extends Service {
@@ -1134,14 +990,16 @@ var ModelSwitchRuntime = class extends Service {
1134
990
  installSwitchCompaction(ctx, () => this.source(), (provider) => this.adapters.get(provider)?.role);
1135
991
  installDeepSeekSearchAdapter(ctx);
1136
992
  installModelSwitchSearchProvider(ctx, this);
1137
- installCapabilitiesRpc(ctx, this.adapters, () => this.capabilities, planCompactRunner(ctx, () => this.source()));
993
+ installCapabilitiesRpc(ctx, this.adapters, () => this.capabilities);
1138
994
  const imageTool = installGenerateImageTool(ctx, this);
1139
995
  ctx.on("loader/volatile-update", () => {
1140
996
  imageTool.reconcile().catch((error) => {
1141
997
  ctx.logger.error("Model Switch: failed to regenerate generate_image schema");
1142
998
  ctx.logger.error(error);
1143
999
  });
1000
+ ctx.emit("model-switch/settings-update");
1144
1001
  });
1002
+ ctx.emit("model-switch/settings-update");
1145
1003
  }
1146
1004
  currentSettings() {
1147
1005
  return { ...this.source() };
@@ -2,12 +2,11 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  import { type ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
3
3
  import type { ModelSwitchAdapterRegistry } from './adapter-registry.js';
4
4
  import type { RuntimeCapabilities } from './runtime-capabilities.js';
5
- import type { PlanCompactRunner } from './plan-compact.js';
6
5
  export declare const CAPABILITIES_RPC_METHOD = "plugin-rpc/model-switch";
7
6
  export interface CapabilitiesSnapshot {
8
7
  readonly revision: number;
9
8
  readonly capabilities: RuntimeCapabilities;
10
9
  }
11
10
  /** Bounded long-poll on the existing registry; each caller owns and disposes its subscription. */
12
- export declare function capabilitiesRpc(registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities, lifetime: AbortSignal, planCompact?: PlanCompactRunner): ConnectionRpcHandler;
13
- export declare function installCapabilitiesRpc(ctx: Context, registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities, planCompact?: PlanCompactRunner): void;
11
+ export declare function capabilitiesRpc(registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities, lifetime: AbortSignal): ConnectionRpcHandler;
12
+ export declare function installCapabilitiesRpc(ctx: Context, registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities): void;
@@ -3,6 +3,7 @@ import type { ModelProviderGroup } from '@deepseek-ai/dsh-api-session-controller
3
3
  import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
4
4
  import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
5
5
  import { type CapabilityRouteView, type MainSettingsView, type ModelSwitchSettingsView, type SubagentSettingsView } from '../client-contract.js';
6
+ import { type ProviderGroupConfig } from '../provider-group.js';
6
7
  import type { RuntimeCapabilities } from '../runtime-capabilities.js';
7
8
  import { type CapabilitiesSnapshot } from './search-capabilities.js';
8
9
  import type { ModelSwitchLocaleKey } from './locales.js';
@@ -15,14 +16,16 @@ export interface ModelSwitchSettingsFace {
15
16
  searchSettings: ConfigViewForm<CapabilityRouteView>;
16
17
  imageSettings: ConfigViewForm<CapabilityRouteView>;
17
18
  autoReviewSettings: ConfigViewForm<CapabilityRouteView>;
19
+ compactionSettings: ConfigViewForm<CapabilityRouteView>;
18
20
  switchSettings: Pick<ConfigForm<ModelSwitchSettingsView>, 'getSnapshot' | 'subscribe'>;
19
21
  };
20
22
  capabilities: RuntimeCapabilities;
21
23
  saveMain: (next: MainSettingsView, expectedRevision: number) => Promise<number>;
22
24
  setSubagent: (field: 'mode' | 'provider' | 'model' | 'reasoningEffort', value: string | undefined) => Promise<void>;
23
- setCapability: (route: 'search' | 'image' | 'autoReview', field: 'provider' | 'model', value: string | undefined) => Promise<void>;
25
+ setCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction', field: 'provider' | 'model', value: string | undefined) => Promise<void>;
24
26
  setCompactOnSwitch: (value: boolean) => Promise<void>;
25
- setPlanCompactThreshold: (value: number) => Promise<void>;
27
+ /** Replace or remove (undefined) one provider's saved capability group. */
28
+ saveProviderGroup: (provider: string, group: ProviderGroupConfig | undefined) => Promise<void>;
26
29
  loadCatalog: () => Promise<readonly ModelProviderGroup[]>;
27
30
  /** Host capabilities long-poll; absent in legacy faces, which keep the static-capabilities path. */
28
31
  loadCapabilities?: (revision?: number, signal?: AbortSignal) => Promise<CapabilitiesSnapshot>;
@@ -1,4 +1,4 @@
1
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
2
- export type SettingsFieldMap<View> = Readonly<Partial<Record<keyof View & string, string>>>;
3
- /** Projection over one shared SettingsScope controller/store, with explicit view-to-owner field mapping. */
4
- export declare function deriveSettingsScope<Source, View>(source: SettingsScope<Source>, project: (value: Source) => View, fields?: SettingsFieldMap<View>): SettingsScope<View>;
1
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
2
+ export type SettingsFieldMap<View> = Readonly<Partial<Record<keyof View & string, string>>>;
3
+ /** Projection over one shared SettingsScope controller/store, with explicit view-to-owner field mapping. */
4
+ export declare function deriveSettingsScope<Source, View>(source: SettingsScope<Source>, project: (value: Source) => View, fields?: SettingsFieldMap<View>): SettingsScope<View>;