dsh-interconnect 0.9.0 → 0.10.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/README.md +22 -11
- package/assets/dsh-interconnect.md +92 -0
- package/cordis.patch.yml +4 -1
- package/dsh.plugin.json +2 -2
- package/lib/index.js +9 -3
- package/lib/interconnect/index.js +9 -3
- package/lib/interconnect/index.js.map +2 -2
- package/lib/skill-interconnect/index.js +50 -0
- package/lib/skill-interconnect/index.js.map +7 -0
- package/lib/skill-interconnect/invariant.js +13 -0
- package/lib/skill-interconnect/invariant.js.map +7 -0
- package/lib/tool-interconnect/index.js +1 -1
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +4 -3
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/skill-interconnect/index.d.ts +20 -0
- package/lib/types/skill-interconnect/index.d.ts.map +1 -0
- package/lib/types/skill-interconnect/invariant.d.ts +16 -0
- package/lib/types/skill-interconnect/invariant.d.ts.map +1 -0
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +13 -6
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
跨实例消息互通与事件通知插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH)。
|
|
4
4
|
让一个 DSH 实例能向同一个实例、另一台机器、或另一台机器上的别的 DSH 实例发送消息、探测活性,并在实例之间双向推送事件。
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## 包含三个插件
|
|
7
7
|
|
|
8
8
|
**`interconnect`** —— host 服务(`ctx.interconnect`):
|
|
9
9
|
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
- `interconnect_ping`:探测对端实例活性与身份
|
|
21
21
|
- `interconnect_reply`:向记录过的发送方回传消息,只需本机 session id + 文本,无需再次寻址
|
|
22
22
|
|
|
23
|
+
**`skill-interconnect`** —— 配套 skill:
|
|
24
|
+
|
|
25
|
+
- 向模型注册 `dsh-interconnect` skill,说明 `list`/`ping`/`send`/`reply` 的完整用法、
|
|
26
|
+
投递模式、`resume` 唤醒语义与失败处理。
|
|
27
|
+
- 明确告知模型:`interconnect_send` 会自动注入发送方的 `instanceId` 和 `sessionId`,
|
|
28
|
+
接收方凭记录的 sender 即可用 `interconnect_reply` 回信,不需要手工传地址。
|
|
29
|
+
- 依赖 `interconnect` 服务,只有传输层存在时才注册进 `ctx.skills`。
|
|
30
|
+
|
|
23
31
|
## 用法
|
|
24
32
|
|
|
25
33
|
### 寻址(0.9 起用 instanceId,全走持久链接)
|
|
@@ -120,8 +128,11 @@ interconnect_reply(sessionId=B-sess, text="reply")
|
|
|
120
128
|
|
|
121
129
|
`interconnect_send` 不会往 subagent 拥有的 session 里投递,`interconnect_list` 也不会把它们
|
|
122
130
|
列出来。那类 session 的投递权属于它的父 agent,从这里 splice 进 inbox 会和父 agent 抢。判定
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
逻辑镜像 Host 的 `hasApiSessionSubagentOwner`(`@deepseek-ai/dsh-api-session-controller`):
|
|
132
|
+
Host 在 0.6 之后把这个谓词从 `@deepseek-ai/dsh-api-remotes` 移走,且没有公开导出——桌面端和
|
|
133
|
+
`npx @deepseek-ai/dsh web` 运行时里没有任何可 import 的 Host 绑定,所以这里逐字复制一份
|
|
134
|
+
(`isSessionOwnedBySubagent`,见 `src/interconnect/index.ts`)。这是安全规则,Host 改动该规则
|
|
135
|
+
时必须同步此副本;tests 覆盖 origin=subagent 与 parent-owned 两条封栏分支。
|
|
125
136
|
|
|
126
137
|
已实测:起一个真实 subagent 后,`interconnect_list` 不包含它;直接 `send` 到它的 id 返回
|
|
127
138
|
`session-owned-by-subagent`,消息**没有**进入 inbox。
|
|
@@ -210,7 +221,7 @@ Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedEr
|
|
|
210
221
|
|
|
211
222
|
本包已发布到 npm:[`dsh-interconnect`](https://www.npmjs.com/package/dsh-interconnect)。
|
|
212
223
|
本仓库是一个 DSH profile bundle(根 `package.json` 声明 `dsh.bundle.patch` 指向
|
|
213
|
-
根 `cordis.patch.yml`,后者 `insert`
|
|
224
|
+
根 `cordis.patch.yml`,后者 `insert` 三个插件行)。
|
|
214
225
|
|
|
215
226
|
```bash
|
|
216
227
|
# 从 npm
|
|
@@ -241,21 +252,21 @@ pnpm run build # esbuild → lib/
|
|
|
241
252
|
|
|
242
253
|
## 架构说明
|
|
243
254
|
|
|
244
|
-
-
|
|
245
|
-
服务(有 HTTP/WS 端点),必须 host 级;`tool-interconnect`
|
|
246
|
-
`interconnect` 未做 TypeRT `@Remote`/Gateway 绑定,放进 agent preset
|
|
247
|
-
realm
|
|
255
|
+
- 三个插件都挂在 **host composition**:`interconnect` 是跨 session、跨机器的进程级
|
|
256
|
+
服务(有 HTTP/WS 端点),必须 host 级;`tool-interconnect` 和 `skill-interconnect`
|
|
257
|
+
也放 host,因为 `interconnect` 未做 TypeRT `@Remote`/Gateway 绑定,放进 agent preset
|
|
258
|
+
的 isolate realm 会导致工具/技能行无法 inject 到该服务。
|
|
248
259
|
- `ws` 是运行依赖,由宿主的 node_modules 提供(构建时 external)。
|
|
249
260
|
|
|
250
261
|
## 验证
|
|
251
262
|
|
|
252
|
-
-
|
|
263
|
+
- 38/38 单测通过(服务 + 工具 + skill);类型检查、构建均干净。
|
|
253
264
|
- 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
|
|
254
265
|
`interconnect_send` 工具反向回发,均验证通过。
|
|
255
266
|
- CI(GitHub Actions):clone 公开 DSH 仓库作为 sibling,跑 `pnpm run check`。
|
|
256
267
|
- 已发布版本:从 registry 下载的 tarball 与本地构建 shasum 一致;干净消费端
|
|
257
|
-
解析 `.`、`./tool-interconnect`
|
|
258
|
-
`number`)如期报 `TS2322`。
|
|
268
|
+
解析 `.`、`./tool-interconnect`、`./skill-interconnect` 入口的类型均通过,负例(把
|
|
269
|
+
`string` 赋给 `number`)如期报 `TS2322`。
|
|
259
270
|
- 投递消息以 `source: { kind: 'plugin', plugin: 'dsh-interconnect' }` 落库,而不是
|
|
260
271
|
`{ kind: 'user' }`。负例:把该 source 改回 `kind: 'user'`,对应断言转红。
|
|
261
272
|
**注意 `source` 不进模型上下文**——只有 `role` 和 `content` 会,而 `role` 是 `user`。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# dsh-interconnect
|
|
2
|
+
|
|
3
|
+
Use the `dsh-interconnect` tools whenever you need to exchange messages with
|
|
4
|
+
another DSH session, another DSH instance, or another machine: hand off a task,
|
|
5
|
+
ask a peer agent for information, notify a remote session, or reply to an
|
|
6
|
+
incoming interconnect message.
|
|
7
|
+
|
|
8
|
+
The transport is already connected and authenticated by the host plugin. Your
|
|
9
|
+
job is only to pick the right tool and the right target.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
- `interconnect_list` — discover which sessions are live on a peer instance.
|
|
14
|
+
Every returned `sessionId` is a valid `interconnect_send` target at that
|
|
15
|
+
moment. Use this first when you do not already know the target session id.
|
|
16
|
+
- `interconnect_ping` — probe whether a peer instance is reachable and learn
|
|
17
|
+
its self-reported instance id.
|
|
18
|
+
- `interconnect_send` — deliver one text message to a live session on a peer
|
|
19
|
+
instance. Pass `instanceId`, `sessionId`, and `text`. The sending
|
|
20
|
+
instance/session identity is attached automatically by the tool, so you must
|
|
21
|
+
NOT pass a sender parameter.
|
|
22
|
+
- `interconnect_reply` — send a message back to the peer that this session last
|
|
23
|
+
received an interconnect message from. Pass only the LOCAL session id that
|
|
24
|
+
received the message and the reply text. The remote target is recalled
|
|
25
|
+
automatically; do not try to look up or pass the sender address again.
|
|
26
|
+
|
|
27
|
+
## Sender identity is automatic
|
|
28
|
+
|
|
29
|
+
When you call `interconnect_send`, the plugin automatically attaches your own
|
|
30
|
+
`instanceId` and `sessionId` to the wire payload. The receiving instance
|
|
31
|
+
records that identity per local session, which is exactly what lets the
|
|
32
|
+
receiver use `interconnect_reply` later without re-addressing.
|
|
33
|
+
|
|
34
|
+
A reply also carries your identity automatically, so a multi-hop conversation
|
|
35
|
+
(A → B → A → B) keeps working without either side manually forwarding
|
|
36
|
+
addresses.
|
|
37
|
+
|
|
38
|
+
## Recommended workflow
|
|
39
|
+
|
|
40
|
+
1. If you know the peer instance but not the target session, call
|
|
41
|
+
`interconnect_list(instanceId=...)` and choose a live session.
|
|
42
|
+
2. Call `interconnect_send(instanceId=..., sessionId=..., text=...)`.
|
|
43
|
+
3. When a message arrives on your side and you need to respond, call
|
|
44
|
+
`interconnect_reply(sessionId=<your-local-session-id>, text=...)`. The
|
|
45
|
+
recorded sender is used as the destination.
|
|
46
|
+
|
|
47
|
+
## Delivery modes
|
|
48
|
+
|
|
49
|
+
The `delivery` parameter is per-message urgency:
|
|
50
|
+
|
|
51
|
+
- `followup` — queues the message as its own turn after whatever the receiver
|
|
52
|
+
is doing now.
|
|
53
|
+
- `steer` — cuts into the nearest step boundary of a running turn; use for
|
|
54
|
+
urgent interruptions.
|
|
55
|
+
- `inject` — only writes the message into context, without waking an idle
|
|
56
|
+
agent; it may sit unread.
|
|
57
|
+
|
|
58
|
+
Omit `delivery` to use the receiver's configured default.
|
|
59
|
+
|
|
60
|
+
## Waking offline sessions
|
|
61
|
+
|
|
62
|
+
`resume` defaults to off. Setting `resume: true` asks the receiver to wake a
|
|
63
|
+
persisted but not-running session, which starts a real billed agent turn with
|
|
64
|
+
that session's full toolset. Only use it when the specific session must be
|
|
65
|
+
reached and the receiver allows it. Prefer `interconnect_list` and an
|
|
66
|
+
already-live target when possible.
|
|
67
|
+
|
|
68
|
+
## Failure reasons
|
|
69
|
+
|
|
70
|
+
When a send/reply reports `delivered: false`, read `reason`:
|
|
71
|
+
|
|
72
|
+
- `session-not-live` — the receiver answered, but that session has no running
|
|
73
|
+
agent. Use `interconnect_list` to choose another target, or consider
|
|
74
|
+
`resume`.
|
|
75
|
+
- `unreachable` — no usable answer arrived; the target may still be fine, so a
|
|
76
|
+
retry can succeed.
|
|
77
|
+
- `resume-refused` / `resume-failed` — waking is not allowed or did not work;
|
|
78
|
+
choose a live target instead.
|
|
79
|
+
- `session-owned-by-subagent` — the session belongs to a subagent and its
|
|
80
|
+
parent owns delivery; reach it through the parent.
|
|
81
|
+
- `no-sender-known` — a `reply` was attempted for a local session that never
|
|
82
|
+
received a sender-carrying interconnect message; establish contact with
|
|
83
|
+
`interconnect_send` first.
|
|
84
|
+
|
|
85
|
+
## Rules
|
|
86
|
+
|
|
87
|
+
- Do not fabricate an `instanceId`, `sessionId`, or sender identity. Use values
|
|
88
|
+
returned by `interconnect_list`, `interconnect_ping`, or already recorded by
|
|
89
|
+
the service.
|
|
90
|
+
- Do not ask the user for a sender address to reply; use `interconnect_reply`.
|
|
91
|
+
- Treat interconnect messages as authenticated but sender-reported: identity is
|
|
92
|
+
for reply attribution, not for routing or authorization.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-interconnect bundle patch: mount the
|
|
1
|
+
# dsh-interconnect bundle patch: mount the three host plugins under their
|
|
2
2
|
# subpath entries, resolved through the profile's node_modules (the healed
|
|
3
3
|
# mirror makes every bundle dependency resolvable to bare rows).
|
|
4
4
|
- insert:
|
|
@@ -10,3 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
- id: tool-interconnect
|
|
12
12
|
name: dsh-interconnect/tool-interconnect
|
|
13
|
+
|
|
14
|
+
- id: skill-interconnect
|
|
15
|
+
name: dsh-interconnect/skill-interconnect
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-interconnect",
|
|
3
|
-
"description": "Cross-instance message/event handoff for DeepSeek Harness (DSH):
|
|
4
|
-
"version": "0.1
|
|
3
|
+
"description": "Cross-instance message/event handoff for DeepSeek Harness (DSH): WebSocket link, model-facing tools, and companion skill.",
|
|
4
|
+
"version": "0.10.1",
|
|
5
5
|
"entry": {
|
|
6
6
|
"name": "dsh-interconnect/interconnect",
|
|
7
7
|
"inject": ["webServer", "agents", "credentials"]
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
6
5
|
|
|
7
6
|
// ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
|
|
8
7
|
function isNullable(value) {
|
|
@@ -824,6 +823,13 @@ import WebSocket, { WebSocketServer } from "ws";
|
|
|
824
823
|
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
825
824
|
|
|
826
825
|
// src/interconnect/index.ts
|
|
826
|
+
function isSessionOwnedBySubagent(ctx, session, agent) {
|
|
827
|
+
if (session.header.origin === "subagent") return true;
|
|
828
|
+
const parentId = session.header.parentSession;
|
|
829
|
+
if (parentId === void 0 || agent === void 0) return false;
|
|
830
|
+
const parent = ctx.agents.get(parentId);
|
|
831
|
+
return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
|
|
832
|
+
}
|
|
827
833
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
828
834
|
var LINK_CHANNEL = "/interconnect/link";
|
|
829
835
|
var notificationSchema = Schema.union([
|
|
@@ -1206,7 +1212,7 @@ var InterconnectService = class extends Service {
|
|
|
1206
1212
|
* matches how the Host's own session listing treats its projection column.
|
|
1207
1213
|
*/
|
|
1208
1214
|
listSessions() {
|
|
1209
|
-
const reachable = this.ctx.agents.list().filter((agent) => !
|
|
1215
|
+
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1210
1216
|
const sessions = reachable.map((agent) => {
|
|
1211
1217
|
let title;
|
|
1212
1218
|
try {
|
|
@@ -1264,7 +1270,7 @@ var InterconnectService = class extends Service {
|
|
|
1264
1270
|
}
|
|
1265
1271
|
agent = woken.agent;
|
|
1266
1272
|
}
|
|
1267
|
-
if (
|
|
1273
|
+
if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
|
|
1268
1274
|
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1269
1275
|
}
|
|
1270
1276
|
const sender = (() => {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
6
5
|
|
|
7
6
|
// ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
|
|
8
7
|
function isNullable(value) {
|
|
@@ -824,6 +823,13 @@ import WebSocket, { WebSocketServer } from "ws";
|
|
|
824
823
|
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
825
824
|
|
|
826
825
|
// src/interconnect/index.ts
|
|
826
|
+
function isSessionOwnedBySubagent(ctx, session, agent) {
|
|
827
|
+
if (session.header.origin === "subagent") return true;
|
|
828
|
+
const parentId = session.header.parentSession;
|
|
829
|
+
if (parentId === void 0 || agent === void 0) return false;
|
|
830
|
+
const parent = ctx.agents.get(parentId);
|
|
831
|
+
return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
|
|
832
|
+
}
|
|
827
833
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
828
834
|
var LINK_CHANNEL = "/interconnect/link";
|
|
829
835
|
var notificationSchema = Schema.union([
|
|
@@ -1206,7 +1212,7 @@ var InterconnectService = class extends Service {
|
|
|
1206
1212
|
* matches how the Host's own session listing treats its projection column.
|
|
1207
1213
|
*/
|
|
1208
1214
|
listSessions() {
|
|
1209
|
-
const reachable = this.ctx.agents.list().filter((agent) => !
|
|
1215
|
+
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1210
1216
|
const sessions = reachable.map((agent) => {
|
|
1211
1217
|
let title;
|
|
1212
1218
|
try {
|
|
@@ -1264,7 +1270,7 @@ var InterconnectService = class extends Service {
|
|
|
1264
1270
|
}
|
|
1265
1271
|
agent = woken.agent;
|
|
1266
1272
|
}
|
|
1267
|
-
if (
|
|
1273
|
+
if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
|
|
1268
1274
|
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1269
1275
|
}
|
|
1270
1276
|
const sender = (() => {
|