dsh-interconnect 0.11.24 → 0.13.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/README.md +141 -24
- package/assets/dsh-interconnect.md +60 -24
- package/dsh.plugin.json +1 -1
- package/lib/index.js +27 -10
- package/lib/interconnect/index.js +27 -10
- package/lib/interconnect/index.js.map +3 -3
- package/lib/tool-interconnect/index.js +105 -24
- package/lib/tool-interconnect/index.js.map +3 -3
- package/lib/types/interconnect/index.d.ts +16 -6
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +38 -9
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,20 +11,22 @@
|
|
|
11
11
|
|
|
12
12
|
- 全走持久 WebSocket 链接:跨实例、跨机器投递消息、枚举 live session、探测活性(`send`/`reply`/`ping`/`list` 经 `/interconnect/link` 的 `msg`/`query` 帧)
|
|
13
13
|
- `/interconnect/link` WebSocket 端点:双向实时事件推流,含心跳与指数退避重连;也承载
|
|
14
|
-
`send`/`reply` 消息(`msg`/`msg-result`
|
|
15
|
-
- 事件 fan-out
|
|
14
|
+
`send`/`reply` 消息(`msg`/`msg-result` 帧)。0.9 起没有 HTTP 端点,也没有 HTTP 回退
|
|
15
|
+
- 事件 fan-out 只在 WebSocket 链路上;入站事件以 `interconnect/event` 发出
|
|
16
16
|
- 共享密钥鉴权(`DSH_INTERCONNECT_TOKEN`,bearer,fail-closed,timing-safe 比较)
|
|
17
17
|
|
|
18
18
|
**`tool-interconnect`** —— 模型可见工具:
|
|
19
19
|
|
|
20
|
-
- `
|
|
20
|
+
- `interconnect_peers`:列出本实例**已配置**的对端及其拨号链路状态(`up`/`down`),用于在不知道
|
|
21
|
+
peer id 时先看有哪些可用目标,也是区分「没配这个 peer」与「配了但链路不通」的唯一入口
|
|
22
|
+
- `interconnect_send`:向对端实例的指定 session 投递消息;默认唤醒没有 live agent 的持久 session(`resume: false` 关闭),可选 `delivery` 选投递模式
|
|
21
23
|
- `interconnect_list`:列出对端实例的 live session(id + 标题 + 状态),用于在不预先知道 session id 时寻址
|
|
22
24
|
- `interconnect_ping`:探测对端实例活性与身份
|
|
23
25
|
- `interconnect_reply`:向记录过的发送方回传消息,只需文本;回信 session 是本 agent 自己的,无需再次寻址
|
|
24
26
|
|
|
25
27
|
**`skill-interconnect`** —— 配套 skill:
|
|
26
28
|
|
|
27
|
-
- 向模型注册 `dsh-interconnect` skill,说明 `list`/`ping`/`send`/`reply` 的完整用法、
|
|
29
|
+
- 向模型注册 `dsh-interconnect` skill,说明 `peers`/`list`/`ping`/`send`/`reply` 的完整用法、
|
|
28
30
|
投递模式、`resume` 唤醒语义与失败处理。
|
|
29
31
|
- 明确告知模型:`interconnect_send` 会自动注入发送方的 `instanceId` 和 `sessionId`,
|
|
30
32
|
接收方凭记录的 sender 即可用 `interconnect_reply` 回信,不需要手工传地址。
|
|
@@ -39,9 +41,31 @@
|
|
|
39
41
|
- `interconnect_send(instanceId="peer", sessionId=..., text=...)`
|
|
40
42
|
- `interconnect_ping(instanceId="peer")`
|
|
41
43
|
- `interconnect_list(instanceId="peer")`
|
|
44
|
+
- `interconnect_peers()`(不接受参数;返回本实例配置的全部对端及其链路状态)
|
|
42
45
|
- `interconnect_reply(text=...)`(只需文本;回信 session 是本 agent 自己的,目标从记录的 sender 解析)
|
|
43
46
|
|
|
44
|
-
`instanceId` 是 `interconnect` 行 `peers` 映射里的键;真正用来拨号的 origin 由该映射的值给出(例如隧道端点 `http://127.0.0.1:13080`),**instanceId 本身从不出现在线上**,也不参与路由——origin
|
|
47
|
+
`instanceId` 是 `interconnect` 行 `peers` 映射里的键;真正用来拨号的 origin 由该映射的值给出(例如隧道端点 `http://127.0.0.1:13080`),**instanceId 本身从不出现在线上**,也不参与路由——origin 才是唯一的拨号依据。
|
|
48
|
+
|
|
49
|
+
对端没配在这里时,`send`/`reply` 返回 `peer-not-configured`,`ping`/`list` 则在返回
|
|
50
|
+
`reachable: false` 的同时带上 `configured: false`——这一支**重试永远不会成功**,只能去补
|
|
51
|
+
`peers` 映射。配了但链路不通是另一支:链路断或两端 token 不一致时返回 `unreachable`
|
|
52
|
+
(`configured: true`),重连成功后就可能通(无 HTTP 回退)。
|
|
53
|
+
|
|
54
|
+
`interconnect_peers` 就是这两支的判据来源,输出每行一个对端:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
peer-a http://127.0.0.1:13080 [up]
|
|
58
|
+
peer-b http://127.0.0.1:13081 [down]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`up`/`down` 是**拨号链路**当前是否可用;`down` 不代表对端没开——重连按指数退避自己跑,改完
|
|
62
|
+
网络或对端重启后无需重启本实例,链路会自己回来。
|
|
63
|
+
|
|
64
|
+
增删或改点 `peers` 同样不需要重启:profile patch 层的改动会重建本插件的 fiber,构造器按新映射
|
|
65
|
+
重新建链。这条已实测(双向对照):patch 层加入 `peers` 后出现拨号、删掉后拨号消失;改
|
|
66
|
+
`instanceId` 后新链接自报的就是新名字。生效的前提是这一层被真正应用——由 `dsh` 启动、组合里带
|
|
67
|
+
`hmr` 行的实例会自己应用(见「安装」一节),其它启动方式只能重启。构造器只在激活时读一次该
|
|
68
|
+
映射,所以改了文件但没被应用时,行为仍是旧的。
|
|
45
69
|
|
|
46
70
|
`interconnect_list` 返回对端**当前 live** 的 session,每一行的 `sessionId` 在调用时刻都是合法的投递目标:
|
|
47
71
|
|
|
@@ -117,15 +141,20 @@ interconnect_reply(text="reply")
|
|
|
117
141
|
|
|
118
142
|
| `reason` | 含义 | 应对 |
|
|
119
143
|
|---|---|---|
|
|
120
|
-
| `session-not-live` | 对端**答复了**,但那个 session
|
|
121
|
-
| `unreachable` |
|
|
122
|
-
| `
|
|
123
|
-
| `resume-
|
|
144
|
+
| `session-not-live` | 对端**答复了**,但那个 session 没有 live agent,且唤醒被关闭(`resume: false`)或对端无法唤醒 | 不要改投其它 session;报告失败,或移除 `resume: false` 后重试 |
|
|
145
|
+
| `unreachable` | 配了该对端,但没拿到可用答复(链路断,或鉴权被拒) | 目标 session 可能完好,链路重连后重试可能成功 |
|
|
146
|
+
| `peer-not-configured` | 本实例的 `peers` 里没有这个 `instanceId`,链路根本不存在 | 重试永远不会成功;补 `peers` 映射(patch 层改完即生效,见「寻址」一节),或改用 `interconnect_peers` 列出的 id |
|
|
147
|
+
| `resume-refused` | 请求了唤醒,但对端不允许(`allowResume: false`) | 再请求也没用;不要改投其它 session,报告失败 |
|
|
148
|
+
| `resume-failed` | 允许唤醒且尝试了,但没得到 live agent(无此持久化 session,或被别的 owner 持有) | 不要改投其它 session;报告失败 |
|
|
124
149
|
| `session-owned-by-subagent` | 该 session 属于 subagent 路由,投递权在它的父 agent | 通过父 agent 触达,别直接投 |
|
|
150
|
+
| `message-too-large` | 编好的帧超过链路帧上限(1 MiB),什么都没写、链路也没断 | 缩短文本,重发原文没用 |
|
|
125
151
|
| `no-sender-known` | `reply` 指向的本地 session 从未记录过发送方(它没收到过带 `sender` 的消息,或对端版本过旧没带 `sender`) | 先用 `interconnect_send` 主动建立联系 |
|
|
126
152
|
|
|
127
153
|
`reason` 恰好在 `delivered` 为 false 时出现。
|
|
128
154
|
|
|
155
|
+
后两种加上 `peer-not-configured` 是**本机产生**的原因,不会从线缆上来:`msgResultSchema` 只接受
|
|
156
|
+
接收方能回答的原因,所以这三条只可能出自本实例自己的服务(对端无法借它们伪造一个本地判定)。
|
|
157
|
+
|
|
129
158
|
只有真的「尝试唤醒但失败」才是 `resume-failed`。没装 api-proxy 的部署里,`agent` lookup
|
|
130
159
|
退化成一次 registry 查询、根本没有唤醒能力,这时报 `session-not-live`——否则会让调用方去重试
|
|
131
160
|
一个永远不可能成功的操作。
|
|
@@ -143,18 +172,22 @@ Host 在 0.6 之后把这个谓词从 `@deepseek-ai/dsh-api-remotes` 移走,
|
|
|
143
172
|
已实测:起一个真实 subagent 后,`interconnect_list` 不包含它;直接 `send` 到它的 id 返回
|
|
144
173
|
`session-owned-by-subagent`,消息**没有**进入 inbox。
|
|
145
174
|
|
|
146
|
-
### 唤醒离线 session(`resume
|
|
175
|
+
### 唤醒离线 session(`resume`,默认开)
|
|
147
176
|
|
|
148
|
-
|
|
177
|
+
投递目标是一个「已持久化但没有运行 agent」的 session 时默认会唤醒它:不带 `resume` 时
|
|
178
|
+
`interconnect_send` 与 `interconnect_reply` 都按 `resume: true` 处理;传 `resume: false`
|
|
179
|
+
才保持原样并返回 `session-not-live`。
|
|
149
180
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
里带着该 session
|
|
153
|
-
|
|
181
|
+
**默认开启是 2026-10-23 的决定。** 实测确认:消息投递到 session 后会触发一次**完整的 agent
|
|
182
|
+
回合**——`wakeDriver()` → `kick()` → `turn()` → `llm.stream()`,即一次计费的模型调用,且
|
|
183
|
+
assembly 里带着该 session 的完整工具集。默认关闭时,最常见的结果不是省下这一次调用,而是
|
|
184
|
+
投递失败后发送方改投另一个正在运行的 session:原会话永远收不到,别的会话收到一段不属于它的
|
|
185
|
+
内容。目标通常是用户关心的那个会话,只是它此刻没在跑;所以默认唤醒它,把「不唤醒」留给明确
|
|
186
|
+
这样要求的调用方。
|
|
154
187
|
|
|
155
|
-
|
|
188
|
+
两侧的控制权不变:
|
|
156
189
|
|
|
157
|
-
-
|
|
190
|
+
- **发送方**可逐条用 `resume: false` 关闭唤醒
|
|
158
191
|
- **接收方**用 `Config.allowResume`(默认 `true`)一票否决——因为花钱和跑工具的是它那台机器;
|
|
159
192
|
拒绝时在跑 lookup 之前就短路,回 `resume-refused`
|
|
160
193
|
|
|
@@ -170,15 +203,19 @@ context** 拥有,实测确认插件 fiber 被 dispose 时会把 resume 出来
|
|
|
170
203
|
唤醒**只把消息放进 inbox**,是否真的开始处理取决于 `delivery`:
|
|
171
204
|
|
|
172
205
|
```text
|
|
173
|
-
#
|
|
174
|
-
interconnect_send(instanceId="peer", sessionId, text,
|
|
206
|
+
# 默认:唤醒并让对方实际处理(会起一个计费回合)
|
|
207
|
+
interconnect_send(instanceId="peer", sessionId, text, delivery="followup")
|
|
175
208
|
|
|
176
209
|
# 唤醒但不起回合:只写入上下文,等对方下次被唤醒时一起读
|
|
177
|
-
interconnect_send(instanceId="peer", sessionId, text,
|
|
210
|
+
interconnect_send(instanceId="peer", sessionId, text, delivery="inject")
|
|
211
|
+
|
|
212
|
+
# 不唤醒:目标保持原样,返回 session-not-live
|
|
213
|
+
interconnect_send(instanceId="peer", sessionId, text, resume=false)
|
|
178
214
|
```
|
|
179
215
|
|
|
180
|
-
|
|
181
|
-
|
|
216
|
+
已实测:唤醒(默认开启)+ `delivery="inject"` 之后目标从非 live 变 live
|
|
217
|
+
(`interconnect_list` 计数 +1),且该 session 日志里只多一条 `agent/inbox/spliced`、
|
|
218
|
+
**后面没有 `turn/start`**。
|
|
182
219
|
|
|
183
220
|
**已知限制**:磁盘格式过旧的 session 无法唤醒,返回 `resume-failed`。这不是本插件的限制——
|
|
184
221
|
Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedError`,同样失败。
|
|
@@ -205,7 +242,7 @@ Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedEr
|
|
|
205
242
|
|---|---|---|
|
|
206
243
|
| `instanceId` | `'dsh'` | 本实例自报的 id,出现在 `ping`/`send`/`list` 的回包里。也作为「对端如何寻址到我」的身份 |
|
|
207
244
|
| `requestTimeoutMs` | `10000` | 出站请求超时,上限 60000 |
|
|
208
|
-
| `peers` | `{}` | **对端路由映射**:`{ [对端 instanceId]: 我拨向它的 origin }`,例如隧道端点 `http://127.0.0.1:13080`。激活时对每个对端自动建持久 WS 链;`send`/`reply`/`ping`/`list` 都按 instanceId
|
|
245
|
+
| `peers` | `{}` | **对端路由映射**:`{ [对端 instanceId]: 我拨向它的 origin }`,例如隧道端点 `http://127.0.0.1:13080`。激活时对每个对端自动建持久 WS 链;`send`/`reply`/`ping`/`list` 都按 instanceId 走对应链接。改这一行不需要重启:profile patch 层的改动会重建 fiber 并按新映射建链 |
|
|
209
246
|
| `delivery` | `'followup'` | 入站消息未带 `delivery` 时的默认投递模式 |
|
|
210
247
|
| `allowResume` | `true` | 是否允许发送方用 `resume` 唤醒本机的离线 session |
|
|
211
248
|
|
|
@@ -250,6 +287,17 @@ manifest 一变就重读 `dsh.profile.bundles` 并重算 patch 层,所以新
|
|
|
250
287
|
安装下重建 `lib/`,都落在这一类。没有 `profileContext` 的启动方式(`hmr` 行被禁用)也只能靠
|
|
251
288
|
重启。
|
|
252
289
|
|
|
290
|
+
`file:` 安装还有一个**文件粒度**的差异,来自 pnpm 用硬链接装本地目录依赖(实测所装副本的
|
|
291
|
+
link 数:`lib/**` 为 2,其余为 1):`lib/**` 是硬链接,仓库里重建后 profile 里那份跟着变,
|
|
292
|
+
重启即可生效;`assets/`、`dsh.plugin.json`、`package.json`、`cordis.patch.yml` 是**安装时刻
|
|
293
|
+
的副本**,改仓库不会传播。skill 正文来自 `assets/`,所以「只重建 + 重启」会得到新代码配旧
|
|
294
|
+
skill 正文,profile 的 `package.json` 里还可能写着很久以前的版本号。要整体刷新就用重装:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
dsh plugin --profile <name> remove dsh-interconnect
|
|
298
|
+
dsh plugin --profile <name> add file:/path/to/dsh-interconnect
|
|
299
|
+
```
|
|
300
|
+
|
|
253
301
|
两端实例用相同的 `DSH_INTERCONNECT_TOKEN` 作为共享密钥,任选一种来源:继承的进程环境变量、
|
|
254
302
|
`$DSH_HOME/.env`、或 `$DSH_HOME/.credentials.yaml` 的 `refs` 段(该 provider 由
|
|
255
303
|
`@deepseek-ai/dsh-base` 挂载;优先级为 继承的环境 > credentials 文档 > 调用目录的 `.env` >
|
|
@@ -261,6 +309,75 @@ refs:
|
|
|
261
309
|
DSH_INTERCONNECT_TOKEN: <两端相同的共享密钥>
|
|
262
310
|
```
|
|
263
311
|
|
|
312
|
+
## 两台机器最小可用
|
|
313
|
+
|
|
314
|
+
下面用 A、B 两台机器说明最短路径:A 要主动发消息给 B,B 也要能回信。五步里前四步两端都要做,
|
|
315
|
+
漏掉哪一步就会停在对应的失败原因上。
|
|
316
|
+
|
|
317
|
+
### 1. 两端各装一次
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
dsh plugin --profile <name> add dsh-interconnect
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
由 `dsh` 启动的实例几秒内自己热加载(见上一节),不用重启。
|
|
324
|
+
|
|
325
|
+
### 2. 两端设同一个共享密钥
|
|
326
|
+
|
|
327
|
+
按上一节任选一种来源,两侧的值必须逐字节相同。任一端没配 token 时端点 fail-closed,直接 403,
|
|
328
|
+
另一侧只会看到链路 `down`。
|
|
329
|
+
|
|
330
|
+
### 3. 打通网络:把 B 的端口映射到 A 的本机端口
|
|
331
|
+
|
|
332
|
+
链接路由 `/interconnect/link` 挂在 B 的**宿主 webserver** 上,也就是 B 那个 DSH 实例监听的端口
|
|
333
|
+
(`dsh web` 就是 GUI 的端口)。同一台机器上就是回环地址;跨机器时先建立隧道:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# 在 A 上执行:B 的 3080 变成 A 本机的 13080;-N 不执行远程命令
|
|
337
|
+
ssh -N -L 13080:127.0.0.1:3080 <B 的用户@主机>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
只用 A→B 单向发消息时,这一条就够了。要让 B 也能主动发给 A,在 B 上反向再建一条(把 A 的端口
|
|
341
|
+
映射成 B 本机的 13081)。
|
|
342
|
+
|
|
343
|
+
### 4. 两端各配一遍 peer
|
|
344
|
+
|
|
345
|
+
改 profile 的 patch 层(不是插件包内的文件;`interconnect` 行由 bundle 插入,这里按键名覆盖它的
|
|
346
|
+
config):
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
# A: $DSH_HOME/profiles/<name>/cordis.patch.yml
|
|
350
|
+
- id: interconnect
|
|
351
|
+
config:
|
|
352
|
+
instanceId: a
|
|
353
|
+
peers:
|
|
354
|
+
b: http://127.0.0.1:13080 # key = B 自报的 instanceId;value = A 拨向 B 的 origin
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
# B: $DSH_HOME/profiles/<name>/cordis.patch.yml
|
|
359
|
+
- id: interconnect
|
|
360
|
+
config:
|
|
361
|
+
instanceId: b
|
|
362
|
+
peers:
|
|
363
|
+
a: http://127.0.0.1:13081 # 没有这一条,B 能收消息但回信会得到 peer-not-configured
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
改完不用重启:patch 层的改动会重建 fiber 并按新映射建链。key 建议与对端自报的 `instanceId`
|
|
367
|
+
一致,否则 B 拨入的那条链路在 A 侧认不出是同一个 peer,事件会重复一份(服务会 warn 一次)。
|
|
368
|
+
|
|
369
|
+
### 5. 按顺序验证
|
|
370
|
+
|
|
371
|
+
1. `interconnect_peers` —— 应看到 `b` 且链路 `up`。
|
|
372
|
+
2. `interconnect_ping(instanceId="b")` —— 应答里是 B 的实例 id。
|
|
373
|
+
3. `interconnect_list(instanceId="b")` —— 列出 B 上 live 的 session。
|
|
374
|
+
4. `interconnect_send(instanceId="b", sessionId=..., text=...)` —— 投递成功。
|
|
375
|
+
5. B 侧 `interconnect_reply(text=...)` 回信,A 侧收到续文。
|
|
376
|
+
|
|
377
|
+
停在哪一步就回查哪一步:`unreachable`(`configured: true`)回查第 2、3 步(token、隧道);
|
|
378
|
+
`configured: false` / `peer-not-configured` 回查第 4 步(映射里没有这个 key);链路一直 `down` 时,
|
|
379
|
+
对端日志里对应的是 401/403,回查第 2 步(两侧 token 是否都有值且逐字节相同)。
|
|
380
|
+
|
|
264
381
|
## 开发
|
|
265
382
|
|
|
266
383
|
依赖 [公开的 DeepSeek Harness monorepo](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -288,7 +405,7 @@ pnpm run build # esbuild → lib/
|
|
|
288
405
|
|
|
289
406
|
## 验证
|
|
290
407
|
|
|
291
|
-
-
|
|
408
|
+
- 179/179 单测通过(服务 + 工具 + skill,含 wire 校验、畸形帧、心跳/池清理、结果帧按 kind 绑定与投影、list 的行数与字节双上限及满页提示、peer 状态与「未配置」分支、本机原因不得从线缆上来的负例等回归用例);
|
|
292
409
|
类型检查、构建均干净。
|
|
293
410
|
- 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
|
|
294
411
|
`interconnect_send` 工具反向回发,均验证通过。
|
|
@@ -5,20 +5,27 @@ another DSH session, another DSH instance, or another machine: hand off a task,
|
|
|
5
5
|
ask a peer agent for information, notify a remote session, or reply to an
|
|
6
6
|
incoming interconnect message.
|
|
7
7
|
|
|
8
|
-
The
|
|
9
|
-
is
|
|
8
|
+
The connections this instance can dial are configured and authenticated already.
|
|
9
|
+
Your job is to pick the right peer, the right tool, and the right target.
|
|
10
10
|
|
|
11
11
|
## Tools
|
|
12
12
|
|
|
13
|
+
- `interconnect_peers` — list the peer instances this instance is configured to
|
|
14
|
+
reach, with each peer's dialed link state. Call this first when you do not
|
|
15
|
+
already know which peer ids exist here: every listed `instanceId` is a valid
|
|
16
|
+
`instanceId` argument for the tools below. A peer shown with link `down` is
|
|
17
|
+
configured but currently cannot carry frames, so it answers nothing until it
|
|
18
|
+
reconnects.
|
|
13
19
|
- `interconnect_list` — discover which sessions are live on a peer instance.
|
|
14
20
|
Every returned `sessionId` is a valid `interconnect_send` target at that
|
|
15
21
|
moment. Use this first when you do not already know the target session id.
|
|
16
22
|
- `interconnect_ping` — probe whether a peer instance is reachable and learn
|
|
17
23
|
its self-reported instance id.
|
|
18
|
-
- `interconnect_send` — deliver one text message to a
|
|
19
|
-
instance. Pass `instanceId`, `sessionId`, and `text`.
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
- `interconnect_send` — deliver one text message to a session on a peer
|
|
25
|
+
instance. Pass `instanceId`, `sessionId`, and `text`. A live session receives
|
|
26
|
+
directly; a persisted session with no live agent is woken by default. The
|
|
27
|
+
sending instance/session identity is attached automatically by the tool, so
|
|
28
|
+
you must NOT pass a sender parameter.
|
|
22
29
|
- `interconnect_reply` — send a message back to the peer that this session last
|
|
23
30
|
received an interconnect message from. Pass only the reply text. The replying
|
|
24
31
|
session is this agent's own session, and the remote target is recalled
|
|
@@ -39,12 +46,29 @@ addresses.
|
|
|
39
46
|
|
|
40
47
|
## Recommended workflow
|
|
41
48
|
|
|
42
|
-
1. If you know the peer
|
|
49
|
+
1. If you do not already know the peer ids available here, call
|
|
50
|
+
`interconnect_peers` and pick one whose link is `up`.
|
|
51
|
+
2. If you know the peer instance but not the target session, call
|
|
43
52
|
`interconnect_list(instanceId=...)` and choose a live session.
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
3. Call `interconnect_send(instanceId=..., sessionId=..., text=...)`. If you
|
|
54
|
+
already know the target session id, send to it directly: a persisted session
|
|
55
|
+
with no live agent is woken by default.
|
|
56
|
+
4. When a message arrives on your side and you need to respond, call
|
|
46
57
|
`interconnect_reply(text=...)`. The replying session is this agent's own
|
|
47
|
-
session, and the recorded sender is used as the destination
|
|
58
|
+
session, and the recorded sender is used as the destination; that sender is
|
|
59
|
+
woken by default when it is no longer live.
|
|
60
|
+
|
|
61
|
+
## Addressing rules
|
|
62
|
+
|
|
63
|
+
- Deliver a message only to the session it is meant for. When delivery fails
|
|
64
|
+
because that session is not live and cannot be woken, report the failure; do
|
|
65
|
+
not forward the same text to a different session, and do not pick another
|
|
66
|
+
session that merely looks related. Delivering to the wrong conversation is
|
|
67
|
+
worse than not delivering: the message is read by the wrong owner, and the
|
|
68
|
+
intended recipient never sees it.
|
|
69
|
+
- Prefer the session id you were given or that the peer sent from. Use
|
|
70
|
+
`interconnect_list` to discover targets only when the target is not already
|
|
71
|
+
known, and do not treat similar titles as the same conversation.
|
|
48
72
|
|
|
49
73
|
## Delivery modes
|
|
50
74
|
|
|
@@ -61,23 +85,34 @@ Omit `delivery` to use the receiver's configured default.
|
|
|
61
85
|
|
|
62
86
|
## Waking offline sessions
|
|
63
87
|
|
|
64
|
-
`resume` defaults to
|
|
65
|
-
persisted session with no live agent, which starts a real billed agent
|
|
66
|
-
that session's full toolset.
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
`resume` defaults to on. Unless `resume: false` is passed, delivering to a
|
|
89
|
+
persisted session with no live agent wakes it, which starts a real billed agent
|
|
90
|
+
turn with that session's full toolset. Set `resume: false` to leave such a
|
|
91
|
+
session asleep and get `session-not-live` instead; the receiver can still refuse
|
|
92
|
+
every wake with `allowResume: false` (reason `resume-refused`).
|
|
93
|
+
|
|
94
|
+
Waking is the default because the target of an interconnect message is usually a
|
|
95
|
+
conversation that matters but is not currently running; a reply that cannot wake
|
|
96
|
+
its sender never arrives, and a send that cannot wake an idle session is
|
|
97
|
+
delivered to whatever session happens to be active instead.
|
|
69
98
|
|
|
70
99
|
## Failure reasons
|
|
71
100
|
|
|
72
101
|
When a send/reply reports `delivered: false`, read `reason`:
|
|
73
102
|
|
|
74
103
|
- `session-not-live` — the receiver answered, but that session has no live
|
|
75
|
-
agent
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
agent, and waking was disabled (`resume: false`) or is impossible on that
|
|
105
|
+
receiver (no resuming agent lookup). Do not deliver the same message to a
|
|
106
|
+
different session; report the failure, or retry with waking enabled.
|
|
107
|
+
- `unreachable` — no usable answer arrived from a peer that IS configured; its
|
|
108
|
+
link may be down, or the two instances may disagree on the shared token, so a
|
|
109
|
+
later retry can succeed.
|
|
110
|
+
- `peer-not-configured` — this instance configures no peer under that
|
|
111
|
+
`instanceId`, so no link exists and none can be dialed. Retrying cannot help:
|
|
112
|
+
report the id you used and let the user add it to the peers mapping, or pick an
|
|
113
|
+
id from `interconnect_peers`.
|
|
114
|
+
- `resume-refused` / `resume-failed` — waking is not allowed or did not work.
|
|
115
|
+
Do not reroute the message to a different session; report the failure.
|
|
81
116
|
- `session-owned-by-subagent` — the session belongs to a subagent and its
|
|
82
117
|
parent owns delivery; reach it through the parent.
|
|
83
118
|
- `message-too-large` — the message is larger than the peer link can carry
|
|
@@ -90,9 +125,10 @@ When a send/reply reports `delivered: false`, read `reason`:
|
|
|
90
125
|
## Rules
|
|
91
126
|
|
|
92
127
|
- Do not fabricate an `instanceId`, `sessionId`, or sender identity. A peer's
|
|
93
|
-
`instanceId` comes from the task you were given or from
|
|
94
|
-
|
|
95
|
-
the sender is carried automatically by the
|
|
128
|
+
`instanceId` comes from the task you were given or from `interconnect_peers`,
|
|
129
|
+
which lists every id this instance can actually reach; a `sessionId` comes from
|
|
130
|
+
`interconnect_list` on that peer; the sender is carried automatically by the
|
|
131
|
+
message you received.
|
|
96
132
|
- Do not ask the user for a sender address to reply; use `interconnect_reply`.
|
|
97
133
|
- Treat interconnect messages as authenticated but sender-reported: identity is
|
|
98
134
|
for reply attribution, not for routing or authorization.
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-interconnect",
|
|
3
3
|
"description": "Cross-instance message/event handoff for DeepSeek Harness (DSH): WebSocket link, model-facing tools, and companion skill.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"entry": {
|
|
6
6
|
"name": "dsh-interconnect/interconnect",
|
|
7
7
|
"inject": ["agents", "credentials"]
|
package/lib/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
5
5
|
import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
6
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/misc.ts
|
|
9
9
|
function isNullable(value) {
|
|
10
10
|
return value === null || value === void 0;
|
|
11
11
|
}
|
|
@@ -27,7 +27,7 @@ function pick(source, keys, forced) {
|
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/types.ts
|
|
31
31
|
function is(type, value) {
|
|
32
32
|
if (arguments.length === 1) return (value2) => is(type, value2);
|
|
33
33
|
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
@@ -136,7 +136,7 @@ function deepEqual(a, b, strict) {
|
|
|
136
136
|
}) ?? Object.keys({ ...a, ...b }).every((key) => deepEqual(a[key], b[key], strict));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/time.ts
|
|
140
140
|
var Time;
|
|
141
141
|
((Time2) => {
|
|
142
142
|
Time2.millisecond = 1;
|
|
@@ -216,7 +216,7 @@ var Time;
|
|
|
216
216
|
Time2.template = template;
|
|
217
217
|
})(Time || (Time = {}));
|
|
218
218
|
|
|
219
|
-
//
|
|
219
|
+
// ../dsh-wt-ic-merge/vendor/schemastery/lib/index.mjs
|
|
220
220
|
var kSchema = Symbol.for("schemastery");
|
|
221
221
|
var kValidationError = Symbol.for("ValidationError");
|
|
222
222
|
globalThis.__schemastery_index__ ??= 0;
|
|
@@ -1108,7 +1108,7 @@ var InterconnectService = class extends Service {
|
|
|
1108
1108
|
* Returns the peer identity when reachable, or undefined when the link is
|
|
1109
1109
|
* not up or no answer arrives.
|
|
1110
1110
|
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1111
|
-
* @returns the peer's identity, or undefined when
|
|
1111
|
+
* @returns the peer's identity, or undefined when there is no usable answer.
|
|
1112
1112
|
*/
|
|
1113
1113
|
async ping(instanceId) {
|
|
1114
1114
|
return this.queryRequest(instanceId, { kind: "ping" });
|
|
@@ -1122,16 +1122,33 @@ var InterconnectService = class extends Service {
|
|
|
1122
1122
|
async list(instanceId) {
|
|
1123
1123
|
return this.queryRequest(instanceId, { kind: "list" });
|
|
1124
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* The peers this instance configures, each with the current state of the link
|
|
1127
|
+
* it dials, in configuration order. This is how a caller tells an id this
|
|
1128
|
+
* instance cannot reach at all (`peer-not-configured`) from one whose link is
|
|
1129
|
+
* merely down (`unreachable`), which the bare `undefined` of `ping`/`list`
|
|
1130
|
+
* cannot express. A configured peer whose link is `down` is still a valid
|
|
1131
|
+
* target: its reconnect runs on its own backoff, so it may answer later.
|
|
1132
|
+
* @returns one row per configured peer.
|
|
1133
|
+
*/
|
|
1134
|
+
peers() {
|
|
1135
|
+
return [...this.linkStates].map(([instanceId, state]) => ({
|
|
1136
|
+
instanceId,
|
|
1137
|
+
origin: state.peer,
|
|
1138
|
+
link: state.writable() ? "up" : "down"
|
|
1139
|
+
}));
|
|
1140
|
+
}
|
|
1125
1141
|
/**
|
|
1126
1142
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1127
|
-
* correlated `msg-result`.
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1143
|
+
* correlated `msg-result`. A `peer-not-configured` result when this instance
|
|
1144
|
+
* configures no route for that id, undefined when the route exists but its
|
|
1145
|
+
* link is down, and a `message-too-large` result when the encoded frame would
|
|
1146
|
+
* not fit the link.
|
|
1131
1147
|
*/
|
|
1132
1148
|
async msgRequest(instanceId, kind, payload) {
|
|
1133
1149
|
const state = this.linkStates.get(instanceId);
|
|
1134
|
-
if (state === void 0
|
|
1150
|
+
if (state === void 0) return { delivered: false, instance: instanceId, reason: "peer-not-configured" };
|
|
1151
|
+
if (!state.writable()) return void 0;
|
|
1135
1152
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1136
1153
|
const frame = {
|
|
1137
1154
|
type: "msg",
|
|
@@ -5,7 +5,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
5
5
|
import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
6
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/misc.ts
|
|
9
9
|
function isNullable(value) {
|
|
10
10
|
return value === null || value === void 0;
|
|
11
11
|
}
|
|
@@ -27,7 +27,7 @@ function pick(source, keys, forced) {
|
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/types.ts
|
|
31
31
|
function is(type, value) {
|
|
32
32
|
if (arguments.length === 1) return (value2) => is(type, value2);
|
|
33
33
|
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
@@ -136,7 +136,7 @@ function deepEqual(a, b, strict) {
|
|
|
136
136
|
}) ?? Object.keys({ ...a, ...b }).every((key) => deepEqual(a[key], b[key], strict));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/time.ts
|
|
140
140
|
var Time;
|
|
141
141
|
((Time2) => {
|
|
142
142
|
Time2.millisecond = 1;
|
|
@@ -216,7 +216,7 @@ var Time;
|
|
|
216
216
|
Time2.template = template;
|
|
217
217
|
})(Time || (Time = {}));
|
|
218
218
|
|
|
219
|
-
//
|
|
219
|
+
// ../dsh-wt-ic-merge/vendor/schemastery/lib/index.mjs
|
|
220
220
|
var kSchema = Symbol.for("schemastery");
|
|
221
221
|
var kValidationError = Symbol.for("ValidationError");
|
|
222
222
|
globalThis.__schemastery_index__ ??= 0;
|
|
@@ -1108,7 +1108,7 @@ var InterconnectService = class extends Service {
|
|
|
1108
1108
|
* Returns the peer identity when reachable, or undefined when the link is
|
|
1109
1109
|
* not up or no answer arrives.
|
|
1110
1110
|
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1111
|
-
* @returns the peer's identity, or undefined when
|
|
1111
|
+
* @returns the peer's identity, or undefined when there is no usable answer.
|
|
1112
1112
|
*/
|
|
1113
1113
|
async ping(instanceId) {
|
|
1114
1114
|
return this.queryRequest(instanceId, { kind: "ping" });
|
|
@@ -1122,16 +1122,33 @@ var InterconnectService = class extends Service {
|
|
|
1122
1122
|
async list(instanceId) {
|
|
1123
1123
|
return this.queryRequest(instanceId, { kind: "list" });
|
|
1124
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* The peers this instance configures, each with the current state of the link
|
|
1127
|
+
* it dials, in configuration order. This is how a caller tells an id this
|
|
1128
|
+
* instance cannot reach at all (`peer-not-configured`) from one whose link is
|
|
1129
|
+
* merely down (`unreachable`), which the bare `undefined` of `ping`/`list`
|
|
1130
|
+
* cannot express. A configured peer whose link is `down` is still a valid
|
|
1131
|
+
* target: its reconnect runs on its own backoff, so it may answer later.
|
|
1132
|
+
* @returns one row per configured peer.
|
|
1133
|
+
*/
|
|
1134
|
+
peers() {
|
|
1135
|
+
return [...this.linkStates].map(([instanceId, state]) => ({
|
|
1136
|
+
instanceId,
|
|
1137
|
+
origin: state.peer,
|
|
1138
|
+
link: state.writable() ? "up" : "down"
|
|
1139
|
+
}));
|
|
1140
|
+
}
|
|
1125
1141
|
/**
|
|
1126
1142
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1127
|
-
* correlated `msg-result`.
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1143
|
+
* correlated `msg-result`. A `peer-not-configured` result when this instance
|
|
1144
|
+
* configures no route for that id, undefined when the route exists but its
|
|
1145
|
+
* link is down, and a `message-too-large` result when the encoded frame would
|
|
1146
|
+
* not fit the link.
|
|
1131
1147
|
*/
|
|
1132
1148
|
async msgRequest(instanceId, kind, payload) {
|
|
1133
1149
|
const state = this.linkStates.get(instanceId);
|
|
1134
|
-
if (state === void 0
|
|
1150
|
+
if (state === void 0) return { delivered: false, instance: instanceId, reason: "peer-not-configured" };
|
|
1151
|
+
if (!state.writable()) return void 0;
|
|
1135
1152
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1136
1153
|
const frame = {
|
|
1137
1154
|
type: "msg",
|