paseo-dsh-direct 0.2.2 → 0.2.4
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/OVERVIEW.md +25 -0
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/dsh.png +0 -0
- package/dsh.svg +2 -10
- package/index.server.ts +2 -2
- package/package.json +2 -1
- package/scripts/build.mjs +2 -1
- package/server/bridge-client.ts +152 -15
- package/server/dsh/host.ts +20 -0
- package/server/dsh/service.ts +125 -1
- package/server/dsh-version.ts +6 -3
- package/server/provider.ts +55 -18
- package/shared/bridge-protocol.ts +25 -0
package/OVERVIEW.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# DeepSeek Harness for Paseo
|
|
2
|
+
|
|
3
|
+
Runs DeepSeek Harness (`dsh`) as a native Paseo agent provider. Paseo clients (desktop, web, iOS, Android) drive a local `dsh` agent with streamed responses, visible reasoning, tool approvals, user questions and session lifecycle management, without going through a generic ACP adapter.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Paseo 0.11.0 or newer.
|
|
8
|
+
- DeepSeek Harness `dsh` on the 0.2 line, installed and configured on the machine that runs the Paseo daemon (model API key and settings are read by `dsh` from its own configuration).
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Streaming assistant text and reasoning ("thinking") as Paseo timeline items.
|
|
13
|
+
- Tool calls with Paseo approval prompts, and `dsh` user questions answered from the Paseo UI.
|
|
14
|
+
- Create, resume, cancel and close sessions; model list and mode switching from the Paseo UI.
|
|
15
|
+
- Token usage and context-window fill in Paseo's usage meter.
|
|
16
|
+
- DSH branding (name and icon) in the provider list.
|
|
17
|
+
|
|
18
|
+
## Processes and environment
|
|
19
|
+
|
|
20
|
+
- **Child process.** For each session the plugin starts the `dsh` executable as a child process and loads a small bridge plugin into it. The two communicate over the child's stdio.
|
|
21
|
+
- **Executable lookup.** In order: the `PASEO_DSH_EXECUTABLE` environment variable, then the Paseo setting `agents.providers.dsh-pi.command`, then `dsh` on `PATH`.
|
|
22
|
+
- **Profile.** The plugin launches `dsh --profile paseo`. If `<DSH_HOME>/profiles/paseo` does not exist, it is created once by running `dsh --profile paseo --from-default-profile web --dump-config`. `DSH_HOME` defaults to `~/.dsh`. Setting `PASEO_DSH_PROFILE` selects another existing profile instead; a missing one is reported as an error, not created.
|
|
23
|
+
- **Environment variables read by the plugin.** `PASEO_DSH_EXECUTABLE`, `PASEO_DSH_PROFILE`, and `DSH_HOME` (to locate the profile directory).
|
|
24
|
+
- **Environment passed to `dsh`.** The child inherits the Paseo daemon's environment.
|
|
25
|
+
- **Network.** The plugin makes no network requests itself. Model API calls are made by `dsh` according to its own configuration.
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Most dsh integrations go through ACP, a generic protocol that every agent can sp
|
|
|
25
25
|
- Tool approvals and dsh's questions to you appear as Paseo's own approval and question cards.
|
|
26
26
|
- Your existing dsh sessions can be imported and resumed in Paseo.
|
|
27
27
|
- dsh's slash commands (`/compact`, `/plan`, ...) appear in Paseo's `/` menu.
|
|
28
|
+
- Paseo's usage meter shows dsh's token counts and how full the context window is.
|
|
28
29
|
|
|
29
30
|
## Versioning
|
|
30
31
|
|
|
@@ -36,7 +37,7 @@ Create a conversation with the **DeepSeek Harness** provider. Models and presets
|
|
|
36
37
|
|
|
37
38
|
The provider supports streamed assistant and reasoning output, tool execution and approval, user questions, model/preset configuration, interruption, and session recovery. It starts DSH with its own `paseo` profile and does not modify Paseo or DSH core.
|
|
38
39
|
|
|
39
|
-
The plugin uses the DSH profile `paseo`. On first start it creates that profile from DSH's official `web` template (`~/.dsh/profiles/paseo`, or under `$DSH_HOME` when set). Model routes, the default model, and permission presets go in `~/.dsh/profiles/paseo/cordis.patch.yml`.
|
|
40
|
+
The plugin uses the DSH profile `paseo`. On first start it creates that profile from DSH's official `web` template (`~/.dsh/profiles/paseo`, or under `$DSH_HOME` when set). Model routes, the default model, and permission presets go in `~/.dsh/profiles/paseo/cordis.patch.yml`. To use a different `dsh` executable, set `agents.providers.dsh-pi.command` in the Paseo configuration or `PASEO_DSH_EXECUTABLE` (which takes precedence); or `PASEO_DSH_PROFILE` to use another existing profile; a profile named there is not created automatically. If the Paseo Daemon has plugin loading disabled, enable `pluginsEnabled` in its configuration before installing the plugin.
|
|
40
41
|
|
|
41
42
|
If a native session is still held by another DSH client (such as a terminal UI), release it there before importing or resuming it from Paseo.
|
|
42
43
|
|
package/README.zh.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- 工具审批和 dsh 向你提的问题,显示成 Paseo 自带的审批卡片和问答卡片。
|
|
26
26
|
- 已有的 dsh 会话可以导入 Paseo 接着用。
|
|
27
27
|
- dsh 的斜杠命令(`/compact`、`/plan` 等)出现在 Paseo 的 `/` 菜单里。
|
|
28
|
+
- Paseo 的用量表显示 dsh 的 token 用量和上下文占用。
|
|
28
29
|
|
|
29
30
|
## 版本号
|
|
30
31
|
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
|
|
37
38
|
Provider 支持连续正文和思考输出、工具执行与审批、用户问答、模型和 preset 配置、中断及会话恢复。它用插件专用的 `paseo` profile 启动 DSH,不修改 Paseo 或 DSH 核心。
|
|
38
39
|
|
|
39
|
-
插件使用名为 `paseo` 的 DSH profile。首次启动时,插件用 DSH 官方的 `web` 模板自动创建它(位置是 `~/.dsh/profiles/paseo`;设置了 `$DSH_HOME` 时在其下)。模型路由、默认模型和权限预设写在 `~/.dsh/profiles/paseo/cordis.patch.yml
|
|
40
|
+
插件使用名为 `paseo` 的 DSH profile。首次启动时,插件用 DSH 官方的 `web` 模板自动创建它(位置是 `~/.dsh/profiles/paseo`;设置了 `$DSH_HOME` 时在其下)。模型路由、默认模型和权限预设写在 `~/.dsh/profiles/paseo/cordis.patch.yml`。可在 Paseo 配置的 `agents.providers.dsh-pi.command` 或 `PASEO_DSH_EXECUTABLE`(后者优先)指定其他 `dsh` 可执行文件,通过 `PASEO_DSH_PROFILE` 改用另一个已存在的 profile;这样指定的 profile 不会被自动创建。如果 Paseo Daemon 默认关闭插件加载,请先在配置中启用 `pluginsEnabled`。
|
|
40
41
|
|
|
41
42
|
如果原生会话仍被另一个 DSH 客户端(如终端界面)占用,请先在那里释放,再从 Paseo 导入或恢复。
|
|
42
43
|
|
package/dsh.png
CHANGED
|
Binary file
|
package/dsh.svg
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
-
<
|
|
3
|
-
<linearGradient id="wave" x1="8" y1="10" x2="56" y2="54" gradientUnits="userSpaceOnUse">
|
|
4
|
-
<stop stop-color="#6A8DFF"/>
|
|
5
|
-
<stop offset="1" stop-color="#3158E6"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<path fill="url(#wave)" d="M8 34c7-13 18-20 33-20 6 0 11 1 15 4-2 13-8 23-18 29-8 5-17 6-27 3 7-2 12-6 15-11-7 2-13 0-18-5Z"/>
|
|
9
|
-
<circle cx="43" cy="25" r="3" fill="#fff"/>
|
|
10
|
-
<path d="M13 42c8 4 17 3 25-2" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity=".9"/>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="currentColor" role="img" aria-label="DeepSeek Harness">
|
|
2
|
+
<path fill-rule="nonzero" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z"/>
|
|
11
3
|
</svg>
|
package/index.server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
-
import { launchDshBridge
|
|
2
|
+
import { launchDshBridge } from "./server/bridge-client.js";
|
|
3
3
|
import { DSH_BRIDGE_SOURCE } from "./server/generated-bridge.js";
|
|
4
4
|
import { createDshProvider } from "./server/provider.js";
|
|
5
5
|
|
|
6
6
|
export default function contribute(server: PluginServerContext) {
|
|
7
|
-
const executable =
|
|
7
|
+
const executable = process.env.PASEO_DSH_EXECUTABLE;
|
|
8
8
|
server.registerProvider(
|
|
9
9
|
createDshProvider({
|
|
10
10
|
executable,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-dsh-direct",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "DeepSeek Harness Direct Provider Plugin for Paseo",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"paseo-plugin.json",
|
|
26
26
|
"dsh.svg",
|
|
27
27
|
"dsh.png",
|
|
28
|
+
"OVERVIEW.md",
|
|
28
29
|
"!server/**/*.test.ts",
|
|
29
30
|
"!server/generated-bridge.ts",
|
|
30
31
|
"!server/generated-version.ts"
|
package/scripts/build.mjs
CHANGED
|
@@ -13,7 +13,8 @@ const result = await build({
|
|
|
13
13
|
write: false,
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// esbuild reports output paths with the platform separator (backslashes on Windows).
|
|
17
|
+
const bridge = result.outputFiles.find(file => file.path.replace(/\\/g, '/').endsWith('/dsh-bridge.mjs'))
|
|
17
18
|
if (!bridge) throw new Error('esbuild did not produce dist/dsh-bridge.mjs')
|
|
18
19
|
await mkdir('dist', { recursive: true })
|
|
19
20
|
await writeFile('dist/dsh-bridge.mjs', bridge.contents)
|
package/server/bridge-client.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
+
import { detectDshVersion } from "./dsh-version.js";
|
|
7
|
+
import type { ProviderLaunch } from "@getpaseo/plugin/server/provider";
|
|
6
8
|
import type {
|
|
7
9
|
BridgeInitializeResult,
|
|
8
10
|
BridgeMethods,
|
|
@@ -16,8 +18,10 @@ type Notification = keyof BridgeNotifications;
|
|
|
16
18
|
|
|
17
19
|
export interface DshBridge {
|
|
18
20
|
readonly initialized: BridgeInitializeResult;
|
|
19
|
-
/** The resolved `dsh` executable this bridge was launched with (see `
|
|
21
|
+
/** The resolved `dsh` executable this bridge was launched with (see `resolveDshLaunch`). */
|
|
20
22
|
readonly executable: string;
|
|
23
|
+
/** Arguments that precede dsh's own arguments, from Paseo's resolved `command`. */
|
|
24
|
+
readonly args?: readonly string[];
|
|
21
25
|
request<Name extends Method>(
|
|
22
26
|
method: Name,
|
|
23
27
|
params: BridgeMethods[Name]["params"],
|
|
@@ -52,9 +56,12 @@ export interface SessionLaunch {
|
|
|
52
56
|
/** Store sessions under the bridge's temporary directory, removed on close. */
|
|
53
57
|
ephemeral?: boolean;
|
|
54
58
|
mcpServers?: readonly BridgeMcpServer[];
|
|
59
|
+
/** The `launch` Paseo resolved for the provider connection: command, leading arguments and the complete daemon environment. */
|
|
60
|
+
providerLaunch?: ProviderLaunch;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export interface LaunchBridgeOptions extends SessionLaunch {
|
|
64
|
+
/** The `PASEO_DSH_EXECUTABLE` value; it takes precedence over `providerLaunch.command`. */
|
|
58
65
|
executable?: string;
|
|
59
66
|
profile?: string;
|
|
60
67
|
bridgeSource: string;
|
|
@@ -123,6 +130,7 @@ export function createBridgePatch(options: {
|
|
|
123
130
|
*/
|
|
124
131
|
export async function ensureDshProfile(options: {
|
|
125
132
|
executable: string;
|
|
133
|
+
args?: readonly string[];
|
|
126
134
|
profile: string;
|
|
127
135
|
env: Readonly<Record<string, string | undefined>>;
|
|
128
136
|
timeoutMs?: number;
|
|
@@ -139,7 +147,13 @@ export async function ensureDshProfile(options: {
|
|
|
139
147
|
// Concurrent first launches share one creation run.
|
|
140
148
|
let creation = profileCreations.get(directory);
|
|
141
149
|
if (!creation) {
|
|
142
|
-
creation = runProfileInit(
|
|
150
|
+
creation = runProfileInit(
|
|
151
|
+
options.executable,
|
|
152
|
+
options.args ?? [],
|
|
153
|
+
options.profile,
|
|
154
|
+
options.env,
|
|
155
|
+
options.timeoutMs ?? 60_000,
|
|
156
|
+
).finally(
|
|
143
157
|
() => profileCreations.delete(directory),
|
|
144
158
|
);
|
|
145
159
|
profileCreations.set(directory, creation);
|
|
@@ -162,6 +176,7 @@ async function isDirectory(directory: string): Promise<boolean> {
|
|
|
162
176
|
|
|
163
177
|
function runProfileInit(
|
|
164
178
|
executable: string,
|
|
179
|
+
leadingArgs: readonly string[],
|
|
165
180
|
profile: string,
|
|
166
181
|
env: Readonly<Record<string, string | undefined>>,
|
|
167
182
|
timeoutMs: number,
|
|
@@ -169,7 +184,7 @@ function runProfileInit(
|
|
|
169
184
|
return new Promise((resolve, reject) => {
|
|
170
185
|
const child = spawn(
|
|
171
186
|
executable,
|
|
172
|
-
["--profile", profile, "--from-default-profile", DEFAULT_PROFILE_TEMPLATE, "--dump-config"],
|
|
187
|
+
[...leadingArgs, "--profile", profile, "--from-default-profile", DEFAULT_PROFILE_TEMPLATE, "--dump-config"],
|
|
173
188
|
{ env, stdio: ["ignore", "ignore", "pipe"], windowsHide: true },
|
|
174
189
|
);
|
|
175
190
|
let stderr = "";
|
|
@@ -199,16 +214,135 @@ function runProfileInit(
|
|
|
199
214
|
});
|
|
200
215
|
}
|
|
201
216
|
|
|
202
|
-
/** The
|
|
203
|
-
export
|
|
204
|
-
|
|
217
|
+
/** The command and leading arguments that start dsh. */
|
|
218
|
+
export interface DshLaunch {
|
|
219
|
+
command: string;
|
|
220
|
+
args: readonly string[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The dsh launch, by precedence: a non-blank `PASEO_DSH_EXECUTABLE` (the most
|
|
225
|
+
* specific signal, kept for existing installs), then the `launch` Paseo
|
|
226
|
+
* resolved from `command` / `agents.providers.dsh-pi.command`, then `dsh` from
|
|
227
|
+
* `PATH`. `launch.args` belong to `launch.command`, so they are dropped when
|
|
228
|
+
* the environment variable wins.
|
|
229
|
+
*/
|
|
230
|
+
export function resolveDshLaunch(configured: string | undefined, launch?: Pick<ProviderLaunch, "command" | "args">): DshLaunch {
|
|
231
|
+
const override = configured?.trim();
|
|
232
|
+
if (override) return { command: override, args: [] };
|
|
233
|
+
if (launch) return { command: launch.command, args: launch.args };
|
|
234
|
+
return { command: "dsh", args: [] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The `dsh` executable chosen by `resolveDshLaunch`. */
|
|
238
|
+
export function resolveDshExecutable(configured: string | undefined, launch?: Pick<ProviderLaunch, "command" | "args">): string {
|
|
239
|
+
return resolveDshLaunch(configured, launch).command;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Text for diagnostics: the command line with its leading arguments. */
|
|
243
|
+
export function describeDshLaunch(launch: DshLaunch): string {
|
|
244
|
+
return [launch.command, ...launch.args].join(" ");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Row ids found in a dumped DSH configuration, in YAML (`id: x`) or JSON (`"id": "x"`) form. */
|
|
248
|
+
export function dumpedRowIds(dump: string): Set<string> {
|
|
249
|
+
const ids = new Set<string>();
|
|
250
|
+
for (const match of dump.matchAll(/(?:^|[\s{,\-])["']?id["']?\s*:\s*["']?([A-Za-z0-9_.@/-]+)["']?/gm)) {
|
|
251
|
+
ids.add(match[1]!);
|
|
252
|
+
}
|
|
253
|
+
return ids;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The surface rows the launcher disables that the dumped configuration does not contain. */
|
|
257
|
+
export function missingSurfaceRows(dump: string): string[] {
|
|
258
|
+
const present = dumpedRowIds(dump);
|
|
259
|
+
return DISABLED_SURFACE_ROWS.filter((row) => !present.has(row));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Fail loudly when the profile no longer has a row the launcher disables.
|
|
264
|
+
* Disabling an absent row has no effect, so a renamed row would leave the
|
|
265
|
+
* browser surface running beside the bridge. Returns normally when the dump
|
|
266
|
+
* cannot be obtained or is empty: the guard has no evidence then, and the real
|
|
267
|
+
* launch reports its own errors.
|
|
268
|
+
*/
|
|
269
|
+
export async function assertSurfaceRowsPresent(options: {
|
|
270
|
+
launch: DshLaunch;
|
|
271
|
+
profile: string;
|
|
272
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
273
|
+
dshVersionOf: (launch: DshLaunch) => Promise<string>;
|
|
274
|
+
dump?: (launch: DshLaunch, profile: string, env: Readonly<Record<string, string | undefined>>) => Promise<string>;
|
|
275
|
+
}): Promise<void> {
|
|
276
|
+
const dump = await (options.dump ?? dumpProfileConfig)(options.launch, options.profile, options.env).catch(() => "");
|
|
277
|
+
if (!dump.trim()) return;
|
|
278
|
+
const missing = missingSurfaceRows(dump);
|
|
279
|
+
if (missing.length === 0) return;
|
|
280
|
+
const version = (await options.dshVersionOf(options.launch).catch(() => "")) || "unknown";
|
|
281
|
+
throw new Error(
|
|
282
|
+
`DSH ${version} profile "${options.profile}" has no ${missing.map((row) => `"${row}"`).join(", ")} row. ` +
|
|
283
|
+
`The plugin disables ${DISABLED_SURFACE_ROWS.join(", ")} to keep the web surface from running beside the bridge, ` +
|
|
284
|
+
`so this dsh does not match the plugin. Install the dsh line this plugin release supports, ` +
|
|
285
|
+
`or recreate the profile from the web template.`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** `dsh --profile <profile> --dump-config` stdout; rejects when dsh fails or exceeds the timeout. */
|
|
290
|
+
function dumpProfileConfig(
|
|
291
|
+
launch: DshLaunch,
|
|
292
|
+
profile: string,
|
|
293
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
294
|
+
): Promise<string> {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const child = spawn(launch.command, [...launch.args, "--profile", profile, "--dump-config"], {
|
|
297
|
+
env,
|
|
298
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
});
|
|
301
|
+
let stdout = "";
|
|
302
|
+
child.stdout.setEncoding("utf8");
|
|
303
|
+
child.stdout.on("data", (chunk: string) => {
|
|
304
|
+
stdout = (stdout + chunk).slice(-1_048_576);
|
|
305
|
+
});
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
child.kill("SIGKILL");
|
|
308
|
+
reject(new Error("dsh --dump-config timed out"));
|
|
309
|
+
}, 20_000);
|
|
310
|
+
child.once("error", (error) => {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
reject(error);
|
|
313
|
+
});
|
|
314
|
+
child.once("exit", (code) => {
|
|
315
|
+
clearTimeout(timeout);
|
|
316
|
+
if (code === 0) resolve(stdout);
|
|
317
|
+
else reject(new Error(`dsh --dump-config exited with code ${String(code)}`));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Names of host services or methods the bridge needs that the handshake reported missing. */
|
|
323
|
+
export function assertBridgeHandshake(initialized: BridgeInitializeResult, dshVersion: string): void {
|
|
324
|
+
if (initialized.protocolVersion !== 1) {
|
|
325
|
+
throw new Error(`Unsupported DSH bridge protocol ${initialized.protocolVersion}`);
|
|
326
|
+
}
|
|
327
|
+
const missing = initialized.missing ?? [];
|
|
328
|
+
if (missing.length === 0) return;
|
|
329
|
+
throw new Error(
|
|
330
|
+
`DSH ${dshVersion || "unknown"} does not provide what the bridge depends on: ${missing.join(", ")}. ` +
|
|
331
|
+
`This dsh does not match the plugin; install the dsh line this plugin release supports.`,
|
|
332
|
+
);
|
|
205
333
|
}
|
|
206
334
|
|
|
207
335
|
export async function launchDshBridge(options: LaunchBridgeOptions): Promise<DshBridge> {
|
|
208
336
|
const profile = options.profile?.trim() || DEFAULT_PROFILE;
|
|
209
|
-
const
|
|
210
|
-
const env = { ...process.env, ...options.env };
|
|
211
|
-
await ensureDshProfile({ executable, profile, env });
|
|
337
|
+
const launch = resolveDshLaunch(options.executable, options.providerLaunch);
|
|
338
|
+
const env = { ...(options.providerLaunch?.env ?? process.env), ...options.env };
|
|
339
|
+
await ensureDshProfile({ executable: launch.command, args: launch.args, profile, env });
|
|
340
|
+
await assertSurfaceRowsPresent({
|
|
341
|
+
launch,
|
|
342
|
+
profile,
|
|
343
|
+
env,
|
|
344
|
+
dshVersionOf: (target) => detectDshVersion(target.command, { args: target.args, env }),
|
|
345
|
+
});
|
|
212
346
|
const directory = await mkdtemp(path.join(tmpdir(), "paseo-dsh-direct-"));
|
|
213
347
|
const bridgePath = path.join(directory, "dsh-bridge.mjs");
|
|
214
348
|
const patchPath = path.join(directory, "patch.json");
|
|
@@ -226,17 +360,18 @@ export async function launchDshBridge(options: LaunchBridgeOptions): Promise<Dsh
|
|
|
226
360
|
{ mode: 0o600 },
|
|
227
361
|
);
|
|
228
362
|
|
|
229
|
-
const child = spawn(
|
|
363
|
+
const child = spawn(launch.command, [...launch.args, "--profile", profile, "--patch", patchPath], {
|
|
230
364
|
env,
|
|
231
365
|
stdio: ["pipe", "pipe", "pipe"],
|
|
232
366
|
windowsHide: true,
|
|
233
367
|
});
|
|
234
|
-
const client = new JsonlBridgeClient(child, directory,
|
|
368
|
+
const client = new JsonlBridgeClient(child, directory, launch);
|
|
235
369
|
try {
|
|
236
370
|
await client.waitUntilReady(options.startupTimeoutMs ?? 15_000);
|
|
237
371
|
client.initialized = await client.request("bridge.initialize", {});
|
|
238
|
-
if (client.initialized.protocolVersion !== 1) {
|
|
239
|
-
|
|
372
|
+
if (client.initialized.protocolVersion !== 1 || (client.initialized.missing?.length ?? 0) > 0) {
|
|
373
|
+
const version = await detectDshVersion(launch.command, { args: launch.args, env }).catch(() => "");
|
|
374
|
+
assertBridgeHandshake(client.initialized, version);
|
|
240
375
|
}
|
|
241
376
|
return client;
|
|
242
377
|
} catch (error) {
|
|
@@ -248,6 +383,7 @@ export async function launchDshBridge(options: LaunchBridgeOptions): Promise<Dsh
|
|
|
248
383
|
class JsonlBridgeClient implements DshBridge {
|
|
249
384
|
initialized!: BridgeInitializeResult;
|
|
250
385
|
readonly executable: string;
|
|
386
|
+
readonly args: readonly string[];
|
|
251
387
|
private nextId = 1;
|
|
252
388
|
private readonly pending = new Map<string | number, PendingRequest>();
|
|
253
389
|
private readonly notifications = new Map<string, Set<(params: never) => void>>();
|
|
@@ -263,9 +399,10 @@ class JsonlBridgeClient implements DshBridge {
|
|
|
263
399
|
constructor(
|
|
264
400
|
private readonly child: ChildProcessWithoutNullStreams,
|
|
265
401
|
private readonly temporaryDirectory: string,
|
|
266
|
-
|
|
402
|
+
launch: DshLaunch,
|
|
267
403
|
) {
|
|
268
|
-
this.executable =
|
|
404
|
+
this.executable = launch.command;
|
|
405
|
+
this.args = launch.args;
|
|
269
406
|
this.exited = new Promise((resolve) => {
|
|
270
407
|
this.resolveExited = resolve;
|
|
271
408
|
});
|
package/server/dsh/host.ts
CHANGED
|
@@ -125,6 +125,26 @@ export interface HostServices {
|
|
|
125
125
|
}>;
|
|
126
126
|
};
|
|
127
127
|
agentDefaultModel: { currentSelection(): ModelSelection } | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Projection registry read cut. The `tokenUsage` and `contextPressure` units
|
|
130
|
+
* carry the figures Paseo renders as session usage; absent when the profile
|
|
131
|
+
* mounts no projection registry.
|
|
132
|
+
*/
|
|
133
|
+
sessionProjections?: {
|
|
134
|
+
snapshot(
|
|
135
|
+
session: HostSession,
|
|
136
|
+
keys?: readonly string[],
|
|
137
|
+
): { values: Partial<Record<string, unknown>> };
|
|
138
|
+
/** Change feed: fires once per client-visible unit whose raw view changed. */
|
|
139
|
+
onChanged(
|
|
140
|
+
listener: (
|
|
141
|
+
session: HostSession,
|
|
142
|
+
key: string,
|
|
143
|
+
value: unknown,
|
|
144
|
+
seq: number,
|
|
145
|
+
) => void,
|
|
146
|
+
): () => void;
|
|
147
|
+
};
|
|
128
148
|
sessionTitle:
|
|
129
149
|
{ rename(session: HostSession, title: string): void } | undefined;
|
|
130
150
|
approval: object | undefined;
|
package/server/dsh/service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
BridgeInitializeResult,
|
|
7
7
|
BridgeMethods,
|
|
8
8
|
BridgeNotifications,
|
|
9
|
+
BridgeUsage,
|
|
9
10
|
DshSessionEvent,
|
|
10
11
|
InteractionRequest,
|
|
11
12
|
ModelSelection,
|
|
@@ -24,6 +25,8 @@ import type {
|
|
|
24
25
|
HostContext,
|
|
25
26
|
HostHandle,
|
|
26
27
|
HostHelpers,
|
|
28
|
+
HostServices,
|
|
29
|
+
HostSession,
|
|
27
30
|
ModelSelectionRef,
|
|
28
31
|
} from "./host.js";
|
|
29
32
|
|
|
@@ -93,6 +96,38 @@ export function savedModel(
|
|
|
93
96
|
return pending ?? used;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
/** Host services and methods every bridge method relies on; `service` alone means the service itself. */
|
|
100
|
+
const REQUIRED_HOST_SURFACE = {
|
|
101
|
+
agents: ["get", "create", "resume"],
|
|
102
|
+
sessionQuery: ["listSessions", "readSession", "observeSession"],
|
|
103
|
+
agentPresets: ["list", "resolve", "mount", "select"],
|
|
104
|
+
llm: ["listProviders", "listModels"],
|
|
105
|
+
} as const;
|
|
106
|
+
|
|
107
|
+
/** The required host services and methods this DSH does not provide. */
|
|
108
|
+
export function missingHostSurface(ctx: HostContext): string[] {
|
|
109
|
+
const missing: string[] = [];
|
|
110
|
+
for (const [service, methods] of Object.entries(REQUIRED_HOST_SURFACE)) {
|
|
111
|
+
let provided: unknown;
|
|
112
|
+
try {
|
|
113
|
+
provided = ctx.get(service as keyof HostServices);
|
|
114
|
+
} catch {
|
|
115
|
+
// A service that is not registered may throw instead of returning undefined.
|
|
116
|
+
provided = undefined;
|
|
117
|
+
}
|
|
118
|
+
if (provided === null || typeof provided !== "object") {
|
|
119
|
+
missing.push(service);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const method of methods) {
|
|
123
|
+
if (typeof (provided as Record<string, unknown>)[method] !== "function") {
|
|
124
|
+
missing.push(`${service}.${method}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return missing;
|
|
129
|
+
}
|
|
130
|
+
|
|
96
131
|
export class BridgeService {
|
|
97
132
|
private readonly owned = new Map<string, RecordEntry>();
|
|
98
133
|
private readonly opening = new Map<string, Promise<SessionOpenResult>>();
|
|
@@ -103,6 +138,8 @@ export class BridgeService {
|
|
|
103
138
|
private readonly preapproved = new Map<string, ReadonlySet<string>>();
|
|
104
139
|
private readonly interactions = new Map<string, PendingInteraction>();
|
|
105
140
|
private readonly subscriptions: Array<() => void> = [];
|
|
141
|
+
/** Last usage payload sent per session, so an unchanged fold is not re-sent. */
|
|
142
|
+
private readonly lastUsage = new Map<string, string>();
|
|
106
143
|
private stopping = false;
|
|
107
144
|
private stopTask?: Promise<{}>;
|
|
108
145
|
|
|
@@ -139,6 +176,19 @@ export class BridgeService {
|
|
|
139
176
|
notify("session.status", { sessionId: String(agent.id), status });
|
|
140
177
|
}),
|
|
141
178
|
);
|
|
179
|
+
// Usage rides the projection change feed rather than the session event
|
|
180
|
+
// stream: it fires exactly when the harness's token fold moves, so no
|
|
181
|
+
// session-event name has to be guessed and an idle session costs nothing.
|
|
182
|
+
const projections = ctx.get("sessionProjections");
|
|
183
|
+
if (projections) {
|
|
184
|
+
this.subscriptions.push(
|
|
185
|
+
projections.onChanged((session, key) => {
|
|
186
|
+
if (key === "tokenUsage" || key === "contextPressure") {
|
|
187
|
+
this.publishUsage(session);
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
142
192
|
this.subscriptions.push(
|
|
143
193
|
ctx.on("approval/request", (request, next) => {
|
|
144
194
|
const sessionId = this.rootFor(request.agent);
|
|
@@ -183,6 +233,70 @@ export class BridgeService {
|
|
|
183
233
|
);
|
|
184
234
|
}
|
|
185
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Fold the harness's token projections into Paseo's usage vocabulary.
|
|
238
|
+
*
|
|
239
|
+
* `tokenUsage` totals and `contextPressure` carry every figure Paseo can
|
|
240
|
+
* render, and both are plain numbers, so no live harness object crosses the
|
|
241
|
+
* wire. Returns undefined when no projection registry is mounted or neither
|
|
242
|
+
* unit is registered: a profile without them degrades to no usage rather
|
|
243
|
+
* than failing the turn.
|
|
244
|
+
*/
|
|
245
|
+
private readUsage(session: HostSession): BridgeUsage | undefined {
|
|
246
|
+
const projections = this.ctx.get("sessionProjections");
|
|
247
|
+
if (!projections) return undefined;
|
|
248
|
+
let values: Partial<Record<string, unknown>>;
|
|
249
|
+
try {
|
|
250
|
+
values = projections.snapshot(session, [
|
|
251
|
+
"tokenUsage",
|
|
252
|
+
"contextPressure",
|
|
253
|
+
]).values;
|
|
254
|
+
} catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const tokens = values.tokenUsage as
|
|
258
|
+
| {
|
|
259
|
+
uncachedInputTokens?: number;
|
|
260
|
+
outputTokens?: number;
|
|
261
|
+
cacheReadTokens?: number;
|
|
262
|
+
}
|
|
263
|
+
| undefined;
|
|
264
|
+
const pressure = values.contextPressure as
|
|
265
|
+
| { contextWindow?: number; pressureTokens?: number }
|
|
266
|
+
| undefined;
|
|
267
|
+
const usage: BridgeUsage = {};
|
|
268
|
+
if (typeof tokens?.uncachedInputTokens === "number")
|
|
269
|
+
usage.inputTokens = tokens.uncachedInputTokens;
|
|
270
|
+
if (typeof tokens?.cacheReadTokens === "number")
|
|
271
|
+
usage.cachedInputTokens = tokens.cacheReadTokens;
|
|
272
|
+
if (typeof tokens?.outputTokens === "number")
|
|
273
|
+
usage.outputTokens = tokens.outputTokens;
|
|
274
|
+
if (typeof pressure?.pressureTokens === "number")
|
|
275
|
+
usage.contextWindowUsedTokens = pressure.pressureTokens;
|
|
276
|
+
if (typeof pressure?.contextWindow === "number")
|
|
277
|
+
usage.contextWindowMaxTokens = pressure.contextWindow;
|
|
278
|
+
// The units materialize at zero before any provider usage lands, so a
|
|
279
|
+
// payload carrying no measured token is warm-up noise, not a reading.
|
|
280
|
+
const measured =
|
|
281
|
+
(usage.inputTokens ?? 0) > 0 ||
|
|
282
|
+
(usage.cachedInputTokens ?? 0) > 0 ||
|
|
283
|
+
(usage.outputTokens ?? 0) > 0 ||
|
|
284
|
+
(usage.contextWindowUsedTokens ?? 0) > 0;
|
|
285
|
+
return measured ? usage : undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Publish one tracked session's usage, skipping an unchanged fold. */
|
|
289
|
+
private publishUsage(session: HostSession): void {
|
|
290
|
+
const sessionId = String(session.id);
|
|
291
|
+
if (!this.tracked.has(sessionId)) return;
|
|
292
|
+
const usage = this.readUsage(session);
|
|
293
|
+
if (!usage) return;
|
|
294
|
+
const signature = JSON.stringify(usage);
|
|
295
|
+
if (this.lastUsage.get(sessionId) === signature) return;
|
|
296
|
+
this.lastUsage.set(sessionId, signature);
|
|
297
|
+
this.notify("session.usage", { sessionId, usage });
|
|
298
|
+
}
|
|
299
|
+
|
|
186
300
|
private rootFor(agent: HostAgent | undefined): string | undefined {
|
|
187
301
|
let id = agent?.id && String(agent.id);
|
|
188
302
|
const seen = new Set<string>();
|
|
@@ -317,7 +431,14 @@ export class BridgeService {
|
|
|
317
431
|
const params = record(input);
|
|
318
432
|
let result: unknown;
|
|
319
433
|
switch (method) {
|
|
320
|
-
case "bridge.initialize":
|
|
434
|
+
case "bridge.initialize": {
|
|
435
|
+
const missing = missingHostSurface(this.ctx);
|
|
436
|
+
if (missing.length > 0) {
|
|
437
|
+
result = { protocolVersion: 1, profile: this.profile, capabilities: {
|
|
438
|
+
stream: true, approvals: false, questions: false, history: true, cancel: true,
|
|
439
|
+
}, catalog: { models: [], presets: [] }, missing } satisfies BridgeInitializeResult;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
321
442
|
result = {
|
|
322
443
|
protocolVersion: 1,
|
|
323
444
|
profile: this.profile,
|
|
@@ -331,6 +452,7 @@ export class BridgeService {
|
|
|
331
452
|
catalog: await this.catalog(),
|
|
332
453
|
} satisfies BridgeInitializeResult;
|
|
333
454
|
break;
|
|
455
|
+
}
|
|
334
456
|
case "session.list": {
|
|
335
457
|
const query = this.ctx.get("sessionQuery");
|
|
336
458
|
const records = await query.listSessions();
|
|
@@ -611,6 +733,7 @@ export class BridgeService {
|
|
|
611
733
|
this.opening.delete(id);
|
|
612
734
|
this.tracked.delete(id);
|
|
613
735
|
this.preapproved.delete(id);
|
|
736
|
+
this.lastUsage.delete(id);
|
|
614
737
|
},
|
|
615
738
|
);
|
|
616
739
|
return task;
|
|
@@ -759,6 +882,7 @@ export class BridgeService {
|
|
|
759
882
|
this.owned.delete(id);
|
|
760
883
|
this.tracked.delete(id);
|
|
761
884
|
this.preapproved.delete(id);
|
|
885
|
+
this.lastUsage.delete(id);
|
|
762
886
|
})();
|
|
763
887
|
this.closing.set(id, task);
|
|
764
888
|
void task.finally(() => this.closing.delete(id)).catch(() => undefined);
|
package/server/dsh-version.ts
CHANGED
|
@@ -8,6 +8,8 @@ const cache = new Map<string, Promise<string>>();
|
|
|
8
8
|
export interface DetectDshVersionOptions {
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
env?: Readonly<Record<string, string | undefined>>;
|
|
11
|
+
/** Arguments placed before `--version`, from Paseo's resolved command. */
|
|
12
|
+
args?: readonly string[];
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -18,10 +20,11 @@ export interface DetectDshVersionOptions {
|
|
|
18
20
|
* executable path for the life of the process.
|
|
19
21
|
*/
|
|
20
22
|
export function detectDshVersion(executable: string, options: DetectDshVersionOptions = {}): Promise<string> {
|
|
21
|
-
const
|
|
23
|
+
const key = JSON.stringify([executable, ...(options.args ?? [])]);
|
|
24
|
+
const cached = cache.get(key);
|
|
22
25
|
if (cached) return cached;
|
|
23
26
|
const probe = runVersionProbe(executable, options);
|
|
24
|
-
cache.set(
|
|
27
|
+
cache.set(key, probe);
|
|
25
28
|
return probe;
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -38,7 +41,7 @@ function runVersionProbe(executable: string, options: DetectDshVersionOptions):
|
|
|
38
41
|
|
|
39
42
|
let child;
|
|
40
43
|
try {
|
|
41
|
-
child = spawn(executable, ["--version"], {
|
|
44
|
+
child = spawn(executable, [...(options.args ?? []), "--version"], {
|
|
42
45
|
env: options.env ?? process.env,
|
|
43
46
|
stdio: ["ignore", "pipe", "ignore"],
|
|
44
47
|
windowsHide: true,
|
package/server/provider.ts
CHANGED
|
@@ -24,7 +24,14 @@ import type {
|
|
|
24
24
|
SlashCommand,
|
|
25
25
|
} from "../shared/bridge-protocol.js";
|
|
26
26
|
import { slashLine } from "../shared/bridge-protocol.js";
|
|
27
|
-
import
|
|
27
|
+
import {
|
|
28
|
+
describeDshLaunch,
|
|
29
|
+
resolveDshLaunch,
|
|
30
|
+
type BridgeMcpServer,
|
|
31
|
+
type DshBridge,
|
|
32
|
+
type DshLaunch,
|
|
33
|
+
type SessionLaunch,
|
|
34
|
+
} from "./bridge-client.js";
|
|
28
35
|
import { detectDshVersion } from "./dsh-version.js";
|
|
29
36
|
import { PLUGIN_VERSION } from "./plugin-version.js";
|
|
30
37
|
import { displayText, toPromptParts } from "./prompt-content.js";
|
|
@@ -47,12 +54,17 @@ const CAPABILITIES = [
|
|
|
47
54
|
const PERMISSION_PRESET_SETTING = "permissionPreset";
|
|
48
55
|
const PLAN_MODE_SETTING = "planMode";
|
|
49
56
|
|
|
57
|
+
type DetectVersion = (
|
|
58
|
+
executable: string,
|
|
59
|
+
options?: { args?: readonly string[]; env?: Readonly<Record<string, string | undefined>> },
|
|
60
|
+
) => Promise<string>;
|
|
61
|
+
|
|
50
62
|
interface DshProviderOptions {
|
|
51
63
|
createBridge(launch?: SessionLaunch): Promise<DshBridge>;
|
|
52
|
-
/** The
|
|
53
|
-
executable
|
|
64
|
+
/** The `PASEO_DSH_EXECUTABLE` value; it outranks the `launch` Paseo resolves. `status()` and `createBridge` use the same precedence. */
|
|
65
|
+
executable?: string;
|
|
54
66
|
/** Detects the local dsh version for the startup compatibility check; defaults to spawning `<exe> --version`. */
|
|
55
|
-
detectDshVersion?:
|
|
67
|
+
detectDshVersion?: DetectVersion;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
/** A prompt or command RPC in flight; it owns any turn DSH starts before the RPC returns. */
|
|
@@ -91,12 +103,20 @@ export function createDshProvider(options: DshProviderOptions): ProviderRegistra
|
|
|
91
103
|
icon: "dsh.svg",
|
|
92
104
|
getCatalogCacheKey: async (options) =>
|
|
93
105
|
options.scope === "workspace" ? `dsh-pi:${options.cwd}` : "dsh-pi:global",
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
command: ["dsh"],
|
|
107
|
+
async status(request) {
|
|
108
|
+
return dshStatus(
|
|
109
|
+
resolveDshLaunch(options.executable, request?.launch),
|
|
110
|
+
request?.launch?.env,
|
|
111
|
+
options.detectDshVersion ?? detectDshVersion,
|
|
112
|
+
);
|
|
96
113
|
},
|
|
97
114
|
async connect(request) {
|
|
98
115
|
if (!request.versions.includes(1)) throw new Error("Provider protocol version 1 is required");
|
|
99
|
-
const
|
|
116
|
+
const providerLaunch = request.launch;
|
|
117
|
+
const createBridge = (launch?: SessionLaunch) =>
|
|
118
|
+
options.createBridge(providerLaunch ? { ...launch, providerLaunch } : launch);
|
|
119
|
+
const bridge = await createBridge();
|
|
100
120
|
const supported = CAPABILITIES.filter(
|
|
101
121
|
(capability) =>
|
|
102
122
|
capability !== "permission" ||
|
|
@@ -105,9 +125,10 @@ export function createDshProvider(options: DshProviderOptions): ProviderRegistra
|
|
|
105
125
|
);
|
|
106
126
|
return createConnection(
|
|
107
127
|
bridge,
|
|
108
|
-
|
|
128
|
+
createBridge,
|
|
109
129
|
negotiateProviderCapabilities(request.capabilities, supported),
|
|
110
130
|
options.detectDshVersion ?? detectDshVersion,
|
|
131
|
+
providerLaunch?.env,
|
|
111
132
|
);
|
|
112
133
|
},
|
|
113
134
|
};
|
|
@@ -120,15 +141,17 @@ export function createDshProvider(options: DshProviderOptions): ProviderRegistra
|
|
|
120
141
|
* shows, since only DSH's own handshake can reject a session.
|
|
121
142
|
*/
|
|
122
143
|
async function dshStatus(
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
launch: DshLaunch,
|
|
145
|
+
env: Readonly<Record<string, string>> | undefined,
|
|
146
|
+
detectVersion: DetectVersion,
|
|
125
147
|
): Promise<ProviderStatus> {
|
|
126
|
-
const dshVersion = await detectVersion(
|
|
148
|
+
const dshVersion = await detectVersion(launch.command, { args: launch.args, ...(env ? { env } : {}) }).catch(() => "");
|
|
127
149
|
if (!dshVersion) {
|
|
128
150
|
return {
|
|
129
151
|
available: false,
|
|
130
152
|
diagnostic:
|
|
131
|
-
`Could not run "${
|
|
153
|
+
`Could not run "${describeDshLaunch(launch)} --version". Install dsh, ` +
|
|
154
|
+
`set agents.providers.dsh-pi.command in Paseo config, or set PASEO_DSH_EXECUTABLE, ` +
|
|
132
155
|
`then restart the Paseo daemon.`,
|
|
133
156
|
};
|
|
134
157
|
}
|
|
@@ -139,9 +162,10 @@ async function dshStatus(
|
|
|
139
162
|
|
|
140
163
|
function createConnection(
|
|
141
164
|
discoveryBridge: DshBridge,
|
|
142
|
-
createBridge:
|
|
165
|
+
createBridge: (launch?: SessionLaunch) => Promise<DshBridge>,
|
|
143
166
|
capabilities: readonly string[],
|
|
144
|
-
detectVersion:
|
|
167
|
+
detectVersion: DetectVersion,
|
|
168
|
+
launchEnv: Readonly<Record<string, string>> | undefined,
|
|
145
169
|
): ProviderConnection {
|
|
146
170
|
const listeners = new Set<(event: ProviderEvent) => void>();
|
|
147
171
|
const sessions = new Map<string, SessionState>();
|
|
@@ -158,7 +182,7 @@ function createConnection(
|
|
|
158
182
|
if (closed) throw new Error("Provider connection is closed");
|
|
159
183
|
validateAdmission(input, sessions, capabilities);
|
|
160
184
|
queueMicrotask(() => {
|
|
161
|
-
void dispatch(input, { discoveryBridge, createBridge, sessions, emit, capabilities, detectVersion }).catch(
|
|
185
|
+
void dispatch(input, { discoveryBridge, createBridge, sessions, emit, capabilities, detectVersion, launchEnv }).catch(
|
|
162
186
|
(error) => failInput(input, error, emit),
|
|
163
187
|
);
|
|
164
188
|
});
|
|
@@ -251,6 +275,15 @@ function listenToSessionBridge(
|
|
|
251
275
|
session.dispatch.deferred = terminal;
|
|
252
276
|
}
|
|
253
277
|
}),
|
|
278
|
+
session.bridge.on("session.usage", ({ sessionId, usage }) => {
|
|
279
|
+
if (!isCurrentSession(sessionId)) return;
|
|
280
|
+
emit({
|
|
281
|
+
type: "session.usage",
|
|
282
|
+
sessionId: session.paseoId,
|
|
283
|
+
...(session.activeTurnId ? { turnId: session.activeTurnId } : {}),
|
|
284
|
+
usage,
|
|
285
|
+
});
|
|
286
|
+
}),
|
|
254
287
|
session.bridge.on("interaction.request", (interaction) => {
|
|
255
288
|
if (!isCurrentSession(interaction.sessionId)) return;
|
|
256
289
|
session.pendingInteractions.set(interaction.requestId, interaction);
|
|
@@ -305,11 +338,12 @@ function failSession(
|
|
|
305
338
|
|
|
306
339
|
interface DispatchState {
|
|
307
340
|
discoveryBridge: DshBridge;
|
|
308
|
-
createBridge:
|
|
341
|
+
createBridge: (launch?: SessionLaunch) => Promise<DshBridge>;
|
|
309
342
|
sessions: Map<string, SessionState>;
|
|
310
343
|
emit(event: ProviderEvent): void;
|
|
311
344
|
capabilities: readonly string[];
|
|
312
|
-
detectVersion:
|
|
345
|
+
detectVersion: DetectVersion;
|
|
346
|
+
launchEnv: Readonly<Record<string, string>> | undefined;
|
|
313
347
|
}
|
|
314
348
|
|
|
315
349
|
async function dispatch(input: ProviderInput, state: DispatchState): Promise<void> {
|
|
@@ -470,7 +504,10 @@ async function openSession(
|
|
|
470
504
|
* Never blocks the session: DSH's own handshake is what can fail startup.
|
|
471
505
|
*/
|
|
472
506
|
async function warnOnDshVersionMismatch(session: SessionState, state: DispatchState): Promise<void> {
|
|
473
|
-
const dshVersion = await state.detectVersion(session.bridge.executable
|
|
507
|
+
const dshVersion = await state.detectVersion(session.bridge.executable, {
|
|
508
|
+
args: session.bridge.args ?? [],
|
|
509
|
+
...(state.launchEnv ? { env: state.launchEnv } : {}),
|
|
510
|
+
}).catch(() => "");
|
|
474
511
|
const status = matchVersionLine(PLUGIN_VERSION, dshVersion);
|
|
475
512
|
if (status === "match") return;
|
|
476
513
|
const message = versionWarningMessage(status, PLUGIN_VERSION, dshVersion);
|
|
@@ -86,6 +86,12 @@ export interface BridgeInitializeResult {
|
|
|
86
86
|
cancel: true;
|
|
87
87
|
};
|
|
88
88
|
catalog: BridgeCatalog;
|
|
89
|
+
/**
|
|
90
|
+
* DSH services or methods the bridge requires that this DSH does not provide,
|
|
91
|
+
* as `service` or `service.method`. Absent or empty when nothing is missing;
|
|
92
|
+
* the plugin refuses to connect otherwise.
|
|
93
|
+
*/
|
|
94
|
+
missing?: string[];
|
|
89
95
|
}
|
|
90
96
|
export interface SessionOpenParams {
|
|
91
97
|
sessionId?: string;
|
|
@@ -202,11 +208,30 @@ export interface BridgeMethods {
|
|
|
202
208
|
};
|
|
203
209
|
"bridge.shutdown": { params: {}; result: {} };
|
|
204
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* One session's token and context figures, in Paseo's usage vocabulary.
|
|
213
|
+
*
|
|
214
|
+
* Field names match `ProviderUsage` so the provider forwards this straight
|
|
215
|
+
* onto `session.usage` without a second translation table.
|
|
216
|
+
*/
|
|
217
|
+
export interface BridgeUsage {
|
|
218
|
+
/** Uncached prompt tokens reported by the provider. */
|
|
219
|
+
inputTokens?: number;
|
|
220
|
+
/** Prompt tokens served from the provider's cache. */
|
|
221
|
+
cachedInputTokens?: number;
|
|
222
|
+
/** Completion tokens produced by the model. */
|
|
223
|
+
outputTokens?: number;
|
|
224
|
+
/** Provider-reported prompt size of the newest request. */
|
|
225
|
+
contextWindowUsedTokens?: number;
|
|
226
|
+
/** Route context capacity paired with that pressure. */
|
|
227
|
+
contextWindowMaxTokens?: number;
|
|
228
|
+
}
|
|
205
229
|
export interface BridgeNotifications {
|
|
206
230
|
"bridge.ready": { protocolVersion: 1 };
|
|
207
231
|
"session.event": { sessionId: string; event: DshSessionEvent };
|
|
208
232
|
"session.stream": { sessionId: string; frame: DshStreamFrame };
|
|
209
233
|
"session.status": { sessionId: string; status: "idle" | "running" };
|
|
234
|
+
"session.usage": { sessionId: string; usage: BridgeUsage };
|
|
210
235
|
"interaction.request": InteractionRequest;
|
|
211
236
|
"interaction.closed": { requestId: string; sessionId: string };
|
|
212
237
|
}
|