@deepseek-ai/dsh-tool-pwsh-persistent 0.1.0-rc.8

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/shell/tool-pwsh-persistent/README.md
5
+ README.md: a57c940801606c2eef450f0e54fecb434c62a406
6
+ README.zh.md: 4bd7ecdad08e504daa3ff6283f8b77ac9428385f
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @deepseek-ai/dsh-tool-pwsh-persistent
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Model-facing `pwsh(command)` backed by one owner-scoped `ctx.terminals` shell. The package owns the tool contract and shell reuse; deployments select the terminal backend (a `terminal-bash` instance configured with `shellDialect: pwsh`) and sandbox policy. It is the Windows counterpart of `tool-bash-persistent`: same persistent-state contract, PowerShell dialect.
6
+
7
+ ## Config
8
+
9
+ | Key | Default | Meaning |
10
+ |---|---:|---|
11
+ | `backendType` | `shell` | Registered terminal backend used for each Agent shell. |
12
+ | `timeoutMs` | `300000` | Wall-clock limit for one command; timeout closes the shell. |
13
+ | `maxOutputChars` | `16000` | Maximum retained command-output characters; fixed diagnostics are added afterward. |
14
+ | `description` | Persistent-shell description | Model-facing environment contract. |
15
+
16
+ ## Model Experience
17
+
18
+ ### Tool schema
19
+
20
+ #### What the model sees
21
+
22
+ The generated [`pwsh` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-pwsh-persistent), including the configured `description`. The plugin contributes no standalone system-prompt section; the deployment owns persona and environment guidance.
23
+
24
+ #### Token effect
25
+
26
+ Fixed schema cost while `pwsh` is visible.
27
+
28
+ #### KV Cache effect
29
+
30
+ Prefix-stable while the configured description and schema remain unchanged.
31
+
32
+ ### Tool results
33
+
34
+ #### What the model sees
35
+
36
+ Commands share one shell per Agent, so cwd, `$env:` variables, functions, and background jobs persist across calls. Results exclude private completion markers, the shell prompt, and the echoed input line (PSReadLine renders submitted input back into the stream; the marker-anchored extraction and the wrapper-source strip remove it). A nonzero wrapped command appends `[exit code: N]` — the exact native exit code when the command ran a native program, `1` for a terminating PowerShell error. A shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither (Windows forced termination reports exit 1 without a signal), then resets and tells the model that the next call starts fresh. Long output keeps the earliest retained prefix plus a clipping notice; if the terminal has already dropped that prefix, the result says so explicitly. Timeout returns bounded partial output, closes the uncertain shell, and reports the reset.
37
+
38
+ #### Token effect
39
+
40
+ Data-dependent. `maxOutputChars` bounds retained command output; fixed clipping, lost-prefix, status, timeout, and reset diagnostics can extend the result.
41
+
42
+ #### KV Cache effect
43
+
44
+ Append-only tool results follow the reusable request prefix.
45
+
46
+ ## Known Limitations and Deferred Work
47
+
48
+ - The tool requires an owning Agent and a real terminal backend with a pwsh dialect (Windows ConPTY or a POSIX pwsh).
49
+ - **Input echo is unavoidable**: PowerShell's PSReadLine renders submitted input back into the terminal stream, and there is no `stty -echo` equivalent. The marker-anchored extraction excludes the echo in complete results; the wrapper-source strip covers fallback paths, but a wrapper that wraps across the terminal width may leave a partial echo in partial-output results, bounded by `maxOutputChars`.
50
+ - Raw ESC characters inside model commands are unsupported: PSReadLine consumes them before execution. The wrapper escapes the control bytes it needs (`[char]27`-built OSC markers, backtick escapes for the body).
51
+ - A model redefinition of the `prompt` function removes the readiness marker; the shell then settles on the silence tier instead of the marker fast path.
52
+ - There is no interactive stdin during a command: a foreground command that reads input blocks until the readiness timeout, which resets the shell.
53
+ - SIGTSTP/SIGHUP are unavailable on Windows (backend-rejected); SIGINT is delivered as a console-wide Ctrl-C input write, which at a prompt cancels the pending line instead of signalling a process.
54
+ - Under the Windows ACL sandbox's read-only mode, pwsh starts in ConstrainedLanguage, which may deny the bootstrap's `[Console]::` encoding pin and prompt marker. Commands can still settle through the printable prompt and silence tier, but non-ASCII output may follow the host code page.
55
+ - The BEL-terminated OSC marker remains a readiness signal only; a BEL event channel to the model stays deferred, aligned with the current implementation.
package/README.zh.md ADDED
@@ -0,0 +1,55 @@
1
+ # @deepseek-ai/dsh-tool-pwsh-persistent
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 模型侧 `pwsh(command)`,由一个 owner 作用域的 `ctx.terminals` shell 支撑。本包拥有工具契约与 shell 复用;部署方选择 terminal backend(配置 `shellDialect: pwsh` 的 `terminal-bash` 实例)与沙箱策略。它是 `tool-bash-persistent` 的 Windows 对应物:相同的持久状态契约,PowerShell 方言。
6
+
7
+ ## 配置
8
+
9
+ | 键 | 默认值 | 含义 |
10
+ |---|---:|---|
11
+ | `backendType` | `shell` | 每个 Agent shell 使用的已注册 terminal backend。 |
12
+ | `timeoutMs` | `300000` | 单条命令的墙钟上限;超时关闭 shell。 |
13
+ | `maxOutputChars` | `16000` | 保留的命令输出字符上限;固定诊断文本在其后追加。 |
14
+ | `description` | 持久 shell 描述 | 模型可见的环境契约。 |
15
+
16
+ ## 模型体验
17
+
18
+ ### 工具 schema
19
+
20
+ #### 模型看到什么
21
+
22
+ 生成的 [`pwsh` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-pwsh-persistent),含配置的 `description`。本插件不贡献独立的 system-prompt 段落;persona 与环境指引由部署方负责。
23
+
24
+ #### Token 影响
25
+
26
+ `pwsh` 可见期间每个请求有固定的 schema 成本。
27
+
28
+ #### KV Cache 影响
29
+
30
+ 配置的 description 与 schema 不变时前缀稳定。
31
+
32
+ ### 工具结果
33
+
34
+ #### 模型看到什么
35
+
36
+ 命令共享每个 Agent 的一个 shell,因此 cwd、`$env:` 变量、函数和后台任务跨调用保留。结果排除私有完成标记、shell 提示符与回显的输入行(PSReadLine 会把提交的输入渲染回输出流;marker 锚定提取与包装器原文剥离将其移除)。非零包装命令追加 `[exit code: N]` —— 命令运行原生程序时是精确的原生退出码,PowerShell 终止性错误为 `1`。shell 在报告状态前退出的,改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]` 或 `[shell exited]`(backend 两者都没有时;Windows 强杀按无 signal 的 exit 1 报告),然后重置并告知模型下一次调用从全新 shell 开始。长输出保留最早的前缀并附裁剪提示;若 PTY 已丢弃该前缀,结果会明确说明。超时返回有界的部分输出、关闭不确定的 shell 并报告重置。
37
+
38
+ #### Token 影响
39
+
40
+ 数据相关。`maxOutputChars` 限制保留的命令输出;固定裁剪、前缀丢失、状态、超时与重置诊断可能扩展结果。
41
+
42
+ #### KV Cache 影响
43
+
44
+ 追加式工具结果跟随可复用的请求前缀。
45
+
46
+ ## 已知限制与延后工作
47
+
48
+ - 工具需要拥有 Agent 与一个真实支持 pwsh 方言的 terminal backend(Windows ConPTY 或 POSIX 上的 pwsh)。
49
+ - **输入回显不可避免**:PowerShell 的 PSReadLine 会把提交的输入渲染回终端流,且没有 `stty -echo` 的对应物。完整结果中 marker 锚定提取排除回显;包装器原文剥离覆盖回退路径,但跨越终端宽度的包装器折行可能在部分输出结果中残留片段回显,受 `maxOutputChars` 约束。
50
+ - 模型命令中的裸 ESC 字符不受支持:PSReadLine 会在执行前吞掉它们。包装器转义它需要的控制字节(`[char]27` 构造的 OSC 标记、body 的反引号转义)。
51
+ - 模型重定义 `prompt` 函数会移除就绪标记;shell 随后退化为静默档而非 marker 快路径。
52
+ - 命令执行期间没有交互 stdin:读取输入的前台命令会阻塞到就绪超时,随后重置 shell。
53
+ - SIGTSTP/SIGHUP 在 Windows 不可用(backend 拒绝);SIGINT 以控制台级 Ctrl-C 输入写入投递,在提示符处取消当前行而非向进程发信号。
54
+ - 在 Windows ACL 沙箱的只读模式下,pwsh 以 ConstrainedLanguage 启动,可能拒绝引导代码通过 `[Console]::` 固定编码并写入 prompt marker。命令仍可通过可打印提示符和静默档结算,但非 ASCII 输出可能沿用宿主代码页。
55
+ - BEL 终结的 OSC 标记仍只是就绪信号;面向模型的 BEL 事件通道保持延后,与当前实现对齐。
package/lib/index.js ADDED
@@ -0,0 +1,407 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { deadline, timeoutOf } from "@deepseek-ai/dsh-timeout";
4
+ import { defineTool } from "@deepseek-ai/dsh-tools";
5
+ //#region lib/types/index.js
6
+ /**
7
+ * Model-facing persistent `pwsh` tool over the owner-scoped PTY seam.
8
+ * @module @deepseek-ai/dsh-tool-pwsh-persistent
9
+ */
10
+ var __addDisposableResource = function(env, value, async) {
11
+ if (value !== null && value !== void 0) {
12
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
13
+ var dispose, inner;
14
+ if (async) {
15
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
16
+ dispose = value[Symbol.asyncDispose];
17
+ }
18
+ if (dispose === void 0) {
19
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
20
+ dispose = value[Symbol.dispose];
21
+ if (async) inner = dispose;
22
+ }
23
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
24
+ if (inner) dispose = function() {
25
+ try {
26
+ inner.call(this);
27
+ } catch (e) {
28
+ return Promise.reject(e);
29
+ }
30
+ };
31
+ env.stack.push({
32
+ value,
33
+ dispose,
34
+ async
35
+ });
36
+ } else if (async) env.stack.push({ async: true });
37
+ return value;
38
+ };
39
+ var __disposeResources = (function(SuppressedError) {
40
+ return function(env) {
41
+ function fail(e) {
42
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
43
+ env.hasError = true;
44
+ }
45
+ var r, s = 0;
46
+ function next() {
47
+ while (r = env.stack.pop()) try {
48
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
49
+ if (r.dispose) {
50
+ var result = r.dispose.call(r.value);
51
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
52
+ fail(e);
53
+ return next();
54
+ });
55
+ } else s |= 1;
56
+ } catch (e) {
57
+ fail(e);
58
+ }
59
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
60
+ if (env.hasError) throw env.error;
61
+ }
62
+ return next();
63
+ };
64
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
65
+ var e = new Error(message);
66
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
67
+ });
68
+ const TRUNCATED_MESSAGE = "<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with Select-String in order to find the line numbers of what you are looking for.</NOTE>";
69
+ const LOST_PREFIX_MESSAGE = "<response clipped><NOTE>The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.</NOTE>\n";
70
+ const SHELL_RESET_MESSAGE = "The persistent pwsh shell was reset; the next pwsh call starts from the workspace with a fresh current directory and environment.";
71
+ const SHELL_PROMPT = "__DSH_PERSISTENT_PWSH_PROMPT__ ";
72
+ const TIMEOUT_CODE = "PERSISTENT_PWSH_TIMEOUT";
73
+ const SCROLLBACK_PAGE_LINES = 1e3;
74
+ const POLL_INTERVAL_MS = 25;
75
+ const DEFAULT_DESCRIPTION = "Run commands in a persistent PowerShell shell. State, including the current directory and exported environment variables, persists across calls for this agent.";
76
+ function maybeTruncate(content, maxOutputChars, incomplete = false) {
77
+ if (content.length <= maxOutputChars && !incomplete) return content;
78
+ return content.length <= maxOutputChars ? content + TRUNCATED_MESSAGE : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE;
79
+ }
80
+ function markers() {
81
+ const nonce = randomUUID();
82
+ return {
83
+ start: `__DSH_PERSISTENT_PWSH_START_${nonce}__`,
84
+ end: `__DSH_PERSISTENT_PWSH_END_${nonce}:`
85
+ };
86
+ }
87
+ /**
88
+ * Escape a command body for embedding in the wrapper's double-quoted string.
89
+ * Backtick escapes keep every character literal: backtick first so the
90
+ * escapes this function inserts are never re-escaped, `$` so no expansion
91
+ * happens at wrapper construction, and `\r\n`/ESC so multi-line commands and
92
+ * raw control bytes ride one physical input line without PSReadLine mangling.
93
+ * @param value - the model's PowerShell command text.
94
+ * @returns the escaped double-quoted-string body.
95
+ */
96
+ function quoteForPwsh(value) {
97
+ return value.replaceAll("`", "``").replaceAll("\"", "`\"").replaceAll("$", "`$").replaceAll("\r", "").replaceAll("\n", "`n").replaceAll("\x1B", "`e");
98
+ }
99
+ function wrapCommand(command, marker) {
100
+ const body = quoteForPwsh(command);
101
+ return `Write-Output '${marker.start}'; $LASTEXITCODE = $null; $__s = 1; try { Invoke-Expression "${body}"; $__ok = $? } catch { $__ok = $false }; if ($null -ne $LASTEXITCODE) { $__s = [int]$LASTEXITCODE } else { $__s = if ($__ok) { 0 } else { 1 } }; Write-Output ('${marker.end}' + $__s)`;
102
+ }
103
+ function stripPrompt(text) {
104
+ let result = text.replace(/\r?\n$/, "");
105
+ while (result.endsWith(SHELL_PROMPT)) result = result.slice(0, -31);
106
+ return result.endsWith("\n") ? result.slice(0, -1) : result;
107
+ }
108
+ function commandOutput(snapshot, marker, wrapper) {
109
+ const text = snapshot.text;
110
+ const end = text.lastIndexOf(marker.end);
111
+ const status = /^(\d+)\r?\n/.exec(text.slice(end + marker.end.length))?.[1];
112
+ if (status === void 0) return void 0;
113
+ const startMarker = text.lastIndexOf(marker.start, end);
114
+ const start = startMarker < 0 ? 0 : startMarker + marker.start.length;
115
+ let captured = text.slice(start, end);
116
+ captured = captured.replaceAll(wrapper, "");
117
+ return {
118
+ text: captured.replace(/^\r?\n/, "").replace(/\r?\n$/, ""),
119
+ incomplete: startMarker < 0,
120
+ exitCode: Number(status)
121
+ };
122
+ }
123
+ function promptCompleted(result) {
124
+ return result.viewport.endsWith(SHELL_PROMPT) || result.viewport.endsWith(`${SHELL_PROMPT}\r\n`) || result.viewport.endsWith(`${SHELL_PROMPT}\n`);
125
+ }
126
+ function partialOutput(snapshot, marker, wrapper, fallback, fallbackTruncated = false) {
127
+ const startMarker = snapshot.text.lastIndexOf(marker.start);
128
+ if (startMarker >= 0) return {
129
+ text: stripPrompt(snapshot.text.slice(startMarker + marker.start.length).replace(/^\r?\n/, "")),
130
+ incomplete: false
131
+ };
132
+ const fallbackStart = fallback.lastIndexOf(marker.start);
133
+ const afterStart = fallbackStart < 0 ? fallback : fallback.slice(fallbackStart + marker.start.length).replace(/^\r?\n/, "");
134
+ const fallbackEnd = afterStart.lastIndexOf(marker.end);
135
+ return {
136
+ text: stripPrompt((fallbackEnd < 0 ? afterStart : afterStart.slice(0, fallbackEnd)).replaceAll(SHELL_PROMPT, "").replaceAll(wrapper, "")),
137
+ incomplete: fallbackTruncated || fallbackStart < 0
138
+ };
139
+ }
140
+ async function pause() {
141
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
142
+ }
143
+ function nextScrollbackOffset(page, offset) {
144
+ if (page.text.length === 0 || page.lineEnd <= offset) return void 0;
145
+ return page.lineEnd;
146
+ }
147
+ function retainedScrollback(ctx, owner, id, latest = ctx.terminals.read(owner, id, {
148
+ offset: 0,
149
+ count: SCROLLBACK_PAGE_LINES
150
+ })) {
151
+ const pages = latest.text.length === 0 ? [] : [latest.text];
152
+ let offset = latest.lineEnd;
153
+ let truncated = latest.truncated;
154
+ while (true) {
155
+ if (offset >= latest.totalLines) break;
156
+ const page = ctx.terminals.read(owner, id, {
157
+ offset,
158
+ count: SCROLLBACK_PAGE_LINES
159
+ });
160
+ truncated ||= page.truncated;
161
+ if (page.text.length > 0) pages.unshift(page.text);
162
+ const next = nextScrollbackOffset(page, offset);
163
+ if (next === void 0 || next >= page.totalLines) break;
164
+ offset = next;
165
+ }
166
+ return {
167
+ text: pages.join("\n"),
168
+ truncated
169
+ };
170
+ }
171
+ function renderCaptured(output, maxOutputChars) {
172
+ const rendered = maybeTruncate(output.text, maxOutputChars, output.incomplete);
173
+ return appendStatusMarker(output.incomplete && output.text.length > 0 ? LOST_PREFIX_MESSAGE + rendered : rendered, output.exitCode !== void 0 && output.exitCode !== 0 ? `[exit code: ${output.exitCode}]` : void 0);
174
+ }
175
+ function appendStatusMarker(content, marker) {
176
+ if (marker === void 0) return content;
177
+ return content.length === 0 ? marker : `${content}\n${marker}`;
178
+ }
179
+ function renderShellExitStatus(content, exitCode, signal) {
180
+ return appendStatusMarker(content, signal !== null ? `[shell killed by signal: ${signal}]` : exitCode !== null ? `[shell exited: code ${exitCode}]` : "[shell exited]");
181
+ }
182
+ /**
183
+ * Render the exited-session result, reset the owner's shell, and reset the
184
+ * message that tells the model the next call starts fresh.
185
+ * @param shells - the owner-scoped registry to reset.
186
+ * @param status - the exited session status (exit code and signal).
187
+ * @returns the complete model-facing result.
188
+ */
189
+ async function respondToSessionExit(ctx, shells, owner, id, status, marker, wrapped, fallback, fallbackTruncated, config) {
190
+ const snapshot = retainedScrollback(ctx, owner, id);
191
+ await shells.reset(owner, "persistent pwsh shell exited");
192
+ return [renderShellExitStatus(renderCaptured(partialOutput(snapshot, marker, wrapped, fallback, fallbackTruncated), config.maxOutputChars), status.exitCode, status.signal), SHELL_RESET_MESSAGE].filter((part) => part.length > 0).join("\n");
193
+ }
194
+ /**
195
+ * The pwsh prompt function that overrides the backend bootstrap value with
196
+ * this tool's own prompt. `[char]27`/`[char]7` build the OSC bytes at runtime
197
+ * because raw ESC characters in submitted input are unreliable under
198
+ * PSReadLine.
199
+ */
200
+ const PWSH_PROMPT_SETUP = "function prompt { [Console]::Write([char]27 + ']133;D;' + [int]$LASTEXITCODE + [char]7); '__DSH_PERSISTENT_PWSH_PROMPT__ ' }";
201
+ function persistentShells(ctx, config) {
202
+ const pending = /* @__PURE__ */ new WeakMap();
203
+ const live = /* @__PURE__ */ new Map();
204
+ const creating = /* @__PURE__ */ new Set();
205
+ const ownerCleanupInstalled = /* @__PURE__ */ new WeakSet();
206
+ const lifecycle = new AbortController();
207
+ const close = async (owner, id, reason) => {
208
+ if (!ctx.terminals.list(owner).some((snapshot) => snapshot.sessionId === id)) return;
209
+ await ctx.terminals.kill(owner, id, reason);
210
+ };
211
+ ctx.effect(() => async () => {
212
+ lifecycle.abort(/* @__PURE__ */ new Error("tool-pwsh-persistent disposed during shell creation"));
213
+ await Promise.allSettled([...creating]);
214
+ const closing = [...live].map(async ([owner, id]) => {
215
+ await close(owner, id, "tool-pwsh-persistent disposed");
216
+ });
217
+ await Promise.all(closing);
218
+ live.clear();
219
+ }, "tool-pwsh-persistent shell cleanup");
220
+ const reset = async (owner, reason) => {
221
+ pending.delete(owner);
222
+ const id = live.get(owner);
223
+ live.delete(owner);
224
+ if (id !== void 0) await close(owner, id, reason);
225
+ };
226
+ const get = (owner, signal) => {
227
+ const existing = pending.get(owner);
228
+ if (existing !== void 0) return existing;
229
+ const combinedSignal = AbortSignal.any([signal, lifecycle.signal]);
230
+ const tracked = (async () => {
231
+ try {
232
+ const cwd = owner.session.header.cwd;
233
+ const spawned = await ctx.terminals.spawn(owner, {
234
+ type: config.backendType,
235
+ ...cwd === void 0 ? {} : { cwd }
236
+ }, combinedSignal);
237
+ live.set(owner, spawned.sessionId);
238
+ if (!ownerCleanupInstalled.has(owner)) {
239
+ ownerCleanupInstalled.add(owner);
240
+ owner.ctx.effect(() => () => {
241
+ pending.delete(owner);
242
+ live.delete(owner);
243
+ }, "tool-pwsh-persistent owner cache cleanup");
244
+ }
245
+ const result = await ctx.terminals.startSend(owner, spawned.sessionId, {
246
+ text: PWSH_PROMPT_SETUP,
247
+ submit: true,
248
+ signal: combinedSignal
249
+ }).done;
250
+ if (result.sessionStatus.kind === "exited" || result.waitReason === "timeout") throw new Error("persistent pwsh shell did not accept initialization");
251
+ return spawned.sessionId;
252
+ } catch (error) {
253
+ await reset(owner, "persistent pwsh initialization failed");
254
+ throw error;
255
+ }
256
+ })().finally(() => {
257
+ creating.delete(tracked);
258
+ });
259
+ creating.add(tracked);
260
+ pending.set(owner, tracked);
261
+ return tracked;
262
+ };
263
+ return {
264
+ get,
265
+ reset
266
+ };
267
+ }
268
+ async function executeCommand(ctx, shells, owner, command, config, upstream) {
269
+ const env_1 = {
270
+ stack: [],
271
+ error: void 0,
272
+ hasError: false
273
+ };
274
+ try {
275
+ const commandDeadline = __addDisposableResource(env_1, deadline(upstream, config.timeoutMs, TIMEOUT_CODE), false);
276
+ const id = await shells.get(owner, commandDeadline.signal);
277
+ const marker = markers();
278
+ const wrapped = wrapCommand(command, marker);
279
+ let first = true;
280
+ let fallback = "";
281
+ let fallbackTruncated = false;
282
+ while (true) {
283
+ const status = ctx.terminals.list(owner).find((session) => session.sessionId === id)?.status;
284
+ if (status?.kind === "exited") return await respondToSessionExit(ctx, shells, owner, id, status, marker, wrapped, fallback, fallbackTruncated, config);
285
+ let operation;
286
+ let result;
287
+ try {
288
+ operation = ctx.terminals.startSend(owner, id, {
289
+ text: first ? wrapped : "",
290
+ submit: first,
291
+ signal: commandDeadline.signal
292
+ });
293
+ first = false;
294
+ result = await operation.done;
295
+ } catch (error) {
296
+ await shells.reset(owner, "persistent pwsh send failed");
297
+ throw error;
298
+ }
299
+ const incremental = operation.readOutput();
300
+ fallback = incremental.delta.length > 0 ? fallback + incremental.delta : result.viewport;
301
+ fallbackTruncated ||= incremental.truncated || result.truncated;
302
+ const latest = ctx.terminals.read(owner, id, {
303
+ offset: 0,
304
+ count: SCROLLBACK_PAGE_LINES
305
+ });
306
+ const timedOut = timeoutOf(commandDeadline.signal, TIMEOUT_CODE);
307
+ if (timedOut !== void 0) {
308
+ const partial = renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, wrapped, fallback, fallbackTruncated), config.maxOutputChars);
309
+ await shells.reset(owner, "persistent pwsh command timed out");
310
+ return [
311
+ `Your command timed out after ${Math.round(timedOut.timeoutMs / 1e3)} seconds or experienced an OOM error. Below is partial output:`,
312
+ partial,
313
+ SHELL_RESET_MESSAGE
314
+ ].join("\n");
315
+ }
316
+ if (commandDeadline.signal.aborted) {
317
+ await shells.reset(owner, "persistent pwsh command aborted");
318
+ commandDeadline.signal.throwIfAborted();
319
+ }
320
+ if (latest.text.includes(marker.end)) {
321
+ const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker, wrapped);
322
+ if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars);
323
+ }
324
+ if (result.sessionStatus.kind === "exited") return await respondToSessionExit(ctx, shells, owner, id, result.sessionStatus, marker, wrapped, fallback, fallbackTruncated, config);
325
+ if (promptCompleted(result)) return renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, wrapped, fallback, fallbackTruncated), config.maxOutputChars);
326
+ await pause();
327
+ }
328
+ } catch (e_1) {
329
+ env_1.error = e_1;
330
+ env_1.hasError = true;
331
+ } finally {
332
+ __disposeResources(env_1);
333
+ }
334
+ }
335
+ /**
336
+ * Register the model-facing persistent `pwsh` tool.
337
+ * @param ctx - plugin context carrying tools and the owner-scoped PTY service.
338
+ * @param config - selected PTY backend and command deadline.
339
+ */
340
+ function registerPersistentPwsh(ctx, config) {
341
+ const shells = persistentShells(ctx, config);
342
+ const queues = /* @__PURE__ */ new WeakMap();
343
+ const serialized = async (owner, operation) => {
344
+ const run = (queues.get(owner) ?? Promise.resolve()).then(operation, operation);
345
+ const tail = run.then(() => void 0, () => void 0);
346
+ queues.set(owner, tail);
347
+ try {
348
+ return await run;
349
+ } finally {
350
+ if (queues.get(owner) === tail) queues.delete(owner);
351
+ }
352
+ };
353
+ ctx.tools.register(defineTool({
354
+ name: "pwsh",
355
+ description: config.description,
356
+ parameters: { command: {
357
+ type: "string",
358
+ required: true,
359
+ description: "The PowerShell command to run. Relative path is preferred in the command."
360
+ } },
361
+ output: {
362
+ schema: { type: "string" },
363
+ render: (_args, value) => [{
364
+ type: "text",
365
+ text: value
366
+ }]
367
+ },
368
+ async execute(args, exec) {
369
+ if (args.command.trim().length === 0) throw new Error("command must be a non-empty string");
370
+ const owner = exec.agent;
371
+ if (owner === void 0) throw new Error("pwsh requires an owning agent session");
372
+ return serialized(owner, async () => {
373
+ exec.signal.throwIfAborted();
374
+ return executeCommand(ctx, shells, owner, args.command, config, exec.signal);
375
+ });
376
+ },
377
+ presentCall: (args) => ({
378
+ card: "terminal",
379
+ title: args.command
380
+ })
381
+ }));
382
+ }
383
+ const name = "tool-pwsh-persistent";
384
+ const inject = ["tools", "terminals"];
385
+ /** Runtime configuration schema for the persistent pwsh tool. */
386
+ const Config = z.object({
387
+ backendType: z.string().default("shell"),
388
+ timeoutMs: z.number().default(3e5),
389
+ maxOutputChars: z.number().default(16e3),
390
+ description: z.string().default(DEFAULT_DESCRIPTION)
391
+ });
392
+ /** Register one owner-scoped persistent `pwsh` tool. */
393
+ function apply(ctx, config) {
394
+ const resolved = {
395
+ backendType: config.backendType ?? "shell",
396
+ timeoutMs: config.timeoutMs ?? 3e5,
397
+ maxOutputChars: config.maxOutputChars ?? 16e3,
398
+ description: config.description ?? DEFAULT_DESCRIPTION
399
+ };
400
+ if (resolved.backendType.trim().length === 0) throw new Error("tool-pwsh-persistent: backendType must be non-empty");
401
+ if (!Number.isSafeInteger(resolved.timeoutMs) || resolved.timeoutMs <= 0) throw new Error("tool-pwsh-persistent: timeoutMs must be a positive safe integer");
402
+ if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) throw new Error("tool-pwsh-persistent: maxOutputChars must be a positive safe integer");
403
+ if (resolved.description.trim().length === 0) throw new Error("tool-pwsh-persistent: description must be non-empty");
404
+ registerPersistentPwsh(ctx, resolved);
405
+ }
406
+ //#endregion
407
+ export { Config, apply, inject, name };
@@ -0,0 +1,24 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-tool-pwsh-persistent`.
4
+ * @module @deepseek-ai/dsh-tool-pwsh-persistent/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-tool-pwsh-persistent";
7
+ /** Cordis companion plugin name. */
8
+ const name = "tool-pwsh-persistent-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: the adapter's private owner-to-shell cache has no
13
+ * observable event or data relation. Lifecycle tests prove its cleanup without
14
+ * adding a public API solely for an invariant.
15
+ */
16
+ const install = () => {};
17
+ /**
18
+ * Register this package's invariant companion.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
23
+ //#endregion
24
+ export { apply, inject, name };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Model-facing persistent `pwsh` tool over the owner-scoped PTY seam.
3
+ * @module @deepseek-ai/dsh-tool-pwsh-persistent
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ import z from '@deepseek-ai/schemastery';
7
+ export declare const name = "tool-pwsh-persistent";
8
+ export declare const inject: string[];
9
+ /** Configuration for the persistent pwsh tool. */
10
+ export interface Config {
11
+ /** PTY backend used for each owner-isolated persistent shell (default `shell`). */
12
+ backendType?: string;
13
+ /** Wall-clock limit for one command (default 300000). */
14
+ timeoutMs?: number;
15
+ /** Maximum returned command-output characters before clipping (default 16000). */
16
+ maxOutputChars?: number;
17
+ /** Model-facing tool description; deployments may describe their environment. */
18
+ description?: string;
19
+ }
20
+ /** Runtime configuration schema for the persistent pwsh tool. */
21
+ export declare const Config: z<Config>;
22
+ /** Register one owner-scoped persistent `pwsh` tool. */
23
+ export declare function apply(ctx: Context, config: Config): void;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-tool-pwsh-persistent`.
3
+ * @module @deepseek-ai/dsh-tool-pwsh-persistent/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "tool-pwsh-persistent-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-tool-pwsh-persistent",
3
+ "description": "Model-facing owner-scoped persistent PowerShell tool backed by the Harness PTY service",
4
+ "version": "0.1.0-rc.8",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/shell/tool-pwsh-persistent"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/invariant.js",
30
+ "lib/types/**/*.d.ts"
31
+ ],
32
+ "license": "MIT",
33
+ "peerDependencies": {
34
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
35
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
36
+ "@deepseek-ai/dsh-terminal": "^0.1.0-rc.8",
37
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
38
+ "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
39
+ "@deepseek-ai/cordis": "^4.0.1"
40
+ },
41
+ "dependencies": {
42
+ "@deepseek-ai/schemastery": "^3.18.1"
43
+ },
44
+ "devDependencies": {
45
+ "@deepseek-ai/cordis-plugin-include": "^1.0.6",
46
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
47
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
48
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
49
+ "@deepseek-ai/dsh-terminal": "^0.1.0-rc.8",
50
+ "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8",
51
+ "@deepseek-ai/dsh-pwsh-local": "^0.1.0-rc.8",
52
+ "@deepseek-ai/dsh-terminal-bash": "^0.1.0-rc.8",
53
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8",
54
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
55
+ "@deepseek-ai/dsh-subprocess-local": "^0.1.0-rc.8",
56
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
57
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
58
+ "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
59
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
60
+ "@deepseek-ai/cordis": "^4.0.1"
61
+ }
62
+ }