dsh-agent-message 1.5.0 → 1.5.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.en.md +12 -12
- package/README.md +12 -12
- package/lib/client.js +28 -18
- package/lib/index.js +84 -16
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -12,8 +12,8 @@ English | [中文](./README.md)
|
|
|
12
12
|
|
|
13
13
|
In DeepSeek Harness, a single process hosts multiple Agent sessions at once. This plugin equips each session with three tools so they can "message" each other:
|
|
14
14
|
|
|
15
|
-
- Before sending, first **list every sendable session** (
|
|
16
|
-
- Once found, **deliver the message to the target session** — ordinary messages always enter a new independent turn; if the target is offline (not loaded since the last process restart), the plugin resumes it through Harness's public API, delivers the message, and
|
|
15
|
+
- Before sending, first **list every sendable independent session** (non-archived, excluding actual subagents, including offline sessions that have not been reopened), and find the target by its title;
|
|
16
|
+
- Once found, **deliver the message to the target session** — ordinary messages always enter a new independent turn; if the target is offline (not loaded since the last process restart), the plugin resumes it through Harness's public API, delivers the message, and keeps the handle loaded for later communication until plugin teardown;
|
|
17
17
|
- When needed, **query the delivery status of a message on demand** (queued / claimed / discarded / unknown), with the target runtime status reported separately for supervision scenarios.
|
|
18
18
|
|
|
19
19
|
Typical scenarios: an orchestrator Agent dispatching work to a developer Agent, two Agents collaborating in a relay, a main session sending instructions to a test session, or a supervisor Agent watching over several workers.
|
|
@@ -33,7 +33,7 @@ Typical scenarios: an orchestrator Agent dispatching work to a developer Agent,
|
|
|
33
33
|
|
|
34
34
|

|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Current relay messages are displayed as visible Agent message cards; clicking the header opens the sender session. The persisted source remains plugin `relay` provenance rather than impersonating human input. The full session id remains in typed source metadata and in a Host-generated model-visible protocol header, so the receiving Agent never has to guess the sender.
|
|
37
37
|
|
|
38
38
|
### Delivery modes (the `mode` parameter of `send_agent_message`)
|
|
39
39
|
|
|
@@ -78,8 +78,8 @@ The plugin ships a `cordis.patch.yml` (pointed to by `dsh.bundle.patch` in `pack
|
|
|
78
78
|
## Usage
|
|
79
79
|
|
|
80
80
|
1. Type `@` at the beginning of session A's composer and choose the target from the native candidate menu; each candidate shows its title and `Running`/`Idle` activity;
|
|
81
|
-
2. `@` only tells A where the relevant session is; it does not mean send. A calls `send_agent_message` when
|
|
82
|
-
3. For an explicit forwarding request, A only delivers and reports the
|
|
81
|
+
2. `@` only tells A where the relevant session is; it does not mean send. A calls `send_agent_message` only when the current request or an orchestration responsibility already granted by the user requires cross-session communication. For example, `@B tell it to stop after opening the draft PR` sends, while `@B analyze its latest conversation result` must not send a message to B;
|
|
82
|
+
3. For an explicit forwarding request, A only delivers and reports whether the message was accepted or failed. It must not execute the forwarded task itself or ask B for an extra acknowledgement. B sends a message to `senderSessionId` only when the body explicitly asks it to return business content;
|
|
83
83
|
4. You can still ask the Agent to call `list_peer_agents` and send directly with a full session id;
|
|
84
84
|
5. Session B receives a native `UserMessage` with a typed relay source plus a minimal Host-generated source header on the first body line, so B does not have to guess the sender. The Client presents it as a visible Agent message card whose header opens the sender session;
|
|
85
85
|
6. (Supervision) Say "check the status of my messages to `<session id>`" — it calls `check_delivery`.
|
|
@@ -99,14 +99,16 @@ Delivery paths of `send_agent_message`:
|
|
|
99
99
|
|
|
100
100
|
Session enumeration, batched titles, and offline log reads use Harness's `sessionQuery.listSessions()`, `readTitleSnapshots()`, and `readSession()` respectively. `SessionId` is the only address; `parentSession` records fork lineage only, and only `origin: subagent` identifies an actual subagent. The plugin does not scan `sessionPersistence` directly to rebuild a parallel session directory.
|
|
101
101
|
|
|
102
|
-
After `send_agent_message` submits the native message to the target Inbox, it immediately returns `accepted` with the native `messageId
|
|
102
|
+
After `send_agent_message` submits the native message to the target Inbox, it immediately returns `accepted` with the native `messageId`; the model receives only a terse “delivered” projection while the complete result stays in tool presentation metadata. `check_delivery` then derives `pending` (still queued), `claimed` (claimed by a turn), `discarded` (cancelled), or `unknown` from Inbox events on demand. `claimed` is transport evidence only: it does not prove that the message was read, answered, or completed. Pre-admission failure remains a Harness tool error and writes nothing to the target Inbox. Runtime state is reported separately as `targetRuntimeStatus`, so unrelated Agent activity never changes the message state. A known `messageId` remains queryable from the target Inbox log after a process restart.
|
|
103
103
|
|
|
104
104
|
Every cross-session message is created by Harness `createUserMessage()`, and `UserMessage.id` is its only message identity. Its source always uses `kind: dsh-agent-message` and `form: relay`, plus the protocol version, sender/target Session ids, and display title. Because current Harness model requests do not expand custom source fields, the Host also writes a minimal `<dsh-agent-message>` header containing only `senderSessionId` on the first body line. The typed source is the durable/UI truth; the header is only the model-visible projection needed for reply addressing. The plugin registers no global system prompt; send admission lives only in the `send_agent_message` tool contract. The Client only projects relay as a visible Agent message card and never rewrites an Agent message as human `user` provenance.
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Relay only means “a message addressed by another session”; it neither requires nor forbids a reply. When the body explicitly requests business content in return, the receiving Agent may send a message to `senderSessionId` with the same tool. Otherwise it must not send a transport acknowledgement or a bare “received.” The plugin does not automatically correlate requests and replies or forward ordinary Agent answers.
|
|
107
107
|
|
|
108
108
|
The composer-side `@` session locator reuses Harness's native `inputTriggers` command marker. The visible selected title is capped at 40 Unicode characters with an ellipsis; submission replaces it with the full stable `@session-...` id for the current Agent. The sent bubble still projects that id with a chat icon and the live session title, so renaming a session does not change the locator target.
|
|
109
109
|
|
|
110
|
+
See [`docs/architecture-v2.md`](./docs/architecture-v2.md) for the current architecture contract.
|
|
111
|
+
|
|
110
112
|
## Directory structure
|
|
111
113
|
|
|
112
114
|
```
|
|
@@ -116,18 +118,16 @@ dsh-agent-message/
|
|
|
116
118
|
│ └── client.js # client half: @session references, session navigation and copy-session-id button
|
|
117
119
|
├── cordis.patch.yml # self-registration patch (pointed to by dsh.bundle.patch)
|
|
118
120
|
├── package.json # DSH plugin manifest (dsh.bundle / dsh.client / dshx.contributes)
|
|
119
|
-
├── docs/ #
|
|
121
|
+
├── docs/ # current architecture and README example screenshot
|
|
120
122
|
├── README.md # Chinese documentation
|
|
121
123
|
└── README.en.md # English documentation
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
## In Development
|
|
125
|
-
|
|
126
|
-
- **Cross-process communication:** Allow Agent sessions running in different DSH processes to exchange messages.
|
|
127
|
-
|
|
128
126
|
## Limitations
|
|
129
127
|
|
|
130
128
|
- The target session must be **non-archived** and present in local persistence; archived sessions are always rejected.
|
|
129
|
+
- These tools are only for communication between independent sessions; actual subagents are neither listed as targets nor allowed to call them.
|
|
130
|
+
- One Session pair, regardless of direction, may receive at most 10 deliveries in a rolling 60-second window. The 11th is rejected before the target Inbox is changed. This window belongs to the current Harness process and resets on restart.
|
|
131
131
|
- Automatically resuming an offline session uses the **default model** (it does not inherit a model manually selected earlier in that session); if resume fails, the message is not written to the target Inbox.
|
|
132
132
|
- Bulk receipt queries without `messageId` rely on in-memory bookkeeping and cover only the most recent 1000 sends in the current process (FIFO eviction). After a restart, a known `messageId` remains queryable, but the volatile `sentAt` and `mode` fields are no longer returned.
|
|
133
133
|
- Cross-process / cross-machine communication is out of scope.
|
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
在 DeepSeek Harness 里,一个进程会同时挂着多个 Agent 会话。本插件给每个会话装上三个工具,让它们能互相"发消息":
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
- 找到后,**把消息投递到目标会话**——普通消息统一进入独立的新 turn;目标离线(进程重启后还没打开)时,插件通过 Harness
|
|
15
|
+
- 发消息前,先**列出所有可发送的独立会话**(未归档、排除真实子代理,含离线未打开的),按标题找到目标;
|
|
16
|
+
- 找到后,**把消息投递到目标会话**——普通消息统一进入独立的新 turn;目标离线(进程重启后还没打开)时,插件通过 Harness 公开接口恢复会话、投递,并保持加载供后续通信,插件卸载时再释放 handle;
|
|
17
17
|
- 需要时,可以**按需查询**某条消息的送达状态(排队中/已认领/被丢弃/未知),并单独查看目标是否正在运行,供监督场景使用。
|
|
18
18
|
|
|
19
19
|
典型场景:编排者 Agent 给开发 Agent 派活、两个 Agent 协作接力、主会话给测试会话发指令、监督者 Agent 盯梢多个 worker。
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|

|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
当前 relay 消息显示为可见的 Agent 消息卡片;点击消息头即可跳转到发送方会话。持久化来源仍是插件 `relay`,不会伪装成人类输入。完整会话 ID 同时保留在 typed source 和 Host 生成的模型可见协议头中,避免接收 Agent 猜测发送方。
|
|
37
37
|
|
|
38
38
|
### 投递模式(`send_agent_message` 的 `mode` 参数)
|
|
39
39
|
|
|
@@ -79,8 +79,8 @@ Agent 会用 bash 执行这条命令,装完自动挂载、所有会话立即
|
|
|
79
79
|
## 使用
|
|
80
80
|
|
|
81
81
|
1. 在会话 A 的输入框开头键入 `@`,从原生候选菜单中选择目标会话;候选会显示标题和“运行中/空闲”;
|
|
82
|
-
2. `@` 只告诉 A 信息或操作的目标在哪里,不代表发送。当前请求或用户已授予的编排职责要求跨会话传递信息时,A
|
|
83
|
-
3. 显式要求转告时,A
|
|
82
|
+
2. `@` 只告诉 A 信息或操作的目标在哪里,不代表发送。当前请求或用户已授予的编排职责要求跨会话传递信息时,A 才调用 `send_agent_message`。例如 `@B 告诉他最后提交 PR draft 就停止` 会发送,`@B 帮我分析他最新的对话结果` 则不应向 B 发消息;
|
|
83
|
+
3. 显式要求转告时,A 只负责投递并报告“已接受”或失败,不代为执行被转发的任务,也不要求 B 额外回复“收到”;如果正文明确要求 B 把业务内容返回 A,B 才向 `senderSessionId` 发送消息;
|
|
84
84
|
4. 也可以让 Agent 调 `list_peer_agents`,再用完整会话 ID 直接发送;
|
|
85
85
|
5. 会话 B 收到的是带 typed relay source 的原生 `UserMessage`;正文首行还有 Host 生成的最小来源协议,B 不需要猜测发送方;Client 将其显示为可见 Agent 消息卡片,并可从消息头打开发送方会话;
|
|
86
86
|
6. (监督场景)说「查一下我发给 `<会话ID>` 的消息状态」——它会调 `check_delivery`。
|
|
@@ -100,14 +100,16 @@ Agent 会用 bash 执行这条命令,装完自动挂载、所有会话立即
|
|
|
100
100
|
|
|
101
101
|
会话枚举、批量标题和离线日志读取分别使用 Harness 的 `sessionQuery.listSessions()`、`readTitleSnapshots()` 与 `readSession()`。`SessionId` 是唯一地址;`parentSession` 只记录分叉血缘,只有 `origin: subagent` 才会被识别为真实子代理。插件不直接扫描 `sessionPersistence` 重建另一份会话目录。
|
|
102
102
|
|
|
103
|
-
`send_agent_message` 成功把原生消息提交给目标 Inbox 后立即返回 `accepted` 和该消息的原生 `messageId
|
|
103
|
+
`send_agent_message` 成功把原生消息提交给目标 Inbox 后立即返回 `accepted` 和该消息的原生 `messageId`;模型只接收简短的“已投递”,完整结果保留在工具呈现元数据中。`check_delivery` 根据 Inbox 事件按需返回 `pending`(仍在排队)、`claimed`(已被某轮认领)、`discarded`(被取消)或 `unknown`。`claimed` 只是传输证据,不表示已读、回复或任务完成。接纳前失败由 Harness 工具错误表示,不写入目标 Inbox。目标是否正在运行通过独立的 `targetRuntimeStatus` 返回,不把 Agent 的整体运行状态误当成某条消息正在处理。指定 `messageId` 时可从目标现有 Inbox 日志恢复状态,因此进程重启后仍可查询。
|
|
104
104
|
|
|
105
105
|
所有跨会话消息都由 Harness `createUserMessage()` 创建,`UserMessage.id` 是唯一消息身份。`source.kind` 固定为 `dsh-agent-message`,`form` 固定为 `relay`,并携带协议版本、发送/目标 Session 和显示标题。由于当前 Harness 不会把自定义 source 字段展开给模型,Host 还会在正文首行写入只含 `senderSessionId` 的最小 `<dsh-agent-message>` 协议头;source 是持久化/UI 真相,协议头只是回复寻址所需的模型可见投影。插件不注册全局系统提示词,发送准入只存在于 `send_agent_message` 的工具合同中。Client 只把 relay 投影为可见的 Agent 消息卡片,不会反向把 Agent 消息伪装成人类 `user` 来源。
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
relay 只表达“另一会话发来的消息”,本身不等于必须回复或禁止回复。正文明确要求返回业务内容时,接收 Agent 可用同一工具向 `senderSessionId` 发送消息;没有明确要求时不回传 transport ack 或单纯的“收到”。插件不自动关联请求与回复,也不自动转发 Agent 的普通回答。
|
|
108
108
|
|
|
109
109
|
输入框的 `@` 会话定位复用 Harness 原生 `inputTriggers` 命令标记:选择后的可见标题最多 40 个 Unicode 字符,超出用省略号;提交给当前 Agent 时换成完整 `@session-...` 稳定 ID。发送后的气泡依然用聊天图标和实时会话标题投影该 ID,显示名称变化不会改变定位目标。
|
|
110
110
|
|
|
111
|
+
完整的现役架构合同见 [`docs/architecture-v2.md`](./docs/architecture-v2.md)。
|
|
112
|
+
|
|
111
113
|
## 目录结构
|
|
112
114
|
|
|
113
115
|
```
|
|
@@ -117,18 +119,16 @@ dsh-agent-message/
|
|
|
117
119
|
│ └── client.js # client 半区:@会话引用、会话导航与复制会话ID按钮
|
|
118
120
|
├── cordis.patch.yml # 自注册补丁(dsh.bundle.patch 指向它)
|
|
119
121
|
├── package.json # DSH 插件清单(dsh.bundle / dsh.client / dshx.contributes)
|
|
120
|
-
├── docs/ #
|
|
122
|
+
├── docs/ # 现役架构与 README 示例截图
|
|
121
123
|
├── README.md # 中文文档
|
|
122
124
|
└── README.en.md # English documentation
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
## 正在开发
|
|
126
|
-
|
|
127
|
-
- **跨进程通信**:让运行在不同 DSH 进程中的 Agent 会话也能互相收发消息。
|
|
128
|
-
|
|
129
127
|
## 限制
|
|
130
128
|
|
|
131
129
|
- 目标会话必须**未归档**且存在于本机持久化里;归档会话一律拒绝发送。
|
|
130
|
+
- 工具只用于独立 Session 之间通信;真实子代理既不会出现在目标列表中,也不能作为调用方使用这些工具。
|
|
131
|
+
- 同一对 Session(不分发送方向)在滚动 60 秒内最多投递 10 条消息;第 11 条会在写入目标 Inbox 前被拒绝。该窗口只属于当前 Harness 进程,重启后清空。
|
|
132
132
|
- 自动恢复离线会话时会使用**默认模型**(不继承它上次手动切换的模型选择);恢复失败时消息不会被写入目标 Inbox。
|
|
133
133
|
- 不指定 `messageId` 的批量回执依赖内存记账,只覆盖本进程最近 1000 条发送记录(FIFO 淘汰);进程重启后仍可凭已知 `messageId` 查询,但不再返回易失的 `sentAt` 和 `mode`。
|
|
134
134
|
- 跨进程/跨机器通信不在本插件范围内。
|
package/lib/client.js
CHANGED
|
@@ -14,6 +14,7 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
function apply(ctx) {
|
|
15
15
|
const senderSelector = '[data-ref-chip="subagent"], [data-context-relay-sender]';
|
|
16
16
|
const referenceSource = "agent-message-session";
|
|
17
|
+
const reactRootSelector = "[data-agent-msg-react-root]";
|
|
17
18
|
const candidateRows = new WeakMap();
|
|
18
19
|
const mountedRoots = new Map();
|
|
19
20
|
const sessionLinks = new Set();
|
|
@@ -53,20 +54,26 @@ window.__ModuleLoader__.load({
|
|
|
53
54
|
if (!root) {
|
|
54
55
|
root = createRoot(host);
|
|
55
56
|
mountedRoots.set(host, root);
|
|
57
|
+
host.setAttribute("data-agent-msg-react-root", "true");
|
|
56
58
|
}
|
|
57
59
|
root.render(content);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function cleanupRoots(node) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
if (!(node instanceof Element)) return;
|
|
64
|
+
const hosts = node.matches(reactRootSelector)
|
|
65
|
+
? [node].concat(Array.from(node.querySelectorAll(reactRootSelector)))
|
|
66
|
+
: Array.from(node.querySelectorAll(reactRootSelector));
|
|
67
|
+
hosts.forEach(function (host) {
|
|
68
|
+
const root = mountedRoots.get(host);
|
|
69
|
+
if (!root) return;
|
|
70
|
+
root.unmount();
|
|
71
|
+
mountedRoots.delete(host);
|
|
69
72
|
});
|
|
73
|
+
const links = node.matches(".agent-msg-session-link")
|
|
74
|
+
? [node].concat(Array.from(node.querySelectorAll(".agent-msg-session-link")))
|
|
75
|
+
: Array.from(node.querySelectorAll(".agent-msg-session-link"));
|
|
76
|
+
links.forEach(function (element) { sessionLinks.delete(element); });
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
function SessionActivity(props) {
|
|
@@ -84,7 +91,7 @@ window.__ModuleLoader__.load({
|
|
|
84
91
|
function RelaySender(props) {
|
|
85
92
|
return React.createElement("span", { className: "agent-msg-relay-sender-content" },
|
|
86
93
|
React.createElement(IconQueueOutline14, { size: 14, className: "agent-msg-session-icon" }),
|
|
87
|
-
React.createElement("span", null, "From Session · " + props.title + ":"));
|
|
94
|
+
React.createElement("span", null, uiText("来自会话 · ", "From Session · ") + props.title + ":"));
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
function enhanceReferenceHost(element, row) {
|
|
@@ -166,7 +173,7 @@ window.__ModuleLoader__.load({
|
|
|
166
173
|
|
|
167
174
|
function titleFrom(text) {
|
|
168
175
|
const line = String(text || "").split("\n", 1)[0].trim();
|
|
169
|
-
const current = line.match(/^From (?:Session|Agent)(?: ·)? (.+?):(?: @session-[\w-]+)?$/);
|
|
176
|
+
const current = line.match(/^(?:From (?:Session|Agent)|来自会话)(?: ·)? (.+?):(?: @session-[\w-]+)?$/);
|
|
170
177
|
const previous = line.match(/^来自 Agent · (.+)$/);
|
|
171
178
|
const legacy = line.match(/^来自 Agent「(.+)」\s*[·::]?$/);
|
|
172
179
|
return (current || previous || legacy)?.[1]?.trim() || "";
|
|
@@ -177,7 +184,6 @@ window.__ModuleLoader__.load({
|
|
|
177
184
|
const label = element.previousElementSibling;
|
|
178
185
|
const title = titleFrom(label?.textContent);
|
|
179
186
|
if (title && label) {
|
|
180
|
-
label.textContent = "";
|
|
181
187
|
label.classList.add("agent-msg-sender-prefix");
|
|
182
188
|
}
|
|
183
189
|
return title;
|
|
@@ -197,13 +203,13 @@ window.__ModuleLoader__.load({
|
|
|
197
203
|
element.dataset.agentMsgSessionId = link.sessionId;
|
|
198
204
|
if (title) element.dataset.agentMsgSenderTitle = title;
|
|
199
205
|
if (relay) renderInto(element, React.createElement(RelaySender, { title: title }));
|
|
200
|
-
else element.textContent = "From Session · " + title + ":";
|
|
206
|
+
else element.textContent = uiText("来自会话 · ", "From Session · ") + title + ":";
|
|
201
207
|
element.classList.add("agent-msg-session-link", "agent-msg-sender-link");
|
|
202
208
|
sessionLinks.add(element);
|
|
203
209
|
element.setAttribute("role", "link");
|
|
204
210
|
element.setAttribute("tabindex", "0");
|
|
205
|
-
element.setAttribute("title", "打开发送方会话");
|
|
206
|
-
element.setAttribute("aria-label", "打开发送方会话:" + (title || link.sessionId));
|
|
211
|
+
element.setAttribute("title", uiText("打开发送方会话", "Open sender session"));
|
|
212
|
+
element.setAttribute("aria-label", uiText("打开发送方会话:", "Open sender session: ") + (title || link.sessionId));
|
|
207
213
|
});
|
|
208
214
|
}
|
|
209
215
|
|
|
@@ -363,14 +369,18 @@ window.__ModuleLoader__.load({
|
|
|
363
369
|
type: "button",
|
|
364
370
|
onClick: onClick,
|
|
365
371
|
onMouseLeave: onMouseLeave,
|
|
366
|
-
title: "复制会话 ID",
|
|
367
|
-
"aria-label": "复制会话 ID",
|
|
372
|
+
title: uiText("复制会话 ID", "Copy session ID"),
|
|
373
|
+
"aria-label": uiText("复制会话 ID", "Copy session ID"),
|
|
368
374
|
className: "agent-msg-copy-id"
|
|
369
|
-
}, copyState === "copied"
|
|
375
|
+
}, copyState === "copied"
|
|
376
|
+
? uiText("已复制", "Copied")
|
|
377
|
+
: copyState === "failed"
|
|
378
|
+
? uiText("复制失败", "Copy failed")
|
|
379
|
+
: uiText("复制ID", "Copy ID"));
|
|
370
380
|
}
|
|
371
381
|
|
|
372
382
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register(
|
|
373
|
-
{ name: "conversation.session.header.actions", id: "copy-session-id", order: 30, label: "复制会话ID" },
|
|
383
|
+
{ name: "conversation.session.header.actions", id: "copy-session-id", order: 30, label: uiText("复制会话ID", "Copy session ID") },
|
|
374
384
|
(props) => React.createElement(CopyButton, { sessionId: props.sessionId })
|
|
375
385
|
));
|
|
376
386
|
|
package/lib/index.js
CHANGED
|
@@ -10,9 +10,14 @@ export function apply(ctx) {
|
|
|
10
10
|
const sent = new Map()
|
|
11
11
|
/** 记账表 FIFO 上限:超过则淘汰最老记录,内存恒定。 */
|
|
12
12
|
const SENT_MAX = 1000
|
|
13
|
+
const PAIR_MESSAGE_LIMIT = 10
|
|
14
|
+
const PAIR_WINDOW_MS = 60_000
|
|
13
15
|
const receiptMeaning = 'claimed 仅表示目标 turn 已从 Inbox 认领消息;传输回执不表示对方已读、回复或完成。'
|
|
14
16
|
/** 插件恢复的 Session handle;保留到插件卸载,避免 idle dispose 移除 Harness store 投影。 */
|
|
15
17
|
const resumedHandles = new Map()
|
|
18
|
+
/** 无向 Session 对 -> 最近成功或正在进行的投递预留;只保护当前 Harness 进程。 */
|
|
19
|
+
const pairSends = new Map()
|
|
20
|
+
let lastPairPruneAt = 0
|
|
16
21
|
|
|
17
22
|
ctx.effect(() => async () => {
|
|
18
23
|
const handles = [...resumedHandles.values()]
|
|
@@ -37,6 +42,34 @@ export function apply(ctx) {
|
|
|
37
42
|
if (sent.size > SENT_MAX) sent.delete(sent.keys().next().value)
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
function reservePairSend(from, to) {
|
|
46
|
+
const now = Date.now()
|
|
47
|
+
if (now < lastPairPruneAt || now - lastPairPruneAt >= PAIR_WINDOW_MS) {
|
|
48
|
+
for (const [key, entries] of pairSends) {
|
|
49
|
+
const active = entries.filter((entry) => now - entry.at < PAIR_WINDOW_MS)
|
|
50
|
+
if (active.length === 0) pairSends.delete(key)
|
|
51
|
+
else pairSends.set(key, active)
|
|
52
|
+
}
|
|
53
|
+
lastPairPruneAt = now
|
|
54
|
+
}
|
|
55
|
+
const key = JSON.stringify([String(from), String(to)].sort())
|
|
56
|
+
const recent = (pairSends.get(key) ?? []).filter((entry) => now - entry.at < PAIR_WINDOW_MS)
|
|
57
|
+
if (recent.length >= PAIR_MESSAGE_LIMIT) {
|
|
58
|
+
pairSends.set(key, recent)
|
|
59
|
+
throw new Error('同一对会话 60 秒内最多投递 10 条消息,请稍后再试')
|
|
60
|
+
}
|
|
61
|
+
const reservation = { at: now }
|
|
62
|
+
recent.push(reservation)
|
|
63
|
+
pairSends.set(key, recent)
|
|
64
|
+
return () => {
|
|
65
|
+
const current = pairSends.get(key)
|
|
66
|
+
if (current === undefined) return
|
|
67
|
+
const index = current.indexOf(reservation)
|
|
68
|
+
if (index !== -1) current.splice(index, 1)
|
|
69
|
+
if (current.length === 0) pairSends.delete(key)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
function titleOf(agent) {
|
|
41
74
|
const service = ctx.get('sessionTitle')
|
|
42
75
|
if (service !== undefined) {
|
|
@@ -71,6 +104,12 @@ export function apply(ctx) {
|
|
|
71
104
|
return header?.origin === 'subagent'
|
|
72
105
|
}
|
|
73
106
|
|
|
107
|
+
function assertPeerCaller(agent) {
|
|
108
|
+
if (agent !== undefined && isSubagentSession(agent.session.header)) {
|
|
109
|
+
throw new Error('子代理不能使用独立会话通信工具')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
74
113
|
function sessionQuery() {
|
|
75
114
|
const query = ctx.get('sessionQuery')
|
|
76
115
|
if (query === undefined) throw new Error('本部署缺少 sessionQuery,无法查询逻辑会话')
|
|
@@ -180,6 +219,7 @@ export function apply(ctx) {
|
|
|
180
219
|
},
|
|
181
220
|
async execute(_args, exec) {
|
|
182
221
|
const me = exec.agent
|
|
222
|
+
assertPeerCaller(me)
|
|
183
223
|
const archived = archivedIds()
|
|
184
224
|
const query = sessionQuery()
|
|
185
225
|
const live = new Map()
|
|
@@ -230,6 +270,7 @@ export function apply(ctx) {
|
|
|
230
270
|
'默认使用 followup 创建独立的新 turn;' +
|
|
231
271
|
'目标离线(进程里未加载)时自动恢复该会话后投递。' +
|
|
232
272
|
'用户无需说出模式名:目标为 running 且整句明确要求立即介入时用 steer,明确要求不打断当前任务、只补充上下文时用 inject;不确定时使用 followup。' +
|
|
273
|
+
'同一对 Session 双向合计 60 秒内最多投递 10 条消息,超过时拒绝本次投递。' +
|
|
233
274
|
'归档会话和子代理一律拒绝。' +
|
|
234
275
|
'注意:它不同于 send_message(后者是给你的后台子代理续聊)。',
|
|
235
276
|
parameters: {
|
|
@@ -239,15 +280,31 @@ export function apply(ctx) {
|
|
|
239
280
|
},
|
|
240
281
|
output: {
|
|
241
282
|
schema: { type: 'json' },
|
|
242
|
-
render(
|
|
243
|
-
return [{ type: 'text', text:
|
|
283
|
+
render() {
|
|
284
|
+
return [{ type: 'text', text: '已投递。' }]
|
|
244
285
|
},
|
|
286
|
+
presentationMeta(_args, value) {
|
|
287
|
+
return value
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
presentResult(_args, result) {
|
|
291
|
+
const meta = result.meta
|
|
292
|
+
if (result.isError || meta === null || typeof meta !== 'object' || Array.isArray(meta)) return
|
|
293
|
+
return {
|
|
294
|
+
card: 'generic',
|
|
295
|
+
title: '消息已投递',
|
|
296
|
+
content: [{ type: 'text', text: meta.text || JSON.stringify(meta, null, 2) }],
|
|
297
|
+
}
|
|
245
298
|
},
|
|
246
299
|
async execute(args, exec) {
|
|
247
300
|
const me = exec.agent
|
|
248
301
|
if (me === undefined) throw new Error('no calling agent')
|
|
302
|
+
assertPeerCaller(me)
|
|
249
303
|
const to = args.to
|
|
250
304
|
if (to === '' || String(to) === String(me.id)) throw new Error('不能给自己发消息')
|
|
305
|
+
if (args.content.includes('<dsh-agent-message>') || args.content.includes('</dsh-agent-message>')) {
|
|
306
|
+
throw new Error('消息正文不能包含保留协议标签 dsh-agent-message')
|
|
307
|
+
}
|
|
251
308
|
|
|
252
309
|
const archived = archivedIds()
|
|
253
310
|
if (archived.has(String(to))) throw new Error('对方会话已归档,无法发送(请先取消归档)')
|
|
@@ -260,6 +317,14 @@ export function apply(ctx) {
|
|
|
260
317
|
throw new Error(mode + ' 仅用于 running 会话;目标当前状态:' + target.status)
|
|
261
318
|
}
|
|
262
319
|
|
|
320
|
+
let inspected
|
|
321
|
+
if (target === undefined) {
|
|
322
|
+
inspected = await readLogicalSession(to)
|
|
323
|
+
if (inspected === undefined) throw new Error('会话不存在:' + to)
|
|
324
|
+
if (isSubagentSession(inspected.session)) throw new Error('目标是子代理,不能通过会话通信插件直接发送')
|
|
325
|
+
if (mode !== 'followup') throw new Error('目标离线(进程里未加载):' + mode + ' 仅用于 running 会话')
|
|
326
|
+
}
|
|
327
|
+
|
|
263
328
|
const myTitle = titleOf(me) || String(me.id)
|
|
264
329
|
const source = {
|
|
265
330
|
kind: name,
|
|
@@ -279,19 +344,21 @@ export function apply(ctx) {
|
|
|
279
344
|
let usedMode = ''
|
|
280
345
|
let targetRuntimeStatus = 'offline'
|
|
281
346
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
347
|
+
const rollbackPairSend = reservePairSend(me.id, to)
|
|
348
|
+
try {
|
|
349
|
+
if (target !== undefined) {
|
|
350
|
+
if (mode === 'followup') { target.followup(message); usedMode = mode }
|
|
351
|
+
else if (mode === 'inject') { target.inject(message); usedMode = 'inject' }
|
|
352
|
+
else { target.steer(message); usedMode = 'steer' }
|
|
353
|
+
targetRuntimeStatus = target.status
|
|
354
|
+
} else {
|
|
355
|
+
const resumed = await resumeAndFollowup(to, message, inspected)
|
|
356
|
+
usedMode = 'followup'
|
|
357
|
+
targetRuntimeStatus = resumed.status
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
rollbackPairSend()
|
|
361
|
+
throw error
|
|
295
362
|
}
|
|
296
363
|
|
|
297
364
|
rememberSent(message.id, String(to), usedMode)
|
|
@@ -324,7 +391,8 @@ export function apply(ctx) {
|
|
|
324
391
|
return [{ type: 'text', text: JSON.stringify(value, null, 2) }]
|
|
325
392
|
},
|
|
326
393
|
},
|
|
327
|
-
async execute(args) {
|
|
394
|
+
async execute(args, exec) {
|
|
395
|
+
assertPeerCaller(exec?.agent)
|
|
328
396
|
const target = agents.get(args.to)
|
|
329
397
|
const targetRuntimeStatus = target !== undefined ? target.status : 'offline'
|
|
330
398
|
if (args.messageId !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-message",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"packageManager": "pnpm@11.1.1",
|
|
5
5
|
"description": "跨会话 Agent 通信:让 DeepSeek Harness 里不同的 Agent 会话互相收发消息。",
|
|
6
6
|
"keywords": ["deepseek", "deepseek-harness", "agent", "multi-agent", "messaging"],
|