@khorsheed/dsh-local-agent-dsh 0.2.0-rc.1 → 0.2.0
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 +16 -0
- package/README.en.md +23 -136
- package/README.i18n.yaml +2 -2
- package/README.md +23 -136
- package/lib/client.js +155 -83
- package/lib/{dsh-cli-provider-UhWvivte.js → dsh-cli-provider-B7s3zH0m.js} +108 -27
- package/lib/dsh-cli-provider-DYRuRpVz.js +1920 -0
- package/lib/dsh-cli-provider.js +1 -1
- package/lib/index.js +40 -27
- package/lib/provision.js +327 -152
- package/lib/types/client/SettingsCard.d.ts +41 -13
- package/lib/types/client/SettingsCard.js +22 -6
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.d.ts +5 -27
- package/lib/types/client/index.js +87 -44
- package/lib/types/client/locales.js +2 -2
- package/lib/types/dsh-cli-provider.js +82 -47
- package/lib/types/index.d.ts +9 -1
- package/lib/types/index.js +35 -21
- package/lib/types/live-driver.js +72 -54
- package/lib/types/model-catalog.d.ts +1 -0
- package/lib/types/model-catalog.js +9 -4
- package/lib/types/model-launch.d.ts +14 -0
- package/lib/types/model-launch.js +58 -0
- package/lib/types/provision.d.ts +166 -119
- package/lib/types/provision.js +376 -166
- package/lib/types/session-mirror.d.ts +55 -0
- package/lib/types/session-mirror.js +155 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +27 -25
- package/lib/invariant.js +0 -33
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -37
package/lib/types/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* namespace (default off); while ON, the controller registers the `dsh`
|
|
5
5
|
* harness into the core registry (scoped home under the shared homes root,
|
|
6
6
|
* `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
|
|
7
|
-
* the parent's
|
|
7
|
+
* the parent's selected API-key model route), and the family delegation tool for it.
|
|
8
8
|
* While OFF — the default — nothing model-visible mounts, so the instance
|
|
9
9
|
* keeps exactly the current behavior (official in-process subagent tools) and
|
|
10
10
|
* the model never sees two overlapping delegation tools at once.
|
|
@@ -22,18 +22,19 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @module @khorsheed/dsh-local-agent-dsh
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { dirname } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
26
27
|
import z from '@deepseek-ai/schemastery';
|
|
27
|
-
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
28
28
|
import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
|
|
29
29
|
import { mountSubagentDelegationTool } from '@khorsheed/dsh-local-agent/tool-factory';
|
|
30
30
|
import { CONTAINER_NODE_OPTIONS, DshCliProvider, dshCliVersion } from './dsh-cli-provider.js';
|
|
31
31
|
import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
|
|
32
32
|
import { LiveDriverSwitch } from './live-switch.js';
|
|
33
33
|
import { DshModelCatalog } from './model-catalog.js';
|
|
34
|
+
import { canInheritModelProvider, localModelLaunch } from './model-launch.js';
|
|
34
35
|
import { DshModelBroker } from './model-broker.js';
|
|
35
36
|
import { listDshSessions } from './records.js';
|
|
36
|
-
import {
|
|
37
|
+
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from './provision.js';
|
|
37
38
|
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
38
39
|
export const name = 'local-agent-dsh';
|
|
39
40
|
/** Services required before the toggle controller can mount. */
|
|
@@ -69,9 +70,10 @@ function volatilize(field) {
|
|
|
69
70
|
}
|
|
70
71
|
export const Config = z.object({
|
|
71
72
|
profileName: z.string().default(DEFAULT_SUB_PROFILE_NAME),
|
|
72
|
-
apiKeyRef: z.string()
|
|
73
|
+
apiKeyRef: z.string(),
|
|
73
74
|
cliLaunch: z.array(z.string()),
|
|
74
75
|
headlessBundleDir: z.string(),
|
|
76
|
+
unitHeadlessBundleDir: z.string(),
|
|
75
77
|
permissions: z.union([z.const('read-only'), z.const('workspace-write'), z.const('danger-full-access')]),
|
|
76
78
|
model: volatilize(SETTINGS_FIELDS.model),
|
|
77
79
|
live: volatilize(SETTINGS_FIELDS.live),
|
|
@@ -100,8 +102,8 @@ const DSH_TOOL_NAME = 'subagent_dsh';
|
|
|
100
102
|
/**
|
|
101
103
|
* This package's own name, passed to the family tool factory as the
|
|
102
104
|
* tool-origin owner (AGENTS.md § Tool origin tagging — a shared factory never
|
|
103
|
-
* hardcodes an owner)
|
|
104
|
-
*
|
|
105
|
+
* hardcodes an owner), the same pattern the worktrees/room `./tool` entries
|
|
106
|
+
* use.
|
|
105
107
|
*/
|
|
106
108
|
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh';
|
|
107
109
|
/**
|
|
@@ -116,10 +118,6 @@ const dshSubagentTool = {
|
|
|
116
118
|
mountSubagentDelegationTool(toolCtx, { provider: 'dsh-cli', toolName: DSH_TOOL_NAME }, PACKAGE_NAME);
|
|
117
119
|
},
|
|
118
120
|
};
|
|
119
|
-
/** Resolve the sub-dsh credential or report unconfigured. */
|
|
120
|
-
async function resolveApiKey(ctx, config) {
|
|
121
|
-
return ctx.credentials.resolve(credentialRef(config.apiKeyRef ?? 'DEEPSEEK_API_KEY'));
|
|
122
|
-
}
|
|
123
121
|
/**
|
|
124
122
|
* Mount the toggle controller: register the settings namespace, then mirror
|
|
125
123
|
* its value into harness/provider/tool registrations, live.
|
|
@@ -181,6 +179,7 @@ export function apply(ctx, config) {
|
|
|
181
179
|
const modelCatalog = new DshModelCatalog({
|
|
182
180
|
llm: () => ctx.get('llm'),
|
|
183
181
|
defaultModel: hostDefaultModel,
|
|
182
|
+
canInheritProvider: provider => canInheritModelProvider(ctx, provider),
|
|
184
183
|
});
|
|
185
184
|
void modelCatalog.read();
|
|
186
185
|
ctx.on('llm/adapters-updated', () => { modelCatalog.invalidate(); });
|
|
@@ -207,14 +206,28 @@ export function apply(ctx, config) {
|
|
|
207
206
|
retireRuntime: childSessionId => currentLiveSwitch?.retireRuntime(childSessionId) ?? Promise.resolve(),
|
|
208
207
|
});
|
|
209
208
|
/**
|
|
210
|
-
* This
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
209
|
+
* This instance's own profile directory — the loader's `baseUrl`, which
|
|
210
|
+
* the host sets to the directory holding the profile's `cordis.yml`. Its
|
|
211
|
+
* direct dependencies are where an installed preset package is found, and
|
|
212
|
+
* the SOURCE every scope's own copy is taken from (the same package this
|
|
213
|
+
* instance's capability catalog measures). Undefined when the loader
|
|
214
|
+
* exposes no file URL, which leaves a scope able to keep the copy it has
|
|
215
|
+
* but unable to take a new one.
|
|
214
216
|
*/
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
217
|
+
const instanceProfileDir = () => {
|
|
218
|
+
const baseUrl = ctx.baseUrl;
|
|
219
|
+
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('file:'))
|
|
220
|
+
return undefined;
|
|
221
|
+
try {
|
|
222
|
+
return dirname(fileURLToPath(new URL('./package.json', baseUrl)));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const profileDirOption = () => {
|
|
229
|
+
const dir = instanceProfileDir();
|
|
230
|
+
return dir === undefined ? {} : { instanceProfileDir: dir };
|
|
218
231
|
};
|
|
219
232
|
const harness = {
|
|
220
233
|
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
@@ -242,7 +255,7 @@ export function apply(ctx, config) {
|
|
|
242
255
|
provision: (scopedHome, options) => {
|
|
243
256
|
const provisioned = provisionDshScope(scopedHome, config, {
|
|
244
257
|
...(options?.preset === undefined ? {} : { preset: options.preset }),
|
|
245
|
-
|
|
258
|
+
...(profileDirOption()),
|
|
246
259
|
});
|
|
247
260
|
return {
|
|
248
261
|
...(provisioned.preset === undefined ? {} : { preset: provisioned.preset }),
|
|
@@ -251,7 +264,8 @@ export function apply(ctx, config) {
|
|
|
251
264
|
},
|
|
252
265
|
isAuthenticated: async () => {
|
|
253
266
|
try {
|
|
254
|
-
|
|
267
|
+
await localModelLaunch(ctx, config, resolveModel() ?? hostDefaultModel());
|
|
268
|
+
return true;
|
|
255
269
|
}
|
|
256
270
|
catch {
|
|
257
271
|
return false;
|
|
@@ -318,7 +332,7 @@ export function apply(ctx, config) {
|
|
|
318
332
|
if (!enabled)
|
|
319
333
|
return;
|
|
320
334
|
// Idempotent: heals a deleted or drifted sub-profile before each round.
|
|
321
|
-
provisionDshScope(homeDir, config,
|
|
335
|
+
provisionDshScope(homeDir, config, profileDirOption());
|
|
322
336
|
disposers.push(ctx.localAgent.register(harness));
|
|
323
337
|
// The live driver is settings-driven WITHIN this enabled generation:
|
|
324
338
|
// the card's toggle (user layer over the YAML composition base) swaps
|
package/lib/types/live-driver.js
CHANGED
|
@@ -19,13 +19,16 @@
|
|
|
19
19
|
* @module @khorsheed/dsh-local-agent-dsh/live-driver
|
|
20
20
|
*/
|
|
21
21
|
import { StringDecoder } from 'node:string_decoder';
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
22
23
|
import { BlockAssembler, createAssistantMessage } from '@deepseek-ai/dsh-llm';
|
|
24
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
23
25
|
import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
24
26
|
import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush } from '@khorsheed/dsh-local-agent';
|
|
25
27
|
import { LIVE_SERVER_NAME, LIVE_WIRE_PROTOCOL_VERSION, } from '@khorsheed/dsh-local-agent-dsh-headless/wire';
|
|
26
|
-
import { DEFAULT_DISPOSE_GRACE_MS, dshLaunchArgv, dshTextTask, registerMemberRun,
|
|
28
|
+
import { DEFAULT_DISPOSE_GRACE_MS, dshLaunchArgv, dshTextTask, registerMemberRun, } from './dsh-cli-provider.js';
|
|
29
|
+
import { localModelLaunch } from './model-launch.js';
|
|
27
30
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from './provision.js';
|
|
28
|
-
import { mirrorDshLiveEvent, mirrorDshSession } from './session-mirror.js';
|
|
31
|
+
import { mirrorDshLiveEvent, mirrorDshSession, closeMirroredOpenStep, mirroredOpenStep } from './session-mirror.js';
|
|
29
32
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
30
33
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
31
34
|
/** Default timeout for a wire request acknowledgement. */
|
|
@@ -77,6 +80,7 @@ class LiveRuntime {
|
|
|
77
80
|
* spawned with no model flag at all. A round whose start model differs
|
|
78
81
|
* retires the runtime instead of silently running the old one.
|
|
79
82
|
*/
|
|
83
|
+
modelLaunchFingerprint;
|
|
80
84
|
configurationKey;
|
|
81
85
|
boundModel;
|
|
82
86
|
reclaimed = false;
|
|
@@ -361,7 +365,7 @@ export class DshLiveDriver {
|
|
|
361
365
|
* The member's live runtime, spawning it (once per member at a time) when
|
|
362
366
|
* absent or dead. `signal` abandons the spawn on cancellation.
|
|
363
367
|
*/
|
|
364
|
-
async ensureRuntime(spec,
|
|
368
|
+
async ensureRuntime(spec, launch, signal) {
|
|
365
369
|
const key = spec.sessionId;
|
|
366
370
|
const startModel = spec.startModel?.trim();
|
|
367
371
|
const existing = this.runtimes.get(key);
|
|
@@ -370,7 +374,8 @@ export class DshLiveDriver {
|
|
|
370
374
|
// to a different one: retire so the respawn binds the asked-for model
|
|
371
375
|
// (the sub-dsh session resumes from disk — only the process is
|
|
372
376
|
// replaced).
|
|
373
|
-
if (
|
|
377
|
+
if (existing.modelLaunchFingerprint !== launch.fingerprint
|
|
378
|
+
|| (spec.configuration !== undefined && existing.configurationKey !== JSON.stringify(spec.configuration))
|
|
374
379
|
|| (spec.configuration === undefined && existing.configurationKey !== undefined)
|
|
375
380
|
|| (startModel !== undefined && startModel !== '' && existing.boundModel !== startModel)) {
|
|
376
381
|
await this.reclaim(key);
|
|
@@ -386,7 +391,7 @@ export class DshLiveDriver {
|
|
|
386
391
|
const pending = this.ensuring.get(key);
|
|
387
392
|
if (pending !== undefined)
|
|
388
393
|
return pending;
|
|
389
|
-
const spawn = this.spawnRuntime(spec,
|
|
394
|
+
const spawn = this.spawnRuntime(spec, launch, signal)
|
|
390
395
|
.finally(() => { this.ensuring.delete(key); });
|
|
391
396
|
this.ensuring.set(key, spawn);
|
|
392
397
|
return spawn;
|
|
@@ -399,7 +404,7 @@ export class DshLiveDriver {
|
|
|
399
404
|
* cancellation mid-spawn reclaims the half-started runtime and throws the
|
|
400
405
|
* abort instead, without touching the breaker.
|
|
401
406
|
*/
|
|
402
|
-
async spawnRuntime(spec,
|
|
407
|
+
async spawnRuntime(spec, launch, signal) {
|
|
403
408
|
const key = spec.sessionId;
|
|
404
409
|
if (this.disposed) {
|
|
405
410
|
throw new LiveChannelUnavailableError('the live driver is disposed');
|
|
@@ -434,7 +439,7 @@ export class DshLiveDriver {
|
|
|
434
439
|
// and the DSH_* facts survive the shared scrub only here.
|
|
435
440
|
env: delegationEnv({
|
|
436
441
|
DSH_HOME: spec.homeDir,
|
|
437
|
-
|
|
442
|
+
...launch.env,
|
|
438
443
|
// Same tsx tsconfig carry-over as the exec path (see startDshCliRun).
|
|
439
444
|
...process.env.TSX_TSCONFIG_PATH === undefined
|
|
440
445
|
? {}
|
|
@@ -455,6 +460,7 @@ export class DshLiveDriver {
|
|
|
455
460
|
// A blank resolution binds no model at all — record exactly what the argv
|
|
456
461
|
// carries so a later start-model comparison never retires needlessly.
|
|
457
462
|
runtime.boundModel = model === undefined || model === '' ? undefined : model;
|
|
463
|
+
runtime.modelLaunchFingerprint = launch.fingerprint;
|
|
458
464
|
runtime.configurationKey = spec.configuration === undefined ? undefined : JSON.stringify(spec.configuration);
|
|
459
465
|
runtime.onDead = () => {
|
|
460
466
|
// Delete only OUR registration: a crash-then-respawn can interleave so
|
|
@@ -523,9 +529,9 @@ export class DshLiveDriver {
|
|
|
523
529
|
if (this.draining || this.disabled)
|
|
524
530
|
throw new LiveChannelUnavailableError('the live driver is unavailable for preparation');
|
|
525
531
|
try {
|
|
526
|
-
provisionDshScope(spec.homeDir, this.config);
|
|
527
|
-
const
|
|
528
|
-
const runtime = await this.ensureRuntime(spec,
|
|
532
|
+
provisionDshScope(spec.homeDir, { ...this.config, inheritModelConfiguration: true });
|
|
533
|
+
const launch = await localModelLaunch(this.ctx, this.config, spec.configuration?.model ?? spec.startModel ?? this.config.modelFor?.(spec.sessionId));
|
|
534
|
+
const runtime = await this.ensureRuntime(spec, launch, signal);
|
|
529
535
|
await runtime.request('session/prepare', { sessionId: spec.sessionId, resume: spec.resume !== undefined });
|
|
530
536
|
signal.throwIfAborted();
|
|
531
537
|
this.armIdleTimer(spec.sessionId);
|
|
@@ -674,6 +680,7 @@ export class DshLiveDriver {
|
|
|
674
680
|
acceptStream(entry.frame);
|
|
675
681
|
}
|
|
676
682
|
};
|
|
683
|
+
let nativeFailure;
|
|
677
684
|
let resolveIdle;
|
|
678
685
|
const idle = new Promise((resolve) => { resolveIdle = resolve; });
|
|
679
686
|
const installHandlers = (rt) => {
|
|
@@ -705,10 +712,10 @@ export class DshLiveDriver {
|
|
|
705
712
|
};
|
|
706
713
|
};
|
|
707
714
|
// Provisioning is idempotent; re-running heals a drifted sub-profile.
|
|
708
|
-
provisionDshScope(spec.homeDir, this.config);
|
|
709
|
-
const
|
|
715
|
+
provisionDshScope(spec.homeDir, { ...this.config, inheritModelConfiguration: true });
|
|
716
|
+
const launch = await localModelLaunch(this.ctx, this.config, spec.configuration?.model ?? spec.startModel ?? this.config.modelFor?.(spec.sessionId));
|
|
710
717
|
const accepted = (async () => {
|
|
711
|
-
const rt = await this.ensureRuntime(spec,
|
|
718
|
+
const rt = await this.ensureRuntime(spec, launch, request.signal);
|
|
712
719
|
runtime = rt;
|
|
713
720
|
if (runAbort.signal.aborted) {
|
|
714
721
|
// Cancelled while the runtime came up and no turn exists sub-side: the
|
|
@@ -751,6 +758,7 @@ export class DshLiveDriver {
|
|
|
751
758
|
return { output: collectOutput(), stopReason: 'aborted' };
|
|
752
759
|
}
|
|
753
760
|
if (reason?.kind === 'error') {
|
|
761
|
+
nativeFailure = { message: reason.error.message, code: reason.error.code };
|
|
754
762
|
throw new Error(`subagent-dsh live: the turn failed: ${reason.error.message}`);
|
|
755
763
|
}
|
|
756
764
|
if (reason?.kind !== 'completed') {
|
|
@@ -777,7 +785,13 @@ export class DshLiveDriver {
|
|
|
777
785
|
}).then(async (settled) => {
|
|
778
786
|
roundSettled = true;
|
|
779
787
|
liveFlush.dispose();
|
|
780
|
-
|
|
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) {
|
|
781
795
|
const blocks = streaming.assembler.interruptedBlocks();
|
|
782
796
|
if (blocks.length > 0) {
|
|
783
797
|
childSession.append('assistant/message', {
|
|
@@ -790,22 +804,6 @@ export class DshLiveDriver {
|
|
|
790
804
|
}
|
|
791
805
|
}
|
|
792
806
|
streamPublisher?.dispose();
|
|
793
|
-
// Identical turn/end bookkeeping to the exec path — but only for a turn
|
|
794
|
-
// that actually opened (a pre-accept cancel records no turn at all).
|
|
795
|
-
if (turnOpened) {
|
|
796
|
-
if (settled.stopReason === 'completed') {
|
|
797
|
-
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
798
|
-
}
|
|
799
|
-
else if (settled.stopReason === 'aborted') {
|
|
800
|
-
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
801
|
-
}
|
|
802
|
-
else {
|
|
803
|
-
childSession.append('turn/end', {
|
|
804
|
-
turn,
|
|
805
|
-
reason: { kind: 'error', error: { message: 'the live round did not complete', code: 'UNKNOWN' } },
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
807
|
// Settlement clears the round's handlers: a late notification drops at
|
|
810
808
|
// the runtime frame instead of landing in a dead closure.
|
|
811
809
|
if (runtime !== undefined) {
|
|
@@ -813,24 +811,18 @@ export class DshLiveDriver {
|
|
|
813
811
|
runtime.onEvent = undefined;
|
|
814
812
|
runtime.onIdle = undefined;
|
|
815
813
|
}
|
|
816
|
-
// A completed answer is not a durable turn until its final boundary
|
|
817
|
-
// and any closing stream checkpoints have reached the child store.
|
|
818
814
|
if (turnOpened) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
void result.then(async () => {
|
|
831
|
-
try {
|
|
832
|
-
if (turnOpened) {
|
|
833
|
-
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 {
|
|
834
826
|
if (mirroredMessages > 0)
|
|
835
827
|
persist();
|
|
836
828
|
await persistQueue.catch(() => { });
|
|
@@ -855,13 +847,35 @@ export class DshLiveDriver {
|
|
|
855
847
|
registry?.recordRoundSettled?.(childSession.id, round);
|
|
856
848
|
}
|
|
857
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;
|
|
858
874
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
this.armIdleTimer(spec.sessionId);
|
|
864
|
-
}
|
|
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;
|
|
865
879
|
});
|
|
866
880
|
// Publication gate: hold start() until the turn is accepted (channel
|
|
867
881
|
// errors still throw for the provider's exec fallback), but NEVER hold it
|
|
@@ -869,7 +883,11 @@ export class DshLiveDriver {
|
|
|
869
883
|
// the result settles aborted.
|
|
870
884
|
await Promise.race([accepted, abortBranch.catch(() => undefined)]);
|
|
871
885
|
return subprocessRunHandle({
|
|
872
|
-
|
|
886
|
+
// Host 0.2.1: a fresh PER-EXECUTION run id — the activation manager
|
|
887
|
+
// rejects a run id naming a live Session (this child session is live),
|
|
888
|
+
// and resume rounds reuse the child session id. The family's durable
|
|
889
|
+
// tracking key (the child session id) rides the staged intent instead.
|
|
890
|
+
id: SessionId(randomUUID()),
|
|
873
891
|
result,
|
|
874
892
|
signal: request.signal,
|
|
875
893
|
onAbort,
|
|
@@ -15,6 +15,7 @@ export declare class DshModelCatalog {
|
|
|
15
15
|
llm: () => DshModelDirectoryFace | undefined;
|
|
16
16
|
defaultModel: () => string | undefined;
|
|
17
17
|
timeoutMs?: number;
|
|
18
|
+
canInheritProvider?: (provider: string) => boolean;
|
|
18
19
|
});
|
|
19
20
|
private key;
|
|
20
21
|
read(): LocalAgentModelDirectory;
|
|
@@ -39,7 +39,8 @@ export class DshModelCatalog {
|
|
|
39
39
|
if (llm === undefined || typeof llm.listProviders !== 'function' || typeof llm.listModels !== 'function') {
|
|
40
40
|
return { entries: [], complete: false, customInput: true, unsupported: true, reason: 'The host exposes no adapter model directory' };
|
|
41
41
|
}
|
|
42
|
-
const
|
|
42
|
+
const allProviders = llm.listProviders();
|
|
43
|
+
const providers = allProviders.filter(provider => this.deps.canInheritProvider?.(provider.id) ?? true);
|
|
43
44
|
const groups = await Promise.allSettled(providers.map(async (provider) => Promise.all((await llm.listModels(provider.id)).map(async (model) => {
|
|
44
45
|
let resolved;
|
|
45
46
|
try {
|
|
@@ -62,10 +63,14 @@ export class DshModelCatalog {
|
|
|
62
63
|
throw new Error('DSH adapters could not list models');
|
|
63
64
|
const entries = groups.flatMap(group => group.status === 'fulfilled' ? group.value : []);
|
|
64
65
|
const defaultModel = this.deps.defaultModel();
|
|
66
|
+
const omitted = allProviders.length - providers.length;
|
|
65
67
|
return {
|
|
66
|
-
entries, complete: failed === 0, customInput: true,
|
|
67
|
-
...defaultModel === undefined ? {} : { defaultModel },
|
|
68
|
-
...failed === 0 ? {} : { reason:
|
|
68
|
+
entries, complete: failed === 0 && providers.length === allProviders.length, customInput: true,
|
|
69
|
+
...defaultModel === undefined || !entries.some(entry => entry.value === defaultModel) ? {} : { defaultModel },
|
|
70
|
+
...(failed === 0 && omitted === 0) ? {} : { reason: [
|
|
71
|
+
...(failed === 0 ? [] : [`${failed} adapter model list(s) unavailable`]),
|
|
72
|
+
...(omitted === 0 ? [] : [`${omitted} provider(s) do not support local API-key inheritance`]),
|
|
73
|
+
].join('; ') },
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { LocalAgentDshConfig } from './index.ts';
|
|
3
|
+
/** Private launch environment: never persisted in a profile, transcript or argv. */
|
|
4
|
+
export declare const MODEL_CONFIG_ENV = "DSH_LOCAL_AGENT_MODEL_CONFIG";
|
|
5
|
+
/** Whether this provider has a supported local configuration transfer contract. */
|
|
6
|
+
export declare function canInheritModelProvider(ctx: Context, provider: string): boolean;
|
|
7
|
+
export interface LocalModelLaunch {
|
|
8
|
+
readonly env: Record<string, string>;
|
|
9
|
+
/** In-memory fingerprint also retires a resident process after endpoint/key changes. */
|
|
10
|
+
readonly fingerprint: string;
|
|
11
|
+
}
|
|
12
|
+
/** Snapshot only the selected local route and resolve only its explicitly named credential. */
|
|
13
|
+
export declare function localModelLaunch(ctx: Context, config: LocalAgentDshConfig, model?: string): Promise<LocalModelLaunch>;
|
|
14
|
+
//# sourceMappingURL=model-launch.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
3
|
+
/** Private launch environment: never persisted in a profile, transcript or argv. */
|
|
4
|
+
export const MODEL_CONFIG_ENV = 'DSH_LOCAL_AGENT_MODEL_CONFIG';
|
|
5
|
+
const MODEL_KEY_ENV = 'DSH_LOCAL_AGENT_MODEL_API_KEY';
|
|
6
|
+
function object(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function settings(ctx) {
|
|
10
|
+
return new Map((ctx.get('settings')?.describe?.() ?? []).flatMap(row => {
|
|
11
|
+
const value = object(row.value);
|
|
12
|
+
return value === undefined ? [] : [[String(row.ns), value]];
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
function piProfile(ctx, provider, values) {
|
|
16
|
+
const directory = ctx.get('llm')?.listConfigurableProviders?.().find(row => row.provider === provider);
|
|
17
|
+
// Only the official pi-ai providers dictionary has this inheritance contract.
|
|
18
|
+
// Other adapter families must explicitly provide a transport before being advertised.
|
|
19
|
+
const ns = directory?.settingsNs ?? 'llm-pi-ai';
|
|
20
|
+
if (ns !== 'llm-pi-ai')
|
|
21
|
+
return undefined;
|
|
22
|
+
if (directory && (directory.settingsPath.length !== 2 || directory.settingsPath[0] !== 'providers' || directory.settingsPath[1] !== provider))
|
|
23
|
+
return undefined;
|
|
24
|
+
return object(object(values.get(ns)?.providers)?.[provider]);
|
|
25
|
+
}
|
|
26
|
+
/** Whether this provider has a supported local configuration transfer contract. */
|
|
27
|
+
export function canInheritModelProvider(ctx, provider) {
|
|
28
|
+
if (provider === 'deepseek-official')
|
|
29
|
+
return true;
|
|
30
|
+
const profile = piProfile(ctx, provider, settings(ctx));
|
|
31
|
+
return typeof profile?.apiKeyEnv === 'string' && profile.apiKeyEnv.trim() !== '';
|
|
32
|
+
}
|
|
33
|
+
/** Snapshot only the selected local route and resolve only its explicitly named credential. */
|
|
34
|
+
export async function localModelLaunch(ctx, config, model) {
|
|
35
|
+
const provider = model?.includes('/') ? model.slice(0, model.indexOf('/')) : 'deepseek-official';
|
|
36
|
+
const values = settings(ctx);
|
|
37
|
+
let inherited, ref, keyName;
|
|
38
|
+
if (provider === 'deepseek-official') {
|
|
39
|
+
const profile = structuredClone(values.get('llm-deepseek') ?? {});
|
|
40
|
+
ref = config.apiKeyRef ?? (typeof profile.apiKeyEnv === 'string' ? profile.apiKeyEnv : 'DEEPSEEK_API_KEY');
|
|
41
|
+
inherited = { deepseek: { ...profile, apiKeyEnv: 'DEEPSEEK_API_KEY' } };
|
|
42
|
+
keyName = 'DEEPSEEK_API_KEY';
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const profile = piProfile(ctx, provider, values);
|
|
46
|
+
if (typeof profile?.apiKeyEnv !== 'string' || profile.apiKeyEnv.trim() === '') {
|
|
47
|
+
throw new Error(`subagent-dsh: model provider "${provider}" has no inheritable API-key route; configure its credential reference in the host Models settings`);
|
|
48
|
+
}
|
|
49
|
+
ref = profile.apiKeyEnv;
|
|
50
|
+
inherited = { piAi: { providers: { [provider]: { ...structuredClone(profile), apiKeyEnv: MODEL_KEY_ENV } } } };
|
|
51
|
+
keyName = MODEL_KEY_ENV;
|
|
52
|
+
}
|
|
53
|
+
const credential = await ctx.get('credentials')?.resolve(credentialRef(ref));
|
|
54
|
+
if (!credential?.value)
|
|
55
|
+
throw new Error(`subagent-dsh: credential ${ref} is unavailable for model provider "${provider}"`);
|
|
56
|
+
const env = { [MODEL_CONFIG_ENV]: JSON.stringify(inherited), [keyName]: credential.value };
|
|
57
|
+
return { env, fingerprint: createHash('sha256').update(JSON.stringify(env)).digest('hex') };
|
|
58
|
+
}
|