dsh-plugin-tool-management 0.12.1 → 0.14.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 +84 -0
- package/README.md +90 -114
- package/README_EN.md +98 -119
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +63 -0
- package/lib/client.js +1086 -95
- package/lib/compat/preset-reach.js +6 -3
- package/lib/context-inject.js +183 -59
- package/lib/index.js +501 -49
- package/lib/mcp/manager.js +118 -71
- package/lib/mcp/secret-guard.js +21 -0
- package/lib/mcp/state-section.js +19 -14
- package/lib/memories/archive-engine.js +199 -0
- package/lib/memories/constants.js +72 -8
- package/lib/memories/index-io.js +1 -1
- package/lib/memories/projection.js +67 -31
- package/lib/memories/service.js +17 -2
- package/lib/memories/snapshot.js +123 -22
- package/lib/op-registry.js +279 -0
- package/lib/ops/candidates.js +11 -0
- package/lib/ops/compat.js +23 -2
- package/lib/ops/sessions.js +1 -1
- package/lib/ops/state-doctor.js +115 -0
- package/lib/prompts/service.js +25 -2
- package/lib/request-gate.js +21 -78
- package/lib/scene-settings.js +17 -0
- package/lib/scenes/candidates.js +56 -0
- package/lib/skills/catalog.js +10 -5
- package/lib/skills/core.js +102 -0
- package/lib/skills/service.js +34 -2
- package/lib/subagents/catalog.js +9 -5
- package/lib/subagents/service.js +37 -4
- package/lib/subagents/tools.js +19 -5
- package/lib/tools/deps.js +15 -0
- package/lib/tools/mcp.js +291 -50
- package/lib/tools/memory.js +170 -19
- package/lib/tools/prompt.js +28 -14
- package/lib/tools/scene.js +351 -0
- package/lib/tools/skills.js +168 -14
- package/lib/tools/subagent.js +129 -3
- package/lib/tools/table.js +290 -0
- package/package.json +2 -2
|
@@ -225,7 +225,7 @@ export function deriveReach(facts, ctx) {
|
|
|
225
225
|
const forceUnderSuppressing = ctx?.inject?.underSuppressingPresets === true;
|
|
226
226
|
const domainOff = (key) => ctx?.inject?.domains?.[key] === false;
|
|
227
227
|
/**
|
|
228
|
-
*
|
|
228
|
+
* 本插件自己的文本(场景 / 记忆 / MCP 服务器与备注 / 技能目录 / 子智能体目录 / 提示词)
|
|
229
229
|
* 走 `agent/pre-step` 注入消息(src/context-inject.ts)—— 不再依赖系统提示词段,所以
|
|
230
230
|
* `persona complete` 压不到它。可达性只看两件事:域开关有没有关、预设压制时有没有开
|
|
231
231
|
* 「仍然注入」。预设信息读不到(`personaUnknown` 且无压制信号)时按可达处理。
|
|
@@ -237,6 +237,7 @@ export function deriveReach(facts, ctx) {
|
|
|
237
237
|
return 'suppressed';
|
|
238
238
|
return 'ok';
|
|
239
239
|
};
|
|
240
|
+
const scene = pluginText('scene');
|
|
240
241
|
const memory = pluginText('memory');
|
|
241
242
|
const mcp = pluginText('mcp');
|
|
242
243
|
/**
|
|
@@ -275,7 +276,7 @@ export function deriveReach(facts, ctx) {
|
|
|
275
276
|
const subagent = pluginText('subagents');
|
|
276
277
|
// MCP 列答的是"服务器清单与备注到不到得了":宿主工具数不再参与 —— 清单为空时注入出去
|
|
277
278
|
// 也是空段(没什么可看的),"有几台 server"由插件页面回答,不是这一列的事。
|
|
278
|
-
return { memory, agentsMd, skillCatalog, subagent, mcp };
|
|
279
|
+
return { scene, memory, agentsMd, skillCatalog, subagent, mcp };
|
|
279
280
|
}
|
|
280
281
|
/** Narrow the roster off a cordis context without throwing. */
|
|
281
282
|
export function presetRosterOf(ctx) {
|
|
@@ -308,6 +309,7 @@ async function composeRow(roster, meta, ctx) {
|
|
|
308
309
|
agentInstructions: 'absent',
|
|
309
310
|
toolSkill: 'absent',
|
|
310
311
|
suppressing: false,
|
|
312
|
+
scene: 'unknown',
|
|
311
313
|
memory: 'unknown',
|
|
312
314
|
agentsMd: 'unknown',
|
|
313
315
|
skillCatalog: 'unknown',
|
|
@@ -328,6 +330,7 @@ async function composeRow(roster, meta, ctx) {
|
|
|
328
330
|
agentInstructions: 'absent',
|
|
329
331
|
toolSkill: 'absent',
|
|
330
332
|
suppressing: false,
|
|
333
|
+
scene: 'unknown',
|
|
331
334
|
memory: 'unknown',
|
|
332
335
|
agentsMd: 'unknown',
|
|
333
336
|
skillCatalog: 'unknown',
|
|
@@ -435,7 +438,7 @@ export function reachNoticeFor(presetId, facts, inject) {
|
|
|
435
438
|
const domainOff = (key) => inject?.domains?.[key] === false;
|
|
436
439
|
if (isSuppressingPreset(facts) && inject?.underSuppressingPresets !== true) {
|
|
437
440
|
parts.push(`预设「${presetId}」声明只要它自己的文本(persona complete / 关闭运行时上下文):` +
|
|
438
|
-
'本插件注入的 ——
|
|
441
|
+
'本插件注入的 —— 场景、记忆、MCP、技能、子智能体、提示词 —— 默认不注入,' +
|
|
439
442
|
'需要时用对应的 list / read 工具按需读取;不要假设你已经看到它们。' +
|
|
440
443
|
'(想让它在这类预设下也注入:插件的「兼容」页 → 注入。)');
|
|
441
444
|
}
|
package/lib/context-inject.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// 与官方 skill-catalog 同款:各自的来源 kind(轨迹里各自一行、各显各的名字)、各自的
|
|
17
17
|
// form、各自的去重。好处是"只改了一个域就只重发那一条";代价是进场景这类多域同时变的
|
|
18
18
|
// 时刻会一次发几条(每条带一句自己的引导语)。五个域与轨迹行名:
|
|
19
|
-
//
|
|
19
|
+
// scene → scene-manager-catalog(场景:启用的场景 + 场景说明,约定)
|
|
20
|
+
// memory → memory-manager-catalog(记忆:各场景下的条目,记录)
|
|
20
21
|
// mcp → mcp-manager-catalog
|
|
21
22
|
// skills → skill-manager-catalog(不叫 skill-catalog:那是官方那条行的名字)
|
|
22
23
|
// subagents → subagent-manager-catalog
|
|
@@ -74,17 +75,23 @@ import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
|
74
75
|
import { clearRuntimeNote, noteRuntime } from './compat/runtime-notes.js';
|
|
75
76
|
/**
|
|
76
77
|
* 权威域顺序(界面勾选、注入消息先后都按它)。
|
|
77
|
-
*
|
|
78
|
+
* 场景排第一、记忆紧跟:场景是"当前模式"的**框架**(有哪些场景、各自是什么约定),
|
|
79
|
+
* 记忆是各场景下的**内容** —— 先给框架再给内容。
|
|
78
80
|
*/
|
|
79
|
-
export const INJECT_DOMAIN_KEYS = ['memory', 'mcp', 'skills', 'subagents', 'prompt'];
|
|
81
|
+
export const INJECT_DOMAIN_KEYS = ['scene', 'memory', 'mcp', 'skills', 'subagents', 'prompt'];
|
|
80
82
|
/**
|
|
81
83
|
* 域 → 消息来源 kind(轨迹行标签,也是去重时的身份)。
|
|
82
84
|
*
|
|
83
85
|
* 命名对齐官方 `*-catalog` 风格与本插件的工具族(`*_manager_*`);改名等于换身份,
|
|
84
86
|
* 旧消息会被当成"不在上下文里"而重发一次,所以这几个字符串是稳定契约。
|
|
87
|
+
*
|
|
88
|
+
* ⚠️ 0.14.0 把 `memory` 的 kind 从 `scene-memory-manager-catalog` 改成
|
|
89
|
+
* `memory-manager-catalog`,并把场景拆成独立的 `scene` 域 —— 升级后每个会话**会重发一次**
|
|
90
|
+
* 这两段(旧消息认不出来)。一次性代价,换来的是两段能各自开关、各自去重。
|
|
85
91
|
*/
|
|
86
92
|
export const INJECT_KIND_OF = {
|
|
87
|
-
|
|
93
|
+
scene: 'scene-manager-catalog',
|
|
94
|
+
memory: 'memory-manager-catalog',
|
|
88
95
|
mcp: 'mcp-manager-catalog',
|
|
89
96
|
skills: 'skill-manager-catalog',
|
|
90
97
|
subagents: 'subagent-manager-catalog',
|
|
@@ -98,22 +105,30 @@ export const INJECT_KIND_OF = {
|
|
|
98
105
|
* 「用了」长得一模一样。有了映射,就能把「投递 N 次 / 调用 M 次」并排摆出来,
|
|
99
106
|
* 措辞与形式的调整才有依据(否则改文案就是猜)。
|
|
100
107
|
*
|
|
101
|
-
* 前缀而不是精确名:`memory_manager_*` 有 list/read/
|
|
102
|
-
*
|
|
108
|
+
* 前缀而不是精确名:`memory_manager_*` 有 list/read/switch/save 等,任何一个
|
|
109
|
+
* 都说明模型确实在读这一域。改工具名等于换身份,这几个字符串是稳定契约。
|
|
110
|
+
*
|
|
111
|
+
* 为什么值是**数组**:工具族与注入域不是一一对应的概念 —— 域是"给模型看的信息分组"
|
|
112
|
+
* (界面上的勾选),族是"操作哪类对象"。0.14.0 里场景与记忆就一度同域两族
|
|
113
|
+
* (`memory_manager_*` + `scene_manager_*`),拆开后现在每个域各一个前缀。留着数组是
|
|
114
|
+
* 为了让"一个域挂多个族"在类型上成立,将来加族不必改类型。顺序不影响判定。
|
|
103
115
|
*/
|
|
104
116
|
export const DOMAIN_TOOL_PREFIX = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
scene: ['scene_manager_'],
|
|
118
|
+
memory: ['memory_manager_'],
|
|
119
|
+
mcp: ['mcp_manager_'],
|
|
120
|
+
skills: ['skill_manager_'],
|
|
121
|
+
subagents: ['subagent_manager_'],
|
|
122
|
+
prompt: ['prompt_manager_'],
|
|
110
123
|
};
|
|
111
124
|
/** 工具名 → 域(`undefined` = 不是本插件的域工具)。纯函数,便于单独推理。 */
|
|
112
125
|
export function domainOfTool(toolName) {
|
|
113
126
|
const name = String(toolName ?? '');
|
|
114
127
|
for (const key of INJECT_DOMAIN_KEYS) {
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
for (const prefix of DOMAIN_TOOL_PREFIX[key]) {
|
|
129
|
+
if (name.startsWith(prefix))
|
|
130
|
+
return key;
|
|
131
|
+
}
|
|
117
132
|
}
|
|
118
133
|
return undefined;
|
|
119
134
|
}
|
|
@@ -170,7 +185,7 @@ const CARRIER_FACT_OF = {
|
|
|
170
185
|
};
|
|
171
186
|
export const DEFAULT_INJECT_SETTINGS = {
|
|
172
187
|
underSuppressingPresets: false,
|
|
173
|
-
domains: { memory: true, mcp: true, skills: true, subagents: true, prompt: true },
|
|
188
|
+
domains: { scene: true, memory: true, mcp: true, skills: true, subagents: true, prompt: true },
|
|
174
189
|
};
|
|
175
190
|
/** 把任意输入夹成合法设置(缺项/类型不对一律退回默认;默认从不阻止注入)。 */
|
|
176
191
|
export function normalizeInjectSettings(raw) {
|
|
@@ -276,45 +291,79 @@ export function explainInjections(domains, settings, facts, agent) {
|
|
|
276
291
|
}
|
|
277
292
|
return reasons;
|
|
278
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* 权威声明的统一句(用户 2026-09-23 看到实际注入后要求精简)。
|
|
296
|
+
*
|
|
297
|
+
* 此前五个域各写一份 —— `本份场景取代…同类场景` / `本份记忆取代…同类记忆` /
|
|
298
|
+
* `本份状态…` / `本份目录…` —— 说的是**同一条规则**却用了四种措辞,模型读到四条不同的句子
|
|
299
|
+
* 还得自己判断它们是不是一条。统一成一句:被取代的是"同类内容",与域无关。
|
|
300
|
+
*
|
|
301
|
+
* 为什么不能并进 `cue`(那能省下整整一行 ≈16 tok/段):用户 2026-09-18 定过"权威声明单独
|
|
302
|
+
* 成句" —— 它和动作句是两种东西(一句说"什么时候用它",一句说"以哪份为准"),合并后容易
|
|
303
|
+
* 被一眼带过。所以这里的收益只有约 6 tok/轮,**主要收益是消除四种措辞**,不是省字节。
|
|
304
|
+
*/
|
|
305
|
+
const SUPERSEDE_NOTE = '本份取代本次会话中更早注入的同类内容。';
|
|
279
306
|
const DOMAIN_FRAME = {
|
|
307
|
+
scene: {
|
|
308
|
+
// 场景段只有标题 + 正文 + 权威声明(**没有 cue 是六个域的共同决定**,理由见 `DomainFrame`)。
|
|
309
|
+
//
|
|
310
|
+
// 这一段的演进值得记下来,因为每一次都是被实际注入推着改的:
|
|
311
|
+
// ① 最早它把「场景说明」当**约定**授权("一律照办,覆盖你的默认做法")—— 而它的实例
|
|
312
|
+
// 是「写代码」这种**标签**,让模型"照办一个标签",这正是它读不懂这段的原因;
|
|
313
|
+
// ② 去掉授权后换成一句定义("场景是用户给这台机器配的工作模式")—— 定义不是动作,
|
|
314
|
+
// 模型读完还是不知道该拿它做什么;
|
|
315
|
+
// ③ 再加一句因果("下面四段都已按它筛过")—— 用户 2026-09-23 看到渲染效果后给了
|
|
316
|
+
// **原则**:「上下文注入就是当前的情况,目的是让 agent 知道现在的情况,不需要它
|
|
317
|
+
// 知道没用的信息,反推更是浪费 token」。于是三句全删。
|
|
318
|
+
//
|
|
319
|
+
// 现在这一段只回答一个问题:**当前处在哪个场景、它是什么**(`**「代码」—— 写代码**`)。
|
|
320
|
+
// 它还比别的段少一层:因果句也删了 —— 它解释的是"另外四段是怎么产生的"(机制),
|
|
321
|
+
// 不是当前情况本身,而且"清单里没有 ≠ 本机没有"这层反推被用户明确判为浪费。
|
|
322
|
+
title: '本机当前的场景',
|
|
323
|
+
supersede: SUPERSEDE_NOTE,
|
|
324
|
+
},
|
|
280
325
|
memory: {
|
|
281
|
-
title: '
|
|
282
|
-
|
|
283
|
-
supersede: '本份记忆取代本次会话中更早注入的同类记忆。',
|
|
326
|
+
title: '本机当前的记忆',
|
|
327
|
+
supersede: SUPERSEDE_NOTE,
|
|
284
328
|
},
|
|
285
329
|
mcp: {
|
|
286
330
|
title: '本机 MCP 服务器的当前状态',
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
331
|
+
// how 只剩"怎么用备注"这半句:前半个分句「工具名是 `mcp__<服务器>__<工具>`」删掉了
|
|
332
|
+
// —— 模型自己的工具表里就是这个命名(`mcp__context7__xxx`),告诉它格式是零信息量。
|
|
333
|
+
how: '带「用户提示:」的行是用户写给这台服务器的决策提示,选服务器之前先看一眼。',
|
|
334
|
+
supersede: SUPERSEDE_NOTE,
|
|
290
335
|
},
|
|
291
336
|
skills: {
|
|
292
337
|
title: '本机技能目录',
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
338
|
+
// 两句都只在预设没挂官方 `skill` 工具时出现,差别只在点名不点名那个取正文的工具
|
|
339
|
+
// (工具被用户在兼容页关掉时不点名 —— 点名一个模型手里没有的工具只会让它去猜名字)。
|
|
340
|
+
how: (toolHidden) => toolHidden('skill_manager_read')
|
|
341
|
+
? '本预设没有官方 `skill` 工具:目录只有摘要,读完再照做。'
|
|
342
|
+
: '本预设没有官方 `skill` 工具:要正文用 `skill_manager_read`(按名字直接给正文与路径);目录只有摘要,读完再照做。',
|
|
343
|
+
supersede: `${SUPERSEDE_NOTE.slice(0, -1)};只列当前可调用的技能。`,
|
|
296
344
|
},
|
|
297
345
|
subagents: {
|
|
298
346
|
title: '可委派的子智能体',
|
|
299
|
-
cue: '在决定自己做还是委派之前,先在这里选人设。',
|
|
300
347
|
// 分界规则(2026-09-17 方案 C,本机实测 session-ee722e23 逼出来的):官方那两个
|
|
301
348
|
// 委派工具(`subagent` / `subagent_fork`)不带人设,而此前没有任何一句话说明何时该
|
|
302
349
|
// 用谁 —— 模型在"审查刚读过的 README"时选了 `subagent_fork`(fork 能继承已读内容、
|
|
303
350
|
// 省一次复述)。现在人设通道也有 `inherit`(同一套 fork 机制),分界只剩"要不要后台跑"。
|
|
304
|
-
|
|
305
|
-
|
|
351
|
+
// 带人设的委派工具被关掉时改说"本会话没有这条通道":不说的话,模型看到有人设目录却
|
|
352
|
+
// 找不到对应的工具,会去拿官方那两个凑(它们不接受人设,等于白跑一趟)。
|
|
353
|
+
how: (toolHidden) => toolHidden('subagent_manager_run')
|
|
354
|
+
? '本会话没有带人设的委派工具;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。'
|
|
355
|
+
: '贴合人设的任务一律用 `subagent_manager_run`(要它看到本次会话就开 `inherit`);官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。',
|
|
356
|
+
supersede: SUPERSEDE_NOTE,
|
|
306
357
|
},
|
|
307
358
|
prompt: {
|
|
308
359
|
title: '本机提示词',
|
|
309
|
-
|
|
310
|
-
supersede: '本份提示词取代本次会话中更早注入的同类提示词。',
|
|
360
|
+
supersede: SUPERSEDE_NOTE,
|
|
311
361
|
},
|
|
312
362
|
};
|
|
313
363
|
/** 域声明里没登记的 key(理论上到不了这里):给一个不出错的通用框架。 */
|
|
314
364
|
const fallbackFrame = (label) => ({
|
|
315
365
|
title: `本机的${label}`,
|
|
316
|
-
|
|
317
|
-
supersede: '本份内容取代本次会话中更早注入的同类内容。',
|
|
366
|
+
supersede: SUPERSEDE_NOTE,
|
|
318
367
|
});
|
|
319
368
|
const domainFrame = (key, label) => DOMAIN_FRAME[key] ?? fallbackFrame(label);
|
|
320
369
|
/**
|
|
@@ -357,21 +406,35 @@ export function escapeFrameBody(body) {
|
|
|
357
406
|
* 同时把**正文也包进标记里**(此前只有引导语在标记内、正文裸奔)—— 官方两条注入行都是
|
|
358
407
|
* 整条包住的,正文里的用户自由文本更需要这层来源标记。
|
|
359
408
|
*/
|
|
360
|
-
export function renderDomainText(section) {
|
|
409
|
+
export function renderDomainText(section, toolHidden = () => false) {
|
|
361
410
|
const frame = domainFrame(section.key, section.label);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
411
|
+
// 层级(2026-09-23 用户看到实际注入后指出「记忆内的场景怎么都是 ## 标题」):**`#` 一级给板块**
|
|
412
|
+
// (场景 / 记忆 / MCP / 技能 / 子智能体 / 提示词),域正文里的 `##` 才是它的下一层
|
|
413
|
+
// (记忆段的 `## 场景:X`、超预算时的 `## 未注入的参考信息`)。此前标题也是 `##`,两者平级,
|
|
414
|
+
// 模型读不出主次 —— 而"哪些内容归在哪个板块/场景下"正是它做判断时要用的结构。
|
|
415
|
+
//
|
|
416
|
+
// 开标签后**必须空一行**:markdown 里 `#` 紧跟在一行文字后面只是**段落续行**,不会被渲染成
|
|
417
|
+
// 标题 —— 用户截图里 `## 本机当前的场景` 就是这么被吞掉的(和 `<system-reminder>` 挤成一段)。
|
|
418
|
+
// 收尾同理:正文末尾先归一成单个空行,免得 `</system-reminder>` 粘在最后一行上。
|
|
419
|
+
// 结构:标题 → 补充说明(有才发)→ 权威声明 → 正文。没有 cue 那一行(见 `DomainFrame` 的注释)。
|
|
420
|
+
const lines = [FRAME_OPEN, '', `# ${frame.title}`];
|
|
421
|
+
const how = typeof frame.how === 'function' ? frame.how(toolHidden) : frame.how;
|
|
422
|
+
if (how !== undefined)
|
|
423
|
+
lines.push(how);
|
|
424
|
+
lines.push(frame.supersede, '', escapeFrameBody(section.text).replace(/\n+$/, ''), '', FRAME_CLOSE);
|
|
366
425
|
return lines.join('\n');
|
|
367
426
|
}
|
|
368
427
|
/** 「已清空」通知正文:某个域曾经注入过、现在没有内容时发一条(纯函数,测试用)。 */
|
|
369
428
|
export function clearedDomainText(key, label) {
|
|
370
429
|
const frame = domainFrame(key, label);
|
|
430
|
+
// 形状与 `renderDomainText` 一致(开标签后空行、板块 `#`、收尾空行)—— 这两条都是同一个
|
|
431
|
+
// 通道发出去的消息,层级与留白不该有两套。
|
|
371
432
|
return [
|
|
372
433
|
FRAME_OPEN,
|
|
373
|
-
|
|
434
|
+
'',
|
|
435
|
+
`# ${frame.title}`,
|
|
374
436
|
'**已清空** —— 本次会话中此前注入的同类内容不再有效。',
|
|
437
|
+
'',
|
|
375
438
|
FRAME_CLOSE,
|
|
376
439
|
].join('\n');
|
|
377
440
|
}
|
|
@@ -523,12 +586,22 @@ export function createContextInjector(deps) {
|
|
|
523
586
|
// 最近活跃的会话(WeakRef:诊断用,不阻止会话被回收)。每次 pre-step 都刷新 ——
|
|
524
587
|
// 包括"空 turn 提前返回"和"这一步没有内容可发"的分支,页面才能如实说"没投过"。
|
|
525
588
|
let lastAgent = null;
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
589
|
+
const emptyLedger = () => {
|
|
590
|
+
const adoption = {};
|
|
591
|
+
for (const key of INJECT_DOMAIN_KEYS)
|
|
592
|
+
adoption[key] = { injected: 0, used: 0, adopted: 0, lastUsedAt: null };
|
|
593
|
+
return { delivered: { count: 0, lastAt: null, byDomain: {} }, adoption, observed: { toolCalls: 0, lastAt: null } };
|
|
594
|
+
};
|
|
595
|
+
const ledgers = new WeakMap();
|
|
596
|
+
/** 取(或新建)这个会话的账本。pre-step 与工具调用两条路都从这里过。 */
|
|
597
|
+
const ledgerFor = (agent) => {
|
|
598
|
+
let ledger = ledgers.get(agent);
|
|
599
|
+
if (ledger === undefined) {
|
|
600
|
+
ledger = emptyLedger();
|
|
601
|
+
ledgers.set(agent, ledger);
|
|
602
|
+
}
|
|
603
|
+
return ledger;
|
|
604
|
+
};
|
|
532
605
|
// 每个 agent 最近一步"在上下文里"的域集合 —— 采纳判定要回答的是"调用发生时正文在不在眼前"。
|
|
533
606
|
// WeakMap:不阻止会话被回收(与 lastAgent 同一考虑)。
|
|
534
607
|
const liveDomainsByAgent = new WeakMap();
|
|
@@ -544,6 +617,8 @@ export function createContextInjector(deps) {
|
|
|
544
617
|
catch {
|
|
545
618
|
agent = undefined;
|
|
546
619
|
}
|
|
620
|
+
// 面板读的是**这一段对话**的账本;会话已经被回收了就没有账本可读,全零 + `hasAgent:false`。
|
|
621
|
+
const ledger = agent === undefined ? emptyLedger() : ledgerFor(agent);
|
|
547
622
|
const visible = agent === undefined
|
|
548
623
|
? new Map()
|
|
549
624
|
: newestDomainTexts(agent, LIVE_KINDS);
|
|
@@ -567,13 +642,29 @@ export function createContextInjector(deps) {
|
|
|
567
642
|
state,
|
|
568
643
|
bytes: bytesOf(text),
|
|
569
644
|
text,
|
|
570
|
-
adoption: { ...adoption[key] },
|
|
645
|
+
adoption: { ...ledger.adoption[key] },
|
|
571
646
|
};
|
|
572
647
|
});
|
|
648
|
+
// 成本汇总:只做算术,不碰投递语义(这条视图的存在不改任何发送文本,因此不会触发重发)。
|
|
649
|
+
let liveBytes = 0;
|
|
650
|
+
let injectedBytes = 0;
|
|
651
|
+
let topDomain = null;
|
|
652
|
+
let mostDelivered = null;
|
|
653
|
+
for (const row of rows) {
|
|
654
|
+
liveBytes += row.bytes;
|
|
655
|
+
const sends = ledger.delivered.byDomain[row.key] ?? 0;
|
|
656
|
+
const spent = row.bytes * sends;
|
|
657
|
+
injectedBytes += spent;
|
|
658
|
+
if (spent > 0 && (topDomain === null || spent > topDomain.bytes))
|
|
659
|
+
topDomain = { key: row.key, label: row.label, bytes: spent };
|
|
660
|
+
if (sends > 0 && (mostDelivered === null || sends > mostDelivered.count))
|
|
661
|
+
mostDelivered = { key: row.key, label: row.label, count: sends };
|
|
662
|
+
}
|
|
573
663
|
return {
|
|
574
664
|
hasAgent: agent !== undefined,
|
|
575
|
-
delivered: { count: delivered.count, lastAt: delivered.lastAt, byDomain: { ...delivered.byDomain } },
|
|
576
|
-
observed: { toolCalls: observed.toolCalls, lastAt: observed.lastAt },
|
|
665
|
+
delivered: { count: ledger.delivered.count, lastAt: ledger.delivered.lastAt, byDomain: { ...ledger.delivered.byDomain } },
|
|
666
|
+
observed: { toolCalls: ledger.observed.toolCalls, lastAt: ledger.observed.lastAt },
|
|
667
|
+
cost: { liveBytes, injectedBytes, topDomain, mostDelivered },
|
|
577
668
|
domains: rows,
|
|
578
669
|
};
|
|
579
670
|
};
|
|
@@ -583,17 +674,30 @@ export function createContextInjector(deps) {
|
|
|
583
674
|
if (key === undefined)
|
|
584
675
|
return;
|
|
585
676
|
const at = Date.now();
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
677
|
+
// 记在**发起这次调用的那个会话**的账上。`exec.agent` 与 pre-step 的 `payload.agent`
|
|
678
|
+
// 是同一个对象(`dsh-scope` 的不变量要求),所以这里
|
|
679
|
+
// 落账的会话与上面 `liveDomainsByAgent` 记现场的会话必然一致。
|
|
680
|
+
let owner = typeof agent === 'object' && agent !== null ? agent : undefined;
|
|
681
|
+
if (owner === undefined) {
|
|
682
|
+
try {
|
|
683
|
+
owner = lastAgent ? lastAgent.deref() : undefined;
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
owner = undefined;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (owner === undefined)
|
|
690
|
+
return;
|
|
691
|
+
const ledger = ledgerFor(owner);
|
|
692
|
+
ledger.observed.toolCalls += 1;
|
|
693
|
+
ledger.observed.lastAt = at;
|
|
694
|
+
const row = ledger.adoption[key];
|
|
589
695
|
row.used += 1;
|
|
590
696
|
row.lastUsedAt = at;
|
|
591
697
|
// 采纳判定:调用发生时该域正文正在这个会话的上下文里。
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
row.adopted += 1;
|
|
596
|
-
}
|
|
698
|
+
const liveKeys = liveDomainsByAgent.get(owner);
|
|
699
|
+
if (liveKeys !== undefined && liveKeys.has(key))
|
|
700
|
+
row.adopted += 1;
|
|
597
701
|
}
|
|
598
702
|
catch { /* 遥测绝不能影响工具本身 */ }
|
|
599
703
|
};
|
|
@@ -632,6 +736,9 @@ export function createContextInjector(deps) {
|
|
|
632
736
|
if (!agent)
|
|
633
737
|
return decision;
|
|
634
738
|
if (typeof agent === 'object') {
|
|
739
|
+
// 账本先建好:下面任何一条提前返回(空 turn / 没内容可发)都要能在实况里读到
|
|
740
|
+
// "这一段对话投了 0 次",而不是读到上一段对话的数字。
|
|
741
|
+
ledgerFor(agent);
|
|
635
742
|
try {
|
|
636
743
|
lastAgent = new WeakRef(agent);
|
|
637
744
|
}
|
|
@@ -687,9 +794,12 @@ export function createContextInjector(deps) {
|
|
|
687
794
|
const published = new Set();
|
|
688
795
|
// 域声明按 key 索引:来源文件(`files`)只在真要发消息时取,所以要能从这里回查声明。
|
|
689
796
|
const domainOf = new Map(domains.map((domain) => [domain.key, domain]));
|
|
797
|
+
// 工具表开关本步读一次(同步快照):`how` 行里点名工具的那几句据此换话术。
|
|
798
|
+
const hiddenNow = deps.hiddenTools !== undefined ? deps.hiddenTools() : null;
|
|
799
|
+
const toolHidden = hiddenNow === null ? () => false : (name) => hiddenNow.has(name);
|
|
690
800
|
for (const section of sections) {
|
|
691
801
|
published.add(section.key);
|
|
692
|
-
const text = renderDomainText(section);
|
|
802
|
+
const text = renderDomainText(section, toolHidden);
|
|
693
803
|
const current = visible.get(section.key);
|
|
694
804
|
if (current !== undefined && current.text === text)
|
|
695
805
|
continue;
|
|
@@ -708,6 +818,7 @@ export function createContextInjector(deps) {
|
|
|
708
818
|
}
|
|
709
819
|
// 曾经注入过、这一轮没有内容的域 → 一条「已清空」;从没注入过的域什么都不用说。
|
|
710
820
|
const labelOf = new Map(domains.map((domain) => [domain.key, domain.label]));
|
|
821
|
+
const clearedKeys = new Set();
|
|
711
822
|
for (const key of INJECT_DOMAIN_KEYS) {
|
|
712
823
|
if (published.has(key))
|
|
713
824
|
continue;
|
|
@@ -719,17 +830,29 @@ export function createContextInjector(deps) {
|
|
|
719
830
|
continue;
|
|
720
831
|
additions.push(clearedMessage(key, label));
|
|
721
832
|
appended.push(key);
|
|
833
|
+
clearedKeys.add(key);
|
|
722
834
|
}
|
|
723
835
|
// 采纳判定要用的现场:**含官方载体**,并并入本步刚投出去的域正文(它们就在这一步的
|
|
724
836
|
// 请求里,模型当场看得到)。判断"发不发"只能用本插件自己的 kind —— 官方正文绝不能
|
|
725
837
|
// 影响去重(见 PLUGIN_KINDS 的注释);判断"模型眼前有没有这份内容"则必须连官方那份
|
|
726
838
|
// 一起算,标准类预设下技能与提示词正是官方在送。两遍扫描,两种口径各自正确。
|
|
839
|
+
// **「已清空」通知要从现场里剔掉**:那条消息也带域标记、也占 `newestDomainTexts` 的键,
|
|
840
|
+
// 但它不是正文。连着它一起算,"用户早把这个域关了、模型凭工具描述去调"的那次调用会被
|
|
841
|
+
// 记成采纳 —— 于是 `adopted` 实际在说"这个域本会话说过话",界面文案里那句"正文正在上下文
|
|
842
|
+
// 里"就成了假话。剔除之后 `used - adopted` 才第一次有意义:调了、但正文不在眼前。
|
|
843
|
+
// `clearedKeys` 那一步是同一个判据的另一半:通知这会儿还没落到会话表面上,光靠扫表面
|
|
844
|
+
// 会漏掉"就是这一步刚被清空"的那个域。
|
|
727
845
|
// 位置在"提前返回"之前:这一步没东西可发时,现场依然是当前状态,同样要记。
|
|
728
846
|
try {
|
|
729
847
|
if (typeof agent === 'object') {
|
|
730
|
-
const liveKeys = new Set(
|
|
848
|
+
const liveKeys = new Set();
|
|
849
|
+
for (const [key, entry] of newestDomainTexts(agent, LIVE_KINDS))
|
|
850
|
+
if (entry.form !== 'notice')
|
|
851
|
+
liveKeys.add(key);
|
|
731
852
|
for (const key of injectedKeys)
|
|
732
853
|
liveKeys.add(key);
|
|
854
|
+
for (const key of clearedKeys)
|
|
855
|
+
liveKeys.delete(key);
|
|
733
856
|
liveDomainsByAgent.set(agent, liveKeys);
|
|
734
857
|
}
|
|
735
858
|
}
|
|
@@ -738,12 +861,13 @@ export function createContextInjector(deps) {
|
|
|
738
861
|
// 没有新增时,只有"拦下了官方消息"才需要返回改动后的 decision;否则原样返回。
|
|
739
862
|
return messages === messagesOf(decision) ? decision : { ...decision, messages: [...messages] };
|
|
740
863
|
}
|
|
741
|
-
|
|
742
|
-
delivered.
|
|
864
|
+
const ledger = ledgerFor(agent);
|
|
865
|
+
ledger.delivered.count += additions.length;
|
|
866
|
+
ledger.delivered.lastAt = Date.now();
|
|
743
867
|
for (const key of appended)
|
|
744
|
-
delivered.byDomain[key] = (delivered.byDomain[key] || 0) + 1;
|
|
868
|
+
ledger.delivered.byDomain[key] = (ledger.delivered.byDomain[key] || 0) + 1;
|
|
745
869
|
for (const key of injectedKeys)
|
|
746
|
-
adoption[key].injected += 1;
|
|
870
|
+
ledger.adoption[key].injected += 1;
|
|
747
871
|
return { ...decision, messages: [...messages, ...additions] };
|
|
748
872
|
}
|
|
749
873
|
catch (error) {
|