dsh-plugin-tool-management 0.12.0 → 0.13.0

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.
@@ -291,7 +291,11 @@ const DOMAIN_FRAME = {
291
291
  skills: {
292
292
  title: '本机技能目录',
293
293
  cue: '需要某项能力时,先在这里找。',
294
- how: '本预设没有官方 `skill` 工具:要正文用 `skill_manager_list` 取源文件路径再读;目录只有摘要,读完再照做。',
294
+ // 两句都只在预设没挂官方 `skill` 工具时出现,差别只在点名不点名那个取正文的工具
295
+ // (工具被用户在兼容页关掉时不点名 —— 点名一个模型手里没有的工具只会让它去猜名字)。
296
+ how: (toolHidden) => toolHidden('skill_manager_read')
297
+ ? '本预设没有官方 `skill` 工具:目录只有摘要,读完再照做。'
298
+ : '本预设没有官方 `skill` 工具:要正文用 `skill_manager_read`(按名字直接给正文与路径);目录只有摘要,读完再照做。',
295
299
  supersede: '本份目录取代本次会话中更早注入的同类目录;只列当前可调用的技能。',
296
300
  },
297
301
  subagents: {
@@ -301,7 +305,11 @@ const DOMAIN_FRAME = {
301
305
  // 委派工具(`subagent` / `subagent_fork`)不带人设,而此前没有任何一句话说明何时该
302
306
  // 用谁 —— 模型在"审查刚读过的 README"时选了 `subagent_fork`(fork 能继承已读内容、
303
307
  // 省一次复述)。现在人设通道也有 `inherit`(同一套 fork 机制),分界只剩"要不要后台跑"。
304
- how: '贴合人设的任务一律用 `subagent_manager_run`(要它看到本次会话就开 `inherit`);官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。',
308
+ // 带人设的委派工具被关掉时改说"本会话没有这条通道":不说的话,模型看到有人设目录却
309
+ // 找不到对应的工具,会去拿官方那两个凑(它们不接受人设,等于白跑一趟)。
310
+ how: (toolHidden) => toolHidden('subagent_manager_run')
311
+ ? '本会话没有带人设的委派工具;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。'
312
+ : '贴合人设的任务一律用 `subagent_manager_run`(要它看到本次会话就开 `inherit`);官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。',
305
313
  supersede: '本份目录取代本次会话中更早注入的同类目录。',
306
314
  },
307
315
  prompt: {
@@ -357,11 +365,12 @@ export function escapeFrameBody(body) {
357
365
  * 同时把**正文也包进标记里**(此前只有引导语在标记内、正文裸奔)—— 官方两条注入行都是
358
366
  * 整条包住的,正文里的用户自由文本更需要这层来源标记。
359
367
  */
360
- export function renderDomainText(section) {
368
+ export function renderDomainText(section, toolHidden = () => false) {
361
369
  const frame = domainFrame(section.key, section.label);
362
370
  const lines = [FRAME_OPEN, `## ${frame.title}`, `**${frame.cue}**`];
363
- if (frame.how !== undefined)
364
- lines.push(frame.how);
371
+ const how = typeof frame.how === 'function' ? frame.how(toolHidden) : frame.how;
372
+ if (how !== undefined)
373
+ lines.push(how);
365
374
  lines.push(frame.supersede, '', escapeFrameBody(section.text), FRAME_CLOSE);
366
375
  return lines.join('\n');
367
376
  }
@@ -523,12 +532,22 @@ export function createContextInjector(deps) {
523
532
  // 最近活跃的会话(WeakRef:诊断用,不阻止会话被回收)。每次 pre-step 都刷新 ——
524
533
  // 包括"空 turn 提前返回"和"这一步没有内容可发"的分支,页面才能如实说"没投过"。
525
534
  let lastAgent = null;
526
- const delivered = { count: 0, lastAt: null, byDomain: {} };
527
- // 采纳遥测:每个域一份计数 + 全局观测面。
528
- const adoption = {};
529
- for (const key of INJECT_DOMAIN_KEYS)
530
- adoption[key] = { injected: 0, used: 0, adopted: 0, lastUsedAt: null };
531
- const observed = { toolCalls: 0, lastAt: null };
535
+ const emptyLedger = () => {
536
+ const adoption = {};
537
+ for (const key of INJECT_DOMAIN_KEYS)
538
+ adoption[key] = { injected: 0, used: 0, adopted: 0, lastUsedAt: null };
539
+ return { delivered: { count: 0, lastAt: null, byDomain: {} }, adoption, observed: { toolCalls: 0, lastAt: null } };
540
+ };
541
+ const ledgers = new WeakMap();
542
+ /** 取(或新建)这个会话的账本。pre-step 与工具调用两条路都从这里过。 */
543
+ const ledgerFor = (agent) => {
544
+ let ledger = ledgers.get(agent);
545
+ if (ledger === undefined) {
546
+ ledger = emptyLedger();
547
+ ledgers.set(agent, ledger);
548
+ }
549
+ return ledger;
550
+ };
532
551
  // 每个 agent 最近一步"在上下文里"的域集合 —— 采纳判定要回答的是"调用发生时正文在不在眼前"。
533
552
  // WeakMap:不阻止会话被回收(与 lastAgent 同一考虑)。
534
553
  const liveDomainsByAgent = new WeakMap();
@@ -544,6 +563,8 @@ export function createContextInjector(deps) {
544
563
  catch {
545
564
  agent = undefined;
546
565
  }
566
+ // 面板读的是**这一段对话**的账本;会话已经被回收了就没有账本可读,全零 + `hasAgent:false`。
567
+ const ledger = agent === undefined ? emptyLedger() : ledgerFor(agent);
547
568
  const visible = agent === undefined
548
569
  ? new Map()
549
570
  : newestDomainTexts(agent, LIVE_KINDS);
@@ -567,13 +588,29 @@ export function createContextInjector(deps) {
567
588
  state,
568
589
  bytes: bytesOf(text),
569
590
  text,
570
- adoption: { ...adoption[key] },
591
+ adoption: { ...ledger.adoption[key] },
571
592
  };
572
593
  });
594
+ // 成本汇总:只做算术,不碰投递语义(这条视图的存在不改任何发送文本,因此不会触发重发)。
595
+ let liveBytes = 0;
596
+ let injectedBytes = 0;
597
+ let topDomain = null;
598
+ let mostDelivered = null;
599
+ for (const row of rows) {
600
+ liveBytes += row.bytes;
601
+ const sends = ledger.delivered.byDomain[row.key] ?? 0;
602
+ const spent = row.bytes * sends;
603
+ injectedBytes += spent;
604
+ if (spent > 0 && (topDomain === null || spent > topDomain.bytes))
605
+ topDomain = { key: row.key, label: row.label, bytes: spent };
606
+ if (sends > 0 && (mostDelivered === null || sends > mostDelivered.count))
607
+ mostDelivered = { key: row.key, label: row.label, count: sends };
608
+ }
573
609
  return {
574
610
  hasAgent: agent !== undefined,
575
- delivered: { count: delivered.count, lastAt: delivered.lastAt, byDomain: { ...delivered.byDomain } },
576
- observed: { toolCalls: observed.toolCalls, lastAt: observed.lastAt },
611
+ delivered: { count: ledger.delivered.count, lastAt: ledger.delivered.lastAt, byDomain: { ...ledger.delivered.byDomain } },
612
+ observed: { toolCalls: ledger.observed.toolCalls, lastAt: ledger.observed.lastAt },
613
+ cost: { liveBytes, injectedBytes, topDomain, mostDelivered },
577
614
  domains: rows,
578
615
  };
579
616
  };
@@ -583,17 +620,30 @@ export function createContextInjector(deps) {
583
620
  if (key === undefined)
584
621
  return;
585
622
  const at = Date.now();
586
- observed.toolCalls += 1;
587
- observed.lastAt = at;
588
- const row = adoption[key];
623
+ // 记在**发起这次调用的那个会话**的账上。`exec.agent` 与 pre-step 的 `payload.agent`
624
+ // 是同一个对象(dsh-scope 的不变量要求,见 review/后续方向.md §2 末),所以这里
625
+ // 落账的会话与上面 `liveDomainsByAgent` 记现场的会话必然一致。
626
+ let owner = typeof agent === 'object' && agent !== null ? agent : undefined;
627
+ if (owner === undefined) {
628
+ try {
629
+ owner = lastAgent ? lastAgent.deref() : undefined;
630
+ }
631
+ catch {
632
+ owner = undefined;
633
+ }
634
+ }
635
+ if (owner === undefined)
636
+ return;
637
+ const ledger = ledgerFor(owner);
638
+ ledger.observed.toolCalls += 1;
639
+ ledger.observed.lastAt = at;
640
+ const row = ledger.adoption[key];
589
641
  row.used += 1;
590
642
  row.lastUsedAt = at;
591
643
  // 采纳判定:调用发生时该域正文正在这个会话的上下文里。
592
- if (typeof agent === 'object' && agent !== null) {
593
- const liveKeys = liveDomainsByAgent.get(agent);
594
- if (liveKeys !== undefined && liveKeys.has(key))
595
- row.adopted += 1;
596
- }
644
+ const liveKeys = liveDomainsByAgent.get(owner);
645
+ if (liveKeys !== undefined && liveKeys.has(key))
646
+ row.adopted += 1;
597
647
  }
598
648
  catch { /* 遥测绝不能影响工具本身 */ }
599
649
  };
@@ -632,6 +682,9 @@ export function createContextInjector(deps) {
632
682
  if (!agent)
633
683
  return decision;
634
684
  if (typeof agent === 'object') {
685
+ // 账本先建好:下面任何一条提前返回(空 turn / 没内容可发)都要能在实况里读到
686
+ // "这一段对话投了 0 次",而不是读到上一段对话的数字。
687
+ ledgerFor(agent);
635
688
  try {
636
689
  lastAgent = new WeakRef(agent);
637
690
  }
@@ -687,9 +740,12 @@ export function createContextInjector(deps) {
687
740
  const published = new Set();
688
741
  // 域声明按 key 索引:来源文件(`files`)只在真要发消息时取,所以要能从这里回查声明。
689
742
  const domainOf = new Map(domains.map((domain) => [domain.key, domain]));
743
+ // 工具表开关本步读一次(同步快照):`how` 行里点名工具的那几句据此换话术。
744
+ const hiddenNow = deps.hiddenTools !== undefined ? deps.hiddenTools() : null;
745
+ const toolHidden = hiddenNow === null ? () => false : (name) => hiddenNow.has(name);
690
746
  for (const section of sections) {
691
747
  published.add(section.key);
692
- const text = renderDomainText(section);
748
+ const text = renderDomainText(section, toolHidden);
693
749
  const current = visible.get(section.key);
694
750
  if (current !== undefined && current.text === text)
695
751
  continue;
@@ -708,6 +764,7 @@ export function createContextInjector(deps) {
708
764
  }
709
765
  // 曾经注入过、这一轮没有内容的域 → 一条「已清空」;从没注入过的域什么都不用说。
710
766
  const labelOf = new Map(domains.map((domain) => [domain.key, domain.label]));
767
+ const clearedKeys = new Set();
711
768
  for (const key of INJECT_DOMAIN_KEYS) {
712
769
  if (published.has(key))
713
770
  continue;
@@ -719,17 +776,29 @@ export function createContextInjector(deps) {
719
776
  continue;
720
777
  additions.push(clearedMessage(key, label));
721
778
  appended.push(key);
779
+ clearedKeys.add(key);
722
780
  }
723
781
  // 采纳判定要用的现场:**含官方载体**,并并入本步刚投出去的域正文(它们就在这一步的
724
782
  // 请求里,模型当场看得到)。判断"发不发"只能用本插件自己的 kind —— 官方正文绝不能
725
783
  // 影响去重(见 PLUGIN_KINDS 的注释);判断"模型眼前有没有这份内容"则必须连官方那份
726
784
  // 一起算,标准类预设下技能与提示词正是官方在送。两遍扫描,两种口径各自正确。
785
+ // **「已清空」通知要从现场里剔掉**:那条消息也带域标记、也占 `newestDomainTexts` 的键,
786
+ // 但它不是正文。连着它一起算,"用户早把这个域关了、模型凭工具描述去调"的那次调用会被
787
+ // 记成采纳 —— 于是 `adopted` 实际在说"这个域本会话说过话",界面文案里那句"正文正在上下文
788
+ // 里"就成了假话。剔除之后 `used - adopted` 才第一次有意义:调了、但正文不在眼前。
789
+ // `clearedKeys` 那一步是同一个判据的另一半:通知这会儿还没落到会话表面上,光靠扫表面
790
+ // 会漏掉"就是这一步刚被清空"的那个域。
727
791
  // 位置在"提前返回"之前:这一步没东西可发时,现场依然是当前状态,同样要记。
728
792
  try {
729
793
  if (typeof agent === 'object') {
730
- const liveKeys = new Set(newestDomainTexts(agent, LIVE_KINDS).keys());
794
+ const liveKeys = new Set();
795
+ for (const [key, entry] of newestDomainTexts(agent, LIVE_KINDS))
796
+ if (entry.form !== 'notice')
797
+ liveKeys.add(key);
731
798
  for (const key of injectedKeys)
732
799
  liveKeys.add(key);
800
+ for (const key of clearedKeys)
801
+ liveKeys.delete(key);
733
802
  liveDomainsByAgent.set(agent, liveKeys);
734
803
  }
735
804
  }
@@ -738,12 +807,13 @@ export function createContextInjector(deps) {
738
807
  // 没有新增时,只有"拦下了官方消息"才需要返回改动后的 decision;否则原样返回。
739
808
  return messages === messagesOf(decision) ? decision : { ...decision, messages: [...messages] };
740
809
  }
741
- delivered.count += additions.length;
742
- delivered.lastAt = Date.now();
810
+ const ledger = ledgerFor(agent);
811
+ ledger.delivered.count += additions.length;
812
+ ledger.delivered.lastAt = Date.now();
743
813
  for (const key of appended)
744
- delivered.byDomain[key] = (delivered.byDomain[key] || 0) + 1;
814
+ ledger.delivered.byDomain[key] = (ledger.delivered.byDomain[key] || 0) + 1;
745
815
  for (const key of injectedKeys)
746
- adoption[key].injected += 1;
816
+ ledger.adoption[key].injected += 1;
747
817
  return { ...decision, messages: [...messages, ...additions] };
748
818
  }
749
819
  catch (error) {