dsh-subagent-profile 0.3.0 → 0.3.2

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/lib/core/pure.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // lib/core/pure.mjs — import-free pure helpers (extracted from index.mjs and grown
1
+ // lib/core/pure.mjs — import-free pure helpers (extracted from index.mjs and grown
2
2
  // by later refactors). No @deepseek-ai imports and no external dependencies:
3
3
  // @deepseek-ai symbols converge in lib/core/shims.mjs (the only such entry point),
4
4
  // and this module is safe to import from bare-CI tests without the junction
@@ -197,6 +197,19 @@ function sanitizeToolFilterField(value, clean, warnings) {
197
197
  if (Object.keys(tf).length > 0) clean.toolFilter = tf;
198
198
  }
199
199
 
200
+ // tokenTier:成本/深度分层(cheap/balanced/premium),供目录排序与结果卡
201
+ // 展示分层。合法值透传;非法值剔除 + warn(与 maxTokens 等超限字段同口径——
202
+ // 剔除而非回填缺省值)。缺省 'balanced' 在 sanitizeProfile 末尾按「字段未提供」
203
+ // 单独回填,非法值不受该回填影响。
204
+ const TOKEN_TIERS = new Set(['cheap', 'balanced', 'premium']);
205
+ function sanitizeTokenTierField(value, clean, warnings) {
206
+ if (typeof value !== 'string' || !TOKEN_TIERS.has(value)) {
207
+ warnings.push({ field: 'tokenTier', reason: 'tokenTier 必须为 cheap/balanced/premium 之一' });
208
+ return;
209
+ }
210
+ clean.tokenTier = value;
211
+ }
212
+
200
213
  // --- 结果回收默认剪枝 ----------------------------------------------------------
201
214
  // 子结果默认复用宿主 `toolResultPruner.pruneContent` 预剪(纯函数、零 LLM),
202
215
  // 在 `textFrom` 之前执行,把回灌进父上下文的体积压到阈值内。
@@ -281,7 +294,7 @@ export function assertResultSchemaConsistency(schema) {
281
294
  const KNOWN_PROFILE_FIELDS = new Set([
282
295
  'id', 'name', 'description', 'persona', 'preset', 'provider', 'model',
283
296
  'reasoningEffort', 'enabled', 'maxTokens', 'maxDepth', 'toolFilter',
284
- 'builtin', 'deleted',
297
+ 'tokenTier', 'builtin', 'deleted',
285
298
  ]);
286
299
 
287
300
  /**
@@ -305,6 +318,7 @@ export function sanitizeProfile(profile, options = {}) {
305
318
  if (profile === null || typeof profile !== 'object' || Array.isArray(profile)) {
306
319
  return { clean, warnings: [{ field: '(root)', reason: 'profile 不是对象' }] };
307
320
  }
321
+ let tokenTierProvided = false;
308
322
  for (const [key, value] of Object.entries(profile)) {
309
323
  if (!KNOWN_PROFILE_FIELDS.has(key)) {
310
324
  warnings.push({ field: key, reason: '未知字段已忽略' });
@@ -327,10 +341,27 @@ export function sanitizeProfile(profile, options = {}) {
327
341
  case 'toolFilter':
328
342
  sanitizeToolFilterField(value, clean, warnings);
329
343
  break;
344
+ case 'tokenTier':
345
+ tokenTierProvided = true;
346
+ sanitizeTokenTierField(value, clean, warnings);
347
+ break;
330
348
  default:
331
349
  clean[key] = value;
332
350
  break;
333
351
  }
334
352
  }
353
+ // tokenTier 缺省 'balanced':仅当字段未被提供时回填;被提供的非法值已在
354
+ // sanitizeTokenTierField 剔除(clean 保持无该键),不会被此回填覆盖。
355
+ if (!tokenTierProvided) clean.tokenTier = 'balanced';
335
356
  return { clean, warnings };
336
357
  }
358
+
359
+ // --- tokenTier 目录排序 ---------------------------------------------------------
360
+ // tokenTier 排序权重:cheap→balanced→premium。dispatch:profiles 目录行按此
361
+ // 升序排列(省 token 方案在前)。缺省/未知 tier 按 balanced 处理,保证旧数据
362
+ // 与无 tokenTier 字段的 profile 落到中间档而非报错。
363
+ export const TIER_ORDER = { cheap: 0, balanced: 1, premium: 2 };
364
+ export function tierSortKey(tokenTier) {
365
+ const order = TIER_ORDER[tokenTier];
366
+ return order === undefined ? TIER_ORDER.balanced : order;
367
+ }
@@ -1,4 +1,4 @@
1
- // lib/core/shims.mjs — facade. The ONLY module that imports the
1
+ // lib/core/shims.mjs — facade. The ONLY module that imports the
2
2
  // @deepseek-ai symbols index.mjs relies on, converging the previously
3
3
  // top-level-scattered import surface. Two failure classes:
4
4
  //
@@ -23,6 +23,7 @@
23
23
  // also verifies the value is a function before accepting it.
24
24
 
25
25
  import { randomUUID } from 'node:crypto';
26
+ import { createRequire } from 'node:module';
26
27
  // Guard-type: static, fail-loud — no fallback. Kept as the only two
27
28
  // static @deepseek-ai imports; a missing export aborts module load with a clear
28
29
  // error BEFORE apply can run, which is the isolation this class exists for.
@@ -186,6 +187,89 @@ function readResult(child, boundary, cancelled) {
186
187
  return { output: finalAssistantOutput(own) ?? [], stopReason };
187
188
  }
188
189
 
190
+ // --- version detection (pure probe) ------------------------------------------
191
+ // 读取 @deepseek-ai 三包(subagent/agent/llm)的 package.json version,与
192
+ // peerDependencies 范围(>=PEER_MIN <PEER_MAX)比对后产出中文 warnings。纯探测:
193
+ // 只读 manifest、不 import 新符号、不触发副作用;每包独立 try/catch,失败记
194
+ // 'unknown'。headless / 宿主裁剪部署下任一包都可能缺失,此时 warnings 非空,
195
+ // 设置页据此在顶部渲染 amber 提示条(不阻断派发)。
196
+ const requirePkg = createRequire(import.meta.url);
197
+ const PEER_MIN = '0.1.0-rc.6';
198
+ const PEER_MAX = '0.2.0';
199
+ const PROBED_PACKAGES = ['dsh-subagent', 'dsh-agent', 'dsh-llm'];
200
+
201
+ // 读单包 version(每包独立 try/catch,失败记 'unknown')。用 createRequire 直接
202
+ // require 包的 package.json(返回解析后的对象),免去 fs 读取与 JSON.parse。
203
+ function readPackageVersion(pkg) {
204
+ try {
205
+ const manifest = requirePkg(`@deepseek-ai/${pkg}/package.json`);
206
+ return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : 'unknown';
207
+ } catch {
208
+ return 'unknown';
209
+ }
210
+ }
211
+
212
+ // 极简 semver 比较:major.minor.patch + 可选 `-预发布` 段(覆盖 peerDependencies
213
+ // 范围判断所需)。预发布 < 正式版;预发布段逐段比较,纯数字段按数值、否则按字典序
214
+ // (`0.1.0-rc.10` > `0.1.0-rc.6`)。
215
+ function compareSemver(a, b) {
216
+ const [aCore, aPre = ''] = a.split('-');
217
+ const [bCore, bPre = ''] = b.split('-');
218
+ const aNums = aCore.split('.').map((n) => Number(n));
219
+ const bNums = bCore.split('.').map((n) => Number(n));
220
+ for (let i = 0; i < 3; i++) {
221
+ const x = aNums[i] ?? 0;
222
+ const y = bNums[i] ?? 0;
223
+ if (x !== y) return x < y ? -1 : 1;
224
+ }
225
+ if (aPre === bPre) return 0;
226
+ if (aPre === '') return 1; // 正式版 > 预发布
227
+ if (bPre === '') return -1;
228
+ const aParts = aPre.split('.');
229
+ const bParts = bPre.split('.');
230
+ const len = Math.max(aParts.length, bParts.length);
231
+ for (let i = 0; i < len; i++) {
232
+ const x = aParts[i];
233
+ const y = bParts[i];
234
+ if (x === undefined) return -1;
235
+ if (y === undefined) return 1;
236
+ const xNumeric = /^\d+$/.test(x);
237
+ const yNumeric = /^\d+$/.test(y);
238
+ if (xNumeric && yNumeric) {
239
+ const diff = Number(x) - Number(y);
240
+ if (diff !== 0) return diff;
241
+ } else if (xNumeric !== yNumeric) {
242
+ return xNumeric ? -1 : 1; // 数字标识 < 非数字标识(semver 约定)
243
+ } else if (x !== y) {
244
+ return x < y ? -1 : 1;
245
+ }
246
+ }
247
+ return 0;
248
+ }
249
+
250
+ // 版本是否落在 peerDependencies 范围(>=PEER_MIN 且 <PEER_MAX)。
251
+ function inPeerRange(version) {
252
+ return compareSemver(version, PEER_MIN) >= 0 && compareSemver(version, PEER_MAX) < 0;
253
+ }
254
+
255
+ // detectVersions(reader?) — 纯探测三包版本并产出 { versions, warnings }。
256
+ // `reader` 可选注入(默认 readPackageVersion),供测试模拟「包缺失 / 版本越界」
257
+ // 而无需删除 node_modules(与 loadSoft 的 importer 注入同一思路)。
258
+ export function detectVersions(reader = readPackageVersion) {
259
+ const versions = {};
260
+ for (const pkg of PROBED_PACKAGES) versions[pkg] = reader(pkg);
261
+ const warnings = [];
262
+ for (const pkg of PROBED_PACKAGES) {
263
+ const version = versions[pkg];
264
+ if (version === 'unknown') {
265
+ warnings.push(`未检测到 @deepseek-ai/${pkg} 版本(包缺失或被宿主裁剪)——请确认其已按 peerDependencies 范围安装`);
266
+ } else if (!inPeerRange(version)) {
267
+ warnings.push(`@deepseek-ai/${pkg} 版本 ${version} 超出 peerDependencies 范围(>=${PEER_MIN} <${PEER_MAX}),派发行为可能与预期不符`);
268
+ }
269
+ }
270
+ return { versions, warnings };
271
+ }
272
+
189
273
  // Test-only access to the local degraded implementations (package imports
190
274
  // resolve here, so the real functions win; __fallbacks lets a test exercise the
191
275
  // fail-soft path without deleting node_modules).
@@ -212,4 +296,6 @@ export {
212
296
  resolveChildAgentOptions,
213
297
  defineTool,
214
298
  readResult,
299
+ // ---- version probe seam (see detectVersions doc) ----
300
+ readPackageVersion,
215
301
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-subagent-profile",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Dispatch one-shot subtasks to derived subagents with per-task overrides (preset/model/provider/reasoningEffort/persona/tool whitelist), a runtime-derived cost guard, a subagent-profiles service, observability metadata, and a web-GUI settings page plus a dispatch tool-call card.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -77,6 +77,7 @@
77
77
  "release": "node scripts/release.mjs",
78
78
  "test": "node --test \"test/**/*.test.mjs\"",
79
79
  "test:bare": "node --test test/pure.test.mjs test/input-schema.test.mjs test/catalog-integrity.test.mjs",
80
- "preflight": "node scripts/preflight.mjs"
80
+ "preflight": "node scripts/preflight.mjs",
81
+ "leak-scan": "node scripts/leak-scan.mjs"
81
82
  }
82
83
  }