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.
- package/CHANGELOG.md +52 -0
- package/README.md +11 -3
- package/README_EN.md +9 -3
- package/docs/update.md +46 -0
- package/lib/client.js +602 -51
- package/lib/context-inject.js +98 -28
- package/lib/index.js +305 -12
- package/lib/mcp/manager.js +115 -57
- package/lib/mcp/state-section.js +5 -2
- package/lib/memories/archive-engine.js +199 -0
- package/lib/memories/constants.js +1 -1
- package/lib/memories/snapshot.js +10 -0
- package/lib/op-registry.js +273 -0
- package/lib/ops/compat.js +15 -2
- package/lib/ops/state-doctor.js +115 -0
- package/lib/request-gate.js +21 -78
- package/lib/scene-settings.js +17 -0
- package/lib/skills/catalog.js +9 -4
- package/lib/skills/core.js +9 -0
- package/lib/subagents/catalog.js +9 -5
- package/lib/subagents/tools.js +9 -2
- package/lib/tools/mcp.js +3 -3
- package/lib/tools/memory.js +105 -5
- package/lib/tools/prompt.js +2 -2
- package/lib/tools/skills.js +81 -3
- package/lib/tools/subagent.js +117 -2
- package/lib/tools/table.js +101 -0
- package/package.json +108 -108
package/lib/context-inject.js
CHANGED
|
@@ -291,7 +291,11 @@ const DOMAIN_FRAME = {
|
|
|
291
291
|
skills: {
|
|
292
292
|
title: '本机技能目录',
|
|
293
293
|
cue: '需要某项能力时,先在这里找。',
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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(
|
|
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
|
-
|
|
742
|
-
delivered.
|
|
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) {
|