@khorsheed/dsh-local-agent-dsh 0.2.0-rc.3 → 0.2.1
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/CHANGELOG.md +13 -1
- package/README.en.md +23 -139
- package/README.i18n.yaml +2 -2
- package/README.md +23 -139
- package/lib/client.js +139 -97
- package/lib/dsh-cli-provider-DYRuRpVz.js +1920 -0
- package/lib/dsh-cli-provider.js +1 -1
- package/lib/index.js +5 -49
- package/lib/tool.js +40 -0
- package/lib/types/client/SettingsCard.js +7 -3
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.js +2 -2
- package/lib/types/client/locales.d.ts +0 -10
- package/lib/types/client/locales.js +0 -10
- package/lib/types/dsh-cli-provider.js +46 -24
- package/lib/types/index.js +10 -38
- package/lib/types/live-driver.js +47 -41
- package/lib/types/session-mirror.d.ts +55 -0
- package/lib/types/session-mirror.js +155 -1
- package/lib/types/tool.d.ts +52 -0
- package/lib/types/tool.js +34 -0
- package/package.json +9 -4
package/lib/dsh-cli-provider.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as MEMBER_BRIDGE_ENTRY_ENV, c as dshTextTask, d as startDshCliRun, i as DshCliProvider, l as registerMemberRun, n as DEFAULT_DISPOSE_GRACE_MS, o as dshCliVersion, r as DEFAULT_LIVE_MIRROR_INTERVAL_MS, s as dshLaunchArgv, t as CONTAINER_NODE_OPTIONS, u as resolveApiKey } from "./dsh-cli-provider-
|
|
1
|
+
import { a as MEMBER_BRIDGE_ENTRY_ENV, c as dshTextTask, d as startDshCliRun, i as DshCliProvider, l as registerMemberRun, n as DEFAULT_DISPOSE_GRACE_MS, o as dshCliVersion, r as DEFAULT_LIVE_MIRROR_INTERVAL_MS, s as dshLaunchArgv, t as CONTAINER_NODE_OPTIONS, u as resolveApiKey } from "./dsh-cli-provider-DYRuRpVz.js";
|
|
2
2
|
import "./provision.js";
|
|
3
3
|
export { CONTAINER_NODE_OPTIONS, DEFAULT_DISPOSE_GRACE_MS, DEFAULT_LIVE_MIRROR_INTERVAL_MS, DshCliProvider, MEMBER_BRIDGE_ENTRY_ENV, dshCliVersion, dshLaunchArgv, dshTextTask, registerMemberRun, resolveApiKey, startDshCliRun };
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { f as DEFAULT_LIVE_IDLE_MS, h as localModelLaunch, i as DshCliProvider, m as canInheritModelProvider, o as dshCliVersion, p as DshLiveDriver, t as CONTAINER_NODE_OPTIONS } from "./dsh-cli-provider-
|
|
1
|
+
import { f as DEFAULT_LIVE_IDLE_MS, h as localModelLaunch, i as DshCliProvider, m as canInheritModelProvider, o as dshCliVersion, p as DshLiveDriver, t as CONTAINER_NODE_OPTIONS } from "./dsh-cli-provider-DYRuRpVz.js";
|
|
2
2
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from "./provision.js";
|
|
3
3
|
import { listDshSessions } from "./records.js";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import z from "@deepseek-ai/schemastery";
|
|
7
7
|
import { ModelDirectoryCache, extendModelDirectory, settingsFace, vol } from "@khorsheed/dsh-local-agent";
|
|
8
|
-
import { mountSubagentDelegationTool } from "@khorsheed/dsh-local-agent/tool-factory";
|
|
9
8
|
//#region lib/types/live-switch.js
|
|
10
9
|
/**
|
|
11
10
|
* The settings-driven live-driver switch: owns the active driver generation
|
|
@@ -404,7 +403,7 @@ const inject = [
|
|
|
404
403
|
*/
|
|
405
404
|
const SETTINGS_FIELDS = {
|
|
406
405
|
enabled: z.boolean().default(false),
|
|
407
|
-
live: z.boolean().default(
|
|
406
|
+
live: z.boolean().default(true),
|
|
408
407
|
liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
|
|
409
408
|
model: z.string(),
|
|
410
409
|
recentModels: z.array(z.string()).default([])
|
|
@@ -446,34 +445,6 @@ const DSH_SETTINGS_NAMESPACE = "local-agent-dsh";
|
|
|
446
445
|
* the YAML config's deployment defaults through the composition base.
|
|
447
446
|
*/
|
|
448
447
|
const DSH_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
449
|
-
/** The delegation tool the toggle mounts while ON. */
|
|
450
|
-
const DSH_TOOL_NAME = "subagent_dsh";
|
|
451
|
-
/**
|
|
452
|
-
* This package's own name, passed to the family tool factory as the
|
|
453
|
-
* tool-origin owner (AGENTS.md § Tool origin tagging — a shared factory never
|
|
454
|
-
* hardcodes an owner), the same pattern the worktrees/room `./tool` entries
|
|
455
|
-
* use.
|
|
456
|
-
*/
|
|
457
|
-
const PACKAGE_NAME = "@khorsheed/dsh-local-agent-dsh";
|
|
458
|
-
/**
|
|
459
|
-
* The `subagent_dsh` mount, as a plugin object over the family factory: the
|
|
460
|
-
* toggle owns its lifecycle, so it mounts dynamically through `ctx.plugin`
|
|
461
|
-
* instead of shipping a `./tool` composition row like the CLI providers.
|
|
462
|
-
*/
|
|
463
|
-
const dshSubagentTool = {
|
|
464
|
-
name: "local-agent-dsh-tool",
|
|
465
|
-
inject: [
|
|
466
|
-
"tools",
|
|
467
|
-
"subagents",
|
|
468
|
-
"localAgent"
|
|
469
|
-
],
|
|
470
|
-
apply: (toolCtx) => {
|
|
471
|
-
mountSubagentDelegationTool(toolCtx, {
|
|
472
|
-
provider: "dsh-cli",
|
|
473
|
-
toolName: DSH_TOOL_NAME
|
|
474
|
-
}, PACKAGE_NAME);
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
448
|
/**
|
|
478
449
|
* Mount the toggle controller: register the settings namespace, then mirror
|
|
479
450
|
* its value into harness/provider/tool registrations, live.
|
|
@@ -485,7 +456,7 @@ function apply(ctx, config) {
|
|
|
485
456
|
ctx.effect(() => {
|
|
486
457
|
const readSettings = () => ({
|
|
487
458
|
enabled: vol(config.enabled) ?? false,
|
|
488
|
-
live: vol(config.live) ??
|
|
459
|
+
live: vol(config.live) ?? true,
|
|
489
460
|
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
|
|
490
461
|
model: vol(config.model),
|
|
491
462
|
recentModels: vol(config.recentModels) ?? []
|
|
@@ -502,6 +473,7 @@ function apply(ctx, config) {
|
|
|
502
473
|
}
|
|
503
474
|
const face = settingsFace(ctx, {
|
|
504
475
|
namespace: DSH_SETTINGS_NAMESPACE,
|
|
476
|
+
harness: "dsh",
|
|
505
477
|
legacySchema: DSH_SETTINGS_SCHEMA,
|
|
506
478
|
legacyBase,
|
|
507
479
|
read: readSettings
|
|
@@ -620,7 +592,6 @@ function apply(ctx, config) {
|
|
|
620
592
|
if (enabled === currentEnabled) return;
|
|
621
593
|
currentEnabled = enabled;
|
|
622
594
|
generation += 1;
|
|
623
|
-
const gen = generation;
|
|
624
595
|
for (const dispose of disposers.splice(0)) dispose();
|
|
625
596
|
if (!enabled) return;
|
|
626
597
|
provisionDshScope(homeDir, config, profileDirOption());
|
|
@@ -632,25 +603,10 @@ function apply(ctx, config) {
|
|
|
632
603
|
if (currentLiveSwitch === liveSwitch) currentLiveSwitch = void 0;
|
|
633
604
|
liveSwitch.dispose();
|
|
634
605
|
});
|
|
635
|
-
ctx.plugin(dshSubagentTool).then((fiber) => {
|
|
636
|
-
if (gen !== generation) {
|
|
637
|
-
fiber.dispose();
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
disposers.push(() => {
|
|
641
|
-
fiber.dispose();
|
|
642
|
-
});
|
|
643
|
-
}, (error) => {
|
|
644
|
-
ctx.logger.warn(`local-agent-dsh: delegation tool mount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
645
|
-
});
|
|
646
606
|
};
|
|
647
|
-
sync(
|
|
648
|
-
const unwatch = face.watch((next) => {
|
|
649
|
-
sync(next.enabled);
|
|
650
|
-
});
|
|
607
|
+
sync(true);
|
|
651
608
|
return () => {
|
|
652
609
|
modelCatalog.dispose();
|
|
653
|
-
unwatch();
|
|
654
610
|
generation += 1;
|
|
655
611
|
for (const dispose of disposers.splice(0)) dispose();
|
|
656
612
|
};
|
package/lib/tool.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mountSubagentDelegationTool, subagentToolConfigSchema } from "@khorsheed/dsh-local-agent/tool-factory";
|
|
2
|
+
//#region lib/types/tool.js
|
|
3
|
+
const PACKAGE_NAME = "@khorsheed/dsh-local-agent-dsh";
|
|
4
|
+
/** This entry's own defaults: a preset row needs no config. */
|
|
5
|
+
const DEFAULT_PROVIDER = "dsh-cli";
|
|
6
|
+
const DEFAULT_TOOL_NAME = "subagent_dsh";
|
|
7
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
8
|
+
const name = "local-agent-dsh-tool";
|
|
9
|
+
/**
|
|
10
|
+
* The family core's `localAgent` service is a declared inject (the
|
|
11
|
+
* owning-family companion exception to the community-service probe rule): a
|
|
12
|
+
* preset's standing scope mounts at registry-activation time, BEFORE the
|
|
13
|
+
* profile's later bundle rows provide the core, so a one-shot ctx.get probe
|
|
14
|
+
* at apply saw ABSENT there and nothing re-ran the row (rc.1 boot order; the
|
|
15
|
+
* worktrees `./tool` entry documents the incident). The declared inject pends
|
|
16
|
+
* the row until the core provides, then the body applies.
|
|
17
|
+
*/
|
|
18
|
+
const inject = [
|
|
19
|
+
"tools",
|
|
20
|
+
"subagents",
|
|
21
|
+
"localAgent"
|
|
22
|
+
];
|
|
23
|
+
const Config = subagentToolConfigSchema(DEFAULT_PROVIDER, DEFAULT_TOOL_NAME);
|
|
24
|
+
/**
|
|
25
|
+
* Plugin body: mount the delegation tool through the family factory, stamped
|
|
26
|
+
* with THIS package as the tool-origin owner (AGENTS.md § Tool origin
|
|
27
|
+
* tagging — the shared factory never hardcodes an owner).
|
|
28
|
+
* @param ctx - Cordis context (the preset's agent-plane mount).
|
|
29
|
+
* @param config - row config (schema defaults already applied; the `??`
|
|
30
|
+
* fallbacks cover a direct ctx.plugin caller that bypassed validation).
|
|
31
|
+
*/
|
|
32
|
+
function apply(ctx, config) {
|
|
33
|
+
const resolved = {
|
|
34
|
+
provider: config.provider ?? DEFAULT_PROVIDER,
|
|
35
|
+
toolName: config.toolName ?? DEFAULT_TOOL_NAME
|
|
36
|
+
};
|
|
37
|
+
mountSubagentDelegationTool(ctx, resolved, PACKAGE_NAME);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { Config, apply, inject, name };
|
|
@@ -78,7 +78,11 @@ function useCardState(active, useSettings, scope, harnessModel, t) {
|
|
|
78
78
|
const write = (field, value) => {
|
|
79
79
|
setSaved(false);
|
|
80
80
|
setError(false);
|
|
81
|
-
|
|
81
|
+
// The host answers a refused write with `false` (ConfigForm), not a rejection.
|
|
82
|
+
void scope.set(field, value).then((accepted) => { if (accepted === false)
|
|
83
|
+
setError(true);
|
|
84
|
+
else
|
|
85
|
+
setSaved(true); }, () => { setError(true); });
|
|
82
86
|
};
|
|
83
87
|
// The suggestion vocabulary: the broker's deduped union (settings + host
|
|
84
88
|
// default selection + recent) when the surface answered, the card's own
|
|
@@ -152,13 +156,13 @@ function useCardState(active, useSettings, scope, harnessModel, t) {
|
|
|
152
156
|
}
|
|
153
157
|
/** The card body both settings surfaces share: the auth block, the delegation switch, the default-model block, and the resident-mode block. */
|
|
154
158
|
function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
|
|
155
|
-
const { snapshot, saved, error, ready,
|
|
159
|
+
const { snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen, } = state;
|
|
156
160
|
// The free-text input, single-sourced for both presentations: bare when no
|
|
157
161
|
// vocabulary exists (a fresh install degrades to exactly the pre-picker
|
|
158
162
|
// field), or inside the select-like field next to its chevron. No datalist:
|
|
159
163
|
// the chevron menu is the single choice list.
|
|
160
164
|
const modelInputElement = (_jsx("input", { type: "text", className: css.modelInput, value: modelValue, placeholder: modelPlaceholder, "aria-label": t('model.title'), disabled: !ready, onChange: (event) => { setModelDraft(event.target.value); } }));
|
|
161
|
-
return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('auth.title') }), _jsx(ProviderAuthBlock, { harness: { id: 'dsh', label: 'dsh' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT }), _jsx("span", { className: css.hint, children: t('auth.host') })] }), _jsxs("section", { className: css.block, children: [
|
|
165
|
+
return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('auth.title') }), _jsx(ProviderAuthBlock, { harness: { id: 'dsh', label: 'dsh' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT }), _jsx("span", { className: css.hint, children: t('auth.host') })] }), _jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('model.title'), _jsx(Tooltip, { label: t('model.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('model.info.aria'), children: "\u24D8" }) })] }), _jsxs("div", { className: css.row, children: [renderModelPicker?.({ value: modelValue, onChange: setModelDraft, disabled: !ready, defaultLabel: modelDefaultItem }) ?? (choices.length > 0 ? (_jsxs("div", { className: css.modelField, ref: modelFieldRef, children: [modelInputElement, _jsx("button", { type: "button", className: css.modelMenuButton, "aria-label": t('model.menu'), "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, disabled: !ready, onClick: () => { toggleModelMenu(); }, children: _jsx(IconChevronDownOutlineMedium, { className: modelMenuOpen ? `${css.modelMenuChevron} ${css.modelMenuChevronOpen}` : css.modelMenuChevron }) }), modelMenuOpen && (_jsxs("div", { className: modelMenuUp ? `${css.modelMenu} ${css.modelMenuUp}` : css.modelMenu, role: "menu", "aria-label": t('model.menu'), children: [_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": modelValue.trim() === '', className: modelValue.trim() === '' ? `${css.modelItemDefault} ${css.modelItemCurrent}` : css.modelItemDefault, onClick: () => { setModelDraft(''); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: modelDefaultItem }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: modelValue.trim() === '' ? '✓' : '' })] }), choices.map(choice => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": choice === modelValue, className: choice === modelValue ? css.modelItemCurrent : css.modelItem, onClick: () => { setModelDraft(choice); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: choice }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: choice === modelValue ? '✓' : '' })] }, choice)))] }))] })) : modelInputElement), _jsx("button", { type: "button", className: css.modelSave, disabled: !ready || modelValue.trim() === storedModel, onClick: () => { saveModel(); }, children: t('model.save') })] }), modelSaved && _jsx("span", { className: css.saved, children: t('model.applied') }), modelError && _jsx("span", { className: css.errorText, children: t('model.error') })] }), _jsxs("section", { className: css.block, children: [_jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.rowLabel, children: [t('live.title'), _jsx(Tooltip, { label: t('live.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('live.info.aria'), children: "\u24D8" }) })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": live, "aria-label": t('live.title'), className: live ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('live', !live); }, children: _jsx("span", { className: css.knob }) })] }), snapshot.status === 'unavailable' && (_jsx("span", { className: css.hint, children: t('live.unavailable') }))] }), saved && _jsx("span", { className: css.saved, children: t('live.applied') }), error && _jsx("span", { className: css.errorText, children: t('live.error') })] }));
|
|
162
166
|
}
|
|
163
167
|
/**
|
|
164
168
|
* The dsh harness settings card of the retired 0.1.5 plugin configuration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
-
* (v0.1
|
|
3
|
+
* (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
4
|
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
5
|
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
6
|
* after adding an icon import or bumping the harness checkout.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bindCardSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Local-agent-dsh plugin, browser half: the dsh settings card — a section of
|
|
4
4
|
* the family core's「本机 Agent 委派」family card on the Plugins page
|
|
@@ -45,7 +45,7 @@ export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
|
45
45
|
export function apply(ctx) {
|
|
46
46
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-dsh: dictionaries');
|
|
47
47
|
const t = ctx.locale.bind(NS);
|
|
48
|
-
const scope =
|
|
48
|
+
const scope = bindCardSettingsScope(ctx, 'dsh', 'local-agent-dsh');
|
|
49
49
|
// The auth block's copy lives in the family core's dictionary; binding is
|
|
50
50
|
// stable per namespace and late dictionary registration still resolves.
|
|
51
51
|
const authT = ctx.locale.bind('local-agent');
|
|
@@ -21,11 +21,6 @@ export declare const zh: {
|
|
|
21
21
|
'model.applied': string;
|
|
22
22
|
'model.error': string;
|
|
23
23
|
'auth.host': string;
|
|
24
|
-
'enable.title': string;
|
|
25
|
-
'enable.switch': string;
|
|
26
|
-
'enable.switch.aria': string;
|
|
27
|
-
'enable.on': string;
|
|
28
|
-
'enable.off': string;
|
|
29
24
|
'live.title': string;
|
|
30
25
|
'live.info.aria': string;
|
|
31
26
|
'live.info': string;
|
|
@@ -61,11 +56,6 @@ export declare const en: {
|
|
|
61
56
|
'model.applied': string;
|
|
62
57
|
'model.error': string;
|
|
63
58
|
'auth.host': string;
|
|
64
|
-
'enable.title': string;
|
|
65
|
-
'enable.switch': string;
|
|
66
|
-
'enable.switch.aria': string;
|
|
67
|
-
'enable.on': string;
|
|
68
|
-
'enable.off': string;
|
|
69
59
|
'live.title': string;
|
|
70
60
|
'live.info.aria': string;
|
|
71
61
|
'live.info': string;
|
|
@@ -21,11 +21,6 @@ export const zh = {
|
|
|
21
21
|
'model.applied': '已保存,下一轮委派生效;进行中的轮次不受影响',
|
|
22
22
|
'model.error': '保存失败,请重试',
|
|
23
23
|
'auth.host': 'dsh 凭据走宿主实例的凭证设置,无需单独登录。',
|
|
24
|
-
'enable.title': 'DeepSeek 委派',
|
|
25
|
-
'enable.switch': '启用',
|
|
26
|
-
'enable.switch.aria': 'DeepSeek 委派开关',
|
|
27
|
-
'enable.on': '已开启:模型可使用 subagent_dsh 委派给本机 dsh',
|
|
28
|
-
'enable.off': '已关闭:委派只走官方内置子代理工具',
|
|
29
24
|
'live.title': '常驻模式(live)',
|
|
30
25
|
'live.info.aria': '常驻模式说明',
|
|
31
26
|
'live.info': '常驻模式 = 成员进程常驻:输出实时流入成员会话、取消不杀进程、崩溃后自动续上原会话;关闭则每轮独立进程,跑完一次性出结果。设置为全局偏好,对所有后续委派生效。',
|
|
@@ -53,11 +48,6 @@ export const en = {
|
|
|
53
48
|
'model.applied': 'Saved — takes effect on the next delegation round; rounds in flight are unaffected',
|
|
54
49
|
'model.error': 'Save failed, please retry',
|
|
55
50
|
'auth.host': 'dsh authenticates through the host instance’s credential settings — no separate sign-in.',
|
|
56
|
-
'enable.title': 'DeepSeek delegation',
|
|
57
|
-
'enable.switch': 'Enabled',
|
|
58
|
-
'enable.switch.aria': 'DeepSeek delegation switch',
|
|
59
|
-
'enable.on': 'ON: the model can delegate to the local dsh via subagent_dsh',
|
|
60
|
-
'enable.off': 'OFF: delegation goes through the official in-process subagent tool only',
|
|
61
51
|
'live.title': 'Resident mode (live)',
|
|
62
52
|
'live.info.aria': 'About resident mode',
|
|
63
53
|
'live.info': 'Resident mode keeps the member process alive: output streams into the member session in real time, cancelling does not kill the process, and a crash resumes the same session. Off means every round runs as an independent one-shot process. The setting is global and applies to every later delegation.',
|
|
@@ -15,11 +15,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
15
15
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
16
16
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
17
17
|
import { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
18
|
-
import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
|
|
18
|
+
import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, persistChildSession, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
|
|
19
19
|
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
|
|
20
20
|
import { LiveChannelUnavailableError } from './live-driver.js';
|
|
21
21
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope, unitSubProfileName } from './provision.js';
|
|
22
|
-
import { mirrorDshSession } from './session-mirror.js';
|
|
22
|
+
import { closeMirroredOpenStep, mirrorDshSession } from './session-mirror.js';
|
|
23
23
|
/**
|
|
24
24
|
* Spawn-env name carrying the bridge entry path into the sub-dsh process,
|
|
25
25
|
* where the headless bundle's mcp-client row reads it (`!!js`). The name is a
|
|
@@ -685,39 +685,61 @@ export async function startDshCliRun(request, spec) {
|
|
|
685
685
|
// reached its endpoint and produced an answer.
|
|
686
686
|
if (settled.stopReason === 'completed')
|
|
687
687
|
markCredentialVerified(ctx, spec.scope);
|
|
688
|
-
// The turn
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
if (spec.childSession !== undefined) {
|
|
693
|
-
if (settled.stopReason === 'completed') {
|
|
694
|
-
spec.childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
695
|
-
}
|
|
696
|
-
else if (settled.stopReason === 'aborted') {
|
|
697
|
-
spec.childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
spec.childSession.append('turn/end', {
|
|
701
|
-
turn,
|
|
702
|
-
reason: { kind: 'error', error: { message: 'dsh exited before the run completed', code: 'UNKNOWN' } },
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
}
|
|
688
|
+
// The turn does NOT close here: turn/end lands in the mirror queue after
|
|
689
|
+
// the settle pass (see below), so every mirrored event precedes it in the
|
|
690
|
+
// stored log — the released-log reader rejects content outside an open
|
|
691
|
+
// turn on every cold read.
|
|
706
692
|
return settled;
|
|
707
693
|
});
|
|
708
694
|
// After the child EXITS — however it ended (completed, killed by dispose, or
|
|
709
695
|
// crashed) — mirror whatever the sub-dsh session already produced into the
|
|
710
696
|
// dsh subagent session, so every round preserves its conversation and real
|
|
711
697
|
// token usage instead of showing turn boundaries only. Waits for the settle
|
|
712
|
-
// chain first
|
|
713
|
-
//
|
|
714
|
-
//
|
|
698
|
+
// chain first AND for the process to actually exit (the sub-dsh flushed its
|
|
699
|
+
// session before exiting) before reading. The turn closes INSIDE the mirror
|
|
700
|
+
// queue, after the settle pass: the copied tail's open step first receives
|
|
701
|
+
// its closure (an interrupted sub-dsh leaves its step — and any advertised
|
|
702
|
+
// tool calls — open, and the released-log reader rejects a turn/end over
|
|
703
|
+
// it), then turn/end lands and is persisted explicitly (the mirror's own
|
|
704
|
+
// persist skips a no-op pass, which is exactly the case that needs the
|
|
705
|
+
// boundary written). Every terminal path closes the window — a failed or
|
|
706
|
+
// cancelled run settles 'error'/'aborted' instead of leaving the turn open.
|
|
715
707
|
void result.then(() => child.done).then(() => {
|
|
716
708
|
if (spec.childSession !== undefined) {
|
|
717
709
|
const childSession = spec.childSession;
|
|
718
710
|
// The settle pass reports the final mirror count even when polling
|
|
719
711
|
// already covered the round (empty delta): it is authoritative.
|
|
720
|
-
return enqueueMirror(() =>
|
|
712
|
+
return enqueueMirror(async () => {
|
|
713
|
+
const settled = await result;
|
|
714
|
+
try {
|
|
715
|
+
await mirrorAndReport(childSession, true);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
// The mirror degrades to a warn; the turn boundary still closes —
|
|
719
|
+
// an open turn at EOF reads as an interrupted tail only until the
|
|
720
|
+
// next resume round tries to open its turn over it.
|
|
721
|
+
ctx.logger.warn(`subagent-dsh: settle mirror failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
722
|
+
}
|
|
723
|
+
closeMirroredOpenStep(childSession);
|
|
724
|
+
// The turn closes at the real settle moment (plus the settle-mirror
|
|
725
|
+
// quiescence), so the timing projection's duration equals the actual
|
|
726
|
+
// CLI runtime.
|
|
727
|
+
if (settled.stopReason === 'completed') {
|
|
728
|
+
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
729
|
+
}
|
|
730
|
+
else if (settled.stopReason === 'aborted') {
|
|
731
|
+
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
childSession.append('turn/end', {
|
|
735
|
+
turn,
|
|
736
|
+
reason: { kind: 'error', error: { message: 'dsh exited before the run completed', code: 'UNKNOWN' } },
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
await persistChildSession(ctx, childSession).catch((error) => {
|
|
740
|
+
ctx.logger.warn(`subagent-dsh: turn/end persistence failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
721
743
|
}
|
|
722
744
|
return undefined;
|
|
723
745
|
}, () => { });
|
package/lib/types/index.js
CHANGED
|
@@ -26,7 +26,6 @@ import { dirname } from 'node:path';
|
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import z from '@deepseek-ai/schemastery';
|
|
28
28
|
import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
|
|
29
|
-
import { mountSubagentDelegationTool } from '@khorsheed/dsh-local-agent/tool-factory';
|
|
30
29
|
import { CONTAINER_NODE_OPTIONS, DshCliProvider, dshCliVersion } from './dsh-cli-provider.js';
|
|
31
30
|
import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
|
|
32
31
|
import { LiveDriverSwitch } from './live-switch.js';
|
|
@@ -57,7 +56,7 @@ export const inject = ['localAgent', 'subagents', 'subprocess', 'credentials', '
|
|
|
57
56
|
*/
|
|
58
57
|
const SETTINGS_FIELDS = {
|
|
59
58
|
enabled: z.boolean().default(false),
|
|
60
|
-
live: z.boolean().default(
|
|
59
|
+
live: z.boolean().default(true),
|
|
61
60
|
liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
|
|
62
61
|
// `model` deliberately carries NO default: an unset key must resolve to
|
|
63
62
|
// undefined, which is what keeps the pre-key behavior byte-identical.
|
|
@@ -97,27 +96,6 @@ export const DSH_SETTINGS_NAMESPACE = 'local-agent-dsh';
|
|
|
97
96
|
* the YAML config's deployment defaults through the composition base.
|
|
98
97
|
*/
|
|
99
98
|
const DSH_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
100
|
-
/** The delegation tool the toggle mounts while ON. */
|
|
101
|
-
const DSH_TOOL_NAME = 'subagent_dsh';
|
|
102
|
-
/**
|
|
103
|
-
* This package's own name, passed to the family tool factory as the
|
|
104
|
-
* tool-origin owner (AGENTS.md § Tool origin tagging — a shared factory never
|
|
105
|
-
* hardcodes an owner), the same pattern the worktrees/room `./tool` entries
|
|
106
|
-
* use.
|
|
107
|
-
*/
|
|
108
|
-
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh';
|
|
109
|
-
/**
|
|
110
|
-
* The `subagent_dsh` mount, as a plugin object over the family factory: the
|
|
111
|
-
* toggle owns its lifecycle, so it mounts dynamically through `ctx.plugin`
|
|
112
|
-
* instead of shipping a `./tool` composition row like the CLI providers.
|
|
113
|
-
*/
|
|
114
|
-
const dshSubagentTool = {
|
|
115
|
-
name: 'local-agent-dsh-tool',
|
|
116
|
-
inject: ['tools', 'subagents', 'localAgent'],
|
|
117
|
-
apply: (toolCtx) => {
|
|
118
|
-
mountSubagentDelegationTool(toolCtx, { provider: 'dsh-cli', toolName: DSH_TOOL_NAME }, PACKAGE_NAME);
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
99
|
/**
|
|
122
100
|
* Mount the toggle controller: register the settings namespace, then mirror
|
|
123
101
|
* its value into harness/provider/tool registrations, live.
|
|
@@ -133,7 +111,7 @@ export function apply(ctx, config) {
|
|
|
133
111
|
// card's writes without a reload.
|
|
134
112
|
const readSettings = () => ({
|
|
135
113
|
enabled: vol(config.enabled) ?? false,
|
|
136
|
-
live: vol(config.live) ??
|
|
114
|
+
live: vol(config.live) ?? true,
|
|
137
115
|
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
|
|
138
116
|
model: vol(config.model),
|
|
139
117
|
recentModels: vol(config.recentModels) ?? [],
|
|
@@ -146,6 +124,8 @@ export function apply(ctx, config) {
|
|
|
146
124
|
}
|
|
147
125
|
const face = settingsFace(ctx, {
|
|
148
126
|
namespace: DSH_SETTINGS_NAMESPACE,
|
|
127
|
+
// Card writes live in the harness's own store (card-settings.ts), never the profile patch.
|
|
128
|
+
harness: 'dsh',
|
|
149
129
|
legacySchema: DSH_SETTINGS_SCHEMA,
|
|
150
130
|
legacyBase,
|
|
151
131
|
read: readSettings,
|
|
@@ -326,7 +306,6 @@ export function apply(ctx, config) {
|
|
|
326
306
|
return;
|
|
327
307
|
currentEnabled = enabled;
|
|
328
308
|
generation += 1;
|
|
329
|
-
const gen = generation;
|
|
330
309
|
for (const dispose of disposers.splice(0))
|
|
331
310
|
dispose();
|
|
332
311
|
if (!enabled)
|
|
@@ -353,20 +332,13 @@ export function apply(ctx, config) {
|
|
|
353
332
|
currentLiveSwitch = undefined;
|
|
354
333
|
liveSwitch.dispose();
|
|
355
334
|
});
|
|
356
|
-
// The
|
|
357
|
-
//
|
|
358
|
-
void ctx.plugin(dshSubagentTool).then((fiber) => {
|
|
359
|
-
if (gen !== generation) {
|
|
360
|
-
void fiber.dispose();
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
disposers.push(() => { void fiber.dispose(); });
|
|
364
|
-
}, (error) => {
|
|
365
|
-
ctx.logger.warn(`local-agent-dsh: delegation tool mount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
366
|
-
});
|
|
335
|
+
// The delegation tool is this package's `./tool` entry, granted per
|
|
336
|
+
// session by a preset (src/tool.ts) — never mounted at the profile root.
|
|
367
337
|
};
|
|
368
|
-
|
|
369
|
-
|
|
338
|
+
// Always on: installing the plugin is the intent (the card's 启用 switch
|
|
339
|
+
// is gone); a preset still decides which sessions see `subagent_dsh`.
|
|
340
|
+
sync(true);
|
|
341
|
+
const unwatch = () => { };
|
|
370
342
|
return () => {
|
|
371
343
|
modelCatalog.dispose();
|
|
372
344
|
unwatch();
|
package/lib/types/live-driver.js
CHANGED
|
@@ -28,7 +28,7 @@ import { LIVE_SERVER_NAME, LIVE_WIRE_PROTOCOL_VERSION, } from '@khorsheed/dsh-lo
|
|
|
28
28
|
import { DEFAULT_DISPOSE_GRACE_MS, dshLaunchArgv, dshTextTask, registerMemberRun, } from './dsh-cli-provider.js';
|
|
29
29
|
import { localModelLaunch } from './model-launch.js';
|
|
30
30
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from './provision.js';
|
|
31
|
-
import { mirrorDshLiveEvent, mirrorDshSession } from './session-mirror.js';
|
|
31
|
+
import { mirrorDshLiveEvent, mirrorDshSession, closeMirroredOpenStep, mirroredOpenStep } from './session-mirror.js';
|
|
32
32
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
33
33
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
34
34
|
/** Default timeout for a wire request acknowledgement. */
|
|
@@ -785,7 +785,13 @@ export class DshLiveDriver {
|
|
|
785
785
|
}).then(async (settled) => {
|
|
786
786
|
roundSettled = true;
|
|
787
787
|
liveFlush.dispose();
|
|
788
|
-
|
|
788
|
+
// The interrupted stream's partial content folds durably only while its
|
|
789
|
+
// step is still OPEN: a graceful sub-dsh unwind writes its own
|
|
790
|
+
// interrupted assistant/message and step/end (both crossed the wire),
|
|
791
|
+
// and an append at the closed step would fail the released-log reader.
|
|
792
|
+
const openStep = turnOpened ? mirroredOpenStep(childSession) : undefined;
|
|
793
|
+
if (turnOpened && streaming !== undefined && streamPublisher !== undefined
|
|
794
|
+
&& openStep !== undefined && openStep.step === streaming.step) {
|
|
789
795
|
const blocks = streaming.assembler.interruptedBlocks();
|
|
790
796
|
if (blocks.length > 0) {
|
|
791
797
|
childSession.append('assistant/message', {
|
|
@@ -798,22 +804,6 @@ export class DshLiveDriver {
|
|
|
798
804
|
}
|
|
799
805
|
}
|
|
800
806
|
streamPublisher?.dispose();
|
|
801
|
-
// Identical turn/end bookkeeping to the exec path — but only for a turn
|
|
802
|
-
// that actually opened (a pre-accept cancel records no turn at all).
|
|
803
|
-
if (turnOpened) {
|
|
804
|
-
if (settled.stopReason === 'completed') {
|
|
805
|
-
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
806
|
-
}
|
|
807
|
-
else if (settled.stopReason === 'aborted') {
|
|
808
|
-
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
childSession.append('turn/end', {
|
|
812
|
-
turn,
|
|
813
|
-
reason: { kind: 'error', error: nativeFailure ?? { message: 'the live round did not complete', code: 'UNKNOWN' } },
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
807
|
// Settlement clears the round's handlers: a late notification drops at
|
|
818
808
|
// the runtime frame instead of landing in a dead closure.
|
|
819
809
|
if (runtime !== undefined) {
|
|
@@ -821,24 +811,18 @@ export class DshLiveDriver {
|
|
|
821
811
|
runtime.onEvent = undefined;
|
|
822
812
|
runtime.onIdle = undefined;
|
|
823
813
|
}
|
|
824
|
-
// A completed answer is not a durable turn until its final boundary
|
|
825
|
-
// and any closing stream checkpoints have reached the child store.
|
|
826
814
|
if (turnOpened) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
void result.then(async () => {
|
|
839
|
-
try {
|
|
840
|
-
if (turnOpened) {
|
|
841
|
-
await persistQueue;
|
|
815
|
+
// Settle reconciliation IN-CHAIN: the file-based mirror pass (the serve
|
|
816
|
+
// side flushed before its idle notification) dedupes against the
|
|
817
|
+
// live-mirrored prefix and catches anything the wire dropped; the
|
|
818
|
+
// final mirror progress report is authoritative, exactly as in the
|
|
819
|
+
// exec path. It runs before turn/end so every mirrored event lands
|
|
820
|
+
// inside the turn window — the released-log reader rejects content
|
|
821
|
+
// outside an open turn on every cold read — and inside the result
|
|
822
|
+
// chain so the round serializer (and a resume round's turn/start)
|
|
823
|
+
// never overtakes the boundary. A mirror fault degrades to a warn;
|
|
824
|
+
// the boundary still closes.
|
|
825
|
+
try {
|
|
842
826
|
if (mirroredMessages > 0)
|
|
843
827
|
persist();
|
|
844
828
|
await persistQueue.catch(() => { });
|
|
@@ -863,13 +847,35 @@ export class DshLiveDriver {
|
|
|
863
847
|
registry?.recordRoundSettled?.(childSession.id, round);
|
|
864
848
|
}
|
|
865
849
|
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
this.ctx.logger.warn(`subagent-dsh: live settle mirror failed: ${thrown(error).message}`);
|
|
852
|
+
}
|
|
853
|
+
// The copied tail's open step (an interrupted round leaves it open —
|
|
854
|
+
// the reader rejects a turn/end over it) receives its closure first.
|
|
855
|
+
closeMirroredOpenStep(childSession);
|
|
856
|
+
// Identical turn/end bookkeeping to the exec path — a completed,
|
|
857
|
+
// interrupted, or failed round each closes its window.
|
|
858
|
+
if (settled.stopReason === 'completed') {
|
|
859
|
+
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
860
|
+
}
|
|
861
|
+
else if (settled.stopReason === 'aborted') {
|
|
862
|
+
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
childSession.append('turn/end', {
|
|
866
|
+
turn,
|
|
867
|
+
reason: { kind: 'error', error: nativeFailure ?? { message: 'the live round did not complete', code: 'UNKNOWN' } },
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
// A completed answer is not a durable turn until its final boundary
|
|
871
|
+
// and any closing stream checkpoints have reached the child store.
|
|
872
|
+
persist();
|
|
873
|
+
await persistQueue;
|
|
866
874
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
this.armIdleTimer(spec.sessionId);
|
|
872
|
-
}
|
|
875
|
+
// Re-arm the idle reaper for every settled round — including one that
|
|
876
|
+
// never opened its turn (the runtime is up and must not leak).
|
|
877
|
+
this.armIdleTimer(spec.sessionId);
|
|
878
|
+
return settled;
|
|
873
879
|
});
|
|
874
880
|
// Publication gate: hold start() until the turn is accepted (channel
|
|
875
881
|
// errors still throw for the provider's exec fallback), but NEVER hold it
|