dsh-loop-engine 1.0.0-rc8 → 1.0.0-rc9
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.md +20 -0
- package/lib/index.js +124 -14
- package/lib/types/driver-core/ownership.d.ts +1 -2
- package/lib/types/engine-pi/agent.d.ts +8 -0
- package/lib/types/engine-pi/loop.d.ts +5 -0
- package/lib/types/engine-pi/probe.d.ts +23 -0
- package/lib/types/provider-route.d.ts +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,26 @@ Restart `dsh web`, then open **Settings → Loop engine**.
|
|
|
26
26
|
> your project root) and retry. Only the installing project can grant this;
|
|
27
27
|
> the plugin cannot pre-approve its own dependencies.
|
|
28
28
|
|
|
29
|
+
## Version compatibility
|
|
30
|
+
|
|
31
|
+
dsh-loop-engine is versioned **independently** of the harness (`1.0.0-rcN`) but
|
|
32
|
+
is bound to a specific harness release via `peerDependencies`. The two must be
|
|
33
|
+
matched — a mismatch fails loudly at boot or session resume:
|
|
34
|
+
|
|
35
|
+
| dsh-loop-engine | Requires harness |
|
|
36
|
+
|---|---|
|
|
37
|
+
| 1.0.0-rc9 | **0.1.2-rc.1** |
|
|
38
|
+
| 1.0.0-rc8 | **0.1.2-rc.1** |
|
|
39
|
+
| 1.0.0-rc7 | 0.1.1-rc.2 |
|
|
40
|
+
|
|
41
|
+
- **1.0.0-rc9 (and 1.0.0-rc8) is not compatible with harness 0.1.1-rc.2 or earlier.** It uses
|
|
42
|
+
the 0.1.2 persistence seam (`SessionPersistence.create` / `open` +
|
|
43
|
+
`SessionHandle`), the `installSection` settings API, `ToolCallId`, and
|
|
44
|
+
`Session.snapshotEvents()` — none of which exist in older harnesses.
|
|
45
|
+
- To use the plugin with an older harness, install the loop-engine release that
|
|
46
|
+
matches it (e.g. `npm i dsh-loop-engine@1.0.0-rc7` for harness 0.1.1-rc.2).
|
|
47
|
+
- The GitHub Release body of each tag states the harness version it targets.
|
|
48
|
+
|
|
29
49
|
### Requirements
|
|
30
50
|
|
|
31
51
|
- For the Claude Code engine: the Claude Code CLI installed and logged in on
|
package/lib/index.js
CHANGED
|
@@ -1019,11 +1019,15 @@ var ClaudeCodeAgent = class {
|
|
|
1019
1019
|
};
|
|
1020
1020
|
|
|
1021
1021
|
// src/driver-core/ownership.ts
|
|
1022
|
-
|
|
1022
|
+
var INACTIVE_FIBER_VALUES = {
|
|
1023
|
+
FAILED: 3,
|
|
1024
|
+
DISPOSED: 4,
|
|
1025
|
+
UNLOADING: 5
|
|
1026
|
+
};
|
|
1023
1027
|
var INACTIVE_STATES = /* @__PURE__ */ new Set([
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1028
|
+
INACTIVE_FIBER_VALUES.UNLOADING,
|
|
1029
|
+
INACTIVE_FIBER_VALUES.DISPOSED,
|
|
1030
|
+
INACTIVE_FIBER_VALUES.FAILED
|
|
1027
1031
|
]);
|
|
1028
1032
|
var FactoryOwnership = class {
|
|
1029
1033
|
constructor(fiber) {
|
|
@@ -1530,7 +1534,7 @@ var AppServerClient = class _AppServerClient {
|
|
|
1530
1534
|
clientInfo: {
|
|
1531
1535
|
name: "dsh-loop-engine",
|
|
1532
1536
|
title: null,
|
|
1533
|
-
version: "1.0.0-
|
|
1537
|
+
version: "1.0.0-rc9"
|
|
1534
1538
|
},
|
|
1535
1539
|
capabilities: { experimentalApi: true, requestAttestation: false }
|
|
1536
1540
|
};
|
|
@@ -3281,14 +3285,32 @@ var PiAgent = class {
|
|
|
3281
3285
|
});
|
|
3282
3286
|
this.requestHeaderLogged = true;
|
|
3283
3287
|
}
|
|
3288
|
+
/**
|
|
3289
|
+
* The harness Session's web-side model selection, if any was stored. The
|
|
3290
|
+
* durable `model/selection` event carries `{ provider, model, ... }`; when a
|
|
3291
|
+
* user picked a model via `/model`, this is the newest pick, and it overrides
|
|
3292
|
+
* the deployment config (which stays the fallback). Returns `undefined` when
|
|
3293
|
+
* no selection was stored, so the deployment config governs.
|
|
3294
|
+
*/
|
|
3295
|
+
dynamicModel() {
|
|
3296
|
+
for (const event of [...this.session.snapshotEvents()].reverse()) {
|
|
3297
|
+
const type = event.type;
|
|
3298
|
+
if (type !== "model/selection") continue;
|
|
3299
|
+
const data = event.data;
|
|
3300
|
+
const model = data?.model;
|
|
3301
|
+
if (typeof model === "string" && model.length > 0) return model;
|
|
3302
|
+
}
|
|
3303
|
+
return void 0;
|
|
3304
|
+
}
|
|
3284
3305
|
/** Build the `pi --mode rpc` argv/cwd/env for one step's child process. */
|
|
3285
3306
|
spawnSpec(cwd) {
|
|
3286
3307
|
const argv = [];
|
|
3308
|
+
const model = this.dynamicModel() ?? this.config.model;
|
|
3287
3309
|
if (this.config.provider !== void 0) argv.push("--provider", this.config.provider);
|
|
3288
|
-
if (
|
|
3289
|
-
argv.push("--model", `${
|
|
3290
|
-
} else if (
|
|
3291
|
-
argv.push("--model",
|
|
3310
|
+
if (model !== void 0 && this.config.thinkingLevel !== void 0) {
|
|
3311
|
+
argv.push("--model", `${model}:${this.config.thinkingLevel}`);
|
|
3312
|
+
} else if (model !== void 0) {
|
|
3313
|
+
argv.push("--model", model);
|
|
3292
3314
|
} else if (this.config.thinkingLevel !== void 0) {
|
|
3293
3315
|
argv.push("--model", `:${this.config.thinkingLevel}`);
|
|
3294
3316
|
}
|
|
@@ -3543,6 +3565,67 @@ var PiAgent = class {
|
|
|
3543
3565
|
}
|
|
3544
3566
|
};
|
|
3545
3567
|
|
|
3568
|
+
// src/engine-pi/probe.ts
|
|
3569
|
+
function waitForExit(child) {
|
|
3570
|
+
return new Promise((resolve5) => {
|
|
3571
|
+
;
|
|
3572
|
+
child.onExit(
|
|
3573
|
+
(code) => resolve5(code ?? 0)
|
|
3574
|
+
);
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
function collectStdout(child) {
|
|
3578
|
+
return new Promise((resolve5) => {
|
|
3579
|
+
let text = "";
|
|
3580
|
+
if (child.stdout.on === void 0) {
|
|
3581
|
+
resolve5("");
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
const out = child.stdout;
|
|
3585
|
+
out.setEncoding("utf8");
|
|
3586
|
+
out.on("data", (data) => {
|
|
3587
|
+
text += String(data);
|
|
3588
|
+
});
|
|
3589
|
+
out.on("end", () => {
|
|
3590
|
+
resolve5(text);
|
|
3591
|
+
});
|
|
3592
|
+
out.on("close", () => {
|
|
3593
|
+
resolve5(text);
|
|
3594
|
+
});
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
function parsePiModelList(output) {
|
|
3598
|
+
const entries = [];
|
|
3599
|
+
for (const line of output.split("\n")) {
|
|
3600
|
+
const trimmed = line.trim();
|
|
3601
|
+
if (trimmed.length === 0) continue;
|
|
3602
|
+
const match = /^(\S+)\s{2,}(\S+)/.exec(trimmed);
|
|
3603
|
+
if (match === null) continue;
|
|
3604
|
+
const provider = match[1];
|
|
3605
|
+
if (provider === "provider") continue;
|
|
3606
|
+
entries.push({ provider, model: match[2] });
|
|
3607
|
+
}
|
|
3608
|
+
return entries;
|
|
3609
|
+
}
|
|
3610
|
+
async function probePiModels(bin, spawn4) {
|
|
3611
|
+
const spec = {
|
|
3612
|
+
argv: [bin, "--mode", "rpc", "--list-models"],
|
|
3613
|
+
cwd: process.cwd(),
|
|
3614
|
+
env: {}
|
|
3615
|
+
};
|
|
3616
|
+
let child;
|
|
3617
|
+
try {
|
|
3618
|
+
child = spawn4(spec);
|
|
3619
|
+
} catch {
|
|
3620
|
+
return [];
|
|
3621
|
+
}
|
|
3622
|
+
const stdoutPromise = collectStdout(child);
|
|
3623
|
+
const exitCode = await waitForExit(child);
|
|
3624
|
+
if (exitCode !== 0) return [];
|
|
3625
|
+
const stdout = await stdoutPromise;
|
|
3626
|
+
return parsePiModelList(stdout);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3546
3629
|
// src/engine-pi/loop.ts
|
|
3547
3630
|
var PI_SANDBOX_MODES = [
|
|
3548
3631
|
"read-only",
|
|
@@ -3555,7 +3638,8 @@ var Config3 = z3.object({
|
|
|
3555
3638
|
provider: z3.string(),
|
|
3556
3639
|
model: z3.string(),
|
|
3557
3640
|
thinkingLevel: z3.string(),
|
|
3558
|
-
env: z3.dict(z3.string()).default({})
|
|
3641
|
+
env: z3.dict(z3.string()).default({}),
|
|
3642
|
+
piCatalogHolder: z3.any()
|
|
3559
3643
|
});
|
|
3560
3644
|
function resolveConfig3(config) {
|
|
3561
3645
|
return {
|
|
@@ -3594,7 +3678,8 @@ function fromSubprocess(handle) {
|
|
|
3594
3678
|
stdout,
|
|
3595
3679
|
stderr,
|
|
3596
3680
|
onExit: (handler) => {
|
|
3597
|
-
|
|
3681
|
+
const onExit = handler;
|
|
3682
|
+
void handle.done.then((outcome) => onExit(outcome.exitCode), handler);
|
|
3598
3683
|
},
|
|
3599
3684
|
terminate: () => handle.terminate()
|
|
3600
3685
|
};
|
|
@@ -3618,6 +3703,14 @@ var PiLoop = class extends Service3 {
|
|
|
3618
3703
|
this.runtime = { ctx };
|
|
3619
3704
|
this.bin = piCliEntrypoint();
|
|
3620
3705
|
this.spawn = (spec) => fromSubprocess(this.runtime.ctx.subprocess.spawn(piSubprocessSpec(spec, PI_DISPOSE_GRACE_MS)));
|
|
3706
|
+
const holder = config.piCatalogHolder;
|
|
3707
|
+
if (holder !== void 0) {
|
|
3708
|
+
void probePiModels(this.bin, (spec) => this.spawn(spec)).then((models) => {
|
|
3709
|
+
holder.entries = [...models];
|
|
3710
|
+
}).catch(() => {
|
|
3711
|
+
holder.entries = [];
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3621
3714
|
ctx.effect(() => () => this.ownership.dispose(), "agentLoopPi.transactions()");
|
|
3622
3715
|
ctx.effect(() => ctx.agents.setFactory(this), "agentLoopPi.setFactory()");
|
|
3623
3716
|
ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
|
|
@@ -5646,12 +5739,24 @@ var HOSTED_PROVIDER_ROUTES = {
|
|
|
5646
5739
|
var HostedEngineRouteAdapter = class extends LlmAdapter {
|
|
5647
5740
|
/**
|
|
5648
5741
|
* @param label - the provider route label this placeholder serves.
|
|
5742
|
+
* @param options - optional catalog source; omit for an empty catalog.
|
|
5649
5743
|
*/
|
|
5650
|
-
constructor(label) {
|
|
5744
|
+
constructor(label, options = {}) {
|
|
5651
5745
|
super();
|
|
5652
5746
|
this.label = label;
|
|
5747
|
+
this.options = options;
|
|
5653
5748
|
}
|
|
5654
5749
|
label;
|
|
5750
|
+
options;
|
|
5751
|
+
/** Advertise the injected Pi models (if any) under this route's provider label. */
|
|
5752
|
+
async listModels(_provider) {
|
|
5753
|
+
const catalog = this.options.listModels?.() ?? [];
|
|
5754
|
+
return catalog.map((entry) => ({
|
|
5755
|
+
provider: this.label,
|
|
5756
|
+
id: `${entry.provider}/${entry.model}`,
|
|
5757
|
+
name: `${entry.provider}/${entry.model}`
|
|
5758
|
+
}));
|
|
5759
|
+
}
|
|
5655
5760
|
stream(_options) {
|
|
5656
5761
|
throw new LlmError5(
|
|
5657
5762
|
`provider "${this.label}" is a hosted loop engine route, not a model endpoint`,
|
|
@@ -6047,6 +6152,7 @@ function apply(ctx, config) {
|
|
|
6047
6152
|
let mountedEngine;
|
|
6048
6153
|
let commandDisposers;
|
|
6049
6154
|
let skillDisposer;
|
|
6155
|
+
const piCatalogHolder = { entries: [] };
|
|
6050
6156
|
let mountAttempts = 0;
|
|
6051
6157
|
let mountRetry;
|
|
6052
6158
|
const CLEAR_RETRY = () => {
|
|
@@ -6096,7 +6202,8 @@ function apply(ctx, config) {
|
|
|
6096
6202
|
return;
|
|
6097
6203
|
}
|
|
6098
6204
|
try {
|
|
6099
|
-
|
|
6205
|
+
const options = engine === "pi" ? { listModels: () => piCatalogHolder.entries } : void 0;
|
|
6206
|
+
routeHandle = llm.registerAdapter([label], new HostedEngineRouteAdapter(label, options));
|
|
6100
6207
|
routeEngine = engine;
|
|
6101
6208
|
} catch (error) {
|
|
6102
6209
|
if (error instanceof Error && error.message.includes("already registered")) {
|
|
@@ -6215,7 +6322,10 @@ function apply(ctx, config) {
|
|
|
6215
6322
|
if (skills !== void 0) {
|
|
6216
6323
|
skillDisposer = skills.registerProvider((control) => new PiSkillProvider(control));
|
|
6217
6324
|
}
|
|
6218
|
-
hostFactory("pi", () => ctx.plugin(PiLoop,
|
|
6325
|
+
hostFactory("pi", () => ctx.plugin(PiLoop, {
|
|
6326
|
+
...piConfig(config),
|
|
6327
|
+
piCatalogHolder
|
|
6328
|
+
}));
|
|
6219
6329
|
};
|
|
6220
6330
|
const mountKimi = () => {
|
|
6221
6331
|
const commands = ctx.get("commands");
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module dsh-loop-engine/driver-core/ownership
|
|
11
11
|
*/
|
|
12
|
-
import { FiberState } from '@deepseek-ai/cordis';
|
|
13
12
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
13
|
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
15
14
|
/** Fiber states that cannot own or serve a new lifecycle. */
|
|
16
|
-
export declare const INACTIVE_STATES: ReadonlySet<
|
|
15
|
+
export declare const INACTIVE_STATES: ReadonlySet<number>;
|
|
17
16
|
/** Factory-level ownership: live agent teardowns plus load-time tracking. */
|
|
18
17
|
export declare class FactoryOwnership {
|
|
19
18
|
private readonly fiber;
|
|
@@ -109,6 +109,14 @@ export declare class PiAgent implements Agent {
|
|
|
109
109
|
private modelLabel;
|
|
110
110
|
/** Append the request header snapshot once per loop instance. */
|
|
111
111
|
private assertRequestHeader;
|
|
112
|
+
/**
|
|
113
|
+
* The harness Session's web-side model selection, if any was stored. The
|
|
114
|
+
* durable `model/selection` event carries `{ provider, model, ... }`; when a
|
|
115
|
+
* user picked a model via `/model`, this is the newest pick, and it overrides
|
|
116
|
+
* the deployment config (which stays the fallback). Returns `undefined` when
|
|
117
|
+
* no selection was stored, so the deployment config governs.
|
|
118
|
+
*/
|
|
119
|
+
private dynamicModel;
|
|
112
120
|
/** Build the `pi --mode rpc` argv/cwd/env for one step's child process. */
|
|
113
121
|
private spawnSpec;
|
|
114
122
|
/**
|
|
@@ -14,6 +14,7 @@ import { Service } from '@deepseek-ai/cordis';
|
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
import z from '@deepseek-ai/schemastery';
|
|
16
16
|
import type { AgentFactory, AgentHandle, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent';
|
|
17
|
+
import type { PiModelEntry } from './probe.ts';
|
|
17
18
|
import type { PiProcess, PiSpawnSpec } from './rpc/client.ts';
|
|
18
19
|
import type { PiSandboxMode, ResolvedConfig } from './types.ts';
|
|
19
20
|
/** Pi CLI sandbox modes a deployment may pin. */
|
|
@@ -38,6 +39,10 @@ export interface Config {
|
|
|
38
39
|
thinkingLevel?: string;
|
|
39
40
|
/** Explicit environment entries passed to the `pi` child. */
|
|
40
41
|
env?: Record<string, string>;
|
|
42
|
+
/** Shared Pi model catalog holder; the loop writes its `pi --list-models` probe result here. */
|
|
43
|
+
piCatalogHolder?: {
|
|
44
|
+
entries: readonly PiModelEntry[];
|
|
45
|
+
};
|
|
41
46
|
}
|
|
42
47
|
/** Schema of the Pi loop plugin configuration. */
|
|
43
48
|
export declare const Config: z<Config>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PiProcess, PiSpawnSpec } from './rpc/client.ts';
|
|
2
|
+
/** One discoverable Pi model: the raw two-part identity `pi` exposes. */
|
|
3
|
+
export interface PiModelEntry {
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly model: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse the output of `pi --list-models`: a column-aligned table whose first
|
|
9
|
+
* two columns are `provider` and `model`. The header row and blank lines are
|
|
10
|
+
* skipped; long model ids simply occupy more columns. Splitting on 2+ spaces
|
|
11
|
+
* yields provider and model in the first two fields regardless of alignment.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parsePiModelList(output: string): PiModelEntry[];
|
|
14
|
+
/**
|
|
15
|
+
* Spawn `pi --list-models` and return the parsed model entries. Failures
|
|
16
|
+
* (non-zero exit, no stdout, spawn throw) resolve to an empty array so the
|
|
17
|
+
* catalog stays advisory and a probe glitch never breaks the engine mount.
|
|
18
|
+
* @param bin - the Pi CLI entrypoint (from `piCliEntrypoint()`); becomes spec.argv[0].
|
|
19
|
+
* @param spawn - the process-spawn adapter; `piSubprocessSpec` prepends the node
|
|
20
|
+
* prefix, so spec.argv must NOT carry it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probePiModels(bin: string, spawn: (spec: PiSpawnSpec) => PiProcess): Promise<readonly PiModelEntry[]>;
|
|
23
|
+
//# sourceMappingURL=probe.d.ts.map
|
|
@@ -12,11 +12,21 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module dsh-loop-engine/provider-route
|
|
14
14
|
*/
|
|
15
|
-
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
15
|
+
import type { GenerateOptions, LlmModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
16
16
|
import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
17
17
|
import type { LoopEngineId } from './settings.ts';
|
|
18
|
+
import type { PiModelEntry } from './engine-pi/probe.ts';
|
|
18
19
|
/** Provider route label each hosted engine logs into its sessions' request/header. */
|
|
19
20
|
export declare const HOSTED_PROVIDER_ROUTES: Readonly<Record<Exclude<LoopEngineId, 'in-process'>, string>>;
|
|
21
|
+
/** Injectable catalog source a hosted engine route can advertise over the placeholder. */
|
|
22
|
+
export interface HostedEngineRouteAdapterOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Optional model catalog generator. When present, `listModels` advertises
|
|
25
|
+
* these entries under this route's provider label; when absent, the catalog
|
|
26
|
+
* stays empty (the default, "engine owns its models" behavior).
|
|
27
|
+
*/
|
|
28
|
+
readonly listModels?: () => readonly PiModelEntry[];
|
|
29
|
+
}
|
|
20
30
|
/**
|
|
21
31
|
* Placeholder adapter serving one hosted engine's provider route label. It
|
|
22
32
|
* inherits the empty catalog and default metadata (the engine's model is not a
|
|
@@ -26,10 +36,14 @@ export declare const HOSTED_PROVIDER_ROUTES: Readonly<Record<Exclude<LoopEngineI
|
|
|
26
36
|
*/
|
|
27
37
|
export declare class HostedEngineRouteAdapter extends LlmAdapter {
|
|
28
38
|
private readonly label;
|
|
39
|
+
private readonly options;
|
|
29
40
|
/**
|
|
30
41
|
* @param label - the provider route label this placeholder serves.
|
|
42
|
+
* @param options - optional catalog source; omit for an empty catalog.
|
|
31
43
|
*/
|
|
32
|
-
constructor(label: string);
|
|
44
|
+
constructor(label: string, options?: HostedEngineRouteAdapterOptions);
|
|
45
|
+
/** Advertise the injected Pi models (if any) under this route's provider label. */
|
|
46
|
+
listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
|
|
33
47
|
stream(_options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
34
48
|
}
|
|
35
49
|
//# sourceMappingURL=provider-route.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loop-engine",
|
|
3
3
|
"description": "Web-switchable agent loop engine selection for the DeepSeek Harness - out-of-tree plugin (Claude Code / Codex / Pi / Kimi Code drivers) maintained by @kuun993, zero main-repo changes",
|
|
4
|
-
"version": "1.0.0-
|
|
4
|
+
"version": "1.0.0-rc9",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|