@khorsheed/dsh-local-agent-dsh 0.1.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +150 -0
- package/README.i18n.yaml +6 -0
- package/README.md +151 -0
- package/cordis.patch.yml +24 -0
- package/lib/client.js +1115 -0
- package/lib/dsh-cli-provider-UhWvivte.js +1701 -0
- package/lib/dsh-cli-provider.js +3 -0
- package/lib/index.js +624 -0
- package/lib/invariant.js +33 -0
- package/lib/provision.js +602 -0
- package/lib/records.js +109 -0
- package/lib/session-log-DOsSz56-.js +88 -0
- package/lib/types/client/SettingsCard.d.ts +78 -0
- package/lib/types/client/SettingsCard.js +200 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +45 -0
- package/lib/types/client/index.js +88 -0
- package/lib/types/client/locales.d.ts +76 -0
- package/lib/types/client/locales.js +67 -0
- package/lib/types/dsh-cli-provider.d.ts +213 -0
- package/lib/types/dsh-cli-provider.js +733 -0
- package/lib/types/index.d.ts +124 -0
- package/lib/types/index.js +347 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +37 -0
- package/lib/types/live-driver.d.ts +180 -0
- package/lib/types/live-driver.js +887 -0
- package/lib/types/live-switch.d.ts +76 -0
- package/lib/types/live-switch.js +100 -0
- package/lib/types/model-broker.d.ts +86 -0
- package/lib/types/model-broker.js +149 -0
- package/lib/types/model-catalog.d.ts +27 -0
- package/lib/types/model-catalog.js +71 -0
- package/lib/types/provision.d.ts +382 -0
- package/lib/types/provision.js +656 -0
- package/lib/types/records.d.ts +41 -0
- package/lib/types/records.js +125 -0
- package/lib/types/session-log.d.ts +58 -0
- package/lib/types/session-log.js +83 -0
- package/lib/types/session-mirror.d.ts +155 -0
- package/lib/types/session-mirror.js +451 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +121 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh harness instantiation of the local-agent family, plus the DeepSeek
|
|
3
|
+
* settings toggle that gates it. Registers the `local-agent-dsh` settings
|
|
4
|
+
* namespace (default off); while ON, the controller registers the `dsh`
|
|
5
|
+
* harness into the core registry (scoped home under the shared homes root,
|
|
6
|
+
* `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
|
|
7
|
+
* the parent's resolved DeepSeek key), and the family delegation tool for it.
|
|
8
|
+
* While OFF — the default — nothing model-visible mounts, so the instance
|
|
9
|
+
* keeps exactly the current behavior (official in-process subagent tools) and
|
|
10
|
+
* the model never sees two overlapping delegation tools at once.
|
|
11
|
+
*
|
|
12
|
+
* The same namespace carries the resident-mode preferences (`live`,
|
|
13
|
+
* `liveMirrorGranularity`): the YAML config is the composition base, the
|
|
14
|
+
* settings card's writes are user-layer overrides, and a LiveDriverSwitch
|
|
15
|
+
* inside each enabled generation hot-swaps driver generations on a change —
|
|
16
|
+
* no reload, in-flight rounds never interrupted.
|
|
17
|
+
*
|
|
18
|
+
* The sub-dsh half lives in the sibling bundle
|
|
19
|
+
* `@khorsheed/dsh-local-agent-dsh-headless`: a headless profile under the
|
|
20
|
+
* scoped home that the provider spawns with `--session-id <uuid>` (fresh) or
|
|
21
|
+
* `--resume <uuid>` (continuation).
|
|
22
|
+
*
|
|
23
|
+
* @module @khorsheed/dsh-local-agent-dsh
|
|
24
|
+
*/
|
|
25
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
26
|
+
import z from '@deepseek-ai/schemastery';
|
|
27
|
+
import type { DshSubProfilePermissions } from './provision.ts';
|
|
28
|
+
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
29
|
+
export declare const name = "local-agent-dsh";
|
|
30
|
+
/** Services required before the toggle controller can mount. */
|
|
31
|
+
export declare const inject: string[];
|
|
32
|
+
/** Plugin config: the sub-dsh profile and the credential it resolves. */
|
|
33
|
+
export interface LocalAgentDshConfig {
|
|
34
|
+
/** Sub-dsh profile name under the scoped home. */
|
|
35
|
+
profileName?: string;
|
|
36
|
+
/** Credential reference resolving the sub-dsh API key. */
|
|
37
|
+
apiKeyRef?: string;
|
|
38
|
+
/**
|
|
39
|
+
* dsh launch argv prefix override (node + entry + launcher flags). Absent
|
|
40
|
+
* means replicate the parent instance's own launch.
|
|
41
|
+
*/
|
|
42
|
+
cliLaunch?: string[];
|
|
43
|
+
/** Override the headless bundle directory the sub-profile symlinks to. */
|
|
44
|
+
headlessBundleDir?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The sub-dsh's permission boundary: the sandbox mode every confined call
|
|
47
|
+
* runs under, paired with the approval policy `dsh-base`'s own preset table
|
|
48
|
+
* pairs it with. Provisioning writes it into the scope's sub-profile as a
|
|
49
|
+
* patch layer, so a scoped home carries its own boundary — including into a
|
|
50
|
+
* container unit that bind-mounts it.
|
|
51
|
+
*
|
|
52
|
+
* Absent — the default — writes no layer at all: the sub-dsh runs whatever
|
|
53
|
+
* `dsh-base` composes (`workspace-write` plus `ask`), byte for byte the
|
|
54
|
+
* behavior before this key existed. Pin `danger-full-access` only where
|
|
55
|
+
* something else IS the boundary (an evaluation unit, a disposable
|
|
56
|
+
* container); on a developer's machine the sub-dsh shares the real home.
|
|
57
|
+
*/
|
|
58
|
+
permissions?: DshSubProfilePermissions;
|
|
59
|
+
/**
|
|
60
|
+
* Live driver: keep one resident sub-dsh serve process per member and drive
|
|
61
|
+
* turns over the family wire (runtime-level interrupt, push-mode mirror)
|
|
62
|
+
* instead of one process per round. Deployment default only — the settings
|
|
63
|
+
* namespace carries it as the composition base, so the settings card can
|
|
64
|
+
* override it live; the exec one-shot stays the fallback whenever the serve
|
|
65
|
+
* channel cannot come up.
|
|
66
|
+
*/
|
|
67
|
+
live?: boolean;
|
|
68
|
+
/** Idle lifetime of an unused resident runtime before reclaim. */
|
|
69
|
+
liveIdleMs?: number;
|
|
70
|
+
/** @deprecated Accepted for old profiles; live output is always incremental. */
|
|
71
|
+
liveMirrorGranularity?: 'event' | 'token';
|
|
72
|
+
/**
|
|
73
|
+
* The model every delegation round starts the sub-dsh with, spelled
|
|
74
|
+
* `provider/model` (a bare id names the model and keeps the instance's
|
|
75
|
+
* provider) — passed as the headless launch's `--model`. Absent — the
|
|
76
|
+
* default — passes no flag at all: the sub-dsh runs the host instance's own
|
|
77
|
+
* `agentDefaultModel` selection, exactly as it did before this key existed.
|
|
78
|
+
*
|
|
79
|
+
* A delegation that names its own model outranks this key; the settings
|
|
80
|
+
* card writes the same key, so a change applies to the next round without a
|
|
81
|
+
* reload.
|
|
82
|
+
*/
|
|
83
|
+
model?: string;
|
|
84
|
+
/**
|
|
85
|
+
* The DeepSeek delegation toggle (settings field since the settings rewrite:
|
|
86
|
+
* the profile row can preset it — a semantic superset of the old
|
|
87
|
+
* settings-only key). OFF — the default — mounts nothing model-visible.
|
|
88
|
+
*/
|
|
89
|
+
enabled?: boolean;
|
|
90
|
+
/** Model identifiers the settings card saved before (settings field). */
|
|
91
|
+
recentModels?: readonly string[];
|
|
92
|
+
}
|
|
93
|
+
export declare const Config: z;
|
|
94
|
+
/**
|
|
95
|
+
* Settings namespace owning the DeepSeek toggle and the live preferences. On
|
|
96
|
+
* 0.1.5 the Cordis config feeds the namespace's composition `base` layer, so
|
|
97
|
+
* a field absent from the user layer inherits the YAML value — the settings
|
|
98
|
+
* card only ever stores deliberate overrides. On rc.1 the namespace is the
|
|
99
|
+
* plugin row id itself: the row config IS the document, and the settings
|
|
100
|
+
* form's user layer rides the profile patch.
|
|
101
|
+
*/
|
|
102
|
+
export declare const DSH_SETTINGS_NAMESPACE = "local-agent-dsh";
|
|
103
|
+
/** The resolved settings the controller and the live switch consume. */
|
|
104
|
+
export interface DshSettings {
|
|
105
|
+
/** The DeepSeek delegation toggle. */
|
|
106
|
+
enabled: boolean;
|
|
107
|
+
/** Resident (live) mode. */
|
|
108
|
+
live: boolean;
|
|
109
|
+
/** @deprecated Legacy granularity; live output is always incremental. */
|
|
110
|
+
liveMirrorGranularity: 'event' | 'token';
|
|
111
|
+
/** The delegation-start model, undefined when unset (pass no `--model`). */
|
|
112
|
+
model: string | undefined;
|
|
113
|
+
/** Model identifiers saved before; the input's suggestions. */
|
|
114
|
+
recentModels: readonly string[];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Mount the toggle controller: register the settings namespace, then mirror
|
|
118
|
+
* its value into harness/provider/tool registrations, live.
|
|
119
|
+
* @param ctx - plugin context carrying the family registry, subagents,
|
|
120
|
+
* subprocess, credentials, and settings services.
|
|
121
|
+
* @param config - validated plugin config.
|
|
122
|
+
*/
|
|
123
|
+
export declare function apply(ctx: Context, config: LocalAgentDshConfig): void;
|
|
124
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh harness instantiation of the local-agent family, plus the DeepSeek
|
|
3
|
+
* settings toggle that gates it. Registers the `local-agent-dsh` settings
|
|
4
|
+
* namespace (default off); while ON, the controller registers the `dsh`
|
|
5
|
+
* harness into the core registry (scoped home under the shared homes root,
|
|
6
|
+
* `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
|
|
7
|
+
* the parent's resolved DeepSeek key), and the family delegation tool for it.
|
|
8
|
+
* While OFF — the default — nothing model-visible mounts, so the instance
|
|
9
|
+
* keeps exactly the current behavior (official in-process subagent tools) and
|
|
10
|
+
* the model never sees two overlapping delegation tools at once.
|
|
11
|
+
*
|
|
12
|
+
* The same namespace carries the resident-mode preferences (`live`,
|
|
13
|
+
* `liveMirrorGranularity`): the YAML config is the composition base, the
|
|
14
|
+
* settings card's writes are user-layer overrides, and a LiveDriverSwitch
|
|
15
|
+
* inside each enabled generation hot-swaps driver generations on a change —
|
|
16
|
+
* no reload, in-flight rounds never interrupted.
|
|
17
|
+
*
|
|
18
|
+
* The sub-dsh half lives in the sibling bundle
|
|
19
|
+
* `@khorsheed/dsh-local-agent-dsh-headless`: a headless profile under the
|
|
20
|
+
* scoped home that the provider spawns with `--session-id <uuid>` (fresh) or
|
|
21
|
+
* `--resume <uuid>` (continuation).
|
|
22
|
+
*
|
|
23
|
+
* @module @khorsheed/dsh-local-agent-dsh
|
|
24
|
+
*/
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import z from '@deepseek-ai/schemastery';
|
|
27
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
28
|
+
import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
|
|
29
|
+
import * as toolModule from '@khorsheed/dsh-local-agent-tool-subagent';
|
|
30
|
+
import { CONTAINER_NODE_OPTIONS, DshCliProvider, dshCliVersion } from './dsh-cli-provider.js';
|
|
31
|
+
import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
|
|
32
|
+
import { LiveDriverSwitch } from './live-switch.js';
|
|
33
|
+
import { DshModelCatalog } from './model-catalog.js';
|
|
34
|
+
import { DshModelBroker } from './model-broker.js';
|
|
35
|
+
import { listDshSessions } from './records.js';
|
|
36
|
+
import { defaultPresetRoot, DEFAULT_SUB_PROFILE_NAME, provisionDshScope, USER_PRESET_DIR } from './provision.js';
|
|
37
|
+
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
38
|
+
export const name = 'local-agent-dsh';
|
|
39
|
+
/** Services required before the toggle controller can mount. */
|
|
40
|
+
export const inject = ['localAgent', 'subagents', 'subprocess', 'credentials', 'settings'];
|
|
41
|
+
/**
|
|
42
|
+
* Runtime schema so the Loader always passes an object, never undefined.
|
|
43
|
+
* The settings fields (`enabled`, `live`, `liveMirrorGranularity`, `model`,
|
|
44
|
+
* `recentModels`) ride the same schema, marked `.volatile()` where the host's
|
|
45
|
+
* schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed): there
|
|
46
|
+
* the SettingsForms writes hot-track the running fiber's Volatile references
|
|
47
|
+
* without a remount, and the official one-shot import moves an old
|
|
48
|
+
* settings.yaml section into this row's config. On 0.1.5 the call is absent,
|
|
49
|
+
* the fields stay plain config keys (a semantic superset — the profile row
|
|
50
|
+
* can now preset them too), and the legacy settings namespace below carries
|
|
51
|
+
* the user layer exactly as before.
|
|
52
|
+
*
|
|
53
|
+
* Bare `z` annotation: `z<LocalAgentDshConfig>` fails schemastery 3.18.4's
|
|
54
|
+
* variance under exactOptionalPropertyTypes (TS2375) and dropping the
|
|
55
|
+
* annotation trips TS2742; the interface stays the apply signature's contract.
|
|
56
|
+
*/
|
|
57
|
+
const SETTINGS_FIELDS = {
|
|
58
|
+
enabled: z.boolean().default(false),
|
|
59
|
+
live: z.boolean().default(false),
|
|
60
|
+
liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
|
|
61
|
+
// `model` deliberately carries NO default: an unset key must resolve to
|
|
62
|
+
// undefined, which is what keeps the pre-key behavior byte-identical.
|
|
63
|
+
model: z.string(),
|
|
64
|
+
recentModels: z.array(z.string()).default([]),
|
|
65
|
+
};
|
|
66
|
+
/** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
|
|
67
|
+
function volatilize(field) {
|
|
68
|
+
return typeof field.volatile === 'function' ? field.volatile() : field;
|
|
69
|
+
}
|
|
70
|
+
export const Config = z.object({
|
|
71
|
+
profileName: z.string().default(DEFAULT_SUB_PROFILE_NAME),
|
|
72
|
+
apiKeyRef: z.string().default('DEEPSEEK_API_KEY'),
|
|
73
|
+
cliLaunch: z.array(z.string()),
|
|
74
|
+
headlessBundleDir: z.string(),
|
|
75
|
+
permissions: z.union([z.const('read-only'), z.const('workspace-write'), z.const('danger-full-access')]),
|
|
76
|
+
model: volatilize(SETTINGS_FIELDS.model),
|
|
77
|
+
live: volatilize(SETTINGS_FIELDS.live),
|
|
78
|
+
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
79
|
+
liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
|
|
80
|
+
enabled: volatilize(SETTINGS_FIELDS.enabled),
|
|
81
|
+
recentModels: volatilize(SETTINGS_FIELDS.recentModels),
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Settings namespace owning the DeepSeek toggle and the live preferences. On
|
|
85
|
+
* 0.1.5 the Cordis config feeds the namespace's composition `base` layer, so
|
|
86
|
+
* a field absent from the user layer inherits the YAML value — the settings
|
|
87
|
+
* card only ever stores deliberate overrides. On rc.1 the namespace is the
|
|
88
|
+
* plugin row id itself: the row config IS the document, and the settings
|
|
89
|
+
* form's user layer rides the profile patch.
|
|
90
|
+
*/
|
|
91
|
+
export const DSH_SETTINGS_NAMESPACE = 'local-agent-dsh';
|
|
92
|
+
/**
|
|
93
|
+
* The card's 0.1.5 schema: `enabled` OFF (default) means the official
|
|
94
|
+
* in-process subagent stays the only delegation path; the live fields mirror
|
|
95
|
+
* the YAML config's deployment defaults through the composition base.
|
|
96
|
+
*/
|
|
97
|
+
const DSH_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
98
|
+
/** The delegation tool the toggle mounts while ON. */
|
|
99
|
+
const DSH_TOOL_NAME = 'subagent_dsh';
|
|
100
|
+
/** Resolve the sub-dsh credential or report unconfigured. */
|
|
101
|
+
async function resolveApiKey(ctx, config) {
|
|
102
|
+
return ctx.credentials.resolve(credentialRef(config.apiKeyRef ?? 'DEEPSEEK_API_KEY'));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Mount the toggle controller: register the settings namespace, then mirror
|
|
106
|
+
* its value into harness/provider/tool registrations, live.
|
|
107
|
+
* @param ctx - plugin context carrying the family registry, subagents,
|
|
108
|
+
* subprocess, credentials, and settings services.
|
|
109
|
+
* @param config - validated plugin config.
|
|
110
|
+
*/
|
|
111
|
+
export function apply(ctx, config) {
|
|
112
|
+
ctx.effect(() => {
|
|
113
|
+
// The settings face hides the two host lines: 0.1.5 serves the legacy
|
|
114
|
+
// namespace scope (register + base); rc.1 reads the row config's volatile
|
|
115
|
+
// fields and re-reads on `settings/document-updated`. Both hot-apply the
|
|
116
|
+
// card's writes without a reload.
|
|
117
|
+
const readSettings = () => ({
|
|
118
|
+
enabled: vol(config.enabled) ?? false,
|
|
119
|
+
live: vol(config.live) ?? false,
|
|
120
|
+
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
|
|
121
|
+
model: vol(config.model),
|
|
122
|
+
recentModels: vol(config.recentModels) ?? [],
|
|
123
|
+
});
|
|
124
|
+
const legacyBase = {};
|
|
125
|
+
for (const field of ['enabled', 'live', 'liveMirrorGranularity', 'model']) {
|
|
126
|
+
const value = vol(config[field]);
|
|
127
|
+
if (value !== undefined)
|
|
128
|
+
legacyBase[field] = value;
|
|
129
|
+
}
|
|
130
|
+
const face = settingsFace(ctx, {
|
|
131
|
+
namespace: DSH_SETTINGS_NAMESPACE,
|
|
132
|
+
legacySchema: DSH_SETTINGS_SCHEMA,
|
|
133
|
+
legacyBase,
|
|
134
|
+
read: readSettings,
|
|
135
|
+
});
|
|
136
|
+
// Read PER ROUND, not captured at apply: the settings card writes the same
|
|
137
|
+
// field, so a change has to reach the next delegation without a
|
|
138
|
+
// plugin reload. Blank is not a model — a whitespace-only value is unset.
|
|
139
|
+
const resolveModel = () => {
|
|
140
|
+
const model = face.get().model?.trim();
|
|
141
|
+
return model === undefined || model === '' ? undefined : model;
|
|
142
|
+
};
|
|
143
|
+
// The host instance's default model selection spelled `provider/model` —
|
|
144
|
+
// what a sub-dsh with no `--model` inherits. Undefined when the service or
|
|
145
|
+
// the selection is unreadable: absence is the honest answer, never a guess.
|
|
146
|
+
const hostDefaultModel = () => {
|
|
147
|
+
try {
|
|
148
|
+
const defaultModel = ctx.get('agentDefaultModel');
|
|
149
|
+
const selection = defaultModel?.currentSelection?.();
|
|
150
|
+
if (typeof selection?.model === 'string' && selection.model !== '') {
|
|
151
|
+
return typeof selection.provider === 'string' && selection.provider !== ''
|
|
152
|
+
? `${selection.provider}/${selection.model}`
|
|
153
|
+
: selection.model;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Degrade: an unreadable selection reports no model instead of
|
|
158
|
+
// breaking the status surface.
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
};
|
|
162
|
+
const modelCatalog = new DshModelCatalog({
|
|
163
|
+
llm: () => ctx.get('llm'),
|
|
164
|
+
defaultModel: hostDefaultModel,
|
|
165
|
+
});
|
|
166
|
+
void modelCatalog.read();
|
|
167
|
+
ctx.on('llm/adapters-updated', () => { modelCatalog.invalidate(); });
|
|
168
|
+
// The member-level model surface: the composer picker's session-level
|
|
169
|
+
// overrides (in-memory, deliberately lost on a host restart), shared by
|
|
170
|
+
// reference with the broker (which writes them), the exec provider (which
|
|
171
|
+
// consults them per round), and the live driver (which binds them at
|
|
172
|
+
// runtime spawn). A switch on a member with a live runtime bound to a
|
|
173
|
+
// different model retires that runtime — the next round respawns onto the
|
|
174
|
+
// new model while the sub-dsh session itself resumes from disk.
|
|
175
|
+
const memberModelOverrides = new Map();
|
|
176
|
+
let currentLiveSwitch;
|
|
177
|
+
const modelBroker = new DshModelBroker({
|
|
178
|
+
ctx,
|
|
179
|
+
settingsModel: resolveModel,
|
|
180
|
+
cliDefault: hostDefaultModel,
|
|
181
|
+
defaultEffort: () => ctx.get('agentDefaultModel')?.currentSelection().reasoningEffort,
|
|
182
|
+
discovered: () => modelCatalog.read().entries.filter(entry => !entry.hidden).map(entry => entry.value),
|
|
183
|
+
catalog: modelCatalog,
|
|
184
|
+
recentModels: () => face.get().recentModels ?? [],
|
|
185
|
+
live: () => face.get().live,
|
|
186
|
+
overrides: memberModelOverrides,
|
|
187
|
+
liveBoundModel: childSessionId => currentLiveSwitch?.boundModel(childSessionId) ?? null,
|
|
188
|
+
retireRuntime: childSessionId => currentLiveSwitch?.retireRuntime(childSessionId) ?? Promise.resolve(),
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
191
|
+
* This deployment's preset root — where a person or a pack installed the
|
|
192
|
+
* presets this instance offers, and the SOURCE every scope's own copy is
|
|
193
|
+
* taken from. Derived the way the roster derives it, from the settings
|
|
194
|
+
* service's harness home when there is one.
|
|
195
|
+
*/
|
|
196
|
+
const instancePresetRoot = () => {
|
|
197
|
+
const home = ctx.get?.('settings')?.home;
|
|
198
|
+
return home === undefined ? defaultPresetRoot() : join(home, USER_PRESET_DIR);
|
|
199
|
+
};
|
|
200
|
+
const harness = {
|
|
201
|
+
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
202
|
+
const driver = currentLiveSwitch?.resolve(binding.childSessionId);
|
|
203
|
+
if (driver === undefined || driver.disabled)
|
|
204
|
+
throw new Error('Enable the native live harness before preparing a coordinator');
|
|
205
|
+
await driver.prepare({ cwd: binding.cwd, homeDir: ctx.localAgent.homeDir('dsh', binding.scope), childSession,
|
|
206
|
+
parentSessionId: binding.parentSessionId, configuration: configuration.resolved, sessionId: binding.childSessionId }, signal);
|
|
207
|
+
},
|
|
208
|
+
name: 'dsh',
|
|
209
|
+
displayName: 'dsh',
|
|
210
|
+
homeEnvVar: 'DSH_HOME',
|
|
211
|
+
delegationProvider: 'dsh-cli',
|
|
212
|
+
modelBroker,
|
|
213
|
+
records: { listSessions: homeDir => listDshSessions(homeDir) },
|
|
214
|
+
// A NAMED scope's directory is provisioned through this hook when the
|
|
215
|
+
// registry materializes it: the sub-profile the round launches from
|
|
216
|
+
// lives inside the scoped home, so a scope needs its own. (The
|
|
217
|
+
// credential does not: dsh authenticates through the host instance,
|
|
218
|
+
// which is why `isAuthenticated` below reads no directory at all.)
|
|
219
|
+
// `options.preset` is the per-condition input an evaluation's
|
|
220
|
+
// `conditions provision` hands in; the scope's own declaration answers
|
|
221
|
+
// for every other call, which is what makes a rostered preset survive
|
|
222
|
+
// a restart of this instance.
|
|
223
|
+
provision: (scopedHome, options) => {
|
|
224
|
+
const provisioned = provisionDshScope(scopedHome, config, {
|
|
225
|
+
...(options?.preset === undefined ? {} : { preset: options.preset }),
|
|
226
|
+
presetRoot: instancePresetRoot(),
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
...(provisioned.preset === undefined ? {} : { preset: provisioned.preset }),
|
|
230
|
+
...(provisioned.presetSnapshot === undefined ? {} : { presetSnapshot: provisioned.presetSnapshot }),
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
isAuthenticated: async () => {
|
|
234
|
+
try {
|
|
235
|
+
return (await resolveApiKey(ctx, config)) !== undefined;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
// The eval snapshot. `sandbox` is the sub-profile's pinned permission
|
|
242
|
+
// boundary, reported exactly when one is configured: absence still
|
|
243
|
+
// means "this scope pins no boundary and runs what dsh-base composes",
|
|
244
|
+
// which is the honest condition-hash input for every deployment that
|
|
245
|
+
// never asked for one. Until T59 there was no knob to report at all,
|
|
246
|
+
// and the dsh rounds inside an evaluation unit paid for it — bash
|
|
247
|
+
// refused every call because the unit has no sandbox backend and a
|
|
248
|
+
// headless sub-dsh has no approval channel.
|
|
249
|
+
// The endpoint is the host instance's model config, which this provider
|
|
250
|
+
// never overrides, so no custom endpoint is ever pinned. The model is
|
|
251
|
+
// the host's default selection the sub-dsh inherits (the headless agent
|
|
252
|
+
// loader reads the same service), reported as `provider/model`; a
|
|
253
|
+
// composition without the service — or a selection that fails to read —
|
|
254
|
+
// reports no model rather than a guessed identifier.
|
|
255
|
+
// The directory is a PARAMETER, not a re-resolution of the default
|
|
256
|
+
// scope: an evaluation snapshot of a named scope must probe the CLI the
|
|
257
|
+
// way that scope's rounds launch it.
|
|
258
|
+
effectiveSettings: async (scopedHome) => {
|
|
259
|
+
const cliVersion = await dshCliVersion(ctx, config, scopedHome).catch(() => undefined);
|
|
260
|
+
// The family's fixed order: the plugin config key (it rides every
|
|
261
|
+
// launch as `--model`) before the host selection the sub-dsh would
|
|
262
|
+
// otherwise inherit.
|
|
263
|
+
const model = resolveModel() ?? hostDefaultModel();
|
|
264
|
+
return {
|
|
265
|
+
drive: face.get().live ? 'live' : 'exec',
|
|
266
|
+
baseUrlSet: false,
|
|
267
|
+
// dsh is the only harness that needs an extra node flag to run
|
|
268
|
+
// inside a container (undici does not read the proxy variables on
|
|
269
|
+
// its own). The provider injects it unconditionally on a container
|
|
270
|
+
// target, so the snapshot states it unconditionally — the condition
|
|
271
|
+
// file records the knob rather than leaving the fairest-to-compare
|
|
272
|
+
// difference between the four harnesses invisible.
|
|
273
|
+
containerNodeOptions: CONTAINER_NODE_OPTIONS,
|
|
274
|
+
// The CONFIGURED boundary, not a read-back of the sub-profile: this
|
|
275
|
+
// key is what provisioning writes, and it is in force for the very
|
|
276
|
+
// first round of a scope whose directory the hook has not
|
|
277
|
+
// materialized yet. Same shape as codex, which reports the sandbox
|
|
278
|
+
// policy its own config key puts on every argv.
|
|
279
|
+
...config.permissions !== undefined ? { sandbox: config.permissions } : {},
|
|
280
|
+
...model !== undefined ? { model } : {},
|
|
281
|
+
...cliVersion !== undefined ? { cliVersion } : {},
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
const homeDir = ctx.localAgent.homeDir('dsh');
|
|
286
|
+
let generation = 0;
|
|
287
|
+
let currentEnabled;
|
|
288
|
+
const disposers = [];
|
|
289
|
+
const sync = (enabled) => {
|
|
290
|
+
// The watch fires on ANY namespace field change; only an enabled flip
|
|
291
|
+
// re-registers — the live fields ride the LiveDriverSwitch instead.
|
|
292
|
+
if (enabled === currentEnabled)
|
|
293
|
+
return;
|
|
294
|
+
currentEnabled = enabled;
|
|
295
|
+
generation += 1;
|
|
296
|
+
const gen = generation;
|
|
297
|
+
for (const dispose of disposers.splice(0))
|
|
298
|
+
dispose();
|
|
299
|
+
if (!enabled)
|
|
300
|
+
return;
|
|
301
|
+
// Idempotent: heals a deleted or drifted sub-profile before each round.
|
|
302
|
+
provisionDshScope(homeDir, config, { presetRoot: instancePresetRoot() });
|
|
303
|
+
disposers.push(ctx.localAgent.register(harness));
|
|
304
|
+
// The live driver is settings-driven WITHIN this enabled generation:
|
|
305
|
+
// the card's toggle (user layer over the YAML composition base) swaps
|
|
306
|
+
// driver generations without a reload. Toggling live OFF drains the
|
|
307
|
+
// retiring generation — new rounds fall back to exec, in-flight rounds
|
|
308
|
+
// finish on their runtime, idle runtimes are reclaimed at once. A
|
|
309
|
+
// legacy granularity setting no longer changes live behavior. Toggling ENABLED off keeps the historical hard semantics:
|
|
310
|
+
// provider unregisters and the switch disposes (disposeAll).
|
|
311
|
+
const liveSwitch = new LiveDriverSwitch(ctx, face, config, {
|
|
312
|
+
modelFor: childSessionId => memberModelOverrides.get(childSessionId) ?? resolveModel(),
|
|
313
|
+
});
|
|
314
|
+
currentLiveSwitch = liveSwitch;
|
|
315
|
+
disposers.push(ctx.subagents.registerProvider(new DshCliProvider(ctx, config, liveSwitch.resolve, resolveModel, childSessionId => memberModelOverrides.get(childSessionId))));
|
|
316
|
+
// The switch's disposal runs after the provider unregisters, so no
|
|
317
|
+
// in-flight round can re-spawn a runtime the teardown already reclaimed.
|
|
318
|
+
disposers.push(() => {
|
|
319
|
+
if (currentLiveSwitch === liveSwitch)
|
|
320
|
+
currentLiveSwitch = undefined;
|
|
321
|
+
liveSwitch.dispose();
|
|
322
|
+
});
|
|
323
|
+
// The family delegation tool is mounted dynamically so the toggle owns
|
|
324
|
+
// its lifecycle — while OFF the model never sees `subagent_dsh`.
|
|
325
|
+
void ctx.plugin(toolModule, { provider: 'dsh-cli', toolName: DSH_TOOL_NAME }).then((fiber) => {
|
|
326
|
+
if (gen !== generation) {
|
|
327
|
+
void fiber.dispose();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
disposers.push(() => { void fiber.dispose(); });
|
|
331
|
+
}, (error) => {
|
|
332
|
+
ctx.logger.warn(`local-agent-dsh: delegation tool mount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
sync(face.get().enabled);
|
|
336
|
+
const unwatch = face.watch((next) => { sync(next.enabled); });
|
|
337
|
+
return () => {
|
|
338
|
+
modelCatalog.dispose();
|
|
339
|
+
unwatch();
|
|
340
|
+
// Bump the generation so a tool mount still in flight disposes itself
|
|
341
|
+
// instead of being pushed onto a dead disposer list.
|
|
342
|
+
generation += 1;
|
|
343
|
+
for (const dispose of disposers.splice(0))
|
|
344
|
+
dispose();
|
|
345
|
+
};
|
|
346
|
+
}, 'local-agent-dsh: toggle controller');
|
|
347
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh`.
|
|
3
|
+
* @module @khorsheed/dsh-local-agent-dsh/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "local-agent-dsh-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
13
|
+
* @returns the installed registration's disposer.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh`.
|
|
3
|
+
* @module @khorsheed/dsh-local-agent-dsh/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'local-agent-dsh-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* The dsh harness definition is this bundle's contract with the core
|
|
12
|
+
* registry: whenever a `dsh` harness registers, its scoped-home variable must
|
|
13
|
+
* be the one this bundle pins, and it must delegate through the `dsh-cli`
|
|
14
|
+
* provider this bundle mounts.
|
|
15
|
+
*/
|
|
16
|
+
const install = (ctx, fail) => {
|
|
17
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
18
|
+
if (eventName !== 'localAgent/harness-added')
|
|
19
|
+
return;
|
|
20
|
+
const harness = args[0];
|
|
21
|
+
if (harness.name !== 'dsh')
|
|
22
|
+
return;
|
|
23
|
+
if (harness.homeEnvVar !== 'DSH_HOME') {
|
|
24
|
+
fail('dsh harness registered with a home env other than the local-agent-dsh contract');
|
|
25
|
+
}
|
|
26
|
+
if (harness.delegationProvider !== 'dsh-cli') {
|
|
27
|
+
fail('dsh harness registered with a delegation provider other than the local-agent-dsh contract');
|
|
28
|
+
}
|
|
29
|
+
}, { global: true });
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Register this package's invariant companion.
|
|
33
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
34
|
+
* @returns the installed registration's disposer.
|
|
35
|
+
*/
|
|
36
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
37
|
+
/* jscpd:ignore-end */
|