dsh-expert-group 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +355 -0
  3. package/THIRD_PARTY_NOTICES.md +35 -0
  4. package/assets/notification-whoosh.wav +0 -0
  5. package/bin/prompt-inspector.mjs +63 -0
  6. package/client-bundle.js +14891 -0
  7. package/client-expert-group-model.js +426 -0
  8. package/client-expert-group-sidebar.js +5401 -0
  9. package/client-freshness.js +364 -0
  10. package/client-model-config.js +2155 -0
  11. package/client-notifications.js +38 -0
  12. package/client.js +6487 -0
  13. package/cordis.patch.yml +10 -0
  14. package/fanout-host.js +1507 -0
  15. package/fanout.js +2981 -0
  16. package/host/composition.js +155 -0
  17. package/host/expert-group-runtime.js +822 -0
  18. package/host/expert-group-snapshot.js +450 -0
  19. package/host/fanout-runtime.js +822 -0
  20. package/host/handoff.js +80 -0
  21. package/host/lineage.js +274 -0
  22. package/host/notifications.js +189 -0
  23. package/host/persistence-health.js +389 -0
  24. package/host/recovery.js +242 -0
  25. package/host/rpc-transport.js +573 -0
  26. package/host/settings-runtime.js +168 -0
  27. package/host/settings-source.js +265 -0
  28. package/host/storage-binding.js +94 -0
  29. package/host/system-toast.js +916 -0
  30. package/image-routing.js +263 -0
  31. package/index.js +9327 -0
  32. package/install.ps1 +219 -0
  33. package/orchestrator.js +239 -0
  34. package/package.json +142 -0
  35. package/prompt-contracts.js +148 -0
  36. package/prompt-inspector.js +584 -0
  37. package/prompt-sidecar.js +610 -0
  38. package/resource-budget.js +540 -0
  39. package/role-registry.js +286 -0
  40. package/rpc-host.js +310 -0
  41. package/scripts/assemble-client.mjs +605 -0
  42. package/scripts/build.mjs +124 -0
  43. package/shared.js +4003 -0
  44. package/storage.js +190 -0
  45. package/uninstall.ps1 +47 -0
@@ -0,0 +1,426 @@
1
+ (function installExpertGroupModel(global) {
2
+ "use strict";
3
+
4
+ // 版本写进存储键和快照边界,防止新旧页面把同一份折叠状态误读成不同语义。
5
+ const SCHEMA_VERSION = 1;
6
+ const SYMBOL = Symbol.for("dsh-expert-group/client-expert-group-model");
7
+ const STATUSES = new Set([
8
+ "queued", "waiting", "running", "succeeded", "failed", "cancelled",
9
+ "timed_out", "interrupted", "idle", "ready", "unknown",
10
+ ]);
11
+ const STATUS_ALIASES = Object.freeze({
12
+ canceled: "cancelled", timeout: "timed_out", timedout: "timed_out", "timed-out": "timed_out",
13
+ success: "succeeded", successful: "succeeded", complete: "succeeded", completed: "succeeded", done: "succeeded",
14
+ error: "failed", errored: "failed", active: "running", in_progress: "running", "in-progress": "running",
15
+ working: "running", pending: "queued", scheduled: "queued", wait: "waiting",
16
+ });
17
+ const COUNT_FIELDS = Object.freeze([
18
+ "queued", "waiting", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted", "pending",
19
+ ]);
20
+ /**
21
+ * 右侧栏对外的**代理运行状态**只有这两个规范值。
22
+ *
23
+ * 官方源字段(DSH 子代理目录)用 activity=running/inactive、Agent status=running/idle
24
+ * 两套词表;协议快照还带着 fan-out 的 queued/waiting/终态。展示层把它们全部收敛成
25
+ * running|ready:只有「明确运行」才是 running,其余一律 ready。
26
+ */
27
+ const RUNTIME_STATUSES = Object.freeze(["running", "ready"]);
28
+ /** 原生代理行的本地化文案(与 DSH ui-subagent locales 同源)。 */
29
+ const MODE_LABELS = Object.freeze({ continuable: "可继续", "one-shot": "一次性" });
30
+ const ACTIVITY_LABELS = Object.freeze({ running: "正在运行", inactive: "当前未运行" });
31
+ /**
32
+ * 默认折叠的状态词表:这四个状态代表「没有正在进行的工作」,默认收起。
33
+ *
34
+ * 提为模块级常量而不是在 defaultExpanded 里每次新建 Set:该函数位于渲染路径上,
35
+ * 未手动覆盖展开状态的每个节点每帧都要判定一次,而词表内容恒定不变。
36
+ */
37
+ const DEFAULT_COLLAPSED_STATUSES = new Set(["idle", "ready", "succeeded", "cancelled"]);
38
+
39
+ function isObject(value) {
40
+ return value !== null && typeof value === "object" && !Array.isArray(value);
41
+ }
42
+
43
+ function text(value) {
44
+ if (typeof value !== "string") return null;
45
+ const valueText = value.trim();
46
+ return valueText.length === 0 ? null : valueText;
47
+ }
48
+
49
+ function firstText() {
50
+ for (const value of arguments) {
51
+ const result = text(value);
52
+ if (result !== null) return result;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function numberOrNull(value) {
58
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
59
+ }
60
+
61
+ function normalizeStatus(value) {
62
+ const source = text(value);
63
+ if (source === null) return "unknown";
64
+ const key = source.toLowerCase().replace(/\s+/g, "_");
65
+ if (STATUSES.has(key)) return key;
66
+ return STATUS_ALIASES[key] || "unknown";
67
+ }
68
+
69
+ function statusFromTask(value) {
70
+ const source = text(value);
71
+ if (source === "pending") return "running";
72
+ if (source === "completed" || source === "complete") return "succeeded";
73
+ if (source === "completed_with_errors") return "failed";
74
+ return normalizeStatus(source);
75
+ }
76
+
77
+ function identityOf(value) {
78
+ const source = isObject(value) ? value : {};
79
+ return firstText(source.id, source.sessionId, source.childId, source.parentTaskId);
80
+ }
81
+
82
+ function runningOf(status, explicit) {
83
+ if (typeof explicit === "boolean") return explicit;
84
+ if (status === "unknown") return null;
85
+ return status === "running" || status === "waiting";
86
+ }
87
+
88
+ /**
89
+ * 活动证据链(圆点与第三行文案共用同一份判定,避免两者互相矛盾)。
90
+ *
91
+ * 优先级固定,先命中先返回:
92
+ * 1. 官方目录项 `activity`:running / inactive——这是 DSH 实时活动轴,最高优先级,
93
+ * 即使 Host snapshot 的旧 running/status 与之冲突也以它为准;
94
+ * 2. 官方目录项可识别的 `status`:running / idle;
95
+ * 3. Host 快照的 `running` 布尔;
96
+ * 4. 协议 `status`:running 为运行,其余已知值(idle/queued/waiting/终态)为未运行。
97
+ * 都不成立时返回 null:展示层不猜——没有明确运行证据就落回 ready、也不加活动文案。
98
+ * @returns {"running"|"inactive"|null}
99
+ */
100
+ function activitySignalOf(node, entry) {
101
+ const source = isObject(node) ? node : {};
102
+ const catalog = isObject(entry) ? entry : {};
103
+ const activity = text(catalog.activity);
104
+ if (activity === "running") return "running";
105
+ if (activity === "inactive") return "inactive";
106
+ const catalogStatus = text(catalog.status);
107
+ if (catalogStatus === "running") return "running";
108
+ if (catalogStatus === "idle") return "inactive";
109
+ if (typeof source.running === "boolean") return source.running ? "running" : "inactive";
110
+ const protocolStatus = normalizeStatus(source.status);
111
+ if (protocolStatus === "running") return "running";
112
+ if (protocolStatus !== "unknown") return "inactive";
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * 归一化代理运行状态:对外只有 running | ready。
118
+ *
119
+ * 只用 activitySignalOf 的活动证据链:明确运行为 running,其余(inactive、缺失、
120
+ * 目录未命中且无其它运行证据)一律 ready。目录 activity 与 Host 旧信号冲突时,
121
+ * 以目录 activity 为准,不把真实 running 降级成 ready。
122
+ *
123
+ * @param {object} node - 快照节点(专家/根/fan-out 任务或分片)。
124
+ * @param {object} [entry] - sessions 快照里的官方目录项。
125
+ * @returns {"running"|"ready"}
126
+ */
127
+ function runtimeStatusOf(node, entry) {
128
+ return activitySignalOf(node, entry) === "running" ? "running" : "ready";
129
+ }
130
+
131
+ /** 只有精确命中词表的 mode 才能映射文案;未知/非法值返回 null(不猜)。 */
132
+ function modeLabelOf(value) {
133
+ const mode = text(value);
134
+ if (mode === null) return null;
135
+ return Object.prototype.hasOwnProperty.call(MODE_LABELS, mode) ? MODE_LABELS[mode] : null;
136
+ }
137
+
138
+ /**
139
+ * 目录项是否显式声明「可续接性未知」(0.1.7-alpha.2 新增的 `unknown`)。
140
+ *
141
+ * unknown 与 one-shot 一样属于**不可续接**:它明确表示宿主尚未解析出可续接性,
142
+ * 因此绝不能回退到节点 continuable 把它显示成「可继续」。
143
+ */
144
+ function isUnknownMode(value) {
145
+ return text(value) === "unknown";
146
+ }
147
+
148
+ /**
149
+ * 第三行的动态摘要:与 DSH 原生代理行同源,组合为 `title · mode · activity`。
150
+ *
151
+ * - title 只接受调用方从 sessions 快照读出的**权威 title**(byId[id].title);
152
+ * - mode:只有目录项 mode 明确为 continuable/one-shot 才显示「可继续/一次性」;
153
+ * 非法或未知 mode 不映射(绝不当成 continuable),仅在 node.continuable 是
154
+ * boolean 时才回退对应文案,否则整段省略。目录项显式为 0.1.7-alpha.2 新增的
155
+ * `unknown` 时,它已表达「可续接性未知」的权威判定,因此**禁止**回退到节点证据:
156
+ * 按不可续接降级,既不显示「可继续」,也不显示占位词;
157
+ * - activity:与圆点共用 activitySignalOf 证据链,因此圆点 ready 时绝不会显示
158
+ * 「正在运行」;只有明确 running/inactive 才映射原生文案,其余省略。
159
+ * 实际存在的值用 " · " 连接;都不可证明时返回空串——绝不输出
160
+ * 「无活动 / 模式未知 / 状态未知」这类占位词。
161
+ *
162
+ * @param {{title?: string|null, entry?: object|null, node?: object|null}} input
163
+ * @returns {string}
164
+ */
165
+ function formatAgentSummary(input) {
166
+ const source = isObject(input) ? input : {};
167
+ const catalog = isObject(source.entry) ? source.entry : {};
168
+ const node = isObject(source.node) ? source.node : {};
169
+ let modeText = modeLabelOf(catalog.mode);
170
+ // 目录项显式给出 unknown:这是「可续接性未知」的权威判定,按不可续接降级,
171
+ // 不再回退节点 continuable(否则会把未知说成「可继续」)。
172
+ if (modeText === null && typeof node.continuable === "boolean" && isUnknownMode(catalog.mode) !== true) {
173
+ modeText = node.continuable ? MODE_LABELS.continuable : MODE_LABELS["one-shot"];
174
+ }
175
+ const activitySignal = activitySignalOf(node, catalog);
176
+ const activityText = activitySignal === null ? null : ACTIVITY_LABELS[activitySignal];
177
+ return [text(source.title), modeText, activityText].filter((value) => value !== null).join(" · ");
178
+ }
179
+
180
+ function normalizeRunSummary(value) {
181
+ if (!isObject(value)) return null;
182
+ const mode = value.mode === "continuable" || value.mode === "one-shot" ? value.mode : "unknown";
183
+ const activity = value.activity === "running" || value.activity === "idle" ? value.activity : "unknown";
184
+ return { command: text(value.command), mode, activity };
185
+ }
186
+
187
+ function normalizeAttempt(value) {
188
+ const source = isObject(value) ? value : {};
189
+ return {
190
+ attempt: numberOrNull(source.attempt),
191
+ status: normalizeStatus(source.status),
192
+ childId: text(source.childId),
193
+ startedAt: numberOrNull(source.startedAt),
194
+ finishedAt: numberOrNull(source.finishedAt),
195
+ };
196
+ }
197
+
198
+ function normalizeShard(value, inherited) {
199
+ const source = isObject(value) ? value : {};
200
+ const defaults = isObject(inherited) ? inherited : {};
201
+ const attempts = Array.isArray(source.attempts) ? source.attempts.map(normalizeAttempt) : [];
202
+ const latest = attempts.length === 0 ? null : attempts[attempts.length - 1];
203
+ const managerStatus = source.status === undefined ? "unknown" : normalizeStatus(source.status);
204
+ // Host 已将 fan-out manager 与 attempt 历史归并成权威状态;客户端仅在旧 Host
205
+ // 没有投影该字段时用最新 attempt 补全,避免浏览器再次解释协议并产生漂移。
206
+ const status = managerStatus !== "unknown" ? managerStatus : (latest?.status ?? "unknown");
207
+ return {
208
+ shardId: text(source.shardId),
209
+ // taskName 是 Host 投影的权威分片任务名(优先任务名称,旧数据回退分片代号);
210
+ // 客户端只在旧 Host 完全没有投影该字段时才用 shardId 兜底,绝不反过来覆盖已有名称。
211
+ taskName: firstText(source.taskName, source.shardId),
212
+ prompt: firstText(source.prompt, source.content),
213
+ role: firstText(source.role, defaults.role),
214
+ provider: firstText(source.provider, defaults.provider),
215
+ model: firstText(source.model, defaults.model),
216
+ effort: firstText(source.effort, source.reasoningEffort, defaults.effort, defaults.reasoningEffort),
217
+ status,
218
+ running: runningOf(status, source.running),
219
+ runSummary: normalizeRunSummary(source.runSummary),
220
+ continuable: typeof source.continuable === "boolean" ? source.continuable : null,
221
+ childId: latest?.childId ?? text(source.childId),
222
+ createdAt: numberOrNull(source.createdAt),
223
+ attempts,
224
+ };
225
+ }
226
+
227
+ function normalizeCounts(value) {
228
+ if (!isObject(value)) return null;
229
+ const result = {};
230
+ for (const field of COUNT_FIELDS) result[field] = numberOrNull(value[field]);
231
+ return result;
232
+ }
233
+
234
+ function normalizeFanout(value) {
235
+ const source = isObject(value) ? value : {};
236
+ const parentTaskId = text(source.parentTaskId);
237
+ const taskStatus = text(source.taskStatus) || text(source.status);
238
+ const status = taskStatus === null ? "unknown" : statusFromTask(taskStatus);
239
+ const rawShards = Array.isArray(source.shards) ? source.shards : [];
240
+ return {
241
+ id: parentTaskId,
242
+ kind: "fanout",
243
+ parentTaskId,
244
+ taskName: firstText(source.taskName, source.title),
245
+ content: firstText(source.content),
246
+ basePrompt: firstText(source.basePrompt, source.content),
247
+ contentSource: text(source.contentSource),
248
+ role: text(source.role),
249
+ provider: text(source.provider),
250
+ model: text(source.model),
251
+ effort: firstText(source.effort, source.reasoningEffort),
252
+ taskStatus,
253
+ status,
254
+ running: runningOf(status, source.running),
255
+ createdAt: numberOrNull(source.createdAt),
256
+ counts: normalizeCounts(source.counts),
257
+ shards: rawShards.map((shard) => normalizeShard(shard, source)),
258
+ };
259
+ }
260
+
261
+ function normalizeSessionNode(value, kind, fallbackId) {
262
+ const source = isObject(value) ? value : {};
263
+ const id = firstText(source.id, source.sessionId, fallbackId);
264
+ const status = normalizeStatus(source.status);
265
+ return {
266
+ id,
267
+ kind,
268
+ sessionId: firstText(source.sessionId, source.id, fallbackId),
269
+ taskName: firstText(source.taskName, source.task, source.title, source.displayTitle, source.label, source.name),
270
+ content: firstText(source.content, source.prompt),
271
+ contentSource: text(source.contentSource),
272
+ role: text(source.role),
273
+ provider: text(source.provider),
274
+ model: text(source.model),
275
+ effort: firstText(source.effort, source.reasoningEffort),
276
+ status,
277
+ running: runningOf(status, source.running),
278
+ runSummary: normalizeRunSummary(source.runSummary),
279
+ continuable: typeof source.continuable === "boolean" ? source.continuable : null,
280
+ createdAt: numberOrNull(source.createdAt),
281
+ children: [],
282
+ };
283
+ }
284
+
285
+ function attemptChildIds(nodes) {
286
+ const ids = new Set();
287
+ for (const node of nodes) {
288
+ if (node.kind !== "fanout") continue;
289
+ for (const shard of node.shards) for (const attempt of shard.attempts) {
290
+ if (attempt.childId !== null) ids.add(attempt.childId);
291
+ }
292
+ }
293
+ return ids;
294
+ }
295
+
296
+ function childSourceList(raw, root) {
297
+ if (Array.isArray(root?.children)) return root.children;
298
+ const expertChildren = Array.isArray(raw?.experts) ? raw.experts : [];
299
+ const fanoutChildren = Array.isArray(raw?.fanoutTasks)
300
+ ? raw.fanoutTasks
301
+ : (Array.isArray(raw?.tasks) ? raw.tasks : []);
302
+ return [...expertChildren, ...fanoutChildren];
303
+ }
304
+
305
+ function normalizeExpertGroupSnapshot(raw) {
306
+ if (!isObject(raw) || raw.schemaVersion !== SCHEMA_VERSION) return null;
307
+ const sourceRoot = isObject(raw.root) ? raw.root : {};
308
+ const rootSessionId = firstText(raw.rootSessionId, raw.sessionId, sourceRoot.sessionId, sourceRoot.id);
309
+ if (rootSessionId === null) return null;
310
+ const root = normalizeSessionNode(sourceRoot, "root", rootSessionId);
311
+ root.sessionId = rootSessionId;
312
+ root.id = rootSessionId;
313
+ const rawChildren = childSourceList(raw, sourceRoot);
314
+ const children = rawChildren.map((child) => {
315
+ const kind = isObject(child) && child.kind === "fanout" ? "fanout" : "expert";
316
+ return kind === "fanout" ? normalizeFanout(child) : normalizeSessionNode(child, "expert");
317
+ });
318
+ const fanoutIds = attemptChildIds(children);
319
+ root.children = children.filter((child) => child.kind !== "expert" || child.sessionId === null || !fanoutIds.has(child.sessionId));
320
+ return {
321
+ schemaVersion: SCHEMA_VERSION,
322
+ sessionId: rootSessionId,
323
+ rootSessionId,
324
+ generatedAt: numberOrNull(raw.generatedAt),
325
+ root,
326
+ };
327
+ }
328
+
329
+ function titleCaseRole(value, fallback) {
330
+ const source = text(value);
331
+ if (source === null) return fallback;
332
+ return source
333
+ .split(/[\s_-]+/)
334
+ .filter(Boolean)
335
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
336
+ .join(" ") || fallback;
337
+ }
338
+
339
+ function formatExpertMetadata(node) {
340
+ const source = isObject(node) ? node : {};
341
+ const role = titleCaseRole(source.role, source.kind === "root" ? "Orchestrator" : "Expert");
342
+ const provider = text(source.provider) || "未知供应商";
343
+ const model = text(source.model) || "未知模型";
344
+ const rawEffort = firstText(source.effort, source.reasoningEffort);
345
+ const effort = rawEffort === null ? "默认" : rawEffort.charAt(0).toUpperCase() + rawEffort.slice(1);
346
+ return [role, provider, model, effort].join(" · ");
347
+ }
348
+
349
+ function defaultExpanded(node) {
350
+ // 根节点是任务树入口,始终保持可见;其余节点才按语义状态决定默认折叠。
351
+ if (isObject(node) && node.kind === "root") return true;
352
+ if (isObject(node) && node.kind === "idle-group") return true;
353
+ const status = normalizeStatus(isObject(node) ? (node.status ?? node.taskStatus) : undefined);
354
+ return !DEFAULT_COLLAPSED_STATUSES.has(status);
355
+ }
356
+
357
+ function taskIdOf(value) {
358
+ if (typeof value === "string") return text(value) || "unknown";
359
+ const source = isObject(value) ? value : {};
360
+ return firstText(source.taskId, source.id, source.parentTaskId, source.sessionId, source.shardId) || "unknown";
361
+ }
362
+
363
+ function safeStorage(storage) {
364
+ if (storage && typeof storage.getItem === "function" && typeof storage.setItem === "function") return storage;
365
+ try {
366
+ const candidate = global.sessionStorage;
367
+ if (candidate && typeof candidate.getItem === "function" && typeof candidate.setItem === "function") return candidate;
368
+ } catch { /* 某些浏览器隐私模式访问 sessionStorage 会同步抛错,按无持久化降级。 */ }
369
+ return null;
370
+ }
371
+
372
+ function createExpansionStore(options) {
373
+ const config = isObject(options) ? options : {};
374
+ const storage = safeStorage(config.storage);
375
+ const rootSessionId = text(config.rootSessionId) || "unknown";
376
+ const keyFor = (value) => "dsh-expert-group:expansion:v" + SCHEMA_VERSION + ":" + encodeURIComponent(rootSessionId) + ":" + encodeURIComponent(taskIdOf(value));
377
+ const readOverride = (key) => {
378
+ if (storage === null) return undefined;
379
+ try {
380
+ const raw = storage.getItem(key);
381
+ if (raw === null || raw === undefined) return undefined;
382
+ if (raw === "true") return true;
383
+ if (raw === "false") return false;
384
+ const parsed = JSON.parse(raw);
385
+ if (typeof parsed === "boolean") return parsed;
386
+ if (isObject(parsed) && typeof parsed.expanded === "boolean"
387
+ && (parsed.schemaVersion === undefined || parsed.schemaVersion === SCHEMA_VERSION)) return parsed.expanded;
388
+ } catch { /* 损坏内容和存储读错误都只表示没有手动 override。 */ }
389
+ return undefined;
390
+ };
391
+ const isExpanded = (value) => {
392
+ const override = readOverride(keyFor(value));
393
+ return override === undefined ? defaultExpanded(value) : override;
394
+ };
395
+ const setExpanded = (value, expanded) => {
396
+ const next = expanded === true;
397
+ if (storage !== null) {
398
+ try {
399
+ storage.setItem(keyFor(value), JSON.stringify({ schemaVersion: SCHEMA_VERSION, rootSessionId, taskId: taskIdOf(value), expanded: next }));
400
+ } catch { /* 配额或隐私策略拒绝写入时仍保留本次渲染的默认行为。 */ }
401
+ }
402
+ return next;
403
+ };
404
+ const toggle = (value) => setExpanded(value, !isExpanded(value));
405
+ const clear = (value) => {
406
+ if (storage === null || typeof storage.removeItem !== "function") return;
407
+ try { storage.removeItem(keyFor(value)); } catch { /* 清理失败不影响页面。 */ }
408
+ };
409
+ return Object.freeze({
410
+ rootSessionId, keyFor, isExpanded, get: isExpanded,
411
+ setExpanded, set: setExpanded, toggle, clear,
412
+ });
413
+ }
414
+
415
+ global[SYMBOL] = Object.freeze({
416
+ SCHEMA_VERSION,
417
+ RUNTIME_STATUSES,
418
+ normalizeExpertGroupSnapshot,
419
+ normalizeStatus,
420
+ runtimeStatusOf,
421
+ formatAgentSummary,
422
+ formatExpertMetadata,
423
+ defaultExpanded,
424
+ createExpansionStore,
425
+ });
426
+ })(globalThis);