dsh-agent-lines 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +40 -0
- package/CHANGELOG.md +39 -0
- package/CODE_OF_CONDUCT.en.md +31 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.en.md +54 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +55 -0
- package/NOTICE +4 -0
- package/README.en.md +317 -0
- package/README.md +310 -2
- package/SECURITY.en.md +7 -0
- package/SECURITY.md +7 -0
- package/bin/dsh-agent-lines.js +155 -0
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.en.md +68 -0
- package/docs/ARCHITECTURE.md +59 -0
- package/docs/DEPLOY.en.md +91 -0
- package/docs/DEPLOY.md +83 -0
- package/docs/PROTOCOL.en.md +144 -0
- package/docs/PROTOCOL.md +136 -0
- package/docs/SECURITY.en.md +59 -0
- package/docs/SECURITY.md +59 -0
- package/docs/assets/first-use.gif +0 -0
- package/docs/assets/social-preview.png +0 -0
- package/docs/en/index.html +85 -0
- package/docs/index.html +85 -0
- package/docs/market-index.json +69 -0
- package/examples/market-index.json +69 -0
- package/lib/delivery.js +111 -0
- package/lib/index.js +432 -0
- package/lib/prompt.js +18 -0
- package/lib/protocol.js +124 -0
- package/lib/store.js +421 -0
- package/lib/tools.js +319 -0
- package/lib/transport.js +21 -0
- package/package.json +66 -4
package/README.md
CHANGED
|
@@ -1,3 +1,311 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-agent-lines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
> DeepSeek Harness 社区插件,不是 DeepSeek 官方产品。
|
|
6
|
+
|
|
7
|
+
> **100% Vibe Coding。** 架构、代码、测试、文档都在 AI agent 会话中完成。它是实验性项目:生产使用前请审阅代码并跑一遍集成测试。
|
|
8
|
+
|
|
9
|
+
DSH agent 的 super agent 平替插件。给 DSH agent 线(line)用的、可寻址的持久消息层。一条线是一个普通 DSH Session 加一个稳定 label;一个 DSH 进程可以承载多条线,管理线还可以通过 `line_create` 在对话里创建新线。
|
|
10
|
+
|
|
11
|
+
## 什么时候用
|
|
12
|
+
|
|
13
|
+
如果所有 worker 都是同一个 DSH 进程里、同一个父 Agent 的子任务,**优先用内置 subagent / Agent Teams**;更简单,不需要插件。
|
|
14
|
+
|
|
15
|
+
需要下面这些时再用 **dsh-agent-lines**:
|
|
16
|
+
|
|
17
|
+
- 线要能按 `agent#line` 寻址;
|
|
18
|
+
- 线是独立 DSH Session,有自己的上下文和生命周期;
|
|
19
|
+
- 收件箱要能跨接收方重启存活;
|
|
20
|
+
- 同机多个 DSH 进程共享一个线数据库;
|
|
21
|
+
- 管理线通过自然语言让模型创建 worker 线。
|
|
22
|
+
|
|
23
|
+
插件只做传输 + 轻量建线,不做任务编排。**交接(handover)没有实现**。
|
|
24
|
+
|
|
25
|
+
## 传输
|
|
26
|
+
|
|
27
|
+
`transport` 默认 `sqlite`(local-first),也是 0.3 唯一发布的 transport;安装插件不需要 Postgres 或 Python 服务。
|
|
28
|
+
|
|
29
|
+
## 架构
|
|
30
|
+
|
|
31
|
+
~~~mermaid
|
|
32
|
+
flowchart LR
|
|
33
|
+
U[用户] --> M[管理线 DSH Session]
|
|
34
|
+
M -->|line_create / line_send| P[dsh-agent-lines]
|
|
35
|
+
P --> Q[(SQLite lines.db)]
|
|
36
|
+
P -->|prompt requestId| W1[前端线 Session]
|
|
37
|
+
P -->|prompt requestId| W2[后端线 Session]
|
|
38
|
+
W1 -->|line_inbox / line_ack| P
|
|
39
|
+
W2 -->|line_inbox / line_ack| P
|
|
40
|
+
~~~
|
|
41
|
+
|
|
42
|
+
DSH 适配层负责线身份和“把消息送进 Session”;transport 负责持久消息和跨进程存储。默认是本地 SQLite。公开架构说明:[English](docs/ARCHITECTURE.en.md) | [中文](docs/ARCHITECTURE.md)。
|
|
43
|
+
|
|
44
|
+
## 要求
|
|
45
|
+
|
|
46
|
+
- DSH `>=0.2.1-rc.5 <0.3.0`
|
|
47
|
+
- Node.js `^22.19.0 || >=24.0.0`
|
|
48
|
+
- 本地文件系统(NFS/CIFS/SMB/FUSE 会被拒绝)
|
|
49
|
+
|
|
50
|
+
## 安装
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
dsh plugin --profile <profile> add dsh-agent-lines@0.3.0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
包尚未发布时,用本地 tarball 安装:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
dsh plugin --profile <profile> add ./dsh-agent-lines-0.3.0.tgz
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
不知道 `<profile>` 是什么,可以直接用一键脚本(自动选 `web`,没有 web 时选唯一 profile):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
curl -fsSL https://raw.githubusercontent.com/Lencen/dsh-agent-lines/main/install.sh -o install.sh
|
|
66
|
+
bash install.sh
|
|
67
|
+
# npm 未发布时:bash install.sh ./dsh-agent-lines-0.3.0.tgz
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
手动安装时,`<profile>` 是 **DSH profile 名**(对应 `~/.dsh/profiles/<profile>`),不是线名,也不是 agent 名。
|
|
71
|
+
|
|
72
|
+
- 已有 profile:先 `ls ~/.dsh/profiles`,填实际目录名,例如 `web`、`headless`;
|
|
73
|
+
- 想隔离安装:用新名字,例如 `agent-lines`,命令会自动初始化:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
dsh plugin --profile agent-lines add dsh-agent-lines@0.3.0
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
不要把 `manager-line`、`research` 这类线名填进 `--profile`。
|
|
80
|
+
|
|
81
|
+
### 管理线配置
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
- id: agent-lines
|
|
85
|
+
name: dsh-agent-lines
|
|
86
|
+
config:
|
|
87
|
+
agentId: agent-demo
|
|
88
|
+
dbPath: /home/me/.dsh/agent-lines/lines.db
|
|
89
|
+
allowCreate: true
|
|
90
|
+
allowClaim: true
|
|
91
|
+
allowSelfSend: false
|
|
92
|
+
dedupeWindowMs: 5000
|
|
93
|
+
maxReplyDepth: 8
|
|
94
|
+
createFrom: manager-line
|
|
95
|
+
maxLines: 16
|
|
96
|
+
childPreset: standard
|
|
97
|
+
lines:
|
|
98
|
+
- label: manager-line
|
|
99
|
+
sessionId: session-lines-manager
|
|
100
|
+
role: 任务对接、拆解与调度
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
只有 `createFrom` 指定的线能调用 `line_create`。worker profile 不要开 `allowCreate`。
|
|
104
|
+
|
|
105
|
+
### Worker profile
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
- id: agent-lines
|
|
109
|
+
name: dsh-agent-lines
|
|
110
|
+
config:
|
|
111
|
+
agentId: agent-demo
|
|
112
|
+
dbPath: /home/me/.dsh/agent-lines/lines.db
|
|
113
|
+
lines:
|
|
114
|
+
- label: frontend-line
|
|
115
|
+
sessionId: session-lines-frontend
|
|
116
|
+
role: 前端
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
旧的单线写法 `line: frontend-line` + `sessionId: ...` 仍然兼容。
|
|
120
|
+
|
|
121
|
+
## 快速开始
|
|
122
|
+
|
|
123
|
+
1. 安装:<code>dsh plugin --profile <profile> add dsh-agent-lines@0.3.0</code>,或用本地 tarball(见“安装”)。
|
|
124
|
+
2. 配置一条管理线:<code>allowCreate: true</code>、<code>allowClaim: true</code>、<code>createFrom: manager-line</code>。
|
|
125
|
+
3. 启动 profile,打开任意根 Session。
|
|
126
|
+
4. 先调用 <code>line_status</code>。如果当前会话未绑定,调用 <code>line_claim_manager({ confirm: "bind-manager" })</code>;CLI 兜底:
|
|
127
|
+
~~~bash
|
|
128
|
+
dsh-agent-lines claim --agent agent-demo --line manager-line --session "$DSH_SESSION_ID"
|
|
129
|
+
~~~
|
|
130
|
+
5. 对管理线说:“创建 frontend-line、backend-line、ops-line,然后分别发一条 hello。”
|
|
131
|
+
6. worker 可以是独立 profile(共享同一个 <code>dbPath</code>),也可以是管理线进程内创建的线。
|
|
132
|
+
|
|
133
|
+
任何新会话先看 <code>line_status</code>:它会告诉你是否绑定、manager 是谁、下一步怎么修。
|
|
134
|
+
|
|
135
|
+
## 首次使用:组建开发团队
|
|
136
|
+
|
|
137
|
+
在新根会话里直接复制下面这段。它会先绑定管理线,再创建一支团队;后续任务再单独发,不要让它自己开工。
|
|
138
|
+
|
|
139
|
+
> 使用 dsh-agent-lines。如果当前会话尚未绑定,先调用 line_claim_manager,confirm 用 "bind-manager"。
|
|
140
|
+
>
|
|
141
|
+
> 你是开发团队负责人:负责任务拆解与分配、协调团队沟通、跟踪成员工作状态与进度、汇总工作结果、总结并汇报。你只做管理和协调,不执行具体开发任务。
|
|
142
|
+
>
|
|
143
|
+
> 请用 line_create 创建成员线,label 用英文,role 用中文:research(调研)、architecture(架构)、uiux(UIUX)、dev(开发)、test(测试)、acceptance(验收)、release(发布)、ops(运维)。
|
|
144
|
+
>
|
|
145
|
+
> 创建后用 line_list 确认,然后待命,等我发第一个任务;不要自行开始具体工作。
|
|
146
|
+
|
|
147
|
+
label 必须是 ASCII 标识符;中文放在 role 里,不要用中文 label。这个提示词只创建一个团队;0.3 不支持同一插件实例同时多个团队。
|
|
148
|
+
|
|
149
|
+
可直接复制的调用(由管理线 Session 执行):
|
|
150
|
+
|
|
151
|
+
~~~js
|
|
152
|
+
line_create({ label: "research", role: "调研" })
|
|
153
|
+
line_create({ label: "architecture", role: "架构" })
|
|
154
|
+
line_create({ label: "uiux", role: "UIUX" })
|
|
155
|
+
line_create({ label: "dev", role: "开发" })
|
|
156
|
+
line_create({ label: "test", role: "测试" })
|
|
157
|
+
line_create({ label: "acceptance", role: "验收" })
|
|
158
|
+
line_create({ label: "release", role: "发布" })
|
|
159
|
+
line_create({ label: "ops", role: "运维" })
|
|
160
|
+
~~~
|
|
161
|
+
|
|
162
|
+
创建后用 line_list 确认。
|
|
163
|
+
|
|
164
|
+

|
|
165
|
+
|
|
166
|
+
真实 DSH Web 演示 profile、通用 label;不含内部数据。
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## 场景:一个管理线 + 多条执行线
|
|
170
|
+
|
|
171
|
+
只要 profile 里有管理线配置,Session 不必预先绑定。打开任意根 Session,直接说:
|
|
172
|
+
|
|
173
|
+
> 你是管理线。先用 line_claim_manager 把自己绑定为管理线,然后负责任务拆分与调度。需要建几个任务执行线,由你安排分配任务;你负责协调执行线中的工作内容、裁决纠纷、回收任务结果、汇总、总结、汇报。
|
|
174
|
+
|
|
175
|
+
接下来会发生:
|
|
176
|
+
|
|
177
|
+
1. 管理线调用 <code>line_create</code> 创建前端线、后端线、运维线、数据线,再用 <code>line_list</code> 确认。
|
|
178
|
+
2. 用户给一个具体任务,例如:“给报表页加 CSV 导出,带测试和一页 runbook。”
|
|
179
|
+
3. 管理线拆解后,用 <code>line_send</code> 发给对应执行线;消息落 SQLite。
|
|
180
|
+
4. 执行线用 <code>line_inbox</code> 收任务、执行、<code>line_ack</code> 确认,需要时用 <code>line_send</code> 回复。
|
|
181
|
+
5. 管理线收集结果;如果前端和后端对接口结构有分歧,管理线裁决并统一口径。
|
|
182
|
+
6. 管理线汇总、总结、汇报;各执行线保留自己的上下文和消息历史。
|
|
183
|
+
|
|
184
|
+
这是一个“提示词组织模式”,不是插件内置的组织架构:怎么拆任务由管理线模型决定。插件提供的是持久传输和硬约束:
|
|
185
|
+
|
|
186
|
+
- <code>maxLines</code> 限制建线数量;
|
|
187
|
+
- <code>noReply</code>、<code>maxReplyDepth</code> 和默认 self-send 禁止,防止自动回声;
|
|
188
|
+
- <code>line_status</code> 诊断 manager 绑定;
|
|
189
|
+
- 一个 Session 只能有一个 writer。
|
|
190
|
+
|
|
191
|
+
如果管理线没绑定,<code>line_status</code> 会给出修复命令;先调用 <code>line_claim_manager</code>(或 CLI)再跑这个场景。
|
|
192
|
+
|
|
193
|
+
## 使用
|
|
194
|
+
|
|
195
|
+
管理线可以完全由对话驱动:
|
|
196
|
+
|
|
197
|
+
> 你是管理线。用 `line_create` 创建 frontend-line、backend-line、ops-line,然后用 `line_list` 确认。
|
|
198
|
+
|
|
199
|
+
工具:
|
|
200
|
+
|
|
201
|
+
| 工具 | 作用 |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `line_status` | 只读:当前会话绑定、manager Session 是否存在/live、线列表、修复命令;未绑定也可见 |
|
|
204
|
+
| `line_claim_manager` | 把当前 live 根 Session 绑成 manager;需要 `allowClaim: true` 和 `confirm: "bind-manager"`;全局工具,不需要 shell |
|
|
205
|
+
| `line_list` | 列出线、状态、未读数、当前线和 role |
|
|
206
|
+
| `line_create` | 仅管理线:在管理线工作区(或指定的已有工作区)创建 DSH Session 并注册为线;同 label 幂等 |
|
|
207
|
+
| `line_move_workspace` | 仅管理线:把已有线的 Session 挂到路径完全匹配的工作区 |
|
|
208
|
+
| `line_send` | 发到 `line` 或 `agent#line`;可选 `idempotencyKey`、`replyTo`、`noReply`、`forceDistinct`、`ttlMs` |
|
|
209
|
+
| `line_inbox` | 读当前线已投递消息 |
|
|
210
|
+
| `line_ack` | 确认已处理 |
|
|
211
|
+
|
|
212
|
+
除 `line_status`、`line_claim_manager` 外,line 工具只注册在已绑定线的 Session 上;这两个控制工具是全局的:status 只读,claim 仅限根会话且受 allowClaim 控制。
|
|
213
|
+
|
|
214
|
+
`line_create` 在管理线所属工作区(没有工作区时回退当前目录)创建普通 DSH Session;它**不会**创建独立 OS 进程、profile 或端口,也不转移已有 Session。
|
|
215
|
+
|
|
216
|
+
创建的线会记录 `created_by`,进程重启时由管理线恢复。line_create 会解析管理线所属工作区并传 workspaceId(DSH 会自动 attach),所以新线出现在该工作区而不是“未分组”;如果管理线本身不在工作区,则回退 cwd,线保持未分组。传 `workspace`(已有路径或 id)可指定其他工作区。若某条线的 Session 还从未落盘,首次投递可能 `session/not-found`;给它发一条任务(或重新创建)即可让它持久化。
|
|
217
|
+
|
|
218
|
+
CLI:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
dsh-agent-lines list
|
|
222
|
+
dsh-agent-lines send --from frontend-line --to backend-line --text "hi"
|
|
223
|
+
dsh-agent-lines inbox --line backend-line
|
|
224
|
+
dsh-agent-lines ack --line backend-line --id msg_...
|
|
225
|
+
dsh-agent-lines claim --agent agent-demo --line manager --session "$DSH_SESSION_ID"
|
|
226
|
+
dsh-agent-lines doctor
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
用 `--db` 或 `DSH_AGENT_LINES_DB` 指向插件同一个数据库。
|
|
230
|
+
|
|
231
|
+
## 工作区
|
|
232
|
+
|
|
233
|
+
DSH 侧边栏按 workspace 的 sessionIds 成员表分组,不是按 cwd。line_create 通过 workspaceRegistry.resolveByPath 找到管理线所属工作区,传 workspaceId,DSH 会在创建时 attachSession,新线就不会落到“未分组”。如果管理线没有 workspace,插件回退 cwd,线仍未分组。
|
|
234
|
+
|
|
235
|
+
line_move_workspace 把已有线挂到目标 workspace。DSH 只在 Session cwd 与 workspace path 完全一致时允许 attach;否则插件返回 WORKSPACE_ATTACH_FAILED。它不能把 Session 迁移到不同目录(那需要新 Session/交接)。
|
|
236
|
+
|
|
237
|
+
## manager 绑定与 claim
|
|
238
|
+
|
|
239
|
+
manager 是一条真实的 DSH Session。你打开另一个会话时,它不是 manager,line 工具会刻意不出现;先调用 `line_status` 看当前绑定和修复命令。
|
|
240
|
+
|
|
241
|
+
根会话可以直接在对话里绑自己,不需要 shell:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
line_claim_manager({ "confirm": "bind-manager" })
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
插件会校验调用者是 live 根会话、释放旧 manager 租约、重绑 manager,并把选择写入 `manager_bindings`。不需要改 profile,也不需要重启;需要 `allowClaim: true`。
|
|
248
|
+
|
|
249
|
+
运维仍可用 CLI 兜底:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
dsh-agent-lines claim --agent agent-demo --line manager --session "$DSH_SESSION_ID"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
claim 不会对每个新会话自动执行:需要用户明确要求,或运维显式调用。
|
|
256
|
+
|
|
257
|
+
## 回声与重复保护
|
|
258
|
+
|
|
259
|
+
- 同 `idempotencyKey` 返回已有消息。
|
|
260
|
+
- 不传 key 时,同一 `dedupeWindowMs`(默认 5 秒)内的相同发送合并为一条;要故意发两条相同正文用 `forceDistinct: true`。
|
|
261
|
+
- `replyTo` 会让 `hop` 递增;超过 `maxReplyDepth`(默认 8)返回 `LINE_REPLY_DEPTH_EXCEEDED`。
|
|
262
|
+
- `noReply: true` 的消息不能被回复;尝试回复返回 `LINE_NO_REPLY`。
|
|
263
|
+
- 默认禁止自己发自己(`LINE_SELF_SEND_FORBIDDEN`);确需给自己留笔记时设 `allowSelfSend: true`。
|
|
264
|
+
|
|
265
|
+
这是传输层保护,不保证两个 agent 不会主动继续聊天;用 `noReply` 和 reply depth 约束。
|
|
266
|
+
|
|
267
|
+
## 投递语义
|
|
268
|
+
|
|
269
|
+
`queued → claimed → delivered → acked`
|
|
270
|
+
|
|
271
|
+
- `delivered`:目标 DSH Session 已接受 prompt。
|
|
272
|
+
- `acked`:agent 显式调用 `line_ack`。
|
|
273
|
+
- **至少一次**;靠 DSH 的 `requestId` 幂等让重试在 Session 日志里“有效一次”。不是分布式 exactly-once。
|
|
274
|
+
- 接收方离线时消息留在队列;0.3 不自动拉起进程。
|
|
275
|
+
- **一个 Session 只能有一个 writer。** 不要在另一个 DSH 进程(例如主 Web UI)里打开某条线的 Session;否则会 `session/writer-held`。关掉另一个视图,或给线独立 Session。
|
|
276
|
+
|
|
277
|
+
## 安全
|
|
278
|
+
|
|
279
|
+
- 插件在 DSH Host 进程内运行,拥有主机权限;只安装可信包。
|
|
280
|
+
- 0.3 默认本地 SQLite,不开网络监听。
|
|
281
|
+
- 只有 `createFrom` 管理线能建线;`maxLines` 限制增长。
|
|
282
|
+
- 对端消息在 prompt 里明确标为 peer content,不是用户指令。
|
|
283
|
+
- 详见 [docs/SECURITY.md](docs/SECURITY.md)。
|
|
284
|
+
|
|
285
|
+
## 限制
|
|
286
|
+
|
|
287
|
+
- 不做交接;不能把 Session 迁移到不同目录。
|
|
288
|
+
- 多线是同进程多 Session,不是进程隔离。
|
|
289
|
+
- 一个插件实例只支持一个 manager/团队;同一个 DSH 进程里同时跑开发、小说等多个团队不支持;独立 profile/DB 只是临时 workaround,不是产品模型。
|
|
290
|
+
- 没有 `line_delete`;建线会产生持久行,测试也会留下真实记录。
|
|
291
|
+
- 空白线 Session 可能需要第一条任务后才落盘。
|
|
292
|
+
- 一个 Session 只能有一个 writer;不要从另一个 DSH 进程打开线 Session。
|
|
293
|
+
|
|
294
|
+
## 状态
|
|
295
|
+
|
|
296
|
+
实验性 `0.3.x`。DSH 处于 developer preview,API 可能变化;请同时锁定 DSH 与插件版本。
|
|
297
|
+
|
|
298
|
+
## 致谢
|
|
299
|
+
|
|
300
|
+
- 人类 vibecoding:**Lencen**
|
|
301
|
+
- 开发 agent:**DeepSeek Harness agent (DSH)**
|
|
302
|
+
- 使用模型:**deepseek v4.1 flash 1017**
|
|
303
|
+
- 邮箱:[iamlencen@qq.com](mailto:iamlencen@qq.com)
|
|
304
|
+
|
|
305
|
+
## 贡献
|
|
306
|
+
|
|
307
|
+
见 [CONTRIBUTING.md](CONTRIBUTING.md);行为准则 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md);文档站 [lencen.github.io/dsh-agent-lines](https://lencen.github.io/dsh-agent-lines/)。
|
|
308
|
+
|
|
309
|
+
## 许可
|
|
310
|
+
|
|
311
|
+
Apache-2.0,见 [LICENSE](LICENSE)。
|
package/SECURITY.en.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
English | [中文](SECURITY.md)
|
|
4
|
+
|
|
5
|
+
See [docs/SECURITY.en.md](docs/SECURITY.en.md).
|
|
6
|
+
|
|
7
|
+
dsh-agent-lines is a DSH Host plugin: it runs in-process with the permissions of the DSH process. Install only reviewed packages. Report vulnerabilities through the repository's private security advisory or a minimal GitHub issue without credentials or customer data.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-agent-lines CLI: inspect and drive the same SQLite queue the plugin uses.
|
|
4
|
+
* It does not start DSH or load any Session.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { LineStore, defaultDbPath } from '../lib/store.js'
|
|
8
|
+
import { LineError, lineId } from '../lib/protocol.js'
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.error(`Usage:
|
|
12
|
+
dsh-agent-lines [--db <path>] [--json] list
|
|
13
|
+
dsh-agent-lines [--db <path>] [--json] send --from <line> --to <line|agent#line> --text <text> [--agent <id>] [--key <key>] [--reply-to <id>]
|
|
14
|
+
dsh-agent-lines [--db <path>] [--json] inbox --line <line> [--agent <id>] [--limit N] [--all]
|
|
15
|
+
dsh-agent-lines [--db <path>] [--json] ack --line <line> --id <messageId> [--agent <id>]
|
|
16
|
+
dsh-agent-lines [--db <path>] [--json] claim --line <line> --session <sessionId> [--agent <id>]
|
|
17
|
+
dsh-agent-lines [--db <path>] [--json] doctor
|
|
18
|
+
dsh-agent-lines [--db <path>] [--json] tail --line <line> [--agent <id>]
|
|
19
|
+
|
|
20
|
+
Environment: DSH_AGENT_LINES_DB overrides the default DB path.
|
|
21
|
+
`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parse(argv) {
|
|
25
|
+
const opts = { json: false, db: process.env.DSH_AGENT_LINES_DB ?? defaultDbPath() }
|
|
26
|
+
const positional = []
|
|
27
|
+
const args = [...argv]
|
|
28
|
+
while (args.length > 0) {
|
|
29
|
+
const token = args.shift()
|
|
30
|
+
if (token === '--json') { opts.json = true; continue }
|
|
31
|
+
if (token === '--db') { opts.db = args.shift() ?? ''; continue }
|
|
32
|
+
if (token.startsWith('--db=')) { opts.db = token.slice(5); continue }
|
|
33
|
+
if (token === '--help' || token === '-h') { usage(); process.exit(0) }
|
|
34
|
+
if (token.startsWith('--')) {
|
|
35
|
+
const key = token.slice(2)
|
|
36
|
+
const next = args[0]
|
|
37
|
+
opts[key] = next === undefined || next.startsWith('--') ? true : args.shift()
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
positional.push(token)
|
|
41
|
+
}
|
|
42
|
+
return { command: positional.shift(), rest: positional, opts }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function print(value, json) {
|
|
46
|
+
if (json) process.stdout.write(JSON.stringify(value, null, 2) + '\n')
|
|
47
|
+
else if (typeof value === 'string') process.stdout.write(value + '\n')
|
|
48
|
+
else process.stdout.write(JSON.stringify(value, null, 2) + '\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function need(value, name) {
|
|
52
|
+
if (value === undefined || String(value).trim() === '') throw new LineError('LINE_INVALID_ARGUMENT', `--${name} is required`)
|
|
53
|
+
return String(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function main() {
|
|
57
|
+
const { command, opts } = parse(process.argv.slice(2))
|
|
58
|
+
if (command === undefined) { usage(); return 2 }
|
|
59
|
+
if (!existsSync(opts.db) && command !== 'doctor') {
|
|
60
|
+
console.error(`[agent-lines] database not found: ${opts.db}`)
|
|
61
|
+
return 2
|
|
62
|
+
}
|
|
63
|
+
const store = new LineStore(opts.db)
|
|
64
|
+
try {
|
|
65
|
+
if (command === 'list') {
|
|
66
|
+
print({ db: store.dbPath, lines: store.listLines(), counts: store.counts() }, opts.json)
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
if (command === 'doctor') {
|
|
70
|
+
print({
|
|
71
|
+
db: store.dbPath,
|
|
72
|
+
exists: existsSync(store.dbPath),
|
|
73
|
+
counts: store.counts(),
|
|
74
|
+
lines: store.listLines(),
|
|
75
|
+
}, opts.json)
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
if (command === 'send') {
|
|
79
|
+
const from = need(opts.from, 'from')
|
|
80
|
+
const to = need(opts.to, 'to')
|
|
81
|
+
const text = need(opts.text, 'text')
|
|
82
|
+
const row = store.send({
|
|
83
|
+
agentId: String(opts.agent ?? 'agent-deepseek'),
|
|
84
|
+
fromLine: from,
|
|
85
|
+
to,
|
|
86
|
+
text,
|
|
87
|
+
idempotencyKey: opts.key,
|
|
88
|
+
replyTo: opts['reply-to'],
|
|
89
|
+
})
|
|
90
|
+
print({ id: row.id, from: `${row.from_agent}#${row.from_line}`, to: `${row.to_agent}#${row.to_line}`, state: row.state, deduplicated: row.deduplicated === true }, opts.json)
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
if (command === 'inbox') {
|
|
94
|
+
const line = need(opts.line, 'line')
|
|
95
|
+
const messages = store.inbox({
|
|
96
|
+
agentId: String(opts.agent ?? 'agent-deepseek'),
|
|
97
|
+
line,
|
|
98
|
+
limit: Number(opts.limit ?? 20),
|
|
99
|
+
includeAcked: opts.all === '' || opts.all === true,
|
|
100
|
+
})
|
|
101
|
+
print({ line, messages }, opts.json)
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
if (command === 'claim') {
|
|
105
|
+
const line = need(opts.line, 'line')
|
|
106
|
+
const sessionId = need(opts.session, 'session')
|
|
107
|
+
const agentId = String(opts.agent ?? 'agent-deepseek')
|
|
108
|
+
const claim = store.requestClaim({ agentId, lineId: lineId(agentId, line), sessionId })
|
|
109
|
+
print({
|
|
110
|
+
claimId: claim.id,
|
|
111
|
+
lineId: claim.line_id,
|
|
112
|
+
sessionId: claim.session_id,
|
|
113
|
+
state: claim.state,
|
|
114
|
+
reused: claim.reused === true,
|
|
115
|
+
note: 'The running plugin will claim this request; ensure allowClaim: true.',
|
|
116
|
+
}, opts.json)
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
if (command === 'ack') {
|
|
120
|
+
const line = need(opts.line, 'line')
|
|
121
|
+
const id = need(opts.id, 'id')
|
|
122
|
+
const row = store.markAck({ agentId: String(opts.agent ?? 'agent-deepseek'), line, messageId: id })
|
|
123
|
+
print({ id: row.id, state: row.state, acked: true, alreadyAcked: row.alreadyAcked === true }, opts.json)
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
if (command === 'tail') {
|
|
127
|
+
const line = need(opts.line, 'line')
|
|
128
|
+
const agentId = String(opts.agent ?? 'agent-deepseek')
|
|
129
|
+
let seen = new Set()
|
|
130
|
+
const tick = () => {
|
|
131
|
+
const messages = store.messagesForLine({ agentId, line, limit: 100 })
|
|
132
|
+
for (const row of messages) {
|
|
133
|
+
if (seen.has(row.id)) continue
|
|
134
|
+
seen.add(row.id)
|
|
135
|
+
print({ id: row.id, from: `${row.from_agent}#${row.from_line}`, state: row.state, text: row.body }, opts.json)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
opts.keepOpen = true
|
|
139
|
+
tick()
|
|
140
|
+
setInterval(tick, 1000)
|
|
141
|
+
process.on('SIGINT', () => { store.close(); process.exit(0) })
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
console.error(`[agent-lines] unknown command: ${command}`)
|
|
145
|
+
usage()
|
|
146
|
+
return 2
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`[agent-lines] ${error?.code ?? 'ERROR'}: ${error?.message ?? String(error)}`)
|
|
149
|
+
return error?.code === 'LINE_LEASE_HELD' ? 3 : 1
|
|
150
|
+
} finally {
|
|
151
|
+
if (opts.keepOpen !== true) store.close()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
process.exitCode = main()
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
English | [中文](ARCHITECTURE.md)
|
|
4
|
+
|
|
5
|
+
This is the public architecture overview. Version plans and release operations
|
|
6
|
+
live outside this repository.
|
|
7
|
+
|
|
8
|
+
## Layers
|
|
9
|
+
|
|
10
|
+
DSH Host
|
|
11
|
+
-> dsh-agent-lines
|
|
12
|
+
LineService identity, line definitions, manager binding, claim/status
|
|
13
|
+
DeliveryLoop owner-process prompt delivery
|
|
14
|
+
Tools line_status, line_claim_manager, line_list, line_create,
|
|
15
|
+
line_move_workspace, line_send, line_inbox, line_ack
|
|
16
|
+
Transport SPI
|
|
17
|
+
SqliteTransport current implementation; local SQLite/WAL
|
|
18
|
+
|
|
19
|
+
The DSH adapter owns who the current Session is and how a message enters a DSH
|
|
20
|
+
Session. The transport owns durable line and message storage.
|
|
21
|
+
|
|
22
|
+
SQLite is the only transport shipped in 0.3. The Transport SPI exists so a
|
|
23
|
+
future transport can implement the same contract without changing the adapter;
|
|
24
|
+
external transports are not part of this release.
|
|
25
|
+
|
|
26
|
+
## Delivery model
|
|
27
|
+
|
|
28
|
+
- A line is an ordinary DSH Session with a stable label.
|
|
29
|
+
- One Session has one writer. The process that owns the Session is the only one
|
|
30
|
+
allowed to prompt it.
|
|
31
|
+
- Delivery uses the message id as the DSH requestId. A retry after a crash
|
|
32
|
+
returns the original acceptance, so the Session log does not gain a duplicate.
|
|
33
|
+
- A lease elects the owner of a line; a stale claim is requeued after the claim
|
|
34
|
+
timeout.
|
|
35
|
+
- delivered means the target Session accepted the prompt; acked means the agent
|
|
36
|
+
called line_ack.
|
|
37
|
+
|
|
38
|
+
## Manager and claim
|
|
39
|
+
|
|
40
|
+
- The manager line is configured by createFrom and must be a live root Session.
|
|
41
|
+
- line_status is global and read-only. It reports current binding, manager
|
|
42
|
+
Session live/persisted, known lines, and a repair command.
|
|
43
|
+
- line_claim_manager is a global tool gated by allowClaim; it requires an
|
|
44
|
+
explicit confirm and accepts only a live root that is not bound to a worker.
|
|
45
|
+
The CLI remains an operator fallback. The plugin validates the Session,
|
|
46
|
+
releases the old manager lease, rebinds, and persists the choice in
|
|
47
|
+
manager_bindings.
|
|
48
|
+
- Claim is not an automatic action for every new Session.
|
|
49
|
+
|
|
50
|
+
## Workspace
|
|
51
|
+
|
|
52
|
+
- DSH groups Sessions by workspace membership (workspace.attachSession), not by
|
|
53
|
+
cwd.
|
|
54
|
+
- line_create resolves the manager workspace and passes workspaceId, so the new
|
|
55
|
+
Session is attached to that workspace.
|
|
56
|
+
- line_move_workspace attaches an existing line only when its Session cwd exactly
|
|
57
|
+
matches the target workspace path. It does not move a Session between
|
|
58
|
+
different directories.
|
|
59
|
+
|
|
60
|
+
## Boundaries
|
|
61
|
+
|
|
62
|
+
- line_create creates a root DSH Session, not a subagent.
|
|
63
|
+
- No handover: no Session transfer, no route switch, no unread migration.
|
|
64
|
+
- No process isolation: multiple lines may share one DSH process.
|
|
65
|
+
- One manager/team per plugin instance; simultaneous teams are not supported.
|
|
66
|
+
- One Session, one writer: do not open a line Session from another DSH process.
|
|
67
|
+
- No line_delete: created lines are durable rows; testing creates real data.
|
|
68
|
+
- No network listener in the default SQLite transport.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 架构
|
|
2
|
+
|
|
3
|
+
[English](ARCHITECTURE.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
这是公开架构说明。版本计划与发布操作放在本仓库之外。
|
|
6
|
+
|
|
7
|
+
## 分层
|
|
8
|
+
|
|
9
|
+
DSH Host
|
|
10
|
+
-> dsh-agent-lines
|
|
11
|
+
LineService 线身份、线定义、manager 绑定、claim/status
|
|
12
|
+
DeliveryLoop owner-process prompt 投递
|
|
13
|
+
Tools line_status、line_claim_manager、line_list、line_create、
|
|
14
|
+
line_move_workspace、line_send、line_inbox、line_ack
|
|
15
|
+
Transport SPI
|
|
16
|
+
SqliteTransport 当前实现;本地 SQLite/WAL
|
|
17
|
+
|
|
18
|
+
DSH 适配层负责“当前 Session 是谁”以及“消息如何进入 DSH Session”;transport
|
|
19
|
+
负责线/消息的持久存储。
|
|
20
|
+
|
|
21
|
+
0.3 只发布 SQLite transport。Transport SPI 只是为未来 transport 留接口;外部
|
|
22
|
+
transport 不属于本版本。
|
|
23
|
+
|
|
24
|
+
## 投递模型
|
|
25
|
+
|
|
26
|
+
- 一条线是一个普通 DSH Session 加一个稳定 label。
|
|
27
|
+
- 一个 Session 只能有一个 writer;只有持有该 Session 的进程可以 prompt 它。
|
|
28
|
+
- 投递把消息 ID 作为 DSH requestId;崩溃后重试会返回原 acceptance,不会在
|
|
29
|
+
Session 日志里多插一条。
|
|
30
|
+
- lease 选出线的 owner;超过 claim timeout 的 stale claim 会被重新排队。
|
|
31
|
+
- delivered 表示目标 Session 接受了 prompt;acked 表示 agent 调用了 line_ack。
|
|
32
|
+
|
|
33
|
+
## manager 与 claim
|
|
34
|
+
|
|
35
|
+
- manager 线由 createFrom 指定,必须是 live 根 Session。
|
|
36
|
+
- line_status 是全局只读工具,返回当前绑定、manager Session 是否 live/persisted、
|
|
37
|
+
已知线列表和修复命令。
|
|
38
|
+
- line_claim_manager 是受 allowClaim 控制的全局工具,要求显式 confirm,且只接受
|
|
39
|
+
未绑定 worker 的 live 根 Session;CLI 仍是运维兜底。插件会校验 Session、释放旧
|
|
40
|
+
manager lease、重绑,并把选择持久化到 manager_bindings。
|
|
41
|
+
- claim 不是每个新 Session 自动执行的动作。
|
|
42
|
+
|
|
43
|
+
## 工作区
|
|
44
|
+
|
|
45
|
+
- DSH 按 workspace 成员表(workspace.attachSession)分组,不是按 cwd。
|
|
46
|
+
- line_create 解析 manager 所属 workspace 并传 workspaceId,新 Session 会挂到
|
|
47
|
+
该工作区。
|
|
48
|
+
- line_move_workspace 只在已有 Session 的 cwd 与目标 workspace path 完全一致时
|
|
49
|
+
允许 attach;不能把 Session 迁移到不同目录。
|
|
50
|
+
|
|
51
|
+
## 边界
|
|
52
|
+
|
|
53
|
+
- line_create 创建的是根 DSH Session,不是 subagent。
|
|
54
|
+
- 不做交接:不迁移 Session、不切换路由、不迁移未读。
|
|
55
|
+
- 没有进程隔离:多条线可以共享一个 DSH 进程。
|
|
56
|
+
- 一个插件实例只支持一个 manager/团队;不支持同时多个团队。
|
|
57
|
+
- 一个 Session 只能有一个 writer;不要从另一个 DSH 进程打开线 Session。
|
|
58
|
+
- 没有 line_delete:建线会留下持久行,测试也会留下真实数据。
|
|
59
|
+
- 默认 SQLite transport 不开网络监听。
|