@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.
Files changed (45) hide show
  1. package/README.en.md +150 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +151 -0
  4. package/cordis.patch.yml +24 -0
  5. package/lib/client.js +1115 -0
  6. package/lib/dsh-cli-provider-UhWvivte.js +1701 -0
  7. package/lib/dsh-cli-provider.js +3 -0
  8. package/lib/index.js +624 -0
  9. package/lib/invariant.js +33 -0
  10. package/lib/provision.js +602 -0
  11. package/lib/records.js +109 -0
  12. package/lib/session-log-DOsSz56-.js +88 -0
  13. package/lib/types/client/SettingsCard.d.ts +78 -0
  14. package/lib/types/client/SettingsCard.js +200 -0
  15. package/lib/types/client/icons.d.ts +18 -0
  16. package/lib/types/client/icons.js +2 -0
  17. package/lib/types/client/index.d.ts +45 -0
  18. package/lib/types/client/index.js +88 -0
  19. package/lib/types/client/locales.d.ts +76 -0
  20. package/lib/types/client/locales.js +67 -0
  21. package/lib/types/dsh-cli-provider.d.ts +213 -0
  22. package/lib/types/dsh-cli-provider.js +733 -0
  23. package/lib/types/index.d.ts +124 -0
  24. package/lib/types/index.js +347 -0
  25. package/lib/types/invariant.d.ts +16 -0
  26. package/lib/types/invariant.js +37 -0
  27. package/lib/types/live-driver.d.ts +180 -0
  28. package/lib/types/live-driver.js +887 -0
  29. package/lib/types/live-switch.d.ts +76 -0
  30. package/lib/types/live-switch.js +100 -0
  31. package/lib/types/model-broker.d.ts +86 -0
  32. package/lib/types/model-broker.js +149 -0
  33. package/lib/types/model-catalog.d.ts +27 -0
  34. package/lib/types/model-catalog.js +71 -0
  35. package/lib/types/provision.d.ts +382 -0
  36. package/lib/types/provision.js +656 -0
  37. package/lib/types/records.d.ts +41 -0
  38. package/lib/types/records.js +125 -0
  39. package/lib/types/session-log.d.ts +58 -0
  40. package/lib/types/session-log.js +83 -0
  41. package/lib/types/session-mirror.d.ts +155 -0
  42. package/lib/types/session-mirror.js +451 -0
  43. package/locale/en.json +6 -0
  44. package/locale/zh.json +6 -0
  45. package/package.json +121 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The settings-driven live-driver switch: owns the active driver generation
3
+ * and retires old ones on a settings change, without a plugin reload.
4
+ *
5
+ * Toggle OFF (or the enabled generation unloading): the retiring generation
6
+ * is DRAINED, not killed — new rounds are refused (the provider's catch falls
7
+ * back to exec), in-flight rounds finish on their runtime, idle runtimes are
8
+ * reclaimed at once. Toggling ON builds the next generation lazily (no
9
+ * process until the first round). Legacy granularity settings are ignored.
10
+ *
11
+ * Composition with the `enabled` toggle: the switch lives INSIDE one enabled
12
+ * generation (index.ts constructs it only while the harness/provider/tool are
13
+ * registered, and disposes it with them). `enabled` gates EXISTENCE — its
14
+ * teardown keeps the historical hard semantics (`dispose()` → disposeAll,
15
+ * which may interrupt); `live` gates the MODE of an enabled generation and
16
+ * never interrupts (drain). While `enabled` is off the live value is inert
17
+ * and applies when the toggle turns on again.
18
+ *
19
+ * @module @khorsheed/dsh-local-agent-dsh — internal, unit-tested directly.
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ import type { SettingsFace } from '@khorsheed/dsh-local-agent';
23
+ import type { LocalAgentDshConfig } from './index.ts';
24
+ import { DshLiveDriver } from './live-driver.ts';
25
+ import type { DshLiveMirrorGranularity } from './session-mirror.ts';
26
+ /** The resolved `local-agent-dsh` settings (rc.1: volatile row config; 0.1.5: schema defaults ← YAML base ← user layer). */
27
+ export interface DshLiveSettings {
28
+ /** The DeepSeek delegation toggle; the switch itself reads only the live fields. */
29
+ enabled: boolean;
30
+ live: boolean;
31
+ liveMirrorGranularity: DshLiveMirrorGranularity;
32
+ }
33
+ export declare class LiveDriverSwitch {
34
+ private readonly ctx;
35
+ private readonly config;
36
+ private readonly extras;
37
+ private liveOn;
38
+ private active;
39
+ /** Retiring generations still draining their in-flight rounds. */
40
+ private readonly draining;
41
+ private readonly unwatch;
42
+ constructor(ctx: Context, face: SettingsFace<DshLiveSettings>, config: LocalAgentDshConfig, extras?: {
43
+ /**
44
+ * Per-spawn model resolver, handed to every driver generation. Receives
45
+ * the member's child session id so the session-level override can
46
+ * outrank the settings value.
47
+ */
48
+ modelFor?: (childSessionId: string) => string | undefined;
49
+ });
50
+ /**
51
+ * Resolve the live driver for one round's member (the provider's call
52
+ * site). A retiring generation still hosting the member gates the new one —
53
+ * the round falls back to exec rather than double-loading the same sub-dsh
54
+ * session into two processes.
55
+ */
56
+ readonly resolve: (childSessionId: string) => DshLiveDriver | undefined;
57
+ /**
58
+ * The model the member's live runtime bound at spawn (the broker's switch
59
+ * check): the identifier, undefined for "bound to no model", null for "no
60
+ * live runtime". Consults only the ACTIVE generation — a draining one is on
61
+ * its way out anyway.
62
+ */
63
+ boundModel(childSessionId: string): string | undefined | null;
64
+ /**
65
+ * Retire the member's resident runtime in the active generation so the next
66
+ * round respawns onto the member's new effective model (the composer's
67
+ * model switch path; the sub-dsh session itself carries over via the
68
+ * on-disk resume).
69
+ */
70
+ retireRuntime(childSessionId: string): Promise<void>;
71
+ /** Mirror the resolved settings into the driver generation. */
72
+ private apply;
73
+ /** Enabled-generation unload: interrupt whatever survives (disposeAll, not drain). */
74
+ dispose(): void;
75
+ }
76
+ //# sourceMappingURL=live-switch.d.ts.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The settings-driven live-driver switch: owns the active driver generation
3
+ * and retires old ones on a settings change, without a plugin reload.
4
+ *
5
+ * Toggle OFF (or the enabled generation unloading): the retiring generation
6
+ * is DRAINED, not killed — new rounds are refused (the provider's catch falls
7
+ * back to exec), in-flight rounds finish on their runtime, idle runtimes are
8
+ * reclaimed at once. Toggling ON builds the next generation lazily (no
9
+ * process until the first round). Legacy granularity settings are ignored.
10
+ *
11
+ * Composition with the `enabled` toggle: the switch lives INSIDE one enabled
12
+ * generation (index.ts constructs it only while the harness/provider/tool are
13
+ * registered, and disposes it with them). `enabled` gates EXISTENCE — its
14
+ * teardown keeps the historical hard semantics (`dispose()` → disposeAll,
15
+ * which may interrupt); `live` gates the MODE of an enabled generation and
16
+ * never interrupts (drain). While `enabled` is off the live value is inert
17
+ * and applies when the toggle turns on again.
18
+ *
19
+ * @module @khorsheed/dsh-local-agent-dsh — internal, unit-tested directly.
20
+ */
21
+ import { DshLiveDriver } from './live-driver.js';
22
+ export class LiveDriverSwitch {
23
+ ctx;
24
+ config;
25
+ extras;
26
+ liveOn = false;
27
+ active;
28
+ /** Retiring generations still draining their in-flight rounds. */
29
+ draining = new Set();
30
+ unwatch;
31
+ constructor(ctx, face, config, extras = {}) {
32
+ this.ctx = ctx;
33
+ this.config = config;
34
+ this.extras = extras;
35
+ this.apply(face.get());
36
+ this.unwatch = face.watch((next) => { this.apply(next); });
37
+ }
38
+ /**
39
+ * Resolve the live driver for one round's member (the provider's call
40
+ * site). A retiring generation still hosting the member gates the new one —
41
+ * the round falls back to exec rather than double-loading the same sub-dsh
42
+ * session into two processes.
43
+ */
44
+ resolve = (childSessionId) => {
45
+ if (this.active === undefined)
46
+ return undefined;
47
+ for (const retiring of this.draining) {
48
+ if (retiring.hasRuntime(childSessionId))
49
+ return undefined;
50
+ }
51
+ return this.active;
52
+ };
53
+ /**
54
+ * The model the member's live runtime bound at spawn (the broker's switch
55
+ * check): the identifier, undefined for "bound to no model", null for "no
56
+ * live runtime". Consults only the ACTIVE generation — a draining one is on
57
+ * its way out anyway.
58
+ */
59
+ boundModel(childSessionId) {
60
+ return this.active?.boundModelOf(childSessionId) ?? null;
61
+ }
62
+ /**
63
+ * Retire the member's resident runtime in the active generation so the next
64
+ * round respawns onto the member's new effective model (the composer's
65
+ * model switch path; the sub-dsh session itself carries over via the
66
+ * on-disk resume).
67
+ */
68
+ async retireRuntime(childSessionId) {
69
+ await this.active?.retireRuntime(childSessionId);
70
+ }
71
+ /** Mirror the resolved settings into the driver generation. */
72
+ apply(next) {
73
+ if (next.live === this.liveOn) {
74
+ return;
75
+ }
76
+ this.liveOn = next.live;
77
+ const retiring = this.active;
78
+ this.active = next.live
79
+ ? new DshLiveDriver(this.ctx, {
80
+ ...this.config,
81
+ ...this.extras.modelFor === undefined ? {} : { modelFor: this.extras.modelFor },
82
+ })
83
+ : undefined;
84
+ if (retiring !== undefined) {
85
+ this.draining.add(retiring);
86
+ void retiring.drain().then(() => { this.draining.delete(retiring); }, (error) => {
87
+ this.draining.delete(retiring);
88
+ this.ctx.logger.warn(`local-agent-dsh: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
89
+ });
90
+ }
91
+ }
92
+ /** Enabled-generation unload: interrupt whatever survives (disposeAll, not drain). */
93
+ dispose() {
94
+ this.unwatch();
95
+ void this.active?.disposeAll();
96
+ for (const retiring of this.draining)
97
+ void retiring.disposeAll();
98
+ this.draining.clear();
99
+ }
100
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The dsh harness's model broker: the member-aware model surface the core
3
+ * gateway routes to (`harnessModel` for the settings card, `memberModel` /
4
+ * `setMemberModel` for the composer picker). It owns the session-level
5
+ * override map — shared with the exec provider (which consults it per round)
6
+ * and the live driver (which binds it at runtime spawn) — and retires a
7
+ * member's resident serve process when a switch leaves it bound to a
8
+ * different model, so the NEXT round respawns onto the new one while the
9
+ * sub-dsh session itself carries over via the on-disk resume.
10
+ *
11
+ * Resolution order is the family's fixed one (first match wins): override →
12
+ * the delegation's recorded model → the plugin-config `model` (settings) →
13
+ * the host instance's default model selection spelled `provider/model`
14
+ * (cliDefault — the selection a sub-dsh with no `--model` inherits) → the
15
+ * CLI's built-in default, which names nothing. The pickable vocabulary is
16
+ * settings + cliDefault + the host's own adapter enumeration (the public
17
+ * `ctx.llm` surface — `listProviders` × `listModels`, spelled `provider/model`
18
+ * exactly as the sub-dsh's `--model` expects; the same trio the host's model
19
+ * picker is built on) + the card's recent-model memory.
20
+ * @module @khorsheed/dsh-local-agent-dsh/model-broker
21
+ */
22
+ import type { MemberConfigurationAdapter } from '@khorsheed/dsh-local-agent';
23
+ import type { DshModelCatalog } from './model-catalog.ts';
24
+ import type { Context } from '@deepseek-ai/cordis';
25
+ import type { LocalAgentModelBroker, LocalAgentModelInfo, LocalAgentModelDirectory, LocalAgentMemberBinding } from '@khorsheed/dsh-local-agent/types';
26
+ /** Everything the broker reads or drives, injected so the unit specs stay small. */
27
+ export interface DshModelBrokerDeps {
28
+ /** Host context carrying the family registry (in-flight reads, delegation records). */
29
+ readonly ctx: Context;
30
+ /** The plugin-config `model` resolver — the settings layer, blank reads unset. */
31
+ readonly settingsModel: () => string | undefined;
32
+ /**
33
+ * The host instance's default model selection spelled `provider/model` —
34
+ * what a round with no `--model` inherits. Undefined when the service or
35
+ * the selection is unreadable (absence is the honest answer, never a guess).
36
+ */
37
+ readonly cliDefault: () => string | undefined;
38
+ readonly defaultEffort?: () => string | undefined;
39
+ /**
40
+ * The host's adapter enumeration spelled `provider/model` — discovered
41
+ * host-side via `ctx.llm` and cached by the caller (refreshed on
42
+ * `llm/adapters-updated`), so this read stays synchronous. Empty when the
43
+ * service is absent: the other choice layers still answer.
44
+ */
45
+ readonly discovered: () => readonly string[];
46
+ readonly catalog?: DshModelCatalog;
47
+ /** The card's recent-model memory (the suggestion vocabulary's tail). */
48
+ readonly recentModels: () => readonly string[];
49
+ /** Whether the live driver is on (the member's rounds bind resident runtimes). */
50
+ readonly live: () => boolean;
51
+ /**
52
+ * The session-level overrides this broker owns. Shared by reference with the
53
+ * exec provider's per-round resolution and the live driver's spawn binding,
54
+ * so a switch reaches every path without a reload.
55
+ */
56
+ readonly overrides: Map<string, string>;
57
+ /**
58
+ * The model the member's live runtime bound at spawn: the identifier,
59
+ * undefined for "bound to no model", null for "no live runtime".
60
+ */
61
+ readonly liveBoundModel: (childSessionId: string) => string | undefined | null;
62
+ /** Retire the member's live runtime; the next round respawns onto the new model. */
63
+ readonly retireRuntime: (childSessionId: string) => Promise<void>;
64
+ }
65
+ export declare class DshModelBroker implements LocalAgentModelBroker {
66
+ private readonly deps;
67
+ constructor(deps: DshModelBrokerDeps);
68
+ configurationAdapter(binding: LocalAgentMemberBinding): MemberConfigurationAdapter;
69
+ modelDirectory(childSessionId?: string, refresh?: boolean): Promise<LocalAgentModelDirectory>;
70
+ followModelDirectory(childSessionId: string | undefined, signal: AbortSignal): AsyncIterable<LocalAgentModelDirectory>;
71
+ /** The member's in-flight rounds (an empty answer on a core that predates the read). */
72
+ private activeDelegations;
73
+ /** The model the member's NEXT spawn binds explicitly: override → recorded → settings. */
74
+ private boundLayer;
75
+ /** Read the model surface; every layer reports its own value so a UI can explain the effective one. */
76
+ modelInfo(childSessionId?: string, delegationModel?: string): LocalAgentModelInfo;
77
+ /**
78
+ * Set (or clear) the member's session-level override. Same-model is a
79
+ * no-op; a different model with a live runtime bound elsewhere retires the
80
+ * runtime so the next round respawns (the sub-dsh session resumes from
81
+ * disk). Throws while a round is in flight — retiring mid-round would kill
82
+ * the run.
83
+ */
84
+ setMemberModel(childSessionId: string, model: string | undefined): Promise<void>;
85
+ }
86
+ //# sourceMappingURL=model-broker.d.ts.map
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The dsh harness's model broker: the member-aware model surface the core
3
+ * gateway routes to (`harnessModel` for the settings card, `memberModel` /
4
+ * `setMemberModel` for the composer picker). It owns the session-level
5
+ * override map — shared with the exec provider (which consults it per round)
6
+ * and the live driver (which binds it at runtime spawn) — and retires a
7
+ * member's resident serve process when a switch leaves it bound to a
8
+ * different model, so the NEXT round respawns onto the new one while the
9
+ * sub-dsh session itself carries over via the on-disk resume.
10
+ *
11
+ * Resolution order is the family's fixed one (first match wins): override →
12
+ * the delegation's recorded model → the plugin-config `model` (settings) →
13
+ * the host instance's default model selection spelled `provider/model`
14
+ * (cliDefault — the selection a sub-dsh with no `--model` inherits) → the
15
+ * CLI's built-in default, which names nothing. The pickable vocabulary is
16
+ * settings + cliDefault + the host's own adapter enumeration (the public
17
+ * `ctx.llm` surface — `listProviders` × `listModels`, spelled `provider/model`
18
+ * exactly as the sub-dsh's `--model` expects; the same trio the host's model
19
+ * picker is built on) + the card's recent-model memory.
20
+ * @module @khorsheed/dsh-local-agent-dsh/model-broker
21
+ */
22
+ import { extendModelDirectory } from '@khorsheed/dsh-local-agent';
23
+ /** Dedupe the choice vocabulary, dropping unset layers and blanks. */
24
+ function dedupeChoices(layers) {
25
+ const seen = new Set();
26
+ const choices = [];
27
+ for (const layer of layers) {
28
+ const value = layer?.trim();
29
+ if (value === undefined || value === '' || seen.has(value))
30
+ continue;
31
+ seen.add(value);
32
+ choices.push(value);
33
+ }
34
+ return choices;
35
+ }
36
+ export class DshModelBroker {
37
+ deps;
38
+ constructor(deps) {
39
+ this.deps = deps;
40
+ }
41
+ configurationAdapter(binding) {
42
+ const resolve = async (selection) => {
43
+ let directory = this.deps.catalog?.read();
44
+ if (directory?.status === 'loading')
45
+ directory = await this.deps.catalog?.refresh();
46
+ const model = selection.model.mode === 'value' ? selection.model.value
47
+ : (selection.model.mode === 'inherit' ? binding.model : undefined) ?? this.deps.settingsModel() ?? this.deps.cliDefault();
48
+ const entry = directory?.entries.find(entry => entry.value === model);
49
+ const requested = selection.effort.mode === 'value' ? selection.effort.value : selection.effort.mode === 'inherit' ? binding.effort : undefined;
50
+ const effort = requested ?? this.deps.defaultEffort?.() ?? entry?.reasoning?.default;
51
+ if (effort !== undefined && !entry?.reasoning?.options.some(option => option.value === effort)) {
52
+ throw new Error('DSH has not advertised this reasoning effort for the selected model');
53
+ }
54
+ return { ...model === undefined ? {} : { model }, ...effort === undefined ? {} : { effort } };
55
+ };
56
+ return {
57
+ validate: async (selection) => { await resolve(selection); }, prepare: resolve,
58
+ apply: async (selection) => { const resolved = await resolve(selection); await this.deps.retireRuntime(binding.childSessionId); return resolved; },
59
+ reconcile: async (state) => {
60
+ if (this.activeDelegations().includes(binding.childSessionId))
61
+ return { active: true, matches: 'unknown', resolved: {} };
62
+ await this.deps.retireRuntime(binding.childSessionId);
63
+ return { active: false, matches: 'current', resolved: await resolve(state.current.selection) };
64
+ },
65
+ };
66
+ }
67
+ async modelDirectory(childSessionId, refresh = false) {
68
+ if (refresh)
69
+ await this.deps.catalog?.refresh();
70
+ return this.modelInfo(childSessionId).directory ?? { entries: [], complete: false, customInput: true, status: 'unsupported', refreshing: false, revision: 0 };
71
+ }
72
+ async *followModelDirectory(childSessionId, signal) {
73
+ if (this.deps.catalog === undefined) {
74
+ yield await this.modelDirectory(childSessionId);
75
+ return;
76
+ }
77
+ for await (const _snapshot of this.deps.catalog.follow(signal))
78
+ yield await this.modelDirectory(childSessionId);
79
+ }
80
+ /** The member's in-flight rounds (an empty answer on a core that predates the read). */
81
+ activeDelegations() {
82
+ const registry = this.deps.ctx.localAgent;
83
+ return typeof registry.activeDelegations === 'function' ? registry.activeDelegations() : [];
84
+ }
85
+ /** The model the member's NEXT spawn binds explicitly: override → recorded → settings. */
86
+ boundLayer(childSessionId) {
87
+ const override = this.deps.overrides.get(childSessionId);
88
+ if (override !== undefined)
89
+ return override;
90
+ const record = this.deps.ctx.localAgent.getDelegation(childSessionId);
91
+ const recorded = record?.model?.trim();
92
+ if (recorded !== undefined && recorded !== '')
93
+ return recorded;
94
+ return this.deps.settingsModel();
95
+ }
96
+ /** Read the model surface; every layer reports its own value so a UI can explain the effective one. */
97
+ modelInfo(childSessionId, delegationModel) {
98
+ const settings = this.deps.settingsModel();
99
+ const cliDefault = this.deps.cliDefault();
100
+ const directory = this.deps.catalog?.read();
101
+ const override = childSessionId === undefined ? undefined : this.deps.overrides.get(childSessionId);
102
+ const delegation = delegationModel?.trim() === '' ? undefined : delegationModel;
103
+ const effective = override ?? delegation ?? settings ?? cliDefault;
104
+ const source = override !== undefined ? 'override'
105
+ : delegation !== undefined ? 'delegation'
106
+ : settings !== undefined ? 'settings'
107
+ : cliDefault !== undefined ? 'cli-config' : 'cli-builtin';
108
+ const inFlight = childSessionId !== undefined && this.activeDelegations().includes(childSessionId);
109
+ return {
110
+ ...effective === undefined ? {} : { effective },
111
+ source,
112
+ ...override === undefined ? {} : { override },
113
+ ...delegation === undefined ? {} : { delegation },
114
+ ...settings === undefined ? {} : { settings },
115
+ ...cliDefault === undefined ? {} : { cliDefault },
116
+ ...directory === undefined ? {} : { directory: extendModelDirectory(directory, [settings, cliDefault], this.deps.recentModels()) },
117
+ choices: dedupeChoices([settings, cliDefault, ...this.deps.discovered(), ...this.deps.recentModels()]),
118
+ live: this.deps.live(),
119
+ switchable: !inFlight,
120
+ ...inFlight ? { reason: '成员有进行中的委派轮次,等其完成后再切换模型' } : {},
121
+ };
122
+ }
123
+ /**
124
+ * Set (or clear) the member's session-level override. Same-model is a
125
+ * no-op; a different model with a live runtime bound elsewhere retires the
126
+ * runtime so the next round respawns (the sub-dsh session resumes from
127
+ * disk). Throws while a round is in flight — retiring mid-round would kill
128
+ * the run.
129
+ */
130
+ async setMemberModel(childSessionId, model) {
131
+ if (typeof this.deps.ctx.localAgent.setMemberModel === 'function')
132
+ return this.deps.ctx.localAgent.setMemberModel(childSessionId, model);
133
+ if (this.activeDelegations().includes(childSessionId)) {
134
+ throw new Error(`subagent-dsh: 成员有进行中的委派轮次,等其完成后再切换模型 (child session ${childSessionId})`);
135
+ }
136
+ const trimmed = model?.trim();
137
+ const next = trimmed === undefined || trimmed === '' ? undefined : trimmed;
138
+ if (this.deps.overrides.get(childSessionId) === next)
139
+ return;
140
+ if (next === undefined)
141
+ this.deps.overrides.delete(childSessionId);
142
+ else
143
+ this.deps.overrides.set(childSessionId, next);
144
+ const bound = this.deps.liveBoundModel(childSessionId);
145
+ if (bound !== null && bound !== this.boundLayer(childSessionId)) {
146
+ await this.deps.retireRuntime(childSessionId);
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,27 @@
1
+ import type { LlmModelInfo, LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
2
+ import type { LocalAgentModelDirectory } from '@khorsheed/dsh-local-agent/types';
3
+ export interface DshModelDirectoryFace {
4
+ listProviders(): readonly {
5
+ id: string;
6
+ }[];
7
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
8
+ resolveModelInfo?(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
9
+ }
10
+ /** The public host adapter catalog used by sub-DSH provisioning. No generation request. */
11
+ export declare class DshModelCatalog {
12
+ private readonly deps;
13
+ private readonly cache;
14
+ constructor(deps: {
15
+ llm: () => DshModelDirectoryFace | undefined;
16
+ defaultModel: () => string | undefined;
17
+ timeoutMs?: number;
18
+ });
19
+ private key;
20
+ read(): LocalAgentModelDirectory;
21
+ refresh(): Promise<LocalAgentModelDirectory>;
22
+ follow(signal: AbortSignal): AsyncIterable<LocalAgentModelDirectory>;
23
+ invalidate(): void;
24
+ dispose(): void;
25
+ private discover;
26
+ }
27
+ //# sourceMappingURL=model-catalog.d.ts.map
@@ -0,0 +1,71 @@
1
+ import { ModelDirectoryCache } from '@khorsheed/dsh-local-agent';
2
+ /** The public host adapter catalog used by sub-DSH provisioning. No generation request. */
3
+ export class DshModelCatalog {
4
+ deps;
5
+ cache;
6
+ constructor(deps) {
7
+ this.deps = deps;
8
+ this.cache = new ModelDirectoryCache({ load: async (_key, signal) => {
9
+ const controller = new AbortController();
10
+ const abort = () => controller.abort();
11
+ signal.addEventListener('abort', abort, { once: true });
12
+ if (signal.aborted)
13
+ abort();
14
+ let timer;
15
+ try {
16
+ return await Promise.race([
17
+ this.discover(controller.signal),
18
+ new Promise((_, reject) => {
19
+ timer = setTimeout(() => { controller.abort(); reject(new Error('DSH model directory timed out')); }, deps.timeoutMs ?? 5_000);
20
+ timer.unref();
21
+ }),
22
+ ]);
23
+ }
24
+ finally {
25
+ if (timer !== undefined)
26
+ clearTimeout(timer);
27
+ signal.removeEventListener('abort', abort);
28
+ }
29
+ } });
30
+ }
31
+ key() { return JSON.stringify({ defaultModel: this.deps.defaultModel() }); }
32
+ read() { return this.cache.read(this.key()); }
33
+ refresh() { return this.cache.refresh(this.key()); }
34
+ follow(signal) { return this.cache.follow(() => this.key(), signal); }
35
+ invalidate() { this.cache.invalidate(this.key()); }
36
+ dispose() { this.cache.dispose(); }
37
+ async discover(signal) {
38
+ const llm = this.deps.llm();
39
+ if (llm === undefined || typeof llm.listProviders !== 'function' || typeof llm.listModels !== 'function') {
40
+ return { entries: [], complete: false, customInput: true, unsupported: true, reason: 'The host exposes no adapter model directory' };
41
+ }
42
+ const providers = llm.listProviders();
43
+ const groups = await Promise.allSettled(providers.map(async (provider) => Promise.all((await llm.listModels(provider.id)).map(async (model) => {
44
+ let resolved;
45
+ try {
46
+ resolved = await llm.resolveModelInfo?.(provider.id, model.id, signal);
47
+ }
48
+ catch { /* Unknown capabilities stay unknown. */ }
49
+ const entry = {
50
+ value: `${provider.id}/${model.id}`, label: model.name ?? model.id, source: 'native',
51
+ ...model.description === undefined ? {} : { description: model.description },
52
+ ...resolved === undefined ? {} : { resolvedModel: `${resolved.provider}/${resolved.id}` },
53
+ ...resolved?.reasoning === undefined ? {} : { reasoning: {
54
+ options: resolved.reasoning.efforts.map(effort => ({ value: String(effort.id), label: effort.name, ...effort.description === undefined ? {} : { description: effort.description } })),
55
+ ...resolved.reasoning.defaultEffort === undefined ? {} : { default: String(resolved.reasoning.defaultEffort) },
56
+ } },
57
+ };
58
+ return entry;
59
+ }))));
60
+ const failed = groups.filter(group => group.status === 'rejected').length;
61
+ if (failed > 0 && failed === groups.length)
62
+ throw new Error('DSH adapters could not list models');
63
+ const entries = groups.flatMap(group => group.status === 'fulfilled' ? group.value : []);
64
+ const defaultModel = this.deps.defaultModel();
65
+ return {
66
+ entries, complete: failed === 0, customInput: true,
67
+ ...defaultModel === undefined ? {} : { defaultModel },
68
+ ...failed === 0 ? {} : { reason: `${failed} adapter model list(s) unavailable` },
69
+ };
70
+ }
71
+ }