@deepseek-ai/dsh-tool-bash-persistent 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +50 -0
- package/README.zh.md +50 -0
- package/lib/index.js +378 -0
- package/lib/invariant.js +24 -0
- package/lib/types/index.d.ts +24 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -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/pty/tool-bash-persistent/README.md
|
|
5
|
+
README.md: cfb3acf41803f5b06ecb3f56ee8ffc29ce9147b1
|
|
6
|
+
README.zh.md: cf35a212159e7d12e1bdaa146b32b397d916a912
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-tool-bash-persistent
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Model-facing `bash(command)` backed by one owner-scoped `ctx.pty` shell. The package owns the tool contract and shell reuse; deployments select the PTY backend and sandbox policy.
|
|
6
|
+
|
|
7
|
+
## Config
|
|
8
|
+
|
|
9
|
+
| Key | Default | Meaning |
|
|
10
|
+
|---|---:|---|
|
|
11
|
+
| `backendType` | `shell` | Registered PTY 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 [`bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash-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 `bash` 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, exported variables, activated environments, functions, and background jobs persist across calls. Results exclude private completion markers and the shell prompt. A nonzero wrapped command appends `[exit code: N]`; 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, 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 PTY has already dropped that prefix, the result says so explicitly instead of presenting a tail as complete output. 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 PTY backend.
|
|
49
|
+
- Explicit `exit` and timeout discard shell state. Cancellation also resets and discards the result, even when a complete status marker is already observable; the next call starts a fresh shell.
|
|
50
|
+
- Environment facts such as network access and package mirrors belong in the configured `description`, not this package's default.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-tool-bash-persistent
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
模型可见的 `bash(command)`,底层复用一个按所有者隔离的 `ctx.pty` shell。该包拥有工具约定和 shell 复用;PTY 后端与沙箱策略由部署选择。
|
|
6
|
+
|
|
7
|
+
## 配置
|
|
8
|
+
|
|
9
|
+
| 键 | 默认值 | 含义 |
|
|
10
|
+
|---|---:|---|
|
|
11
|
+
| `backendType` | `shell` | 每个 Agent shell 使用的已注册 PTY 后端。 |
|
|
12
|
+
| `timeoutMs` | `300000` | 单条命令的墙钟时间上限;超时会关闭 shell。 |
|
|
13
|
+
| `maxOutputChars` | `16000` | 命令输出最多保留的字符数;固定诊断会在此后追加。 |
|
|
14
|
+
| `description` | 持久 shell 描述 | 面向模型的环境约定。 |
|
|
15
|
+
|
|
16
|
+
## 模型体验
|
|
17
|
+
|
|
18
|
+
### 工具 schema
|
|
19
|
+
|
|
20
|
+
#### 模型所见
|
|
21
|
+
|
|
22
|
+
生成的 [`bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash-persistent),其中包含配置的 `description`。本插件不贡献独立系统提示词段;persona 与环境指导由部署负责。
|
|
23
|
+
|
|
24
|
+
#### Token 影响
|
|
25
|
+
|
|
26
|
+
`bash` 可见时产生固定的 schema 成本。
|
|
27
|
+
|
|
28
|
+
#### KV Cache 影响
|
|
29
|
+
|
|
30
|
+
配置的描述与 schema 不变时前缀稳定。
|
|
31
|
+
|
|
32
|
+
### 工具结果
|
|
33
|
+
|
|
34
|
+
#### 模型所见
|
|
35
|
+
|
|
36
|
+
每个 Agent 的命令共享一个 shell,因此 cwd、导出的环境变量、已激活环境、函数和后台任务会跨调用保留。结果不包含私有完成标记和 shell 提示符。经封装的命令以非零状态结束时,结果会追加 `[exit code: N]`;若 shell 在报告该状态前退出,则改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或在后端既未提供退出码也未提供信号时追加 `[shell exited]`;随后重置 shell,并告知模型下次调用从新 shell 开始。长输出保留仍可读取的最早前缀并追加截断提示;若 PTY 已丢弃真正的开头,结果会明确说明,而不是把尾部伪装成完整输出。超时返回有界的部分输出、关闭状态不确定的 shell,并报告该重置。
|
|
37
|
+
|
|
38
|
+
#### Token 影响
|
|
39
|
+
|
|
40
|
+
随数据变化。`maxOutputChars` 限制保留的命令输出;固定的截断、前缀丢失、状态、超时与重置诊断可能使结果更长。
|
|
41
|
+
|
|
42
|
+
#### KV Cache 影响
|
|
43
|
+
|
|
44
|
+
工具结果以追加方式位于可复用请求前缀之后。
|
|
45
|
+
|
|
46
|
+
## 已知限制与延后工作
|
|
47
|
+
|
|
48
|
+
- 工具需要拥有它的 Agent 和真实 PTY 后端。
|
|
49
|
+
- 显式 `exit` 与超时会丢弃 shell 状态。取消同样会重置 shell 并丢弃结果,即使已经能观察到完整状态标记也是如此;下次调用创建新 shell。
|
|
50
|
+
- 网络访问、软件包镜像等环境事实应写入配置的 `description`,而非包默认描述。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
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 `bash` tool over the owner-scoped PTY seam.
|
|
8
|
+
* @module @deepseek-ai/dsh-tool-bash-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 `grep -n` 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 bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment.";
|
|
71
|
+
const SHELL_PROMPT = "__DSH_PERSISTENT_BASH_PROMPT__ ";
|
|
72
|
+
const TIMEOUT_CODE = "PERSISTENT_BASH_TIMEOUT";
|
|
73
|
+
const SCROLLBACK_PAGE_LINES = 1e3;
|
|
74
|
+
const POLL_INTERVAL_MS = 25;
|
|
75
|
+
const DEFAULT_DESCRIPTION = "Run commands in a persistent bash 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_BASH_START_${nonce}__`,
|
|
84
|
+
end: `__DSH_PERSISTENT_BASH_END_${nonce}:`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function quoteForBash(value) {
|
|
88
|
+
return `$'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'").replaceAll("\r", "\\r").replaceAll("\n", "\\n")}'`;
|
|
89
|
+
}
|
|
90
|
+
function wrapCommand(command, marker) {
|
|
91
|
+
return `printf '%s\\n' ${quoteForBash(marker.start)}; eval -- ${quoteForBash(command)}; __dsh_persistent_bash_status=$?; printf '%s%s\\n' ${quoteForBash(marker.end)} "$__dsh_persistent_bash_status"`;
|
|
92
|
+
}
|
|
93
|
+
function stripPrompt(text) {
|
|
94
|
+
let result = text.replace(/\r?\n$/, "");
|
|
95
|
+
while (result.endsWith(SHELL_PROMPT)) result = result.slice(0, -31);
|
|
96
|
+
return result.endsWith("\n") ? result.slice(0, -1) : result;
|
|
97
|
+
}
|
|
98
|
+
function commandOutput(snapshot, marker) {
|
|
99
|
+
const text = snapshot.text;
|
|
100
|
+
const end = text.lastIndexOf(marker.end);
|
|
101
|
+
const status = /^(\d+)\r?\n/.exec(text.slice(end + marker.end.length))?.[1];
|
|
102
|
+
if (status === void 0) return void 0;
|
|
103
|
+
const startMarker = text.lastIndexOf(marker.start, end);
|
|
104
|
+
const start = startMarker < 0 ? 0 : startMarker + marker.start.length;
|
|
105
|
+
return {
|
|
106
|
+
text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, "")),
|
|
107
|
+
incomplete: startMarker < 0,
|
|
108
|
+
exitCode: Number(status)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function promptCompleted(result) {
|
|
112
|
+
return result.viewport.endsWith(SHELL_PROMPT) || result.viewport.endsWith(`${SHELL_PROMPT}\r\n`) || result.viewport.endsWith(`${SHELL_PROMPT}\n`);
|
|
113
|
+
}
|
|
114
|
+
function partialOutput(snapshot, marker, fallback, fallbackTruncated = false) {
|
|
115
|
+
const startMarker = snapshot.text.lastIndexOf(marker.start);
|
|
116
|
+
if (startMarker >= 0) return {
|
|
117
|
+
text: stripPrompt(snapshot.text.slice(startMarker + marker.start.length).replace(/^\r?\n/, "")),
|
|
118
|
+
incomplete: false
|
|
119
|
+
};
|
|
120
|
+
const fallbackStart = fallback.lastIndexOf(marker.start);
|
|
121
|
+
const afterStart = fallbackStart < 0 ? fallback : fallback.slice(fallbackStart + marker.start.length).replace(/^\r?\n/, "");
|
|
122
|
+
const fallbackEnd = afterStart.lastIndexOf(marker.end);
|
|
123
|
+
return {
|
|
124
|
+
text: stripPrompt((fallbackEnd < 0 ? afterStart : afterStart.slice(0, fallbackEnd)).replaceAll(SHELL_PROMPT, "")),
|
|
125
|
+
incomplete: fallbackTruncated || fallbackStart < 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function pause() {
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
130
|
+
}
|
|
131
|
+
function nextScrollbackOffset(page, offset) {
|
|
132
|
+
if (page.text.length === 0 || page.lineEnd <= offset) return void 0;
|
|
133
|
+
return page.lineEnd;
|
|
134
|
+
}
|
|
135
|
+
function retainedScrollback(ctx, owner, id, latest = ctx.pty.read(owner, id, {
|
|
136
|
+
offset: 0,
|
|
137
|
+
count: SCROLLBACK_PAGE_LINES
|
|
138
|
+
})) {
|
|
139
|
+
const pages = latest.text.length === 0 ? [] : [latest.text];
|
|
140
|
+
let offset = latest.lineEnd;
|
|
141
|
+
let truncated = latest.truncated;
|
|
142
|
+
while (true) {
|
|
143
|
+
if (offset >= latest.totalLines) break;
|
|
144
|
+
const page = ctx.pty.read(owner, id, {
|
|
145
|
+
offset,
|
|
146
|
+
count: SCROLLBACK_PAGE_LINES
|
|
147
|
+
});
|
|
148
|
+
truncated ||= page.truncated;
|
|
149
|
+
if (page.text.length > 0) pages.unshift(page.text);
|
|
150
|
+
const next = nextScrollbackOffset(page, offset);
|
|
151
|
+
if (next === void 0 || next >= page.totalLines) break;
|
|
152
|
+
offset = next;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
text: pages.join("\n"),
|
|
156
|
+
truncated
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function renderCaptured(output, maxOutputChars) {
|
|
160
|
+
const rendered = maybeTruncate(output.text, maxOutputChars, output.incomplete);
|
|
161
|
+
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);
|
|
162
|
+
}
|
|
163
|
+
function appendStatusMarker(content, marker) {
|
|
164
|
+
if (marker === void 0) return content;
|
|
165
|
+
return content.length === 0 ? marker : `${content}\n${marker}`;
|
|
166
|
+
}
|
|
167
|
+
function renderShellExitStatus(content, exitCode, signal) {
|
|
168
|
+
return appendStatusMarker(content, signal !== null ? `[shell killed by signal: ${signal}]` : exitCode !== null ? `[shell exited: code ${exitCode}]` : "[shell exited]");
|
|
169
|
+
}
|
|
170
|
+
function persistentShells(ctx, config) {
|
|
171
|
+
const pending = /* @__PURE__ */ new WeakMap();
|
|
172
|
+
const live = /* @__PURE__ */ new Map();
|
|
173
|
+
const creating = /* @__PURE__ */ new Set();
|
|
174
|
+
const ownerCleanupInstalled = /* @__PURE__ */ new WeakSet();
|
|
175
|
+
const lifecycle = new AbortController();
|
|
176
|
+
const close = async (owner, id, reason) => {
|
|
177
|
+
if (!ctx.pty.list(owner).some((snapshot) => snapshot.sessionId === id)) return;
|
|
178
|
+
await ctx.pty.kill(owner, id, reason);
|
|
179
|
+
};
|
|
180
|
+
ctx.effect(() => async () => {
|
|
181
|
+
lifecycle.abort(/* @__PURE__ */ new Error("tool-bash-persistent disposed during shell creation"));
|
|
182
|
+
await Promise.allSettled([...creating]);
|
|
183
|
+
const closing = [...live].map(async ([owner, id]) => {
|
|
184
|
+
await close(owner, id, "tool-bash-persistent disposed");
|
|
185
|
+
});
|
|
186
|
+
await Promise.all(closing);
|
|
187
|
+
live.clear();
|
|
188
|
+
}, "tool-bash-persistent shell cleanup");
|
|
189
|
+
const reset = async (owner, reason) => {
|
|
190
|
+
pending.delete(owner);
|
|
191
|
+
const id = live.get(owner);
|
|
192
|
+
live.delete(owner);
|
|
193
|
+
if (id !== void 0) await close(owner, id, reason);
|
|
194
|
+
};
|
|
195
|
+
const get = (owner, signal) => {
|
|
196
|
+
const existing = pending.get(owner);
|
|
197
|
+
if (existing !== void 0) return existing;
|
|
198
|
+
const combinedSignal = AbortSignal.any([signal, lifecycle.signal]);
|
|
199
|
+
const tracked = (async () => {
|
|
200
|
+
try {
|
|
201
|
+
const cwd = owner.session.header.cwd;
|
|
202
|
+
const spawned = await ctx.pty.spawn(owner, {
|
|
203
|
+
type: config.backendType,
|
|
204
|
+
...cwd === void 0 ? {} : { cwd }
|
|
205
|
+
}, combinedSignal);
|
|
206
|
+
live.set(owner, spawned.sessionId);
|
|
207
|
+
if (!ownerCleanupInstalled.has(owner)) {
|
|
208
|
+
ownerCleanupInstalled.add(owner);
|
|
209
|
+
owner.ctx.effect(() => () => {
|
|
210
|
+
pending.delete(owner);
|
|
211
|
+
live.delete(owner);
|
|
212
|
+
}, "tool-bash-persistent owner cache cleanup");
|
|
213
|
+
}
|
|
214
|
+
const result = await ctx.pty.startSend(owner, spawned.sessionId, {
|
|
215
|
+
text: `stty -echo; PS1=${quoteForBash(SHELL_PROMPT)}`,
|
|
216
|
+
submit: true,
|
|
217
|
+
signal: combinedSignal
|
|
218
|
+
}).done;
|
|
219
|
+
if (result.sessionStatus.kind === "exited" || result.waitReason === "timeout") throw new Error("persistent bash shell did not accept initialization");
|
|
220
|
+
return spawned.sessionId;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
await reset(owner, "persistent bash initialization failed");
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
})().finally(() => {
|
|
226
|
+
creating.delete(tracked);
|
|
227
|
+
});
|
|
228
|
+
creating.add(tracked);
|
|
229
|
+
pending.set(owner, tracked);
|
|
230
|
+
return tracked;
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
get,
|
|
234
|
+
reset
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function executeCommand(ctx, shells, owner, command, config, upstream) {
|
|
238
|
+
const env_1 = {
|
|
239
|
+
stack: [],
|
|
240
|
+
error: void 0,
|
|
241
|
+
hasError: false
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
const commandDeadline = __addDisposableResource(env_1, deadline(upstream, config.timeoutMs, TIMEOUT_CODE), false);
|
|
245
|
+
const id = await shells.get(owner, commandDeadline.signal);
|
|
246
|
+
const marker = markers();
|
|
247
|
+
const wrapped = wrapCommand(command, marker);
|
|
248
|
+
let first = true;
|
|
249
|
+
let fallback = "";
|
|
250
|
+
let fallbackTruncated = false;
|
|
251
|
+
while (true) {
|
|
252
|
+
let operation;
|
|
253
|
+
let result;
|
|
254
|
+
try {
|
|
255
|
+
operation = ctx.pty.startSend(owner, id, {
|
|
256
|
+
text: first ? wrapped : "",
|
|
257
|
+
submit: first,
|
|
258
|
+
signal: commandDeadline.signal
|
|
259
|
+
});
|
|
260
|
+
first = false;
|
|
261
|
+
result = await operation.done;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
await shells.reset(owner, "persistent bash send failed");
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
const incremental = operation.readOutput();
|
|
267
|
+
fallback = incremental.delta.length > 0 ? fallback + incremental.delta : result.viewport;
|
|
268
|
+
fallbackTruncated ||= incremental.truncated || result.truncated;
|
|
269
|
+
const latest = ctx.pty.read(owner, id, {
|
|
270
|
+
offset: 0,
|
|
271
|
+
count: SCROLLBACK_PAGE_LINES
|
|
272
|
+
});
|
|
273
|
+
const timedOut = timeoutOf(commandDeadline.signal, TIMEOUT_CODE);
|
|
274
|
+
if (timedOut !== void 0) {
|
|
275
|
+
const partial = renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, fallback, fallbackTruncated), config.maxOutputChars);
|
|
276
|
+
await shells.reset(owner, "persistent bash command timed out");
|
|
277
|
+
return [
|
|
278
|
+
`Your command timed out after ${Math.round(timedOut.timeoutMs / 1e3)} seconds or experienced an OOM error. Below is partial output:`,
|
|
279
|
+
partial,
|
|
280
|
+
SHELL_RESET_MESSAGE
|
|
281
|
+
].join("\n");
|
|
282
|
+
}
|
|
283
|
+
if (commandDeadline.signal.aborted) {
|
|
284
|
+
await shells.reset(owner, "persistent bash command aborted");
|
|
285
|
+
commandDeadline.signal.throwIfAborted();
|
|
286
|
+
}
|
|
287
|
+
if (latest.text.includes(marker.end)) {
|
|
288
|
+
const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker);
|
|
289
|
+
if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars);
|
|
290
|
+
}
|
|
291
|
+
if (result.sessionStatus.kind === "exited") {
|
|
292
|
+
const snapshot = retainedScrollback(ctx, owner, id, latest);
|
|
293
|
+
await shells.reset(owner, "persistent bash shell exited");
|
|
294
|
+
return [renderShellExitStatus(renderCaptured(partialOutput(snapshot, marker, fallback, fallbackTruncated), config.maxOutputChars), result.sessionStatus.exitCode, result.sessionStatus.signal), SHELL_RESET_MESSAGE].filter((part) => part.length > 0).join("\n");
|
|
295
|
+
}
|
|
296
|
+
if (promptCompleted(result)) return renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, fallback, fallbackTruncated), config.maxOutputChars);
|
|
297
|
+
await pause();
|
|
298
|
+
}
|
|
299
|
+
} catch (e_1) {
|
|
300
|
+
env_1.error = e_1;
|
|
301
|
+
env_1.hasError = true;
|
|
302
|
+
} finally {
|
|
303
|
+
__disposeResources(env_1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Register the model-facing persistent `bash` tool.
|
|
308
|
+
* @param ctx - plugin context carrying tools and the owner-scoped PTY service.
|
|
309
|
+
* @param config - selected PTY backend and command deadline.
|
|
310
|
+
*/
|
|
311
|
+
function registerPersistentBash(ctx, config) {
|
|
312
|
+
const shells = persistentShells(ctx, config);
|
|
313
|
+
const queues = /* @__PURE__ */ new WeakMap();
|
|
314
|
+
const serialized = async (owner, operation) => {
|
|
315
|
+
const run = (queues.get(owner) ?? Promise.resolve()).then(operation, operation);
|
|
316
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
317
|
+
queues.set(owner, tail);
|
|
318
|
+
try {
|
|
319
|
+
return await run;
|
|
320
|
+
} finally {
|
|
321
|
+
if (queues.get(owner) === tail) queues.delete(owner);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
ctx.tools.register(defineTool({
|
|
325
|
+
name: "bash",
|
|
326
|
+
description: config.description,
|
|
327
|
+
parameters: { command: {
|
|
328
|
+
type: "string",
|
|
329
|
+
required: true,
|
|
330
|
+
description: "The bash command to run. Relative path is preferred in the command."
|
|
331
|
+
} },
|
|
332
|
+
output: {
|
|
333
|
+
schema: { type: "string" },
|
|
334
|
+
render: (_args, value) => [{
|
|
335
|
+
type: "text",
|
|
336
|
+
text: value
|
|
337
|
+
}]
|
|
338
|
+
},
|
|
339
|
+
async execute(args, exec) {
|
|
340
|
+
if (args.command.trim().length === 0) throw new Error("command must be a non-empty string");
|
|
341
|
+
const owner = exec.agent;
|
|
342
|
+
if (owner === void 0) throw new Error("bash requires an owning agent session");
|
|
343
|
+
return serialized(owner, async () => {
|
|
344
|
+
exec.signal.throwIfAborted();
|
|
345
|
+
return executeCommand(ctx, shells, owner, args.command, config, exec.signal);
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
presentCall: (args) => ({
|
|
349
|
+
card: "terminal",
|
|
350
|
+
title: args.command
|
|
351
|
+
})
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
const name = "tool-bash-persistent";
|
|
355
|
+
const inject = ["tools", "pty"];
|
|
356
|
+
/** Runtime configuration schema for the persistent Bash tool. */
|
|
357
|
+
const Config = z.object({
|
|
358
|
+
backendType: z.string().default("shell"),
|
|
359
|
+
timeoutMs: z.number().default(3e5),
|
|
360
|
+
maxOutputChars: z.number().default(16e3),
|
|
361
|
+
description: z.string().default(DEFAULT_DESCRIPTION)
|
|
362
|
+
});
|
|
363
|
+
/** Register one owner-scoped persistent `bash` tool. */
|
|
364
|
+
function apply(ctx, config) {
|
|
365
|
+
const resolved = {
|
|
366
|
+
backendType: config.backendType ?? "shell",
|
|
367
|
+
timeoutMs: config.timeoutMs ?? 3e5,
|
|
368
|
+
maxOutputChars: config.maxOutputChars ?? 16e3,
|
|
369
|
+
description: config.description ?? DEFAULT_DESCRIPTION
|
|
370
|
+
};
|
|
371
|
+
if (resolved.backendType.trim().length === 0) throw new Error("tool-bash-persistent: backendType must be non-empty");
|
|
372
|
+
if (!Number.isSafeInteger(resolved.timeoutMs) || resolved.timeoutMs <= 0) throw new Error("tool-bash-persistent: timeoutMs must be a positive safe integer");
|
|
373
|
+
if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) throw new Error("tool-bash-persistent: maxOutputChars must be a positive safe integer");
|
|
374
|
+
if (resolved.description.trim().length === 0) throw new Error("tool-bash-persistent: description must be non-empty");
|
|
375
|
+
registerPersistentBash(ctx, resolved);
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-bash-persistent`.
|
|
4
|
+
* @module @deepseek-ai/dsh-tool-bash-persistent/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-tool-bash-persistent";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "tool-bash-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 surface 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 `bash` tool over the owner-scoped PTY seam.
|
|
3
|
+
* @module @deepseek-ai/dsh-tool-bash-persistent
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
export declare const name = "tool-bash-persistent";
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
/** Configuration for the persistent Bash 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 Bash tool. */
|
|
21
|
+
export declare const Config: z<Config>;
|
|
22
|
+
/** Register one owner-scoped persistent `bash` 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-bash-persistent`.
|
|
3
|
+
* @module @deepseek-ai/dsh-tool-bash-persistent/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "tool-bash-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,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-tool-bash-persistent",
|
|
3
|
+
"description": "Model-facing owner-scoped persistent Bash tool backed by the Harness PTY service",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/pty/tool-bash-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": "BSD-3-Clause",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
35
|
+
"@deepseek-ai/dsh-pty": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.5-rc.1",
|
|
46
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-pty": "^0.0.1-rc.1",
|
|
51
|
+
"@deepseek-ai/dsh-pty-local": "^0.0.1-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.0.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
59
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
60
|
+
}
|
|
61
|
+
}
|