dsh-plugin-t-expert 0.3.41 → 0.3.45
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 +6 -0
- package/lib/bootstrap.js +15 -3
- package/lib/client.js +5 -3
- package/lib/index.js +92 -10
- package/lib/teams/harness-compat.js +368 -22
- package/lib/teams/members.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,11 @@ CLI 会被拒绝 —— 任何插件都一样)。
|
|
|
23
23
|
|
|
24
24
|
- 依赖:DSH 宿主 `0.1.5-rc.1` / `0.1.5-rc.2`;Node `^22.19.0 || >=24`;`pnpm` 在 PATH 上。
|
|
25
25
|
`python3` 只在**保存小队**时用到(缺它插件照常加载,只是小队编译会报错并回滚)。
|
|
26
|
+
团队引擎对接的是宿主**未文档化**的内部投递契约,实测过的宿主版本记在
|
|
27
|
+
`lib/teams/harness-compat.js` 的 `TESTED_HARNESS_VERSIONS`:版本不在矩阵里、或契约形状变了,
|
|
28
|
+
日志会有一条 `[t-team] ... contract ...`,提示段也会写明(见「常见问题」最后两行)。
|
|
29
|
+
符号口径变了(上游不再用 `Symbol.for`)时插件会动态 import 宿主的 `internal` 子路径拿**真身**
|
|
30
|
+
符号并自动重挂引擎 —— 能自救,但日志里照样留一条,方便你把实测矩阵补齐。
|
|
26
31
|
- 装完**重启 DSH** 即可用;第一次怎么上手见下面的「快速上手」。
|
|
27
32
|
|
|
28
33
|
## 快速上手
|
|
@@ -133,6 +138,7 @@ CLI 会被拒绝 —— 任何插件都一样)。
|
|
|
133
138
|
| 保存小队报错 | 缺 `python3`,或新定义没通过编译 —— **失败会自动回滚**,盘上仍是旧配置,设置页会说明引擎是否可用 |
|
|
134
139
|
| 定时任务没有按时跑 | 设置 → T专家 → 「显示定时任务按钮」若被关掉,宿主会停掉**全部**定时触发(手动执行也会被拒),这是有意为之 |
|
|
135
140
|
| 改完插件看不到变化 | 客户端与宿主代码都要重启 DSH 才生效;设置 → T专家 标题行有当前版本号(`v0.3.x`)可核对 |
|
|
141
|
+
| 升级 DSH 后团队功能变得可疑(叫不醒成员、已退休成员又干活) | 宿主的 subagent 内部投递契约变了:日志里有一条 `[t-team] ... contract ...`,模型看到的提示段也会写明「宿主版本不在实测矩阵 / 形状与实测不符」。改 `lib/teams/harness-compat.js`:按新宿主核对 `HARNESS_ROUTES` 的形参个数,实测通过后把版本加进 `TESTED_HARNESS_VERSIONS` |
|
|
136
142
|
|
|
137
143
|
## 卸载与反馈
|
|
138
144
|
|
package/lib/bootstrap.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* 数据目录根的 {@link MANIFEST_NAME} 同步记录,调用方据此在**状态变化时**告警一次,
|
|
36
36
|
* 而不是每次启动都刷屏。
|
|
37
37
|
*/
|
|
38
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
39
39
|
import { dirname, join, resolve } from "node:path";
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
|
|
@@ -234,18 +234,30 @@ function readPackageVersion(snapshotDir) {
|
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
236
|
* 原子写(先写临时文件再 rename):同步记录与合并后的 JSON 都不能留半截。
|
|
237
|
+
*
|
|
238
|
+
* 临时名带 pid + 时间戳(与 `catalog.js` 的 `writeFileAtomic` 同口径):两个 DSH 实例
|
|
239
|
+
* 同时首启、对同一个 `~/.t-team/` 播种时,固定的 `<path>.tmp` 会互相覆盖对方的半截写入,
|
|
240
|
+
* 唯一化之后两边各写各的临时文件,rename 才是各自原子的。
|
|
237
241
|
* @param path - 目标路径。
|
|
238
242
|
* @param text - 内容。
|
|
239
243
|
*/
|
|
240
244
|
function writeFileAtomic(path, text) {
|
|
241
|
-
const tmp = `${path}.tmp`;
|
|
245
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
242
246
|
writeFileSync(tmp, text);
|
|
243
247
|
renameSync(tmp, path);
|
|
244
248
|
}
|
|
245
249
|
|
|
246
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* 两个文件内容是否逐字节相同。
|
|
252
|
+
*
|
|
253
|
+
* 先比 `size`:大小不同必然内容不同,直接短路 —— 省掉两次全量读取。这条路径命中的是
|
|
254
|
+
* "包内条目被改过 / 用户改过译文正文" 这类**确实有差异**的文件(升级、补译后),
|
|
255
|
+
* 让 seedData 不必把两份都读进内存再逐字节比对才发现它们不一样。
|
|
256
|
+
* 大小相同才回退到全量字节比较(不同内容同长度虽罕见,但必须判准,不能只看 size)。
|
|
257
|
+
*/
|
|
247
258
|
function sameContent(from, to) {
|
|
248
259
|
try {
|
|
260
|
+
if (statSync(from).size !== statSync(to).size) return false;
|
|
249
261
|
return readFileSync(from).equals(readFileSync(to));
|
|
250
262
|
} catch {
|
|
251
263
|
return false;
|
package/lib/client.js
CHANGED
|
@@ -20018,7 +20018,7 @@ var scheduleWorkspaceSchema = external_exports.object({
|
|
|
20018
20018
|
|
|
20019
20019
|
// src/client.jsx
|
|
20020
20020
|
var NS = "t-team";
|
|
20021
|
-
var CLIENT_BUILD_VERSION = "0.3.
|
|
20021
|
+
var CLIENT_BUILD_VERSION = "0.3.45";
|
|
20022
20022
|
var uiPrefs = { showTaskBadge: true, showScheduleButton: true, showActivePulse: true };
|
|
20023
20023
|
var UI_PREF_KEYS = Object.keys(uiPrefs);
|
|
20024
20024
|
var uiPrefsListeners = /* @__PURE__ */ new Set();
|
|
@@ -20803,7 +20803,8 @@ var CSS = `
|
|
|
20803
20803
|
.t-team-job-badge-dot[data-live="true"]{background:var(--dsw-alias-state-success-primary);animation:t-team-job-pulse 1.25s ease-in-out infinite}
|
|
20804
20804
|
.t-team-job-badge-count{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
|
|
20805
20805
|
@keyframes t-team-job-pulse{0%,to{opacity:.42}50%{opacity:1}}
|
|
20806
|
-
|
|
20806
|
+
/* \u4EFB\u52A1\u6D6E\u5C42\u523B\u610F**\u4E0D\u5E26\u6295\u5F71**\uFF08\u7528\u6237\u8981\u6C42\uFF09\uFF1A\u9760 1px \u8FB9\u6846 + \u6BDB\u73BB\u7483\u4E0E\u5E95\u4E0B\u5185\u5BB9\u5206\u5C42\uFF0C\u522B\u518D\u987A\u624B\u52A0\u56DE box-shadow\u3002 */
|
|
20807
|
+
.t-team-job-panel{position:fixed;z-index:60;display:flex;flex-direction:column;width:380px;max-height:min(460px,72vh);box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));backdrop-filter:blur(20px) saturate(1.08);color:var(--dsw-alias-label-primary);animation:t-team-job-panel-in .16s ease-out}
|
|
20807
20808
|
@keyframes t-team-job-panel-in{0%{opacity:0}to{opacity:1}}
|
|
20808
20809
|
.t-team-job-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:44px;padding:0 10px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2);cursor:grab;touch-action:none;user-select:none}
|
|
20809
20810
|
.t-team-job-panel-head:active{cursor:grabbing}
|
|
@@ -20927,7 +20928,8 @@ button:has(.t-team-schedule-glyph):hover{background:var(--dsw-alias-button-float
|
|
|
20927
20928
|
/* \u63D0\u793A\u8BCD\u6846\u4E0B\u65B9\u7684\u300CT\u4E13\u5BB6\u300D\u6311\u9009\u5668\uFF1A\u6309\u94AE + \u53EA\u542B\u4E13\u5BB6\u5217\u8868\u7684\u6D6E\u5C42\uFF08\u7C7B\u540D\u72EC\u7ACB\uFF0C\u907F\u514D\u4E0E\u65E2\u6709\u7C7B\u4E92\u76F8\u8986\u76D6\uFF09 */
|
|
20928
20929
|
.t-team-expert-pick{position:relative;display:flex;align-items:center;gap:8px;margin-top:2px}
|
|
20929
20930
|
.t-team-expert-pick-hint{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
20930
|
-
.t-team-
|
|
20931
|
+
/* \u540C\u4EFB\u52A1\u6D6E\u5C42\uFF1A\u4E0D\u5E26\u6295\u5F71\uFF08\u6DF1\u8272\u4E3B\u9898\u4E0B\u4F1A\u53D8\u6210\u5916\u53D1\u5149\uFF0C\u89C1 .t-team-job-panel \u5904\u7684\u8BF4\u660E\uFF09\u3002 */
|
|
20932
|
+
.t-team-expert-pick-panel{position:absolute;left:0;top:calc(100% + 6px);z-index:26;display:flex;flex-direction:column;gap:6px;width:min(420px,90vw);max-height:300px;box-sizing:border-box;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3))}
|
|
20931
20933
|
.t-team-expert-pick-search{box-sizing:border-box;width:100%;padding:7px 9px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
|
|
20932
20934
|
.t-team-expert-pick-search:focus{outline:none;border-color:var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))}
|
|
20933
20935
|
.t-team-expert-pick-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;overflow:auto;min-height:0}
|
package/lib/index.js
CHANGED
|
@@ -68,6 +68,9 @@ import { installTTeamGestureBoundary } from "./teams/command.js";
|
|
|
68
68
|
// 设置页「团队」标签复用引擎自己的快照收集与停止实现(不重写一套)。
|
|
69
69
|
import { collectArchivedTeamsActivity, collectTeamsActivity } from "./teams/snapshot.js";
|
|
70
70
|
import { haltTeamWork } from "./teams/tools.js";
|
|
71
|
+
// 宿主 subagent 契约体检(B-2 软闸门):引擎用的是上游**未文档化**的内部投递契约,
|
|
72
|
+
// 未实测的宿主版本/契约漂移必须对模型可见(见 harness-compat.js 的说明)。
|
|
73
|
+
import { TESTED_HARNESS_VERSIONS, probeHarnessContract, resolveHarnessAuthority } from "./teams/harness-compat.js";
|
|
71
74
|
|
|
72
75
|
export const name = NS;
|
|
73
76
|
/**
|
|
@@ -507,6 +510,10 @@ export function apply(ctx, config) {
|
|
|
507
510
|
divisionLabels = catalog.labels ?? {};
|
|
508
511
|
loadError = null;
|
|
509
512
|
loaded = true;
|
|
513
|
+
// 名册重载后让规模缓存失效:提示段的「a N-expert, M-division roster」现算一次即可跟上
|
|
514
|
+
// 运行时增删(rosterScale 按目录永久缓存,不清就会滞后到下次重启,与本文件「绝不对模型
|
|
515
|
+
// 说谎」的口径相悖)。只在真发生重载时删一次,不给每次渲染添 I/O。
|
|
516
|
+
rosterScaleCache.delete(config.root);
|
|
510
517
|
stamp = await fingerprint(); // 记录加载后的指纹
|
|
511
518
|
} catch (error) {
|
|
512
519
|
loadError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1257,6 +1264,12 @@ export function apply(ctx, config) {
|
|
|
1257
1264
|
* 的那个分叉。提示段靠它区分「引擎从未挂载过」与「挂载过、但这次热重载失败」。
|
|
1258
1265
|
*/
|
|
1259
1266
|
reloadFailure: "",
|
|
1267
|
+
/**
|
|
1268
|
+
* 这次重挂失败的**来源**:`"squad"`(用户保存小队触发的热重载)或 `"harness"`(宿主投递符号
|
|
1269
|
+
* 换了口径后的自动重挂)。提示段的标题与建议按它换 —— 否则会把"宿主契约变了"说成
|
|
1270
|
+
* "你保存的小队没生效",文案与事实不一致。
|
|
1271
|
+
*/
|
|
1272
|
+
reloadFailureKind: "squad",
|
|
1260
1273
|
};
|
|
1261
1274
|
/** 首次探到「调度成功但工具没注册」时的告警只打一次(此后每轮都探,但不再刷日志)。 */
|
|
1262
1275
|
let engineNotReadyWarned = false;
|
|
@@ -1318,11 +1331,23 @@ export function apply(ctx, config) {
|
|
|
1318
1331
|
installTTeamGestureBoundary(ctx, () => engineConfig.profiles ?? {});
|
|
1319
1332
|
gestureBoundaryInstalled = true;
|
|
1320
1333
|
};
|
|
1334
|
+
/**
|
|
1335
|
+
* 宿主 subagent 契约体检结果(B-2 软闸门):**懒算一次 + 每次挂载成功刷新**。
|
|
1336
|
+
*
|
|
1337
|
+
* 为什么不用"每次渲染现算":下面的 `text:` 是**每次请求**都求值的,而这个体检要读安装树里的
|
|
1338
|
+
* `package.json`(同步 I/O)。契约在同一进程里不会变(同一份安装、同一个服务实例),算一次就够;
|
|
1339
|
+
* 挂载/热重载是"宿主可能刚被换过"的最近观测点,顺手刷一遍不会多花什么。
|
|
1340
|
+
* 探测本身**绝不抛错**(它是诊断,不是门禁),所以放在提示段里是安全的。
|
|
1341
|
+
*/
|
|
1342
|
+
let harnessReport;
|
|
1343
|
+
const refreshHarnessReport = () => (harnessReport = probeHarnessContract(ctx));
|
|
1344
|
+
const harnessReportOf = () => (harnessReport ??= probeHarnessContract(ctx));
|
|
1321
1345
|
try {
|
|
1322
1346
|
// 首次挂载(启动期读不到配置就降级,不假装就绪;原因见 prompt 段与日志)。
|
|
1323
1347
|
if (initialEngineConfig.detail === "") {
|
|
1324
1348
|
engineFiber = ctx.plugin(teamsEngine, teamsEngineArgs());
|
|
1325
1349
|
engineState.ok = true;
|
|
1350
|
+
refreshHarnessReport();
|
|
1326
1351
|
installGestureBoundaryOnce();
|
|
1327
1352
|
} else {
|
|
1328
1353
|
ctx.logger?.error?.(`[t-team] ${initialEngineConfig.detail}`);
|
|
@@ -1361,11 +1386,14 @@ export function apply(ctx, config) {
|
|
|
1361
1386
|
* · 全程不向上抛(保存已经成功,抛错会让用户以为没保存成)。
|
|
1362
1387
|
* @returns `{ ok, detail }`:`detail` 只在失败时非空,且必须是**可操作**的原因。
|
|
1363
1388
|
*/
|
|
1364
|
-
const reloadEngine = async () => {
|
|
1389
|
+
const reloadEngine = async (origin = "squad") => {
|
|
1365
1390
|
/** 重载失败的统一收口:留在提示段与日志里("引擎还在用旧小队"与"引擎整个没了"要分清)。 */
|
|
1366
1391
|
const reloadFailed = (detail) => {
|
|
1367
1392
|
engineState.reloadFailure = detail;
|
|
1368
|
-
|
|
1393
|
+
engineState.reloadFailureKind = origin === "harness" ? "harness" : "squad";
|
|
1394
|
+
ctx.logger?.error?.(origin === "harness"
|
|
1395
|
+
? `[t-team] 宿主投递符号换了口径,自动重挂团队引擎失败:${detail}`
|
|
1396
|
+
: `[t-team] 小队已保存,但团队引擎热重载失败:${detail}`);
|
|
1369
1397
|
return { ok: false, detail };
|
|
1370
1398
|
};
|
|
1371
1399
|
const fresh = loadEngineConfig();
|
|
@@ -1406,11 +1434,34 @@ export function apply(ctx, config) {
|
|
|
1406
1434
|
engineState.ok = true;
|
|
1407
1435
|
lastMountError = "";
|
|
1408
1436
|
engineState.reloadFailure = "";
|
|
1437
|
+
refreshHarnessReport();
|
|
1409
1438
|
// 启动期因"还没有小队配置"而没挂上时,这里补装手势边界:现在引擎已经起来了。
|
|
1410
1439
|
installGestureBoundaryOnce();
|
|
1411
1440
|
return { ok: true, detail: "" };
|
|
1412
1441
|
};
|
|
1413
1442
|
|
|
1443
|
+
/**
|
|
1444
|
+
* 契约报告 → 一行**给模型看**的说明,**只在未实测时非空**。
|
|
1445
|
+
*
|
|
1446
|
+
* 为什么只在未实测时出现:已实测(版本在矩阵内、形状一致)是常态,常态不该占提示词预算;
|
|
1447
|
+
* 而未实测正是"将来某天叫不醒成员"的唯一解释来源 —— 那时用户问起来,模型必须答得上来。
|
|
1448
|
+
* 文案与日志里的那条(harnessContractWarning)同源同因,但这里讲的是"怎么办"。
|
|
1449
|
+
* @param report - probeHarnessContract() 的结果。
|
|
1450
|
+
* @returns 中文单行;已实测时为空串。
|
|
1451
|
+
*/
|
|
1452
|
+
const harnessContractLine = (report) => {
|
|
1453
|
+
if (report === undefined || report.tested) return "";
|
|
1454
|
+
const why = report.reason === "contract-drift"
|
|
1455
|
+
? `形状与实测不符(${report.drift.join(";")})`
|
|
1456
|
+
: report.reason === "symbol-renamed"
|
|
1457
|
+
? "宿主把投递符号换成了非全局符号(已自动改用动态 import 得到的权威符号)"
|
|
1458
|
+
: report.version === ""
|
|
1459
|
+
? "宿主版本未识别"
|
|
1460
|
+
: `宿主版本 ${report.version} 不在本插件的实测矩阵(${TESTED_HARNESS_VERSIONS.join(" / ")})内`;
|
|
1461
|
+
const where = report.route === "none" ? "当前没有可用的投递路径" : `投递走 ${report.route}`;
|
|
1462
|
+
return `宿主 subagent 契约:${why},${where}。团队功能按实测契约尽力运行;升级 DSH 后若出现「叫不醒成员 / 已退休成员被复活」,先按这条排查。`;
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1414
1465
|
// 已声明的降级也要**对模型可见**:否则"团队功能没了"这件事只留在日志里,
|
|
1415
1466
|
// 用户问起来模型只能说不知道 —— 那正是规范禁止的"静默跳过缺失的引用对象"。
|
|
1416
1467
|
// 门槛用 engineReady()(真机探测)而不是 engineState.ok:两者在「引擎 fiber 被挂起」时不同。
|
|
@@ -1421,22 +1472,53 @@ export function apply(ctx, config) {
|
|
|
1421
1472
|
if (context.agent?.session?.header?.parentSession !== undefined) return "";
|
|
1422
1473
|
if (engineReady()) {
|
|
1423
1474
|
// 引擎可用、但上一次热重载没成功:盘上的小队定义已经变了,运行中的引擎还是旧的。
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1475
|
+
const contract = harnessContractLine(harnessReportOf());
|
|
1476
|
+
if (engineState.reloadFailure === "")
|
|
1477
|
+
return contract;
|
|
1478
|
+
// 两种来源都要说清,但**不能说错**:宿主投递符号换口径后的自动重挂失败,与"你保存的小队
|
|
1479
|
+
// 没生效"是两件事(B-3,2026-09-16)。
|
|
1480
|
+
const byHarness = engineState.reloadFailureKind === "harness";
|
|
1481
|
+
return [
|
|
1482
|
+
byHarness
|
|
1483
|
+
? "## T专家 团队引擎自动重挂失败(宿主投递契约换了口径)"
|
|
1484
|
+
: "## T专家 小队定义已保存,但团队引擎没有换成新配置",
|
|
1485
|
+
byHarness
|
|
1486
|
+
? `宿主的 subagent 投递符号与实测口径不同,插件已改用动态 import 得到的权威符号并自动重挂引擎,但这次重挂失败了。原因:${engineState.reloadFailure}`
|
|
1487
|
+
: `这次保存已经生效(/t 列表就是新定义),但引擎仍在使用上一次的小队配置,所以用新改的小队建队会失败。原因:${engineState.reloadFailure}`,
|
|
1488
|
+
byHarness
|
|
1489
|
+
? "团队功能此刻不可用;重启 DSH 会在解析权威符号后重新挂载。用户问起时请原样转述上面的原因。"
|
|
1490
|
+
: "需要重新保存一次小队定义让引擎再试一次,或重启 DSH。用户问起时请原样转述上面的原因。",
|
|
1491
|
+
contract,
|
|
1492
|
+
].filter((line) => line !== "").join("\n");
|
|
1431
1493
|
}
|
|
1432
1494
|
return [
|
|
1433
1495
|
"## T专家 团队引擎当前不可用",
|
|
1434
1496
|
`团队功能(/t 建队、t_team_* 工具)现在不可用。原因:${engineUnreadyDetail()}`,
|
|
1435
1497
|
"专家名册、@ 召唤与 summon_t_expert 不受影响。用户需要团队功能时,请原样转述上面的原因,不要假装能建队。",
|
|
1436
|
-
|
|
1498
|
+
harnessContractLine(harnessReportOf()),
|
|
1499
|
+
].filter((line) => line !== "").join("\n");
|
|
1437
1500
|
},
|
|
1438
1501
|
});
|
|
1439
1502
|
|
|
1503
|
+
// ---- 权威契约解析(B-3,2026-09-16)----
|
|
1504
|
+
// 动态 import 宿主的 `@deepseek-ai/dsh-subagent/internal`:拿到投递符号的**真身**与官方适配
|
|
1505
|
+
// 函数(`queueHostSubagentPrompt`),替代"按 Symbol.for 猜键 + 自己拼参数"。解析细节与失败语义
|
|
1506
|
+
// 见 lib/teams/harness-compat.js 的 resolveHarnessAuthority。
|
|
1507
|
+
//
|
|
1508
|
+
// 为什么要在这里重挂:解析是异步的,而引擎首挂是同步的 —— 宿主真把符号换成非全局符号时,
|
|
1509
|
+
// 首挂那一刻守卫按旧键找不到函数(B-1 硬闸门 → 引擎降级)。解析落地后立刻用权威键重挂一次,
|
|
1510
|
+
// 引擎就能起来,用户不必重启 DSH。**只有换了口径才重挂**:常态(键没变)一行都不做。
|
|
1511
|
+
void resolveHarnessAuthority().then((authority) => {
|
|
1512
|
+
refreshHarnessReport();
|
|
1513
|
+
if (authority.changed !== true) return;
|
|
1514
|
+
ctx.logger?.warn?.(`[t-team] 宿主投递符号换了口径(权威符号:${String(authority.deliver)}),已改用权威符号并重挂团队引擎。`);
|
|
1515
|
+
return reloadEngine("harness");
|
|
1516
|
+
}).catch((error) => {
|
|
1517
|
+
// 解析函数自身不抛(失败会写进报告),这里只兜住"注入方/宿主意外":绝不让它变成
|
|
1518
|
+
// unhandled rejection 污染宿主日志。
|
|
1519
|
+
ctx.logger?.warn?.(`[t-team] 权威契约解析异常(继续按全局符号工作):${error instanceof Error ? error.message : String(error)}`);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1440
1522
|
// ---- 小队定义服务(设置页「小队」标签用;写回 teams.json 后自动重跑 team-profiles.py)----
|
|
1441
1523
|
const teamsFile = join(dirname(config.root), "teams.json");
|
|
1442
1524
|
// 编译器**只认包内那份**(随插件版本走)。刻意**不再回退**数据目录里的 `team-profiles.py`:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
1
3
|
import { SubagentError } from '@deepseek-ai/dsh-subagent';
|
|
2
4
|
/**
|
|
3
5
|
* Exact protocol exported by dsh-subagent/internal in Alpha.5 and rc.1.
|
|
@@ -10,6 +12,77 @@ const hostPromptQueue = Symbol.for('dsh.subagent.queuePrompt');
|
|
|
10
12
|
// 0.1.5 keeps queueHostSubagentPrompt but replaces its symbol with this
|
|
11
13
|
// delivery-mode-aware implementation (packages/subagent/subagent/src/internal.ts).
|
|
12
14
|
const hostPromptDeliver = Symbol.for('dsh.subagent.deliverPrompt');
|
|
15
|
+
/**
|
|
16
|
+
* 全局共识符号("约定俗成的键")。
|
|
17
|
+
*
|
|
18
|
+
* 为什么默认就用它:上游**刻意**用 `Symbol.for`(全局符号注册表)而不是模块内 `Symbol()` ——
|
|
19
|
+
* 同一个实现会同时存在于"打包过的运行时入口"与"未打包的 internal 子路径"两处,只有全局注册表
|
|
20
|
+
* 才能让两边对上暗号(见上游 internal.ts 的注释)。所以我们按同一约定对接,不需要 import
|
|
21
|
+
* 任何东西就能认得那两个键。
|
|
22
|
+
*
|
|
23
|
+
* 但它**不是权威**:符号串改了、或者上游换成非全局符号,这里就找不到了(B-1 会响亮拒绝)。
|
|
24
|
+
* 权威在 `@deepseek-ai/dsh-subagent/internal` 的导出里,由 `resolveHarnessAuthority()` 动态取
|
|
25
|
+
* (见该函数的说明)——这里只是它的**兜底默认值**。
|
|
26
|
+
*/
|
|
27
|
+
const GLOBAL_SYMBOLS = { deliver: hostPromptDeliver, queue: hostPromptQueue };
|
|
28
|
+
/**
|
|
29
|
+
* 当前生效的符号键:默认是上面那两个全局符号,解析出权威符号后换成权威那份。
|
|
30
|
+
* 所有读 `ctx.subagents[键]` 的地方都必须走 `routeKey()` / `activeSymbols`,不许再直接引用
|
|
31
|
+
* 全局符号 —— 否则"换了键"只改了自检、运行期还按旧键找。
|
|
32
|
+
*/
|
|
33
|
+
const activeSymbols = { ...GLOBAL_SYMBOLS };
|
|
34
|
+
/** 路由 → 运行期真正要读的属性键(字符串方法名,或 symbol)。 */
|
|
35
|
+
function routeKey(route) {
|
|
36
|
+
return route.symbol === undefined ? route.key : activeSymbols[route.symbol];
|
|
37
|
+
}
|
|
38
|
+
/** 路由 → 人类可读标签(符号路由现算,换了键时标签跟着变,不会说谎)。 */
|
|
39
|
+
function routeLabel(route) {
|
|
40
|
+
return route.symbol === undefined ? route.label : `subagents[${String(activeSymbols[route.symbol])}]`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 宿主投递契约的**期望形状**(B-1/B-2)。
|
|
44
|
+
*
|
|
45
|
+
* 为什么要把"我们反推出来的口径"写成一张表:本文件对接的是**未文档化内部实现** ——
|
|
46
|
+
* 上游改个形参个数、换个转发形态,TypeScript 看不见、tsc 也拦不住,症状只会是
|
|
47
|
+
* "队长叫不醒成员"或"共享服务被按错的参数调用"。表在这里,运行期就能据此做签名断言,
|
|
48
|
+
* 自检也拿它当单一真源(见 tools/verify.mjs 的 [13f] 与 [19] 的端到端那两条)。
|
|
49
|
+
*
|
|
50
|
+
* `arity` 两档,区别是**我们有多确信**:
|
|
51
|
+
* · 数字 = 内部符号函数(`Symbol.for(...)`),形参个数是逐字核对上游源码得到的。上游加必填
|
|
52
|
+
* 参数、调整顺序、改转发形态都会让 `Function.length` 变 —— 那正是要被拦下来的。
|
|
53
|
+
* · null = 公开方法(`followup` / `sendMessage`):名字是公开的,但实现允许少声明形参
|
|
54
|
+
* (短实现、转发实现、自检桩),所以只校验"可调用",不校验个数。
|
|
55
|
+
*
|
|
56
|
+
* `role` 说明这条路能拿来干什么:`queue` 才是"宿主写给某个子代理的一轮"(FIFO 投递),
|
|
57
|
+
* `steer` 是 steer 语义 —— **绝不能拿它顶替投递**(见 `queueMemberPrompt` 的说明)。
|
|
58
|
+
*/
|
|
59
|
+
export const HARNESS_ROUTES = [
|
|
60
|
+
{ key: 'followup', label: 'subagents.followup', arity: null, role: 'queue' },
|
|
61
|
+
{ symbol: 'deliver', label: 'subagents[deliverPrompt symbol]', arity: 6, role: 'queue' },
|
|
62
|
+
{ symbol: 'queue', label: 'subagents[queuePrompt symbol]', arity: 5, role: 'queue' },
|
|
63
|
+
{ key: 'sendMessage', label: 'subagents.sendMessage', arity: null, role: 'steer' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 逐条路由的签名问题(空数组 = 形状与实测一致)。**只读**,不改任何东西、也不抛错。
|
|
68
|
+
* @param runtime - `ctx.subagents` 服务对象。
|
|
69
|
+
* @returns 人类可读的问题清单。
|
|
70
|
+
*/
|
|
71
|
+
function deliverySignatureProblems(runtime) {
|
|
72
|
+
const problems = [];
|
|
73
|
+
for (const route of HARNESS_ROUTES) {
|
|
74
|
+
// 只对内部符号函数断言:公开方法的形参个数不是契约的一部分(见 HARNESS_ROUTES 的说明)。
|
|
75
|
+
if (route.arity === null)
|
|
76
|
+
continue;
|
|
77
|
+
const fn = runtime?.[routeKey(route)];
|
|
78
|
+
if (typeof fn !== 'function')
|
|
79
|
+
continue;
|
|
80
|
+
if (fn.length !== route.arity) {
|
|
81
|
+
problems.push(`${routeLabel(route)} declares ${fn.length} parameters, expected ${route.arity}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return problems;
|
|
85
|
+
}
|
|
13
86
|
function boundary(runtime) {
|
|
14
87
|
return runtime;
|
|
15
88
|
}
|
|
@@ -66,7 +139,7 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
66
139
|
});
|
|
67
140
|
return;
|
|
68
141
|
}
|
|
69
|
-
if ((typeof runtime[
|
|
142
|
+
if ((typeof runtime[activeSymbols.queue] !== 'function' && typeof runtime[activeSymbols.deliver] !== 'function') || typeof runtime.sendMessage !== 'function') {
|
|
70
143
|
return unsupported('missing continuable setup and modern host queue');
|
|
71
144
|
}
|
|
72
145
|
const installed = new WeakSet();
|
|
@@ -116,47 +189,92 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
116
189
|
};
|
|
117
190
|
}, 't-team: member lifecycle compatibility');
|
|
118
191
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Queue a distinct host-authored turn; never substitute model-message steer.
|
|
194
|
+
*
|
|
195
|
+
* B-1:候选路由按 `HARNESS_ROUTES` 的顺序取第一条**形状对得上**的;内部符号函数的形参个数
|
|
196
|
+
* 与实测不符时**直接跳过它**,而不是拿错的参数表去调 ——
|
|
197
|
+
* 前者是"这条投递走不了(响亮)",后者是"按错顺序调共享服务(静默)",后者更糟。
|
|
198
|
+
*/
|
|
199
|
+
export async function queueMemberPrompt(runtime, parent, childId, content, signal, retried = false) {
|
|
121
200
|
const host = boundary(runtime);
|
|
122
201
|
const source = { kind: 'plugin', plugin: 't-team' };
|
|
123
|
-
|
|
124
|
-
|
|
202
|
+
const skipped = [];
|
|
203
|
+
// ⓪ 官方适配函数优先:`@deepseek-ai/dsh-subagent/internal` 导出的 queueHostSubagentPrompt 就是
|
|
204
|
+
// 「投递给某个子代理一轮」的正式写法(上游内部也是 `runtime[deliverPrompt](…, 'queue')`)。
|
|
205
|
+
// 走它等于把**反推出来的调用形态**换成上游自己维护的那一份:上游改投递模式时,我们跟着受益。
|
|
206
|
+
// 前置条件与它自己一样:运行时上那个符号方法得在(否则它内部会 TypeError)。
|
|
207
|
+
const adapter = authorityState.adapter;
|
|
208
|
+
if (typeof adapter === 'function'
|
|
209
|
+
&& typeof host[activeSymbols.deliver] === 'function'
|
|
210
|
+
&& (authorityState.adapterArity === undefined || authorityState.adapterArity === HARNESS_ADAPTER_ARITY)) {
|
|
211
|
+
return adapter(host, parent, childId, content, source, signal);
|
|
212
|
+
}
|
|
213
|
+
for (const route of HARNESS_ROUTES) {
|
|
214
|
+
if (route.role !== 'queue')
|
|
215
|
+
continue;
|
|
216
|
+
const fn = host[routeKey(route)];
|
|
217
|
+
if (typeof fn !== 'function')
|
|
218
|
+
continue;
|
|
219
|
+
if (route.arity !== null && fn.length !== route.arity) {
|
|
220
|
+
skipped.push(`${routeLabel(route)} declares ${fn.length} parameters, expected ${route.arity}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// 公开的 `followup` 收 options 包;两个内部符号收扁平参数(deliver 还多一个投递模式)。
|
|
224
|
+
if (route.key === 'followup') {
|
|
225
|
+
return fn.call(runtime, parent, childId, content, { source, signal });
|
|
226
|
+
}
|
|
227
|
+
if (route.symbol === 'deliver') {
|
|
228
|
+
return fn.call(runtime, parent, childId, content, source, signal, 'queue');
|
|
229
|
+
}
|
|
230
|
+
return fn.call(runtime, parent, childId, content, source, signal);
|
|
125
231
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return
|
|
232
|
+
// 都不可用:如果权威解析还没回来,**等它一次再重试**(投递是异步的,等得起)。
|
|
233
|
+
// 这一等救的是"符号口径变了、而首挂又早于解析完成"的窗口;只有失败路径才会走到这里。
|
|
234
|
+
if (!retried && !authorityState.settled) {
|
|
235
|
+
await resolveHarnessAuthority();
|
|
236
|
+
return queueMemberPrompt(runtime, parent, childId, content, signal, true);
|
|
237
|
+
}
|
|
238
|
+
return unsupported(skipped.length > 0
|
|
239
|
+
? `no usable host FIFO delivery route (${skipped.join('; ')})`
|
|
240
|
+
: 'missing host FIFO delivery');
|
|
133
241
|
}
|
|
134
242
|
/** Guard all resumable delivery paths, preserving the native service receiver. */
|
|
135
243
|
export function guardSubagentDelivery(ctx, isRetired) {
|
|
136
244
|
const runtime = ctx.subagents;
|
|
137
245
|
const host = boundary(runtime);
|
|
138
246
|
const legacy = host.followup;
|
|
139
|
-
const queue = host[
|
|
140
|
-
const deliver = host[
|
|
247
|
+
const queue = host[activeSymbols.queue];
|
|
248
|
+
const deliver = host[activeSymbols.deliver];
|
|
141
249
|
const send = host.sendMessage;
|
|
142
250
|
if (typeof legacy !== 'function' && ((typeof queue !== 'function' && typeof deliver !== 'function') || typeof send !== 'function')) {
|
|
143
251
|
return unsupported('cannot install complete retired-member guard');
|
|
144
252
|
}
|
|
253
|
+
// B-1:**签名断言**,且刻意放在可写性探测之前 —— 先确认"我们理解这个函数",再谈"能不能改它"。
|
|
254
|
+
//
|
|
255
|
+
// 为什么这里用**硬失败**(降级引擎)而不是像调用路径那样跳过:下面包的是**共享的**
|
|
256
|
+
// `ctx.subagents` 服务,四个入口一个不少地换成我们的包装。只要有一条路由的形参口径不明,
|
|
257
|
+
// 包装层就可能按错的参数顺序转发 —— 受害的不是"我们这一条投递",而是所有走这个服务对象的
|
|
258
|
+
// 投递,而且完全静默。宁可这一刻响一声(引擎降级、原因进日志与提示段),也不装一个签名不明的守卫。
|
|
259
|
+
const signatureProblems = deliverySignatureProblems(host);
|
|
260
|
+
if (signatureProblems.length > 0) {
|
|
261
|
+
return unsupported(`undocumented delivery signature changed (${signatureProblems.join('; ')}); re-verify this adapter against the installed @deepseek-ai/dsh-subagent and extend TESTED_HARNESS_VERSIONS`);
|
|
262
|
+
}
|
|
145
263
|
// A-1/E-1:**先探测可写性再动手**。过去直接赋值,若上游把服务冻结(`Object.freeze`)
|
|
146
264
|
// 或改成只有 getter 的访问器,strict mode 下这里会抛一个与"版本不支持"毫无关系的裸
|
|
147
265
|
// TypeError;现在会得到明确原因。同时这也是一次"契约变了"的早期信号。
|
|
148
266
|
const targets = [
|
|
149
267
|
...(typeof legacy === 'function' ? ['followup'] : []),
|
|
150
|
-
...(typeof queue === 'function' ? [
|
|
151
|
-
...(typeof deliver === 'function' ? [
|
|
268
|
+
...(typeof queue === 'function' ? [activeSymbols.queue] : []),
|
|
269
|
+
...(typeof deliver === 'function' ? [activeSymbols.deliver] : []),
|
|
152
270
|
...(typeof send === 'function' ? ['sendMessage'] : []),
|
|
153
271
|
];
|
|
154
272
|
assertAssignable(host, targets);
|
|
155
273
|
ctx.effect(() => {
|
|
156
274
|
const descriptors = new Map([
|
|
157
275
|
['followup', Object.getOwnPropertyDescriptor(host, 'followup')],
|
|
158
|
-
[
|
|
159
|
-
[
|
|
276
|
+
[activeSymbols.queue, Object.getOwnPropertyDescriptor(host, activeSymbols.queue)],
|
|
277
|
+
[activeSymbols.deliver, Object.getOwnPropertyDescriptor(host, activeSymbols.deliver)],
|
|
160
278
|
['sendMessage', Object.getOwnPropertyDescriptor(host, 'sendMessage')],
|
|
161
279
|
]);
|
|
162
280
|
let active = true;
|
|
@@ -184,9 +302,9 @@ export function guardSubagentDelivery(ctx, isRetired) {
|
|
|
184
302
|
if (typeof legacy === 'function')
|
|
185
303
|
host.followup = guardedLegacy;
|
|
186
304
|
if (typeof queue === 'function')
|
|
187
|
-
host[
|
|
305
|
+
host[activeSymbols.queue] = guardedQueue;
|
|
188
306
|
if (typeof deliver === 'function')
|
|
189
|
-
host[
|
|
307
|
+
host[activeSymbols.deliver] = guardedDeliver;
|
|
190
308
|
if (typeof send === 'function')
|
|
191
309
|
host.sendMessage = guardedSend;
|
|
192
310
|
// Cordis wraps method reads in fresh Proxies. Compare the actual own
|
|
@@ -205,11 +323,239 @@ export function guardSubagentDelivery(ctx, isRetired) {
|
|
|
205
323
|
if (typeof legacy === 'function')
|
|
206
324
|
restore('followup', guardedLegacy);
|
|
207
325
|
if (typeof queue === 'function')
|
|
208
|
-
restore(
|
|
326
|
+
restore(activeSymbols.queue, guardedQueue);
|
|
209
327
|
if (typeof deliver === 'function')
|
|
210
|
-
restore(
|
|
328
|
+
restore(activeSymbols.deliver, guardedDeliver);
|
|
211
329
|
if (typeof send === 'function')
|
|
212
330
|
restore('sendMessage', guardedSend);
|
|
213
331
|
};
|
|
214
332
|
}, 't-team: retired member guard');
|
|
215
333
|
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------- B-3:权威契约解析(动态 import)
|
|
336
|
+
|
|
337
|
+
const HARNESS_INTERNAL_SUBPATH = '@deepseek-ai/dsh-subagent/internal';
|
|
338
|
+
/**
|
|
339
|
+
* `queueHostSubagentPrompt(runtime, parent, childId, content, source, signal)` 的形参个数(6)。
|
|
340
|
+
* 与 HARNESS_ROUTES 的 arity 同一性质:逐字核对上游导出面得到的;对不上就不用它(见下)。
|
|
341
|
+
*/
|
|
342
|
+
const HARNESS_ADAPTER_ARITY = 6;
|
|
343
|
+
/**
|
|
344
|
+
* 权威契约的当前状态(`resolveHarnessAuthority()` 的落点)。
|
|
345
|
+
* 初值 = "还没解析":一切按全局共识符号走(见 GLOBAL_SYMBOLS)。
|
|
346
|
+
*/
|
|
347
|
+
let authorityState = { settled: false, ok: false, changed: false, adapter: undefined, adapterArity: undefined, error: '' };
|
|
348
|
+
let authorityPromise;
|
|
349
|
+
const defaultImporter = (specifier) => import(specifier);
|
|
350
|
+
/**
|
|
351
|
+
* 解析宿主 `@deepseek-ai/dsh-subagent/internal` 这个**未文档化但已导出**的子路径,拿到两样东西:
|
|
352
|
+
* · `deliverSubagentPrompt` —— 权威的投递符号(我们按 `Symbol.for` 猜的那个键的**真身**);
|
|
353
|
+
* · `queueHostSubagentPrompt` —— 官方适配函数(上游自己就是 `runtime[deliverPrompt](…, 'queue')`)。
|
|
354
|
+
*
|
|
355
|
+
* 为什么用动态而不是静态 import:那个子路径在 Alpha.2 上**不存在**,静态 import 会让插件在那些
|
|
356
|
+
* 版本上整个加载不起来(见文件顶部说明)。动态 import 失败只是少一条兜底路径,不影响加载。
|
|
357
|
+
*
|
|
358
|
+
* 为什么值得做:全局符号是"约定",这个子路径是"**导出面**"(带类型声明)。上游把符号串改掉、
|
|
359
|
+
* 或换成非全局符号时,全局符号就找不到了 —— B-1 会因此响亮拒绝装守卫、引擎降级;而这里能拿到
|
|
360
|
+
* 真身,于是还能continue工作:`activeSymbols` 就地更换,所有读取点自动跟着走。
|
|
361
|
+
*
|
|
362
|
+
* 失败语义:解析不到(老宿主 / 安装树取不到 / 导出面变了)**不算错误** —— 退回全局共识符号,
|
|
363
|
+
* 报告里如实写 `authority.error`,绝不猜。
|
|
364
|
+
*
|
|
365
|
+
* @param options - `importer` 可注入解析器。自检用它喂一个"换了键"的假模块来验证这条路径,
|
|
366
|
+
* 验完再喂回默认解析器还原(见 tools/verify.mjs 的 [13g])。
|
|
367
|
+
* @returns 权威契约报告 `{ ok, deliver, adapter, adapterArity, changed, error }`。
|
|
368
|
+
*/
|
|
369
|
+
export function resolveHarnessAuthority(options = {}) {
|
|
370
|
+
const injected = typeof options.importer === 'function' ? options.importer : undefined;
|
|
371
|
+
if (injected !== undefined)
|
|
372
|
+
return loadHarnessAuthority(injected).then(applyHarnessAuthority);
|
|
373
|
+
return (authorityPromise ??= loadHarnessAuthority(defaultImporter).then(applyHarnessAuthority));
|
|
374
|
+
}
|
|
375
|
+
async function loadHarnessAuthority(importer) {
|
|
376
|
+
const report = { ok: false, deliver: undefined, adapter: undefined, adapterArity: undefined, changed: false, error: '' };
|
|
377
|
+
try {
|
|
378
|
+
const mod = await importer(HARNESS_INTERNAL_SUBPATH);
|
|
379
|
+
const deliver = mod?.deliverSubagentPrompt;
|
|
380
|
+
if (typeof deliver === 'symbol') {
|
|
381
|
+
report.ok = true;
|
|
382
|
+
report.deliver = deliver;
|
|
383
|
+
// "换了键"= 权威符号与全局共识符号不是同一个对象(rc.1/rc.2 上两者相等,什么都不用变)。
|
|
384
|
+
report.changed = deliver !== GLOBAL_SYMBOLS.deliver;
|
|
385
|
+
}
|
|
386
|
+
if (typeof mod?.queueHostSubagentPrompt === 'function') {
|
|
387
|
+
report.adapter = mod.queueHostSubagentPrompt;
|
|
388
|
+
report.adapterArity = mod.queueHostSubagentPrompt.length;
|
|
389
|
+
}
|
|
390
|
+
if (!report.ok)
|
|
391
|
+
report.error = 'internal subpath resolved but exports no delivery symbol';
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
report.error = error instanceof Error ? error.message : String(error);
|
|
395
|
+
}
|
|
396
|
+
return report;
|
|
397
|
+
}
|
|
398
|
+
/** 把解析结果落到模块状态(`activeSymbols.deliver` / `authorityState`)。 */
|
|
399
|
+
function applyHarnessAuthority(report) {
|
|
400
|
+
authorityState = {
|
|
401
|
+
settled: true,
|
|
402
|
+
ok: report.ok,
|
|
403
|
+
changed: report.changed,
|
|
404
|
+
adapter: report.adapter,
|
|
405
|
+
adapterArity: report.adapterArity,
|
|
406
|
+
error: report.error,
|
|
407
|
+
};
|
|
408
|
+
if (report.ok)
|
|
409
|
+
activeSymbols.deliver = report.deliver;
|
|
410
|
+
return report;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------- B-2:运行期软闸门
|
|
414
|
+
|
|
415
|
+
const HARNESS_PACKAGE = '@deepseek-ai/dsh-subagent';
|
|
416
|
+
/** 版本探测结果缓存(一份安装在一个进程里不会变;解析不到就记空串,绝不重试、也绝不猜)。 */
|
|
417
|
+
let detectedVersion;
|
|
418
|
+
/**
|
|
419
|
+
* 读宿主 `@deepseek-ai/dsh-subagent` 的版本(best-effort;读不到返回空串)。
|
|
420
|
+
*
|
|
421
|
+
* 为什么读它:本文件对接的是**未文档化内部实现**,实测矩阵只能人工维护(见
|
|
422
|
+
* `TESTED_HARNESS_VERSIONS`)。把"宿主到底是哪一版"记下来,出问题时才能一句话说清是不是
|
|
423
|
+
* 版本没在矩阵里,而不是让维护者从 `deliverPrompt` 的形参个数往回猜。
|
|
424
|
+
*
|
|
425
|
+
* 用**本文件自己的位置**做解析基准(`createRequire(import.meta.url)`):这个文件既然能静态
|
|
426
|
+
* import 到 `@deepseek-ai/dsh-subagent`,就一定能 resolve 到它的 `package.json`。解析失败不算
|
|
427
|
+
* 错误(某些封装安装树里可能取不到),报告里会写 `version: ''`,由形状探测兜底。
|
|
428
|
+
* @returns 形如 `0.1.5-rc.2` 的版本串,或空串。
|
|
429
|
+
*/
|
|
430
|
+
function detectHarnessVersion() {
|
|
431
|
+
if (detectedVersion !== undefined)
|
|
432
|
+
return detectedVersion;
|
|
433
|
+
detectedVersion = '';
|
|
434
|
+
try {
|
|
435
|
+
const require = createRequire(import.meta.url);
|
|
436
|
+
const manifest = JSON.parse(readFileSync(require.resolve(`${HARNESS_PACKAGE}/package.json`), 'utf8'));
|
|
437
|
+
if (typeof manifest?.version === 'string')
|
|
438
|
+
detectedVersion = manifest.version;
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
/* 读不到就只按形状判断;报告里的 reason 会如实写 version-unknown */
|
|
442
|
+
}
|
|
443
|
+
return detectedVersion;
|
|
444
|
+
}
|
|
445
|
+
/** 当前会真正被 `queueMemberPrompt` 用的那条投递路由(没有可用路由时返回 `none`)。 */
|
|
446
|
+
function usableDeliveryRoute(runtime) {
|
|
447
|
+
const adapter = authorityState.adapter;
|
|
448
|
+
if (typeof adapter === 'function' && typeof runtime?.[activeSymbols.deliver] === 'function') {
|
|
449
|
+
return `@deepseek-ai/dsh-subagent/internal queueHostSubagentPrompt`;
|
|
450
|
+
}
|
|
451
|
+
for (const route of HARNESS_ROUTES) {
|
|
452
|
+
if (route.role !== 'queue')
|
|
453
|
+
continue;
|
|
454
|
+
const fn = runtime?.[routeKey(route)];
|
|
455
|
+
if (typeof fn !== 'function')
|
|
456
|
+
continue;
|
|
457
|
+
if (route.arity !== null && fn.length !== route.arity)
|
|
458
|
+
continue;
|
|
459
|
+
return routeLabel(route);
|
|
460
|
+
}
|
|
461
|
+
return 'none';
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* 宿主 subagent 契约体检(B-2 的探测半边)—— **纯读、绝不抛错**:它是诊断,不是门禁。
|
|
465
|
+
*
|
|
466
|
+
* 和 B-1 的分工:B-1 是"装守卫那一刻的硬断言"(签名不符就不装,见 `guardSubagentDelivery`),
|
|
467
|
+
* 这里则给"引擎一切正常、但宿主是未实测版本"这种**没有症状的状态**留一个可读的结论 ——
|
|
468
|
+
* 上游改契约最常见的结局不是立刻崩,而是某天"队长叫不醒成员",那时只有这条结论能解释原因。
|
|
469
|
+
*
|
|
470
|
+
* @param ctx - 插件上下文(只读 `ctx.subagents`;服务缺席也算一种结果,不抛错)。
|
|
471
|
+
* @param options - `version` 可覆盖版本探测(自检用:用确定的值验证判定规则,不依赖本机装了什么)。
|
|
472
|
+
* @returns 契约报告:`version` / `tested` / `reason` / `drift` / `route` / `routes` / `signature`。
|
|
473
|
+
*/
|
|
474
|
+
export function probeHarnessContract(ctx, options = {}) {
|
|
475
|
+
const runtime = ctx?.subagents;
|
|
476
|
+
const routes = HARNESS_ROUTES.map((route) => {
|
|
477
|
+
const fn = runtime?.[routeKey(route)];
|
|
478
|
+
const present = typeof fn === 'function';
|
|
479
|
+
return {
|
|
480
|
+
label: routeLabel(route),
|
|
481
|
+
role: route.role,
|
|
482
|
+
present,
|
|
483
|
+
arity: present ? fn.length : undefined,
|
|
484
|
+
expected: route.arity === null ? undefined : route.arity,
|
|
485
|
+
ok: present && (route.arity === null || fn.length === route.arity),
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
const drift = [...deliverySignatureProblems(runtime)];
|
|
489
|
+
// 官方适配函数也是契约的一部分:形参个数对不上就**不用它**(调用路径见 queueMemberPrompt),
|
|
490
|
+
// 并且要出现在报告里 —— 否则"我们悄悄退回了自己拼参数的调用形态"这件事没人知道。
|
|
491
|
+
if (typeof authorityState.adapter === 'function'
|
|
492
|
+
&& authorityState.adapterArity !== undefined
|
|
493
|
+
&& authorityState.adapterArity !== HARNESS_ADAPTER_ARITY) {
|
|
494
|
+
drift.push(`${HARNESS_INTERNAL_SUBPATH} queueHostSubagentPrompt declares ${authorityState.adapterArity} parameters, expected ${HARNESS_ADAPTER_ARITY}`);
|
|
495
|
+
}
|
|
496
|
+
const version = typeof options.version === 'string' ? options.version : detectHarnessVersion();
|
|
497
|
+
const reason = drift.length > 0
|
|
498
|
+
? 'contract-drift'
|
|
499
|
+
: authorityState.changed
|
|
500
|
+
? 'symbol-renamed'
|
|
501
|
+
: version === ''
|
|
502
|
+
? 'version-unknown'
|
|
503
|
+
: TESTED_HARNESS_VERSIONS.includes(version) ? '' : 'untested-version';
|
|
504
|
+
return {
|
|
505
|
+
version,
|
|
506
|
+
tested: reason === '',
|
|
507
|
+
reason,
|
|
508
|
+
drift,
|
|
509
|
+
route: usableDeliveryRoute(runtime),
|
|
510
|
+
// 权威解析(B-3)的状态:`changed` 为真表示宿主的投递符号**换了口径**、已自动改用真身。
|
|
511
|
+
authority: {
|
|
512
|
+
settled: authorityState.settled,
|
|
513
|
+
ok: authorityState.ok,
|
|
514
|
+
changed: authorityState.changed,
|
|
515
|
+
adapter: typeof authorityState.adapter === 'function',
|
|
516
|
+
error: authorityState.error,
|
|
517
|
+
},
|
|
518
|
+
routes,
|
|
519
|
+
signature: `${version}|${reason}|${drift.join('; ')}`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* 契约报告 → 一句**可执行**的诊断(供日志与提示段共用)。
|
|
524
|
+
* @param report - `probeHarnessContract()` 的结果。
|
|
525
|
+
* @returns 英文单行描述(与 `unsupported()` 的消息同语言,日志里成一条线)。
|
|
526
|
+
*/
|
|
527
|
+
export function harnessContractWarning(report) {
|
|
528
|
+
const where = report.route === 'none' ? 'no usable delivery route' : `delivery goes through ${report.route}`;
|
|
529
|
+
if (report.reason === 'contract-drift') {
|
|
530
|
+
return `subagent contract drifted from the tested shape (${report.drift.join('; ')}); ${where}. Waking members / blocking retired members can now misbehave — re-verify lib/teams/harness-compat.js against the installed @deepseek-ai/dsh-subagent and extend TESTED_HARNESS_VERSIONS.`;
|
|
531
|
+
}
|
|
532
|
+
if (report.reason === 'symbol-renamed') {
|
|
533
|
+
return `@deepseek-ai/dsh-subagent no longer keys host delivery with the documented global symbol; switched to the authoritative symbol exported by ${HARNESS_INTERNAL_SUBPATH} (${where}). Team features keep working — re-verify this adapter against the installed host and extend TESTED_HARNESS_VERSIONS so the switch stops being silent.`;
|
|
534
|
+
}
|
|
535
|
+
if (report.reason === 'version-unknown') {
|
|
536
|
+
return `could not resolve the installed @deepseek-ai/dsh-subagent version; shape matches the tested contract, ${where}. If team features misbehave after a Harness upgrade, check this line first.`;
|
|
537
|
+
}
|
|
538
|
+
return `@deepseek-ai/dsh-subagent ${report.version} is not in the tested matrix (${TESTED_HARNESS_VERSIONS.join(' / ')}); shape matches the tested contract, ${where}. Team features run on a best-effort contract — re-verify and extend TESTED_HARNESS_VERSIONS.`;
|
|
539
|
+
}
|
|
540
|
+
/** 已经报过的契约签名:同一份签名只报一次,避免每次挂载/热重载都刷同一条日志。 */
|
|
541
|
+
const reportedContracts = new Set();
|
|
542
|
+
/**
|
|
543
|
+
* 体检 + **同签名只报一次**的告警(B-2 的出声半边)。
|
|
544
|
+
*
|
|
545
|
+
* 为什么按签名去重而不是"一个布尔量只报一次":宿主换了版本、或形状变了,`signature` 就变了 ——
|
|
546
|
+
* 那是一次**新的**事实,必须再出声("报过一次就永远闭嘴"正是要避免的另一种静默)。
|
|
547
|
+
* 已实测(版本在矩阵内且无漂移)时**完全不出声**:常态不该占日志,也不该占提示词预算。
|
|
548
|
+
*
|
|
549
|
+
* @param ctx - 插件上下文。
|
|
550
|
+
* @param logger - 宿主 logger(缺席时静默降级 —— 不写 console:桌面/Web 里 console 无人可见)。
|
|
551
|
+
* @param options - 同 `probeHarnessContract`。
|
|
552
|
+
* @returns 契约报告(调用方可以直接拿去渲染状态行)。
|
|
553
|
+
*/
|
|
554
|
+
export function reportHarnessContract(ctx, logger, options = {}) {
|
|
555
|
+
const report = probeHarnessContract(ctx, options);
|
|
556
|
+
if (report.tested || reportedContracts.has(report.signature))
|
|
557
|
+
return report;
|
|
558
|
+
reportedContracts.add(report.signature);
|
|
559
|
+
logger?.warn?.(`[t-team] ${harnessContractWarning(report)}`);
|
|
560
|
+
return report;
|
|
561
|
+
}
|
package/lib/teams/members.js
CHANGED
|
@@ -16,7 +16,7 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
|
16
16
|
import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent';
|
|
17
17
|
import { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
import { guardSubagentDelivery, installContinuableMemberSetup, queueMemberPrompt, sessionOwnEvents } from "./harness-compat.js";
|
|
19
|
+
import { guardSubagentDelivery, installContinuableMemberSetup, queueMemberPrompt, reportHarnessContract, sessionOwnEvents } from "./harness-compat.js";
|
|
20
20
|
import { acknowledgeMailbox, appendMailbox, CAPTAIN_KEY, createMessage, readRetiredMemberIds, readTeamSync, readTeam, releaseMailboxDelivery, withTeamLock, writeTeam } from "./state.js";
|
|
21
21
|
import { appendTeamEvent, captainSessionOf } from "./events.js";
|
|
22
22
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
@@ -576,6 +576,11 @@ export function interruptMember(ctx, captain, childId) {
|
|
|
576
576
|
* untouched while the delivery boundary still prevents further model turns.
|
|
577
577
|
*/
|
|
578
578
|
export function installRetiredMemberGuard(ctx, stateDir) {
|
|
579
|
+
// B-2:装守卫的同时做一次契约体检,**在装之前**报 ——
|
|
580
|
+
// 未实测的宿主版本、或形状与实测不符,都要在日志里留下一句可执行的话;否则上游换了契约
|
|
581
|
+
// 的唯一症状是"队长叫不醒成员 / 已退休成员被复活",那时只能靠猜(见 harness-compat.js)。
|
|
582
|
+
// 已实测时完全不出声;同一份签名只报一次(挂载与每次热重载都会经过这里)。
|
|
583
|
+
reportHarnessContract(ctx, ctx.logger);
|
|
579
584
|
guardSubagentDelivery(ctx, async (parent, childId) => {
|
|
580
585
|
const retired = await readRetiredMemberIds(join(parent.session.header.cwd ?? process.cwd(), stateDir));
|
|
581
586
|
return retired.has(childId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-t-expert",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.45",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "T Expert — a 323-expert, 22-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin, with a built-in multi-agent team engine (T Team).",
|
|
6
6
|
"license": "MIT",
|