dsh-agent-lines 0.0.0-stage → 0.3.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/CHANGELOG.en.md +47 -0
- package/CHANGELOG.md +46 -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 +267 -0
- package/README.md +260 -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,261 @@
|
|
|
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 的 super agent 平替插件。
|
|
10
|
+
|
|
11
|
+
你不用每次新开会话、手动把背景复制给几个 agent。让一个负责人 agent 带一支长期协作的团队:每个成员有名字、各负责一块,任务和消息不会丢,结果由负责人汇总给你。
|
|
12
|
+
|
|
13
|
+
## 什么时候用
|
|
14
|
+
|
|
15
|
+
如果你遇到这些情况,再考虑安装:
|
|
16
|
+
|
|
17
|
+
- 你有一个长期项目,不想每次都从头解释背景;
|
|
18
|
+
- 你希望几个 agent 长期分工,比如调研、架构、开发、测试、发布、运维;
|
|
19
|
+
- 你需要给每个角色一个固定名字,能直接点名派任务;
|
|
20
|
+
- 任务做到一半关掉窗口、重启 DSH,消息和上下文还要在;
|
|
21
|
+
- 你希望一个负责人 agent 自己拆任务、建团队、追进度、收结果,最后向你汇报。
|
|
22
|
+
|
|
23
|
+
如果只是同一次会话里的临时子任务,直接用 DSH 内置的 subagent / Agent Teams,不需要这个插件。
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
一条命令安装(推荐,不需要自己找 profile):
|
|
28
|
+
|
|
29
|
+
curl -fsSL https://raw.githubusercontent.com/Lencen/dsh-agent-lines/main/install.sh -o install.sh
|
|
30
|
+
bash install.sh
|
|
31
|
+
|
|
32
|
+
脚本会自动选择 DSH Web 使用的 profile,安装后检查 agent-lines 是否可用;如果判断不了,会列出已有配置并提示你怎么指定。
|
|
33
|
+
|
|
34
|
+
离线或 npm 不可用时,用本地 tarball:
|
|
35
|
+
|
|
36
|
+
bash install.sh ./dsh-agent-lines-0.3.1.tgz
|
|
37
|
+
|
|
38
|
+
安装完成后,脚本会打印管理线配置文件的路径。把下面“管理线配置”整段粘贴进去一次即可。
|
|
39
|
+
|
|
40
|
+
### 管理线配置
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- id: agent-lines
|
|
44
|
+
name: dsh-agent-lines
|
|
45
|
+
config:
|
|
46
|
+
agentId: agent-demo
|
|
47
|
+
dbPath: /home/me/.dsh/agent-lines/lines.db
|
|
48
|
+
allowCreate: true
|
|
49
|
+
allowClaim: true
|
|
50
|
+
allowSelfSend: false
|
|
51
|
+
dedupeWindowMs: 5000
|
|
52
|
+
maxReplyDepth: 8
|
|
53
|
+
createFrom: manager-line
|
|
54
|
+
maxLines: 16
|
|
55
|
+
childPreset: standard
|
|
56
|
+
lines:
|
|
57
|
+
- label: manager-line
|
|
58
|
+
sessionId: session-lines-manager
|
|
59
|
+
role: 任务对接、拆解与调度
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
只有 `createFrom` 指定的线能调用 `line_create`。worker profile 不要开 `allowCreate`。
|
|
63
|
+
|
|
64
|
+
## 快速开始
|
|
65
|
+
|
|
66
|
+
1. 运行上面的安装脚本。
|
|
67
|
+
2. 按脚本提示打开配置文件,把“管理线配置”整段粘贴进去(只做一次)。
|
|
68
|
+
3. 启动 DSH Web,打开任意根会话。
|
|
69
|
+
4. 先调用 line_status;如果当前会话未绑定,调用 line_claim_manager({ confirm: "bind-manager" })。
|
|
70
|
+
5. 直接说“按首次使用创建团队”,然后等它建好成员线;第一个任务再单独发。
|
|
71
|
+
|
|
72
|
+
## 首次使用:组建开发团队
|
|
73
|
+
|
|
74
|
+
在新根会话里直接复制下面这段。它会先绑定管理线,再创建一支团队;后续任务再单独发,不要让它自己开工。
|
|
75
|
+
|
|
76
|
+
> 使用 dsh-agent-lines。如果当前会话尚未绑定,先调用 line_claim_manager,confirm 用 "bind-manager"。
|
|
77
|
+
>
|
|
78
|
+
> 你是开发团队负责人:负责任务拆解与分配、协调团队沟通、跟踪成员工作状态与进度、汇总工作结果、总结并汇报。你只做管理和协调,不执行具体开发任务。
|
|
79
|
+
>
|
|
80
|
+
> 请用 line_create 创建成员线,label 用英文,role 用中文:research(调研)、architecture(架构)、uiux(UIUX)、dev(开发)、test(测试)、acceptance(验收)、release(发布)、ops(运维)。
|
|
81
|
+
>
|
|
82
|
+
> 创建后用 line_list 确认,然后待命,等我发第一个任务;不要自行开始具体工作。
|
|
83
|
+
|
|
84
|
+
label 必须是 ASCII 标识符;中文放在 role 里,不要用中文 label。这个提示词只创建一个团队;0.3 不支持同一插件实例同时多个团队。
|
|
85
|
+
|
|
86
|
+
可直接复制的调用(由管理线 Session 执行):
|
|
87
|
+
|
|
88
|
+
~~~js
|
|
89
|
+
line_create({ label: "research", role: "调研" })
|
|
90
|
+
line_create({ label: "architecture", role: "架构" })
|
|
91
|
+
line_create({ label: "uiux", role: "UIUX" })
|
|
92
|
+
line_create({ label: "dev", role: "开发" })
|
|
93
|
+
line_create({ label: "test", role: "测试" })
|
|
94
|
+
line_create({ label: "acceptance", role: "验收" })
|
|
95
|
+
line_create({ label: "release", role: "发布" })
|
|
96
|
+
line_create({ label: "ops", role: "运维" })
|
|
97
|
+
~~~
|
|
98
|
+
|
|
99
|
+
创建后用 line_list 确认。
|
|
100
|
+
|
|
101
|
+

|
|
102
|
+
|
|
103
|
+
真实 DSH Web 演示 profile、通用 label;不含内部数据。
|
|
104
|
+
|
|
105
|
+
## 场景:一个管理线 + 多条执行线
|
|
106
|
+
|
|
107
|
+
负责人先建好团队,后面才是具体任务。
|
|
108
|
+
|
|
109
|
+
1. 负责人按角色建立成员:调研、架构、UIUX、开发、测试、验收、发布、运维。
|
|
110
|
+
2. 你给负责人一个具体任务,例如“给报表页加 CSV 导出,带测试和一页 runbook”。
|
|
111
|
+
3. 负责人把任务拆开,派给对应的成员;每条消息都会留在本地。
|
|
112
|
+
4. 成员收到任务、执行、确认收到,需要时回复负责人。
|
|
113
|
+
5. 负责人收集结果,处理成员之间的分歧,统一口径。
|
|
114
|
+
6. 你看到负责人的汇总;每个成员保留自己的上下文和消息记录。
|
|
115
|
+
|
|
116
|
+
这只是“用提示词组织团队”的方式,不是插件内置的组织架构;怎么拆任务由负责人模型决定。技术细节(消息、状态、工具)在下面的“使用”和“投递语义”。
|
|
117
|
+
|
|
118
|
+
## 使用
|
|
119
|
+
|
|
120
|
+
管理线可以完全由对话驱动:
|
|
121
|
+
|
|
122
|
+
> 你是管理线。用 `line_create` 创建 frontend-line、backend-line、ops-line,然后用 `line_list` 确认。
|
|
123
|
+
|
|
124
|
+
工具:
|
|
125
|
+
|
|
126
|
+
| 工具 | 作用 |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `line_status` | 只读:当前会话绑定、manager Session 是否存在/live、线列表、修复命令;未绑定也可见 |
|
|
129
|
+
| `line_claim_manager` | 把当前 live 根 Session 绑成 manager;需要 `allowClaim: true` 和 `confirm: "bind-manager"`;全局工具,不需要 shell |
|
|
130
|
+
| `line_list` | 列出线、状态、未读数、当前线和 role |
|
|
131
|
+
| `line_create` | 仅管理线:在管理线工作区(或指定的已有工作区)创建 DSH Session 并注册为线;同 label 幂等 |
|
|
132
|
+
| `line_move_workspace` | 仅管理线:把已有线的 Session 挂到路径完全匹配的工作区 |
|
|
133
|
+
| `line_send` | 发到 `line` 或 `agent#line`;可选 `idempotencyKey`、`replyTo`、`noReply`、`forceDistinct`、`ttlMs` |
|
|
134
|
+
| `line_inbox` | 读当前线已投递消息 |
|
|
135
|
+
| `line_ack` | 确认已处理 |
|
|
136
|
+
|
|
137
|
+
除 `line_status`、`line_claim_manager` 外,line 工具只注册在已绑定线的 Session 上;这两个控制工具是全局的:status 只读,claim 仅限根会话且受 allowClaim 控制。
|
|
138
|
+
|
|
139
|
+
`line_create` 在管理线所属工作区(没有工作区时回退当前目录)创建普通 DSH Session;它**不会**创建独立 OS 进程、profile 或端口,也不转移已有 Session。
|
|
140
|
+
|
|
141
|
+
创建的线会记录 `created_by`,进程重启时由管理线恢复。line_create 会解析管理线所属工作区并传 workspaceId(DSH 会自动 attach),所以新线出现在该工作区而不是“未分组”;如果管理线本身不在工作区,则回退 cwd,线保持未分组。传 `workspace`(已有路径或 id)可指定其他工作区。若某条线的 Session 还从未落盘,首次投递可能 `session/not-found`;给它发一条任务(或重新创建)即可让它持久化。
|
|
142
|
+
|
|
143
|
+
CLI:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
dsh-agent-lines list
|
|
147
|
+
dsh-agent-lines send --from frontend-line --to backend-line --text "hi"
|
|
148
|
+
dsh-agent-lines inbox --line backend-line
|
|
149
|
+
dsh-agent-lines ack --line backend-line --id msg_...
|
|
150
|
+
dsh-agent-lines claim --agent agent-demo --line manager --session "$DSH_SESSION_ID"
|
|
151
|
+
dsh-agent-lines doctor
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
用 `--db` 或 `DSH_AGENT_LINES_DB` 指向插件同一个数据库。
|
|
155
|
+
|
|
156
|
+
## 传输
|
|
157
|
+
|
|
158
|
+
`transport` 默认 `sqlite`(local-first),也是 0.3 唯一发布的 transport;安装插件不需要 Postgres 或 Python 服务。
|
|
159
|
+
|
|
160
|
+
## 架构
|
|
161
|
+
|
|
162
|
+
~~~mermaid
|
|
163
|
+
flowchart LR
|
|
164
|
+
U[用户] --> M[管理线 DSH Session]
|
|
165
|
+
M -->|line_create / line_send| P[dsh-agent-lines]
|
|
166
|
+
P --> Q[(SQLite lines.db)]
|
|
167
|
+
P -->|prompt requestId| W1[前端线 Session]
|
|
168
|
+
P -->|prompt requestId| W2[后端线 Session]
|
|
169
|
+
W1 -->|line_inbox / line_ack| P
|
|
170
|
+
W2 -->|line_inbox / line_ack| P
|
|
171
|
+
~~~
|
|
172
|
+
|
|
173
|
+
DSH 适配层负责线身份和“把消息送进 Session”;transport 负责持久消息和跨进程存储。默认是本地 SQLite。公开架构说明:[English](docs/ARCHITECTURE.en.md) | [中文](docs/ARCHITECTURE.md)。
|
|
174
|
+
|
|
175
|
+
## 要求
|
|
176
|
+
|
|
177
|
+
- DSH `>=0.2.1-rc.5 <0.3.0`
|
|
178
|
+
- Node.js `^22.19.0 || >=24.0.0`
|
|
179
|
+
- 本地文件系统(NFS/CIFS/SMB/FUSE 会被拒绝)
|
|
180
|
+
|
|
181
|
+
## 工作区
|
|
182
|
+
|
|
183
|
+
DSH 侧边栏按 workspace 的 sessionIds 成员表分组,不是按 cwd。line_create 通过 workspaceRegistry.resolveByPath 找到管理线所属工作区,传 workspaceId,DSH 会在创建时 attachSession,新线就不会落到“未分组”。如果管理线没有 workspace,插件回退 cwd,线仍未分组。
|
|
184
|
+
|
|
185
|
+
line_move_workspace 把已有线挂到目标 workspace。DSH 只在 Session cwd 与 workspace path 完全一致时允许 attach;否则插件返回 WORKSPACE_ATTACH_FAILED。它不能把 Session 迁移到不同目录(那需要新 Session/交接)。
|
|
186
|
+
|
|
187
|
+
## manager 绑定与 claim
|
|
188
|
+
|
|
189
|
+
manager 是一条真实的 DSH Session。你打开另一个会话时,它不是 manager,line 工具会刻意不出现;先调用 `line_status` 看当前绑定和修复命令。
|
|
190
|
+
|
|
191
|
+
根会话可以直接在对话里绑自己,不需要 shell:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
line_claim_manager({ "confirm": "bind-manager" })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
插件会校验调用者是 live 根会话、释放旧 manager 租约、重绑 manager,并把选择写入 `manager_bindings`。不需要改 profile,也不需要重启;需要 `allowClaim: true`。
|
|
198
|
+
|
|
199
|
+
运维仍可用 CLI 兜底:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
dsh-agent-lines claim --agent agent-demo --line manager --session "$DSH_SESSION_ID"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
claim 不会对每个新会话自动执行:需要用户明确要求,或运维显式调用。
|
|
206
|
+
|
|
207
|
+
## 回声与重复保护
|
|
208
|
+
|
|
209
|
+
- 同 `idempotencyKey` 返回已有消息。
|
|
210
|
+
- 不传 key 时,同一 `dedupeWindowMs`(默认 5 秒)内的相同发送合并为一条;要故意发两条相同正文用 `forceDistinct: true`。
|
|
211
|
+
- `replyTo` 会让 `hop` 递增;超过 `maxReplyDepth`(默认 8)返回 `LINE_REPLY_DEPTH_EXCEEDED`。
|
|
212
|
+
- `noReply: true` 的消息不能被回复;尝试回复返回 `LINE_NO_REPLY`。
|
|
213
|
+
- 默认禁止自己发自己(`LINE_SELF_SEND_FORBIDDEN`);确需给自己留笔记时设 `allowSelfSend: true`。
|
|
214
|
+
|
|
215
|
+
这是传输层保护,不保证两个 agent 不会主动继续聊天;用 `noReply` 和 reply depth 约束。
|
|
216
|
+
|
|
217
|
+
## 投递语义
|
|
218
|
+
|
|
219
|
+
`queued → claimed → delivered → acked`
|
|
220
|
+
|
|
221
|
+
- `delivered`:目标 DSH Session 已接受 prompt。
|
|
222
|
+
- `acked`:agent 显式调用 `line_ack`。
|
|
223
|
+
- **至少一次**;靠 DSH 的 `requestId` 幂等让重试在 Session 日志里“有效一次”。不是分布式 exactly-once。
|
|
224
|
+
- 接收方离线时消息留在队列;0.3 不自动拉起进程。
|
|
225
|
+
- **一个 Session 只能有一个 writer。** 不要在另一个 DSH 进程(例如主 Web UI)里打开某条线的 Session;否则会 `session/writer-held`。关掉另一个视图,或给线独立 Session。
|
|
226
|
+
|
|
227
|
+
## 安全
|
|
228
|
+
|
|
229
|
+
- 插件在 DSH Host 进程内运行,拥有主机权限;只安装可信包。
|
|
230
|
+
- 0.3 默认本地 SQLite,不开网络监听。
|
|
231
|
+
- 只有 `createFrom` 管理线能建线;`maxLines` 限制增长。
|
|
232
|
+
- 对端消息在 prompt 里明确标为 peer content,不是用户指令。
|
|
233
|
+
- 详见 [docs/SECURITY.md](docs/SECURITY.md)。
|
|
234
|
+
|
|
235
|
+
## 限制
|
|
236
|
+
|
|
237
|
+
- 不做交接;不能把 Session 迁移到不同目录。
|
|
238
|
+
- 多线是同进程多 Session,不是进程隔离。
|
|
239
|
+
- 一个插件实例只支持一个 manager/团队;同一个 DSH 进程里同时跑开发、小说等多个团队不支持;独立 profile/DB 只是临时 workaround,不是产品模型。
|
|
240
|
+
- 没有 `line_delete`;建线会产生持久行,测试也会留下真实记录。
|
|
241
|
+
- 空白线 Session 可能需要第一条任务后才落盘。
|
|
242
|
+
- 一个 Session 只能有一个 writer;不要从另一个 DSH 进程打开线 Session。
|
|
243
|
+
|
|
244
|
+
## 状态
|
|
245
|
+
|
|
246
|
+
实验性 `0.3.x`。DSH 处于 developer preview,API 可能变化;请同时锁定 DSH 与插件版本。
|
|
247
|
+
|
|
248
|
+
## 致谢
|
|
249
|
+
|
|
250
|
+
- 人类 vibecoding:**Lencen**
|
|
251
|
+
- 开发 agent:**DeepSeek Harness agent (DSH)**
|
|
252
|
+
- 使用模型:**deepseek v4.1 flash 1017**
|
|
253
|
+
- 邮箱:[iamlencen@qq.com](mailto:iamlencen@qq.com)
|
|
254
|
+
|
|
255
|
+
## 贡献
|
|
256
|
+
|
|
257
|
+
见 [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/)。
|
|
258
|
+
|
|
259
|
+
## 许可
|
|
260
|
+
|
|
261
|
+
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 不开网络监听。
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Deploy
|
|
2
|
+
|
|
3
|
+
English | [中文](DEPLOY.md)
|
|
4
|
+
|
|
5
|
+
## One process, one or more lines
|
|
6
|
+
|
|
7
|
+
0.3 supports multiple lines in one DSH process, each bound to an ordinary DSH Session. A manager line with `allowCreate: true` and `createFrom` can create more lines with `line_create`; those lines live in the same DSH process and the manager's working directory.
|
|
8
|
+
|
|
9
|
+
It is still valid to run one line per DSH process (0.1 style) and share a `dbPath` across processes:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
DSH process A (profile lines-a, port 3081, line frontend) ─┐
|
|
13
|
+
DSH process B (profile lines-b, port 3082, line backend) ─┼─ ~/.dsh/agent-lines/lines.db
|
|
14
|
+
DSH process C (profile lines-c, port 3083, line asset) ─┘
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`line_create` does **not** spawn a process/profile/port. If you need process isolation or different working directories per line, provision each line as its own DSH profile/process instead.
|
|
18
|
+
|
|
19
|
+
## Where to put the DB
|
|
20
|
+
|
|
21
|
+
- Local disk only. NFS/CIFS/SMB/FUSE are refused at startup.
|
|
22
|
+
- Default: `~/.dsh/agent-lines/lines.db`.
|
|
23
|
+
- The directory is created `0700`; the DB is chmod `0600`.
|
|
24
|
+
|
|
25
|
+
## Session ownership
|
|
26
|
+
|
|
27
|
+
A DSH Session has one active writer. Never open a line's Session in another DSH process (for example, the main Web UI at 3080) while the line process is serving it. DSH will reject the plugin's prompt with `session/writer-held`, and the queued message will eventually fail after the retry budget.
|
|
28
|
+
|
|
29
|
+
If you need to inspect a line's Session, use the line process's own Web UI (it is the writer), or stop the line process first. For production, give each line its own Session and do not browse that Session from another DSH instance.
|
|
30
|
+
|
|
31
|
+
## Always-on lines
|
|
32
|
+
|
|
33
|
+
A line only consumes messages while its DSH process is alive. For an always-on line, run the DSH web profile as a user service:
|
|
34
|
+
|
|
35
|
+
```ini
|
|
36
|
+
[Unit]
|
|
37
|
+
Description=DSH web (agent line frontend)
|
|
38
|
+
After=network.target
|
|
39
|
+
|
|
40
|
+
[Service]
|
|
41
|
+
Type=simple
|
|
42
|
+
# Replace the path with the dsh binary of your installation.
|
|
43
|
+
ExecStart=%h/.npm-global/bin/dsh --profile lines-a web --no-open --host 127.0.0.1 --port 3081
|
|
44
|
+
Restart=on-failure
|
|
45
|
+
RestartSec=5
|
|
46
|
+
|
|
47
|
+
[Install]
|
|
48
|
+
WantedBy=default.target
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If the process is down, messages stay `queued` and are delivered after restart. 0.3 does not start processes on demand.
|
|
52
|
+
|
|
53
|
+
## Manager binding and claim
|
|
54
|
+
|
|
55
|
+
If the configured manager Session is stale, a root Session can call:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
line_claim_manager({ "confirm": "bind-manager" })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The global tool checks that the Session is a live root, releases the old manager lease, rebinds the line, starts a new delivery loop, and persists the choice in `manager_bindings`. `allowClaim: true` must be set; no restart is required. The CLI fallback is:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
dsh-agent-lines claim --agent agent-demo --line manager --session "$DSH_SESSION_ID"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If the configured manager Session is missing at startup, the plugin does not acquire it and does not report it online; `line_status` shows the missing state and the repair command.
|
|
68
|
+
|
|
69
|
+
## Session id
|
|
70
|
+
|
|
71
|
+
Configured lines need an existing `sessionId`; find it in the DSH session list/log, or use `dsh-agent-lines doctor` to inspect the queue. A wrong id fails permanently with `session/not-found` and the message moves to `failed`.
|
|
72
|
+
|
|
73
|
+
`line_create` generates a new `session-<uuid>` in the manager workspace (or cwd fallback) and returns it. The new line is an ordinary DSH Session, not a subagent; it does not get its own OS process.
|
|
74
|
+
|
|
75
|
+
Dynamic lines store `created_by`; a manager with `allowCreate` restores its created lines on startup and re-acquires their leases. If another process still owns a line, restore skips it. A blank Session that was never materialized may fail its first delivery; send a task or recreate it.
|
|
76
|
+
|
|
77
|
+
## Upgrades
|
|
78
|
+
|
|
79
|
+
1. Stop the DSH process (or let systemd restart it).
|
|
80
|
+
2. `dsh plugin --profile <p> add dsh-agent-lines@<version>`.
|
|
81
|
+
3. Restart.
|
|
82
|
+
4. Run `dsh-agent-lines doctor` and check leases/lines.
|
|
83
|
+
|
|
84
|
+
The SQLite schema is versioned with `PRAGMA user_version`; 0.3 is version 6 (adds `lines.role`, `lines.created_by`, `claim_requests`, `manager_bindings`, and message `hop`/`no_reply`/`root_message_id`). Existing version-1 databases are migrated in place on open. Back up `lines.db` before upgrading across minor versions.
|
|
85
|
+
|
|
86
|
+
## Uninstall / rollback
|
|
87
|
+
|
|
88
|
+
1. Remove the plugin entry from the profile patch and run `dsh plugin --profile <p> remove dsh-agent-lines`.
|
|
89
|
+
2. Restart DSH.
|
|
90
|
+
3. The DB is not deleted automatically. Move or delete `lines.db` only after you no longer need the queued messages.
|
|
91
|
+
4. The plugin never modifies `~/.dsh/sessions`.
|