dsh-model-switch 0.4.24 → 0.4.25

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.
@@ -1,6 +1,7 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { Entry, Loader } from '@deepseek-ai/cordis-plugin-loader';
3
3
  import type { BasicCompactionConfig } from '@deepseek-ai/dsh-compaction-basic';
4
+ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
4
5
  /** Cordis plugin identity; matches the cordis.patch.yml insert row id. */
5
6
  export declare const name = "model-switch-compaction";
6
7
  /** 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. */
@@ -8,17 +9,21 @@ export declare const inject: string[];
8
9
  /**
9
10
  * Both identifiers trimmed and non-empty; undefined means "official default".
10
11
  *
11
- * The summarization route stays deployment-wide on purpose. The official
12
- * compaction transaction durably records the provider/model that wrote the
13
- * summary, so a per-session rewrite of the summarization call would make that
14
- * record false; a deployment that needs provider-scoped summarization uses the
15
- * official exact-target `modelPolicies` table, which this row forwards
16
- * verbatim from the profile's `compaction-basic` row config.
12
+ * Each summarization call derives its route per session (the session's provider
13
+ * group → the flat compaction card → the official row's `modelPolicies` /
14
+ * conversation-target fallback) and mutates `options.provider`/`options.model`
15
+ * in place before `ctx.llm.stream(options)` so the official `summarizeWithLlm`
16
+ * returns and durably records the exact route that wrote the summary in
17
+ * `compaction/summary`.
17
18
  */
18
19
  export interface CompactionRoute {
19
20
  readonly provider: string;
20
21
  readonly model: string;
21
22
  }
23
+ /** Public llm face the official compaction engine uses (`stream` + `resolveModelInfo`). */
24
+ export interface CompactionLlm {
25
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
26
+ }
22
27
  /**
23
28
  * Resolve the configurable summarization route from live settings.
24
29
  * @param settings - Model Switch settings carrying the optional compaction pair.
@@ -48,6 +53,45 @@ export declare function officialCompactionEntry(loader: Loader | undefined): Ent
48
53
  * @returns A plain object spread; missing or invalid configs become `{}`.
49
54
  */
50
55
  export declare function forwardedConfig(entry: Entry): BasicCompactionConfig;
56
+ /**
57
+ * Resolve the compaction summarization route for one session id.
58
+ *
59
+ * Looks up the session from `ctx.sessions` when available, derives its current
60
+ * provider (`pending` selection preferred over `lastUsed`, then logged routes),
61
+ * and resolves the provider group's compaction pair before falling back to the
62
+ * deployment-wide compaction pair.
63
+ * @param ctx - this row's context.
64
+ * @param sessionId - the session id carried on `GenerateOptions.sessionId`.
65
+ * @returns The derived compaction route, or undefined for the engine's own route.
66
+ */
67
+ export declare function resolveSessionCompactionRoute(ctx: Context, sessionId: string | undefined): CompactionRoute | undefined;
68
+ /**
69
+ * Wrap the real llm service so `purpose === 'compaction'` calls resolve their
70
+ * summarization route per session while keeping `compaction/summary` truthful.
71
+ *
72
+ * `summarizeWithLlm` builds a mutable `options` literal with `purpose: 'compaction'`
73
+ * and `sessionId: agent.session.id`, calls `ctx.llm.stream(options)`, and then
74
+ * returns `{ provider: options.provider, model: options.model }` to `commitCompactionBody`.
75
+ * Mutating `options.provider` and `options.model` in place before delegating to
76
+ * `target.stream(options)` ensures the durable `compaction/summary` event
77
+ * records the exact provider/model that wrote the summary. All other members
78
+ * (such as `resolveModelInfo`) pass through bound to `real`.
79
+ * @param real - the host's `ctx.llm`.
80
+ * @param resolve - per-session route resolver; undefined keeps `options` untouched.
81
+ * @param onError - optional error sink when the resolver throws.
82
+ * @returns A transparent Proxy over `real`.
83
+ */
84
+ export declare function compactionLlm<T extends CompactionLlm>(real: T, resolve: (sessionId: string) => CompactionRoute | undefined, onError?: (cause: unknown) => void): T;
85
+ /**
86
+ * Build the child context the compaction engine mounts under.
87
+ *
88
+ * When `ctx.llm` is present, shadows it with {@link compactionLlm} on an
89
+ * extended context so the child engine fiber resolves the per-session wrapper
90
+ * while registering `ctx.compaction` on the shared context tree.
91
+ * @param ctx - this row's context.
92
+ * @returns A context shadowing `llm` when available, else `ctx`.
93
+ */
94
+ export declare function compactionScope(ctx: Context): Context;
51
95
  /**
52
96
  * Whether the compaction capability is actually served from this context.
53
97
  *
@@ -12,7 +12,6 @@ export interface ModelSwitchSettings {
12
12
  searchModel?: string;
13
13
  imageProvider?: string;
14
14
  imageModel?: string;
15
- compactOnSwitch: boolean;
16
15
  /** Reviewer provider for the Auto preset; both fields unset = follow the session model. */
17
16
  autoReviewProvider?: string;
18
17
  /** Reviewer model for the Auto preset; set together with `autoReviewProvider`. */
@@ -38,7 +37,6 @@ export interface Config {
38
37
  searchModel: Volatile<string | undefined>;
39
38
  imageProvider: Volatile<string | undefined>;
40
39
  imageModel: Volatile<string | undefined>;
41
- compactOnSwitch: Volatile<boolean>;
42
40
  autoReviewProvider: Volatile<string | undefined>;
43
41
  autoReviewModel: Volatile<string | undefined>;
44
42
  compactProvider: Volatile<string | undefined>;
@@ -1,23 +1,17 @@
1
1
  /**
2
- * Provider configuration groups: per-provider presets for the session-naming
3
- * and subagent routes.
2
+ * Provider configuration groups: per-provider presets for the compaction,
3
+ * auto-review, session-naming, and subagent routes.
4
4
  *
5
5
  * A group is a DERIVED policy, not a write: `src/session-route.ts` resolves the
6
6
  * routes one session should use from its own provider plus this stored table, so
7
7
  * switching a session's provider never rewrites deployment-wide settings and two
8
8
  * sessions on different providers cannot overwrite each other. The field names
9
9
  * below are the flat settings field names a group pins, which keeps the stored
10
- * shape identical to the earlier write-through design.
11
- *
12
- * Legacy entries may still carry `compactProvider`/`compactModel`/
13
- * `autoReviewProvider`/`autoReviewModel`: normalization preserves them so an
14
- * older stored document is never silently rewritten into a different meaning,
15
- * while the consumption side ignores them (a group save from the current UI
16
- * writes '' for every unsupported field, which normalization drops).
10
+ * shape identical across releases.
17
11
  *
18
12
  * All functions here are pure and never throw on malformed storage.
19
13
  */
20
- /** Flat settings fields a provider group carries; the first four are legacy-only. */
14
+ /** Flat settings fields a provider group carries across all four per-session capabilities. */
21
15
  export type ProviderGroupRouteField = 'compactProvider' | 'compactModel' | 'autoReviewProvider' | 'autoReviewModel' | 'subagentProvider' | 'subagentModel' | 'subagentReasoningEffort' | 'titleProvider' | 'titleModel';
22
16
  /** Stored entry: the owning provider plus every route field; '' means the capability stays at its default. */
23
17
  export interface ProviderGroupEntry {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-switch",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "Explicit model routing plus composer/Plan picker for DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +0,0 @@
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,56 +0,0 @@
1
- /**
2
- * Compensate the Host's own deployment-default write after a Session-only
3
- * model switch.
4
- *
5
- * `session.selectModel` installs the Session selection and, on Hosts that
6
- * persist a selection as the deployment default, also writes that default.
7
- * The plugin switches a Session only, so the captured Main default is restored
8
- * afterwards.
9
- *
10
- * That Host write is fenced by a settings revision, and its timing is not part
11
- * of the released contract: the 0.1.x generation awaited the default write
12
- * before its RPC resolved, while the 0.2.x generation fires it without
13
- * awaiting. A fixed `captured + 1` fence therefore either refuses the restore
14
- * or silently leaves the default switched. Poll-by-revision is used instead:
15
- * wait for the observed revision to leave the captured one, then fence on the
16
- * revision actually observed.
17
- */
18
- import type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client';
19
- import type { ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
20
- import type { MainSettingsView } from '../../client-contract.ts';
21
- /** The Main Settings namespace surface this module needs. */
22
- export interface MainDefaultForm {
23
- getSnapshot(): ConfigFormSnapshot<MainSettingsView>;
24
- subscribe(listener: () => void): () => void;
25
- mutate(ops: readonly SettingsPathOpView[], expectedRevision?: number): Promise<boolean>;
26
- }
27
- /** Outcome of one compensation attempt. */
28
- export interface MainDefaultRestore {
29
- /** The captured Main default is the effective one again (or no write occurred). */
30
- restored: boolean;
31
- /** A newer revision won instead; the concurrent edit is left untouched. */
32
- conflict: boolean;
33
- }
34
- export interface MainDefaultRestoreOptions {
35
- /** Timeout primitive; tests inject a deterministic one. */
36
- wait?: (ms: number) => Promise<void>;
37
- /** How long to await the unawaited Host default write. */
38
- timeoutMs?: number;
39
- /** Total fences, including one retry after a concurrent revision change. */
40
- attempts?: number;
41
- }
42
- /** Field writes restoring one complete Main selection; the Host replaces the section. */
43
- export declare function mainDefaultOps(selection: MainSettingsView): SettingsPathOpView[];
44
- /**
45
- * Restore the Main default captured before a Session-only model switch.
46
- *
47
- * Never throws and never blocks a Plan answer: a failed compensation is
48
- * reported, not enforced, because the Session selection the human asked for is
49
- * already installed by the time this runs.
50
- *
51
- * @param form - the official Main Settings namespace form.
52
- * @param before - the snapshot captured before the switch.
53
- * @param options - wait/timeout/attempt overrides for tests.
54
- * @returns Whether the captured default stands again, and whether a newer edit won.
55
- */
56
- export declare function restoreMainDefault(form: MainDefaultForm, before: ConfigFormSnapshot<MainSettingsView>, options?: MainDefaultRestoreOptions): Promise<MainDefaultRestore>;
@@ -1,48 +0,0 @@
1
- /**
2
- * Apply a provider's saved capability group whenever the seat session's
3
- * provider changes — on any switch surface.
4
- *
5
- * The official model-selection directory projects the Session's durable
6
- * `model/selection` events into `store.current` (the sole emitter of those
7
- * events is the `session.selectModel` RPC), so a switch performed through the
8
- * TUI or any other client lands in the same store this plugin already reads.
9
- * One watcher therefore covers every surface for sessions open in this Web
10
- * UI, where the previous wiring only covered this plugin's own two call sites.
11
- *
12
- * Semantics stay exactly as before: a provider change applies the target
13
- * provider's group when one exists; a provider without a group, a same-model
14
- * change within one provider, and the subscription baseline itself write
15
- * nothing. A failed apply is reported and never disturbs the switch.
16
- */
17
- import type { ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
18
- /** The directory store slice the watcher observes. */
19
- export interface DirectoryWatchSource {
20
- subscribe: (listener: () => void) => () => void;
21
- getSnapshot: () => {
22
- current: ModelSelection | null;
23
- };
24
- }
25
- export interface ProviderGroupWatchOptions {
26
- source: DirectoryWatchSource;
27
- /** Adopt the target provider's saved group; a group-less provider must write nothing. */
28
- apply: (provider: string) => Promise<void>;
29
- /** Reported apply failure; defaults to a console warning. */
30
- onFailure?: (error: unknown) => void;
31
- }
32
- export interface ProviderGroupWatch {
33
- /** Detach from the store; in-flight applies are left to finish. */
34
- dispose: () => void;
35
- }
36
- /**
37
- * Watch one directory store and apply groups on provider changes.
38
- *
39
- * - The first observed provider (or the one already loaded) is the baseline
40
- * and never triggers a write, so mounting and initial load stay silent.
41
- * - Applies are serialized: a change arriving mid-apply coalesces to the
42
- * latest target and runs once the current apply finishes, so two writes
43
- * never interleave pair writes.
44
- * - `current` being null (catalog still loading) is ignored, not a change.
45
- * @param options - the store to watch and the apply to run.
46
- * @returns the watch handle.
47
- */
48
- export declare function createProviderGroupWatch(options: ProviderGroupWatchOptions): ProviderGroupWatch;
@@ -1,45 +0,0 @@
1
- import type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client';
2
- import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
3
- interface RemoteError {
4
- code: string;
5
- message: string;
6
- }
7
- interface RemoteResult<T> {
8
- ok: boolean;
9
- value?: T;
10
- error?: RemoteError;
11
- }
12
- interface NamespaceView {
13
- ns: string;
14
- value: unknown;
15
- base?: unknown;
16
- user?: unknown;
17
- revision: number;
18
- }
19
- interface DescribeValue {
20
- writable: boolean;
21
- namespaces: readonly NamespaceView[];
22
- }
23
- export interface RemoteSettingsFace {
24
- describe(): Promise<RemoteResult<DescribeValue>>;
25
- mutate(ns: string, ops: readonly unknown[], expectedRevision: number | undefined): Promise<RemoteResult<NamespaceView>>;
26
- }
27
- /** Host-backed settings scope used by this plugin on authenticated remote browsers. */
28
- export declare class RemoteSettingsScope<T> implements SettingsScope<T> {
29
- private readonly api;
30
- private readonly namespace;
31
- private readonly decode;
32
- private snapshot;
33
- private readonly listeners;
34
- constructor(api: RemoteSettingsFace, namespace: string, decode: (value: unknown) => T | undefined);
35
- getSnapshot: () => SettingsScopeSnapshot<T>;
36
- subscribe: (listener: () => void) => (() => void);
37
- reload(): Promise<void>;
38
- set(field: string, value: unknown): Promise<void>;
39
- unset(field: string): Promise<void>;
40
- mutate(ops: readonly SettingsPathOpView[], expectedRevision?: number): Promise<void>;
41
- private write;
42
- private accept;
43
- private publish;
44
- }
45
- export {};
@@ -1,72 +0,0 @@
1
- /** Send-time context check and optional old-model compaction before a switched request. */
2
- import type { Context } from '@deepseek-ai/cordis';
3
- import { type UserMessage } from '@deepseek-ai/dsh-llm';
4
- declare module '@deepseek-ai/dsh-llm' {
5
- interface MessageSourceMap {
6
- 'model-switch': {
7
- kind: 'model-switch';
8
- form: 'notice';
9
- summary: string;
10
- };
11
- }
12
- }
13
- /** Provider and model selected for one model-routed request. */
14
- export interface SwitchRoute {
15
- readonly provider: string;
16
- readonly model: string;
17
- }
18
- /**
19
- * Compare two fully specified model routes.
20
- * @param left - first route, if one was resolved.
21
- * @param right - second route, if one was resolved.
22
- * @returns Whether both routes identify the same provider and model.
23
- */
24
- export declare function sameSwitchRoute(left: SwitchRoute | undefined, right: SwitchRoute | undefined): boolean;
25
- /**
26
- * Decide whether a changed, complete route needs send-time context protection.
27
- * @param enabled - whether the user enabled send-time protection.
28
- * @param previous - route used by the preceding request.
29
- * @param selected - route assembled for the pending request.
30
- * @returns Whether the pending request changes its provider or model.
31
- */
32
- export declare function shouldProtectSwitch(enabled: boolean, previous: SwitchRoute | undefined, selected: SwitchRoute | undefined): boolean;
33
- /**
34
- * Input budget for the target window. Reserves output (maxTokens or 20%) plus a
35
- * fixed estimator margin. `undefined` means the window cannot be used.
36
- * @param contextWindow - adapter-owned combined request/response capacity.
37
- * @param maxTokens - optional generation cap from the live agent options.
38
- * @returns Tokens available to the request input, or undefined for an invalid window.
39
- */
40
- export declare function switchInputBudget(contextWindow: number, maxTokens?: number): number | undefined;
41
- /**
42
- * Determine whether an estimated input exceeds its target route budget.
43
- * @param tokens - estimated input tokens.
44
- * @param budget - input budget for the target window.
45
- * @returns Whether the budget is unusable or the estimate is too large.
46
- */
47
- export declare function overSwitchBudget(tokens: number, budget: number): boolean;
48
- /**
49
- * Build one transcript-visible Model Switch progress notice.
50
- * @param text - localized notice text shown in the transcript.
51
- * @param summary - bounded event summary for durable session metadata.
52
- * @returns A plugin-authored user message for the session log.
53
- */
54
- export declare function switchNotice(text: string, summary: string): UserMessage;
55
- /**
56
- * Extract a complete route from partial request configuration.
57
- * @param value - candidate provider and model values.
58
- * @returns A complete route, or undefined when either identifier is absent.
59
- */
60
- export declare function routeOf(value: {
61
- provider?: string;
62
- model?: string;
63
- } | undefined): SwitchRoute | undefined;
64
- /**
65
- * Install send-time switch protection on the host context.
66
- * @param ctx - host context that owns agent-loop, llm, and tokenMeter.
67
- * @param settings - live Model Switch settings; `compactOnSwitch !== false` is on.
68
- * @param roleOf - provider-role lookup used to exclude native Agent runtimes.
69
- */
70
- export declare function installSwitchCompaction(ctx: Context, settings: () => {
71
- compactOnSwitch?: boolean;
72
- }, roleOf?: (provider: string) => 'llm' | 'agent' | undefined): void;