dsh-plugin-prompt-tool 0.7.2 → 0.8.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/README.md +185 -46
- package/engine/THIRD_PARTY_LICENSES +25 -0
- package/engine/actions.mjs +931 -0
- package/engine/anchor-match.mjs +6 -3
- package/engine/character-tools.mjs +11 -0
- package/engine/classify-task.mjs +18 -0
- package/engine/compaction-epoch.mjs +41 -20
- package/engine/compositions/library/agent-instructions.yml +9 -0
- package/engine/compositions/library/command-goal.yml +2 -6
- package/engine/compositions/library/compaction.yml +2 -32
- package/engine/compositions/library/delegation-ptc.yml +58 -0
- package/engine/compositions/library/delegation.yml +5 -38
- package/engine/compositions/library/persistent-shell.yml +4 -13
- package/engine/compositions/library/planning.yml +8 -23
- package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/skill-filesystem.yml +2 -8
- package/engine/compositions/library/tool-ask-user.yml +2 -3
- package/engine/compositions/library/tool-bash.yml +4 -12
- package/engine/compositions/library/tool-cordis.yml +7 -0
- package/engine/compositions/library/tool-fs-search.yml +2 -11
- package/engine/compositions/library/tool-fs.yml +2 -5
- package/engine/compositions/library/tool-goal.yml +2 -6
- package/engine/compositions/library/tool-jobs.yml +2 -18
- package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
- package/engine/compositions/library/tool-plugin-manager.yml +8 -0
- package/engine/compositions/library/tool-present.yml +7 -0
- package/engine/compositions/library/tool-presentation.yml +9 -0
- package/engine/compositions/library/tool-pwsh.yml +2 -6
- package/engine/compositions/library/tool-skill.yml +7 -0
- package/engine/compositions/library/tool-todo.yml +2 -4
- package/engine/compositions/library/tool-web.yml +2 -1
- package/engine/compositions/source/local/character-tools.yml +5 -0
- package/engine/compositions/source/local/declared-triggers.yml +12 -0
- package/engine/compositions/source/local/filesystem-editor.yml +30 -0
- package/engine/compositions/source/local/instruction-hint.yml +15 -0
- package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
- package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
- package/engine/compositions/source/local/run-code-env.yml +4 -7
- package/engine/compositions/source/local/session-var-tools.yml +5 -0
- package/engine/compositions/source/local/skill-search.yml +3 -9
- package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
- package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
- package/engine/compositions/source/local/tool-config-engine.yml +8 -0
- package/engine/compositions/source/local/tool-git-bash.yml +15 -0
- package/engine/compositions/source/local/world-book-tools.yml +5 -0
- package/engine/condition.mjs +83 -0
- package/engine/declared-triggers.mjs +93 -0
- package/engine/executor.mjs +364 -185
- package/engine/fields.mjs +219 -0
- package/engine/fillers.mjs +12 -141
- package/engine/host-package.mjs +51 -0
- package/engine/instruction-hint.mjs +285 -0
- package/engine/interpolate.mjs +157 -29
- package/engine/layers.mjs +495 -75
- package/engine/predicates.mjs +753 -0
- package/engine/prompt-config-engine.mjs +63 -8
- package/engine/run-code-env.mjs +48 -31
- package/engine/schema.mjs +444 -103
- package/engine/sdk-strip.mjs +365 -0
- package/engine/session-var-tools.mjs +11 -0
- package/engine/session-vars.mjs +8 -6
- package/engine/shared.mjs +137 -0
- package/engine/skill-search.mjs +34 -21
- package/engine/st-macros.d.mts +22 -0
- package/engine/st-macros.mjs +208 -0
- package/engine/st-render.mjs +105 -0
- package/engine/st-world-book.d.mts +10 -0
- package/engine/st-world-book.mjs +259 -0
- package/engine/strategies.mjs +31 -20
- package/engine/subagent-tool-policy-core.mjs +400 -0
- package/engine/subagent-tool-policy.mjs +327 -0
- package/engine/tool-config-engine.mjs +207 -145
- package/engine/tool-definition.mjs +143 -0
- package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
- package/engine/trigger-editor-meta.mjs +48 -0
- package/engine/trigger-spec.mjs +257 -0
- package/engine/trigger.mjs +304 -0
- package/engine/world-book-tools.mjs +11 -0
- package/lib/client.js +16495 -5456
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +628 -382
- package/lib/index.mjs +5391 -2942
- package/lib/paths-CY5psBqE.d.mts +1233 -0
- package/lib/preset-package-BnJqrU4R.mjs +16792 -0
- package/lib/preset-transfer.d.mts +2 -0
- package/lib/preset-transfer.mjs +2 -0
- package/package.json +91 -49
- package/preset/pt-cordis/preset.yml +43 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
- package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
- package/preset/pt-custom/preset.yml +15 -0
- package/preset/pt-minimal/preset.yml +27 -0
- package/preset/pt-ptc/preset.yml +45 -0
- package/preset/pt-standard/preset.yml +43 -0
- package/templates/14-first-turn-anchor.yml +4 -4
- package/templates/15-guide-auto.yml +3 -3
- package/templates/16-custom-fallback.yml +1 -0
- package/templates/18-placeholder-env-facts.yml +3 -3
- package/templates/20-system-section.yml +3 -2
- package/templates/65-turn-stop.yml +18 -0
- package/templates/66-subagent-start.yml +14 -0
- package/templates/75-subagent-end.yml +15 -0
- package/templates/tools/10-shell-command.yml +1 -1
- package/templates/tools/40-delegate-character.yml +1 -1
- package/templates/tools/50-delegate-worldbook.yml +1 -1
- package/templates/tools/60-delegate-session-var.yml +1 -1
- package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
- package/engine/anchor-turn.mjs +0 -68
- package/engine/code-presentation.mjs +0 -103
- package/engine/compositions/library/anchor-turn.yml +0 -10
- package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
- package/engine/compositions/library/code-presentation.yml +0 -8
- package/engine/compositions/library/context-gate.yml +0 -39
- package/engine/compositions/library/cot-drip.yml +0 -11
- package/engine/compositions/library/custom-bash.yml +0 -14
- package/engine/compositions/library/deliberation-gate.yml +0 -11
- package/engine/compositions/library/official-agent-instructions.yml +0 -17
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
- package/engine/compositions/library/official-tool-bash.yml +0 -6
- package/engine/compositions/library/official-tool-cordis.yml +0 -13
- package/engine/compositions/library/official-tool-presentation.yml +0 -7
- package/engine/compositions/library/official-tool-skill.yml +0 -14
- package/engine/compositions/library/persona.yml +0 -13
- package/engine/compositions/library/prompt-config-engine.yml +0 -16
- package/engine/compositions/library/run-code-env.yml +0 -15
- package/engine/compositions/library/skill-search.yml +0 -14
- package/engine/compositions/library/str-replace-editor.yml +0 -10
- package/engine/compositions/library/tool-bootstrap.yml +0 -14
- package/engine/compositions/library/tool-config-engine.yml +0 -9
- package/engine/compositions/library/tool-filter.yml +0 -8
- package/engine/compositions/source/local/code-presentation.yml +0 -8
- package/engine/compositions/source/local/context-gate.yml +0 -36
- package/engine/compositions/source/local/custom-bash.yml +0 -11
- package/engine/compositions/source/local/str-replace-editor.yml +0 -10
- package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
- package/engine/compositions/source/local/tool-filter.yml +0 -8
- package/engine/context-gate.mjs +0 -305
- package/engine/cot-drip.mjs +0 -137
- package/engine/deliberation-gate.mjs +0 -127
- package/engine/tool-bootstrap.mjs +0 -476
- package/engine/tool-filter.mjs +0 -80
- package/lib/preset-core.d.mts +0 -10
- package/lib/preset-core.mjs +0 -77
- package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
- package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
- package/preset/anchored/preset.yml +0 -296
- package/preset/creative/preset.yml +0 -67
- package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
- package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
- package/preset/custom/preset.yml +0 -12
- package/preset/liangshen/preset.yml +0 -75
- package/preset/minimal/preset.yml +0 -45
- package/preset/ptc/preset.yml +0 -58
- package/preset/standard/preset.yml +0 -57
- package/skills/manifest.json +0 -7
- package/skills/sandboxmod/SKILL.md +0 -49
- package/skills/web ui/SKILL.md +0 -42
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk-strip — PTC SDK 声明文本的保守裁剪(B3 T2 第 (6) 类动作的实现层)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:官方 `dsh-tools` 在 `mode: ptc` 下把可见工具投影成 `tools:sdk`
|
|
5
|
+
* 段(`renderToolsSdk` / `renderToolsSdkPy`),而 `run_code` 程序里的绑定建自
|
|
6
|
+
* `registry.schemas(exec.agent)`(安装包 `dsh-tools/lib/types/ptc.js:576`)。
|
|
7
|
+
* 因此「把工具从装配目录里剔掉」既不改 SDK 正文、也不改绑定——正文里仍写着被剔
|
|
8
|
+
* 工具的形态。本模块是**剩余呈现补救**:只对官方已产出的正文做删行,不生成、
|
|
9
|
+
* 不新增任何声明,任何不确定一律原样返回(保守失败)。
|
|
10
|
+
*
|
|
11
|
+
* 只有两种官方形态被识别(其余一律原样返回):
|
|
12
|
+
* - TypeScript:`interface ToolArgsMap {…}` 与 `interface ToolOutputMap {…}` 的
|
|
13
|
+
* 成员(成员可跨行;键用 `JSON.stringify` 转义,见 `ts-types.js` 的 `renderKey`);
|
|
14
|
+
* - Python:`class Tools(Protocol):` 的成员(`async def <name>(self, args: …)` 或
|
|
15
|
+
* `# tools["<name>"](args: …)` 下标注释)以及只被被剔工具引用的 `TypedDict` 类。
|
|
16
|
+
*
|
|
17
|
+
* 裁剪后必须自校验:解析回读、成员集合等于「原集合 − 名单」、保留成员逐字节未变、
|
|
18
|
+
* 整体行多重集只减不增。任一不成立 → 返回原文。
|
|
19
|
+
* 自校验用的是**本模块的再解析**(engine 自包含,不 import `@deepseek-ai/dsh-tools`
|
|
20
|
+
* 的渲染器):它证明「只删不增 + 保留部分逐字节不变 + 集合正确」,**不**证明结果
|
|
21
|
+
* 等于「官方对裁剪后名单的重新渲染」(剩余的空行/未用 import 属可接受的残留)。
|
|
22
|
+
*
|
|
23
|
+
* 已知上限(保守失败,不是可用性缺口):
|
|
24
|
+
* - 只删到 `class Tools(Protocol):` 正文一条语句都不剩时**放弃裁剪**——Python 的
|
|
25
|
+
* 纯注释类体不是合法语法,而补 `pass` 属于"新增文本",与"只删不增"冲突。
|
|
26
|
+
* 真要「一个工具都不给」,正确层是名单/guard(执行边界),不是文本裁剪。
|
|
27
|
+
* - 不做变量引用改写、不处理未识别的自定义渲染器(官方只有 ts/python 两个)。
|
|
28
|
+
*
|
|
29
|
+
* @module engine/sdk-strip
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** 官方 PTC SDK 段名(`dsh-tools/lib/index.js` 的 `sdkSection().name`)。 */
|
|
33
|
+
export const SDK_SECTION_NAME = 'tools:sdk'
|
|
34
|
+
|
|
35
|
+
/** 官方 TS 投影的两个成员表头(`ts-types.js:279-280`)。 */
|
|
36
|
+
const TS_MAPS = ['interface ToolArgsMap {', 'interface ToolOutputMap {']
|
|
37
|
+
/** TS 成员行:`<2 空格><裸键 | JSON 字符串键>: `。 */
|
|
38
|
+
const TS_MEMBER_KEY = /^ {2}(?:"((?:[^"\\]|\\.)*)"|([A-Za-z_$][A-Za-z0-9_$]*)): /
|
|
39
|
+
/** TS 成员的单行 JSDoc(`docLines(description, 1)`)。 */
|
|
40
|
+
const TS_DOC = /^ {2}\/\*\* .* \*\/$/
|
|
41
|
+
/** Python 协议头与尾。 */
|
|
42
|
+
const PY_PROTOCOL_HEADER = 'class Tools(Protocol):'
|
|
43
|
+
const PY_FOOTER = 'tools: Tools'
|
|
44
|
+
/** Python 成员:具名方法或下标注释(`py-types.js:753-771`)。 */
|
|
45
|
+
const PY_MEMBER_DEF = /^ {4}async def ([^ (]+)\(self, args: /
|
|
46
|
+
const PY_MEMBER_SUBSCRIPT = /^ {4}# tools\[("(?:[^"\\]|\\.)*")\]\(args: /
|
|
47
|
+
/** Python 类声明(TypedDict / 其它基类;`ToolCallError` 永不被删)。 */
|
|
48
|
+
const PY_CLASS_HEADER = /^class ([A-Za-z_][A-Za-z0-9_]*)\(([A-Za-z_][A-Za-z0-9_]*)\):$/
|
|
49
|
+
/** 永不可删的类名(错误类型由 `from typing` 之外的固定文本声明)。 */
|
|
50
|
+
const PY_PINNED_CLASSES = new Set(['ToolCallError'])
|
|
51
|
+
|
|
52
|
+
/** 把名单归一为 Set;非法的非集合输入视为空名单(调用方已 fail loud)。 */
|
|
53
|
+
function nameSetOf(names) {
|
|
54
|
+
if (names instanceof Set) return names
|
|
55
|
+
if (Array.isArray(names)) return new Set(names.filter((item) => typeof item === 'string' && item.length > 0))
|
|
56
|
+
return new Set()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 字符串感知的花括号增量:字符串字面量里的 `{}` 不参与计数
|
|
61
|
+
* (`const`/`enum` 的标量由 `JSON.stringify` 渲染,可以含任意花括号与引号)。
|
|
62
|
+
*/
|
|
63
|
+
function braceDelta(line) {
|
|
64
|
+
let depth = 0
|
|
65
|
+
let quote = ''
|
|
66
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
67
|
+
const char = line[index]
|
|
68
|
+
if (quote !== '') {
|
|
69
|
+
if (char === '\\') index += 1
|
|
70
|
+
else if (char === quote) quote = ''
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (char === '"' || char === "'" || char === '`') quote = char
|
|
74
|
+
else if (char === '{') depth += 1
|
|
75
|
+
else if (char === '}') depth -= 1
|
|
76
|
+
}
|
|
77
|
+
return depth
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** JSON 字符串键的还原(`renderKey` 用的是 `JSON.stringify`)。 */
|
|
81
|
+
function decodeQuotedKey(body) {
|
|
82
|
+
try {
|
|
83
|
+
const decoded = JSON.parse(`"${body}"`)
|
|
84
|
+
return typeof decoded === 'string' ? decoded : undefined
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 定位 `header … 空行/`}` 块;识别不出返回 undefined(保守失败)。 */
|
|
91
|
+
function findBraceBlock(lines, header) {
|
|
92
|
+
const start = lines.indexOf(header)
|
|
93
|
+
if (start < 0) return undefined
|
|
94
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
95
|
+
if (lines[index] === '}') return { start, end: index }
|
|
96
|
+
}
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 解析一个 TS 成员表的成员跨度;形态不认识返回 undefined。 */
|
|
101
|
+
function parseTsMembers(lines, block) {
|
|
102
|
+
const members = []
|
|
103
|
+
let index = block.start + 1
|
|
104
|
+
while (index < block.end) {
|
|
105
|
+
const docStart = TS_DOC.test(lines[index]) ? index : -1
|
|
106
|
+
const keyLine = docStart >= 0 ? index + 1 : index
|
|
107
|
+
if (keyLine >= block.end) return undefined
|
|
108
|
+
const match = TS_MEMBER_KEY.exec(lines[keyLine])
|
|
109
|
+
if (match === null) return undefined
|
|
110
|
+
const key = match[1] !== undefined ? decodeQuotedKey(match[1]) : match[2]
|
|
111
|
+
if (typeof key !== 'string' || key.length === 0) return undefined
|
|
112
|
+
let end = -1
|
|
113
|
+
let depth = 0
|
|
114
|
+
for (let cursor = keyLine; cursor < block.end; cursor += 1) {
|
|
115
|
+
depth += braceDelta(lines[cursor])
|
|
116
|
+
if (depth < 0) return undefined
|
|
117
|
+
if (depth === 0 && lines[cursor].endsWith(';')) { end = cursor; break }
|
|
118
|
+
}
|
|
119
|
+
if (end < 0) return undefined
|
|
120
|
+
members.push({ start: docStart >= 0 ? docStart : keyLine, end, key })
|
|
121
|
+
index = end + 1
|
|
122
|
+
}
|
|
123
|
+
return members
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 行多重集包含:`after` 的每一行都能在 `before` 里找到未用尽的同款(= 只删不增)。 */
|
|
127
|
+
function linesOnlyRemoved(before, after) {
|
|
128
|
+
// ponytail: O(行长) 计数表,够用;段文本是单次 assembly 的投影,不做字典序指纹。
|
|
129
|
+
const pool = new Map()
|
|
130
|
+
for (const line of before) pool.set(line, (pool.get(line) ?? 0) + 1)
|
|
131
|
+
for (const line of after) {
|
|
132
|
+
const left = pool.get(line) ?? 0
|
|
133
|
+
if (left === 0) return false
|
|
134
|
+
pool.set(line, left - 1)
|
|
135
|
+
}
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 保留成员必须逐字节未变:删除是纯行删除,故按"删掉的行数"精确定位回读。 */
|
|
140
|
+
function retainedSpansIntact(before, after, spans, drop) {
|
|
141
|
+
const beforeDrop = (line) => {
|
|
142
|
+
let count = 0
|
|
143
|
+
for (const index of drop) if (index < line) count += 1
|
|
144
|
+
return count
|
|
145
|
+
}
|
|
146
|
+
for (const span of spans) {
|
|
147
|
+
const at = span.start - beforeDrop(span.start)
|
|
148
|
+
const slice = before.slice(span.start, span.end + 1)
|
|
149
|
+
if (after.slice(at, at + slice.length).join('\n') !== slice.join('\n')) return false
|
|
150
|
+
}
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** TS 载荷:删掉两个成员表里命中名单的成员(含其 JSDoc 行)。 */
|
|
155
|
+
function stripTypeScript(lines, names) {
|
|
156
|
+
const blocks = TS_MAPS.map((header) => findBraceBlock(lines, header))
|
|
157
|
+
if (blocks.some((block) => block === undefined)) return undefined
|
|
158
|
+
const parsed = []
|
|
159
|
+
for (const block of blocks) {
|
|
160
|
+
const members = parseTsMembers(lines, block)
|
|
161
|
+
if (members === undefined) return undefined
|
|
162
|
+
parsed.push(members)
|
|
163
|
+
}
|
|
164
|
+
const keysBefore = parsed.map((members) => members.map((member) => member.key))
|
|
165
|
+
const removed = new Set()
|
|
166
|
+
const drop = new Set()
|
|
167
|
+
for (const members of parsed) {
|
|
168
|
+
for (const member of members) {
|
|
169
|
+
if (!names.has(member.key)) continue
|
|
170
|
+
removed.add(member.key)
|
|
171
|
+
for (let line = member.start; line <= member.end; line += 1) drop.add(line)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (removed.size === 0) return undefined
|
|
175
|
+
const after = lines.filter((_line, index) => !drop.has(index))
|
|
176
|
+
if (!linesOnlyRemoved(lines, after)) return undefined
|
|
177
|
+
if (!retainedSpansIntact(lines, after, parsed.flat().filter((member) => !names.has(member.key)), drop)) return undefined
|
|
178
|
+
// 回读自校验:两个表的成员键必须恰好等于「原键集合 − 名单」。
|
|
179
|
+
for (const [position, header] of TS_MAPS.entries()) {
|
|
180
|
+
const block = findBraceBlock(after, header)
|
|
181
|
+
if (block === undefined) return undefined
|
|
182
|
+
const members = parseTsMembers(after, block)
|
|
183
|
+
if (members === undefined) return undefined
|
|
184
|
+
const expected = keysBefore[position].filter((key) => !names.has(key))
|
|
185
|
+
const actual = members.map((member) => member.key)
|
|
186
|
+
if (actual.length !== expected.length) return undefined
|
|
187
|
+
for (const [index, key] of actual.entries()) if (key !== expected[index]) return undefined
|
|
188
|
+
}
|
|
189
|
+
// 名单里但正文中不存在的工具:不是错误(呈现与装配不同源),但不得因此放行整体裁剪。
|
|
190
|
+
return after.join('\n')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Python 协议正文的成员跨度(语句 + 其后附带的 docstring / 描述行)。 */
|
|
194
|
+
function parsePyMembers(lines, from, to) {
|
|
195
|
+
const starts = []
|
|
196
|
+
for (let index = from; index < to; index += 1) {
|
|
197
|
+
const line = lines[index]
|
|
198
|
+
if (PY_MEMBER_DEF.test(line)) {
|
|
199
|
+
starts.push({ line: index, name: PY_MEMBER_DEF.exec(line)[1] })
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const subscript = PY_MEMBER_SUBSCRIPT.exec(line)
|
|
203
|
+
if (subscript !== null) {
|
|
204
|
+
const name = decodeQuotedKey(subscript[1].slice(1, -1))
|
|
205
|
+
if (typeof name !== 'string' || name.length === 0) return undefined
|
|
206
|
+
starts.push({ line: index, name })
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return starts.map((entry, position) => ({
|
|
210
|
+
...entry,
|
|
211
|
+
start: entry.line,
|
|
212
|
+
end: (position + 1 < starts.length ? starts[position + 1].line : to) - 1,
|
|
213
|
+
}))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Python 类块(含 4 空格缩进体)。 */
|
|
217
|
+
function parsePyClasses(lines, from, to) {
|
|
218
|
+
const blocks = []
|
|
219
|
+
for (let index = from; index < to; index += 1) {
|
|
220
|
+
const match = PY_CLASS_HEADER.exec(lines[index])
|
|
221
|
+
if (match === null) continue
|
|
222
|
+
let end = index + 1
|
|
223
|
+
while (end < to && lines[end].startsWith(' ')) end += 1
|
|
224
|
+
blocks.push({ start: index, end: end - 1, name: match[1], base: match[2] })
|
|
225
|
+
index = end - 1
|
|
226
|
+
}
|
|
227
|
+
return blocks
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 文本里的标识符词元(类可达性用)。 */
|
|
231
|
+
function identifiersOf(text, known) {
|
|
232
|
+
const found = new Set()
|
|
233
|
+
for (const token of text.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) if (known.has(token[0])) found.add(token[0])
|
|
234
|
+
return found
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Python 载荷:删协议成员,再删只被被删成员引用的类声明。 */
|
|
238
|
+
function stripPython(lines, names) {
|
|
239
|
+
const header = lines.indexOf(PY_PROTOCOL_HEADER)
|
|
240
|
+
const footer = lines.lastIndexOf(PY_FOOTER)
|
|
241
|
+
if (header < 0 || footer <= header) return undefined
|
|
242
|
+
const classRegionStart = lines.findIndex((line) => /^from typing import /.test(line))
|
|
243
|
+
if (classRegionStart < 0 || classRegionStart >= header) return undefined
|
|
244
|
+
const classes = parsePyClasses(lines, classRegionStart + 1, header)
|
|
245
|
+
if (classes.length === 0) return undefined
|
|
246
|
+
const members = parsePyMembers(lines, header + 1, footer)
|
|
247
|
+
if (members === undefined || members.length === 0) return undefined
|
|
248
|
+
|
|
249
|
+
const drop = new Set()
|
|
250
|
+
const removed = new Set()
|
|
251
|
+
const keptMembers = []
|
|
252
|
+
for (const member of members) {
|
|
253
|
+
if (!names.has(member.name)) { keptMembers.push(member); continue }
|
|
254
|
+
removed.add(member.name)
|
|
255
|
+
for (let line = member.start; line <= member.end; line += 1) drop.add(line)
|
|
256
|
+
}
|
|
257
|
+
if (removed.size === 0) return undefined
|
|
258
|
+
// 删空协议体(只剩注释)不是合法 Python,而补 pass 属于"新增":保守放弃。
|
|
259
|
+
// ponytail: 这是刻意的保守上限,不是缺口——真要「一个工具都不给」应在名单/guard
|
|
260
|
+
// 层表达(执行边界),文本裁剪只做剩余呈现补救;若将来需要裁剪到空,改法是把
|
|
261
|
+
// pruned 名单交回官方渲染器重生全文,而不是在这里补 pass。
|
|
262
|
+
const body = lines.slice(header + 1, footer)
|
|
263
|
+
const hadStatement = body.some((line) => PY_MEMBER_DEF.test(line))
|
|
264
|
+
const keptStatement = body.some((line, offset) => !drop.has(header + 1 + offset) && PY_MEMBER_DEF.test(line))
|
|
265
|
+
if (hadStatement && !keptStatement) return undefined
|
|
266
|
+
|
|
267
|
+
const keptText = keptMembers.map((member) => lines.slice(member.start, member.end + 1).join('\n')).join('\n')
|
|
268
|
+
const known = new Set(classes.map((block) => block.name))
|
|
269
|
+
const reachable = new Set(PY_PINNED_CLASSES)
|
|
270
|
+
const queue = [...identifiersOf(keptText, known)]
|
|
271
|
+
while (queue.length > 0) {
|
|
272
|
+
const name = queue.pop()
|
|
273
|
+
if (reachable.has(name)) continue
|
|
274
|
+
reachable.add(name)
|
|
275
|
+
const block = classes.find((item) => item.name === name)
|
|
276
|
+
if (block === undefined) continue
|
|
277
|
+
for (const token of identifiersOf(lines.slice(block.start, block.end + 1).join('\n'), known)) queue.push(token)
|
|
278
|
+
}
|
|
279
|
+
for (const block of classes) {
|
|
280
|
+
if (reachable.has(block.name)) continue
|
|
281
|
+
for (let line = block.start; line <= block.end; line += 1) drop.add(line)
|
|
282
|
+
// 类块后的一个空行一并删掉,避免留下连续空行(仍是删除,不新增)。
|
|
283
|
+
if (lines[block.end + 1] === '') drop.add(block.end + 1)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const after = lines.filter((_line, index) => !drop.has(index))
|
|
287
|
+
if (!linesOnlyRemoved(lines, after)) return undefined
|
|
288
|
+
// 保留的成员与类必须逐字节未变。
|
|
289
|
+
const keptClassSpans = classes.filter((block) => reachable.has(block.name))
|
|
290
|
+
if (!retainedSpansIntact(lines, after, [...keptMembers, ...keptClassSpans], drop)) return undefined
|
|
291
|
+
// 被剔工具名不得以成员形态残留。
|
|
292
|
+
const tail = after.join('\n')
|
|
293
|
+
for (const name of removed) {
|
|
294
|
+
if (tail.includes(` async def ${name}(self, args: `)) return undefined
|
|
295
|
+
if (tail.includes(` # tools[${JSON.stringify(name)}](args: `)) return undefined
|
|
296
|
+
}
|
|
297
|
+
// 协议头之后只允许出现"被删成员"这一次差异,其余逐字节不变。
|
|
298
|
+
const headerAt = after.indexOf(PY_PROTOCOL_HEADER)
|
|
299
|
+
if (headerAt < 0) return undefined
|
|
300
|
+
const expectedTail = lines.slice(header).filter((_line, offset) => !drop.has(header + offset))
|
|
301
|
+
if (after.slice(headerAt).join('\n') !== expectedTail.join('\n')) return undefined
|
|
302
|
+
// 保留类仍声明、被删类不再声明。
|
|
303
|
+
const declaredAfter = new Set(parsePyClasses(after, 0, headerAt).map((block) => block.name))
|
|
304
|
+
for (const block of keptClassSpans) if (!declaredAfter.has(block.name)) return undefined
|
|
305
|
+
for (const block of classes) if (!reachable.has(block.name) && declaredAfter.has(block.name)) return undefined
|
|
306
|
+
return tail
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 读出一段 SDK 文本声明的全部工具名(allow 名单需要「正文里有哪些工具」才能算差集)。
|
|
311
|
+
*
|
|
312
|
+
* @param text 官方 `tools:sdk` 段的已解析文本。
|
|
313
|
+
* @returns 声明的工具名数组;形态不认识时返回空数组(调用方按"无可裁"处理)。
|
|
314
|
+
*/
|
|
315
|
+
export function sdkToolNames(text) {
|
|
316
|
+
if (typeof text !== 'string' || text.length === 0) return []
|
|
317
|
+
const lines = text.split(text.includes('\r\n') ? '\r\n' : '\n')
|
|
318
|
+
try {
|
|
319
|
+
if (lines.includes(TS_MAPS[0]) || lines.includes(TS_MAPS[1])) {
|
|
320
|
+
const names = new Set()
|
|
321
|
+
for (const header of TS_MAPS) {
|
|
322
|
+
const block = findBraceBlock(lines, header)
|
|
323
|
+
if (block === undefined) return []
|
|
324
|
+
const members = parseTsMembers(lines, block)
|
|
325
|
+
if (members === undefined) return []
|
|
326
|
+
for (const member of members) names.add(member.key)
|
|
327
|
+
}
|
|
328
|
+
return [...names]
|
|
329
|
+
}
|
|
330
|
+
const header = lines.indexOf(PY_PROTOCOL_HEADER)
|
|
331
|
+
const footer = lines.lastIndexOf(PY_FOOTER)
|
|
332
|
+
if (header < 0 || footer <= header) return []
|
|
333
|
+
const members = parsePyMembers(lines, header + 1, footer)
|
|
334
|
+
if (members === undefined) return []
|
|
335
|
+
return [...new Set(members.map((member) => member.name))]
|
|
336
|
+
} catch {
|
|
337
|
+
return []
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 按名单裁剪 SDK 声明文本;只删不增,任何不确定一律返回原文。
|
|
343
|
+
*
|
|
344
|
+
* @param text 官方 `tools:sdk` 段的已解析文本(`assembly.sections[].text`)。
|
|
345
|
+
* @param names 要剔除的工具名集合(Set 或字符串数组)。
|
|
346
|
+
* @returns 裁剪后的文本;空名单、形态不认识、自校验失败一律返回 `text` 原值。
|
|
347
|
+
*/
|
|
348
|
+
export function stripSdkDeclarations(text, names) {
|
|
349
|
+
const strip = nameSetOf(names)
|
|
350
|
+
// 空名单零开销:不切行、不解析、不分配(与 tool-filter 的"两者都空 = 不过滤"同纪律)。
|
|
351
|
+
if (strip.size === 0) return text
|
|
352
|
+
if (typeof text !== 'string' || text.length === 0) return text
|
|
353
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n'
|
|
354
|
+
const lines = text.split(eol)
|
|
355
|
+
const isTs = lines.includes(TS_MAPS[0]) || lines.includes(TS_MAPS[1])
|
|
356
|
+
const isPy = lines.includes(PY_PROTOCOL_HEADER)
|
|
357
|
+
if (isTs === isPy) return text
|
|
358
|
+
try {
|
|
359
|
+
const stripped = isTs ? stripTypeScript(lines, strip) : stripPython(lines, strip)
|
|
360
|
+
return typeof stripped === 'string' ? stripped : text
|
|
361
|
+
} catch {
|
|
362
|
+
// 裁剪不是执行边界:任何解析异常都退回原文,绝不改成"少写一点"的正文。
|
|
363
|
+
return text
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 会话变量工具预设模块。 */
|
|
2
|
+
export const name = "session-var-tools"
|
|
3
|
+
|
|
4
|
+
export function apply(ctx) {
|
|
5
|
+
const service = ctx.get("pt-session-var-tools")
|
|
6
|
+
if (service === null || service === undefined || typeof service.mount !== "function") {
|
|
7
|
+
ctx.logger?.warn(name + ": service unavailable; session-var tools not mounted for this preset")
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
ctx.effect(() => service.mount(ctx), name + ": mount")
|
|
11
|
+
}
|
package/engine/session-vars.mjs
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
/** session 对象上的变量属性键(字符串常量,跨模块实例一致)。 */
|
|
12
12
|
export const SESSION_VARS_KEY = '__pt_session_vars__'
|
|
13
13
|
|
|
14
|
-
function varsOf(session) {
|
|
14
|
+
function varsOf(session, create = false) {
|
|
15
15
|
if (session === null || typeof session !== 'object') return undefined
|
|
16
|
-
let vars = session[SESSION_VARS_KEY]
|
|
16
|
+
let vars = Object.hasOwn(session, SESSION_VARS_KEY) ? session[SESSION_VARS_KEY] : undefined
|
|
17
17
|
if (vars === undefined) {
|
|
18
|
-
|
|
18
|
+
if (!create) return undefined
|
|
19
|
+
vars = Object.create(null)
|
|
19
20
|
session[SESSION_VARS_KEY] = vars
|
|
20
21
|
}
|
|
21
22
|
return vars
|
|
@@ -30,14 +31,15 @@ export function sessionVarsSnapshot(session) {
|
|
|
30
31
|
/** 读取单个会话变量(未设置 → undefined)。 */
|
|
31
32
|
export function getSessionVar(session, key) {
|
|
32
33
|
const vars = varsOf(session)
|
|
33
|
-
return vars === undefined ? undefined : vars[String(key)]
|
|
34
|
+
return vars === undefined || !Object.hasOwn(vars, String(key)) ? undefined : vars[String(key)]
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** 设置会话变量(值转字符串;空值仍记录)。 */
|
|
37
38
|
export function setSessionVar(session, key, value) {
|
|
38
|
-
const vars = varsOf(session)
|
|
39
|
+
const vars = varsOf(session, true)
|
|
39
40
|
if (vars === undefined) return
|
|
40
|
-
|
|
41
|
+
// defineProperty 同时保护仍在运行的旧实例创建的普通对象(__proto__ 不能触发 setter)。
|
|
42
|
+
Object.defineProperty(vars, String(key), { value: String(value ?? ''), writable: true, enumerable: true, configurable: true })
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/** 清除会话变量;key 缺省时清空全部。 */
|
package/engine/shared.mjs
CHANGED
|
@@ -26,6 +26,28 @@ export function booleanOption(pluginName, value, field, fallback) {
|
|
|
26
26
|
return value
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 必填整数配置:缺键 / 非整数 / 越界一律 fail loud。
|
|
31
|
+
* 默认值归模板与预设(组合源 yml 的 config 或本预设 moduleConfigs),引擎不兜底。
|
|
32
|
+
*/
|
|
33
|
+
export function requiredInt(pluginName, value, field, minimum) {
|
|
34
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
35
|
+
throw new TypeError(`${pluginName}: ${field} must be an integer >= ${minimum}`)
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 必填文本配置:缺键 / 非字符串 fail loud(引导措辞归模板与预设,引擎不内置文案);
|
|
42
|
+
* 空串 = 显式留空,返回 undefined 供调用方跳过注册(无正文即无该能力行为)。
|
|
43
|
+
*/
|
|
44
|
+
export function requiredText(pluginName, value, field) {
|
|
45
|
+
if (typeof value !== 'string') {
|
|
46
|
+
throw new TypeError(`${pluginName}: ${field} must be a string — 默认文案归模板/预设,请在本预设或组合源提供`)
|
|
47
|
+
}
|
|
48
|
+
return value.length > 0 ? value : undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
/** Validate the shared config envelope and unknown-key contract. */
|
|
30
52
|
export function validateConfig(pluginName, source, allowedKeys) {
|
|
31
53
|
const config = source === undefined ? {} : source
|
|
@@ -77,6 +99,15 @@ export function isDelegated(session) {
|
|
|
77
99
|
return (session?.header?.delegationDepth ?? 0) > 0
|
|
78
100
|
}
|
|
79
101
|
|
|
102
|
+
/**
|
|
103
|
+
* 读取当前 Session 的不可变事件快照。
|
|
104
|
+
* 正式 API 是 snapshotEvents()(DSH 0.1.2-alpha.4+);宿主缺失该接口时按空日志处理,不再读取旧 events 数组。
|
|
105
|
+
*/
|
|
106
|
+
export function sessionEvents(session) {
|
|
107
|
+
const snapshot = session?.snapshotEvents?.()
|
|
108
|
+
return Array.isArray(snapshot) ? snapshot : []
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
/** True when a model id looks like a Flash-family model. */
|
|
81
112
|
export function isFlashModel(modelId) {
|
|
82
113
|
return typeof modelId === 'string' && /flash/i.test(modelId)
|
|
@@ -116,3 +147,109 @@ export function keepDisposer(ctx, disposer, label) {
|
|
|
116
147
|
// 极简测试桩没有 effect:保持注册随进程,测试自行隔离。
|
|
117
148
|
}
|
|
118
149
|
}
|
|
150
|
+
/** 会话态 Map 上限:session 数超出后整体清空防无界增长
|
|
151
|
+
* (进程内快路径,真相在 durable 事件流,清空仅触发一次冷扫重建)。 */
|
|
152
|
+
export const MAX_TRACKED_SESSIONS = 4096
|
|
153
|
+
|
|
154
|
+
/** 按会话 key 取 Map 条目;超限时清空后重建(防 session 数无界增长)。 */
|
|
155
|
+
export function sessionMapGet(map, key, create) {
|
|
156
|
+
let entry = map.get(key)
|
|
157
|
+
if (entry === undefined) {
|
|
158
|
+
if (map.size >= MAX_TRACKED_SESSIONS) map.clear()
|
|
159
|
+
entry = create()
|
|
160
|
+
map.set(key, entry)
|
|
161
|
+
}
|
|
162
|
+
return entry
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 会话态声明的统一访问接口(B4 T1 步骤一)。
|
|
167
|
+
*
|
|
168
|
+
* 收敛「每个模块自己维护会话态 Map、自己决定上限与清空策略、自己决定复位时机」,
|
|
169
|
+
* 但**不改变任何既有策略**:键类型、淘汰策略、复位语义逐项由调用方按现状声明,
|
|
170
|
+
* 本函数只把它们收进同一套读写入口。它**不替代** `sessionMapGet`(后者仍是
|
|
171
|
+
* 「按 id + 超限全清」这一档的实现,本函数在 `evict` 缺省且按 id 索引时逐字复用它)。
|
|
172
|
+
*
|
|
173
|
+
* ## 三个必答项(正好对应现状的三处差异)
|
|
174
|
+
*
|
|
175
|
+
* 1. **键类型**:`weak: true` 用会话**对象身份**(`WeakMap`,随对象回收、无上限,
|
|
176
|
+
* 既有先例见原 `context-gate` 的延迟状态表,它已随模块删除);缺省用 `session.id`
|
|
177
|
+
* (跨对象稳定的快路径键)。
|
|
178
|
+
* 2. **淘汰策略**:按 id 索引时缺省 `clear()` 全清(`MAX_TRACKED_SESSIONS` 上限,
|
|
179
|
+
* 清空只触发一次冷扫重建);`evict: 'oldest'` 是**另一档**——只删早于当前新建的
|
|
180
|
+
* 一个条目,与 `clear()` **不等价**,只有调用方明确声明要用它时才生效(既有两处
|
|
181
|
+
* 见 `layers` 的去重 memo,B4 保留原状并注释)。
|
|
182
|
+
* 3. **复位语义**:`reset` **缺省完全不订阅** `session/event`(零开销,模块自行在其
|
|
183
|
+
* 既有监听里处理复位);给了 `reset` 才注册一条监听,且**只有 `reset` 返回 true
|
|
184
|
+
* 才删除该会话条目**——由调用方按**状态字段**决定,不按模块决定
|
|
185
|
+
* (`tool-bootstrap` 的 `stage` 与 `promotion` 策略相反,是这条的现成反例)。
|
|
186
|
+
*
|
|
187
|
+
* **纪律**:进程内状态是快路径,真相在 durable 事件流;迁移到本接口**不得**让任何判定
|
|
188
|
+
* 从 durable 事实退化为进程内状态,也不得改变任何门控/过滤的结果。
|
|
189
|
+
*
|
|
190
|
+
* @param {object} ctx 挂载期 ctx(只有声明了 `reset` 才需要它;缺 `on` 时 watchdog 不订阅)
|
|
191
|
+
* @param {() => any} create 新会话条目的工厂
|
|
192
|
+
* @param {object} [options]
|
|
193
|
+
* @param {boolean} [options.weak] 用会话对象身份做键(WeakMap,无上限)
|
|
194
|
+
* @param {number} [options.limit] 按 id 索引时的条目上限,缺省 `MAX_TRACKED_SESSIONS`
|
|
195
|
+
* @param {'clear'|'oldest'|null} [options.evict] 超限淘汰策略,缺省 `'clear'`
|
|
196
|
+
* @param {(session: unknown, event: unknown) => boolean} [options.reset] 复位判定
|
|
197
|
+
* @returns {{get: Function, peek: Function, set: Function, delete: Function, size: () => number, keys: Function, clear: Function}}
|
|
198
|
+
*/
|
|
199
|
+
export function sessionState(ctx, create, { weak = false, limit = MAX_TRACKED_SESSIONS, evict = 'clear', reset } = {}) {
|
|
200
|
+
const store = weak ? new WeakMap() : new Map()
|
|
201
|
+
const keyOf = (session) => (weak ? session : session?.id)
|
|
202
|
+
// ponytail: 无上限档(weak / limit: null / evict: null)与有上限档共用一条读写路径,
|
|
203
|
+
// 只在上限判定处分支;未声明上限时不创建任何额外计数器。
|
|
204
|
+
const finite = !weak && typeof limit === 'number' && limit > 0 && evict !== null
|
|
205
|
+
/** 超限处理:`clear` 与 `oldest` 是**两档不同语义**,绝不互相降级。 */
|
|
206
|
+
const settle = () => {
|
|
207
|
+
if (!finite) return
|
|
208
|
+
if (store.size < limit) return
|
|
209
|
+
if (evict === 'oldest') store.delete(store.keys().next().value)
|
|
210
|
+
else store.clear()
|
|
211
|
+
}
|
|
212
|
+
const peek = (session) => (keyOf(session) === undefined ? undefined : store.get(keyOf(session)))
|
|
213
|
+
const set = (session, value) => {
|
|
214
|
+
const key = keyOf(session)
|
|
215
|
+
// 无 id 的会话**不记账**(与 `predicates.mjs:343` 的既有决定一致):共用一个 Map 键
|
|
216
|
+
// 会让两个会话串味,所以这里返回 undefined 而不是伪造一个存不住的条目。
|
|
217
|
+
if (key === undefined) return undefined
|
|
218
|
+
settle()
|
|
219
|
+
store.set(key, value)
|
|
220
|
+
return value
|
|
221
|
+
}
|
|
222
|
+
const get = (session) => {
|
|
223
|
+
const known = peek(session)
|
|
224
|
+
return known === undefined ? set(session, create()) : known
|
|
225
|
+
}
|
|
226
|
+
const remove = (session) => {
|
|
227
|
+
const key = keyOf(session)
|
|
228
|
+
return key === undefined ? false : store.delete(key)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 复位:只在调用方声明了 reset 时订阅,且删除与否由 reset 的返回值决定。
|
|
232
|
+
if (typeof reset === 'function' && typeof ctx?.on === 'function') {
|
|
233
|
+
const disposer = ctx.on('session/event', (session, event) => {
|
|
234
|
+
try {
|
|
235
|
+
if (reset(session, event) === true) remove(session)
|
|
236
|
+
} catch {
|
|
237
|
+
// 复位失败不得影响事件流上的其它监听器(与各模块既有降级纪律一致)。
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
keepDisposer(ctx, disposer, 'session-state-reset')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
get,
|
|
245
|
+
peek,
|
|
246
|
+
set,
|
|
247
|
+
delete: remove,
|
|
248
|
+
size: () => (weak ? NaN : store.size),
|
|
249
|
+
keys: () => (weak ? [] : [...store.keys()]),
|
|
250
|
+
clear: () => { if (!weak) store.clear() },
|
|
251
|
+
weak,
|
|
252
|
+
limit: finite ? limit : null,
|
|
253
|
+
evict: finite ? evict : null,
|
|
254
|
+
}
|
|
255
|
+
}
|
package/engine/skill-search.mjs
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* NOTE: this plugin REPLACES the `dsh-tool-skill` row in the composition —
|
|
25
25
|
* the composition must NOT mount both, or the catalog injection returns.
|
|
26
26
|
*/
|
|
27
|
+
import { importHostPackage } from './host-package.mjs'
|
|
28
|
+
|
|
29
|
+
const { isModelInvocable, renderSkillContent } = await importHostPackage('@deepseek-ai/dsh-skill')
|
|
27
30
|
|
|
28
31
|
/** Cordis plugin name used by loader diagnostics. */
|
|
29
32
|
export const name = 'skill-search'
|
|
@@ -47,9 +50,14 @@ function toJsonSchema(spec) {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/** Register the two on-demand skill tools. */
|
|
53
|
+
/**
|
|
54
|
+
* 本模块**刻意保持单参形态**:它不读任何 config 键,因此没有白名单可声明。
|
|
55
|
+
* B2 T4 要求「四个无白名单插件行补字段声明与校验」,本行按 PLAN 的例外处理——
|
|
56
|
+
* 若将来需要 config,先确认组合行与调用方确实都在传 config,再补 defineConfig 声明。
|
|
57
|
+
*/
|
|
50
58
|
export function apply(ctx) {
|
|
51
59
|
/** Normalize a query into lowercase tokens for simple substring matching. */
|
|
52
|
-
const tokens = (text) => (text || '').toLowerCase().split(/[
|
|
60
|
+
const tokens = (text) => (text || '').toLowerCase().split(/[^\p{L}\p{N}_-]+/u).filter(Boolean)
|
|
53
61
|
|
|
54
62
|
ctx.tools.register({
|
|
55
63
|
name: 'skill_search',
|
|
@@ -65,12 +73,13 @@ export function apply(ctx) {
|
|
|
65
73
|
const wanted = tokens(args.query)
|
|
66
74
|
const scope = exec?.agent ?? ctx
|
|
67
75
|
try {
|
|
68
|
-
const
|
|
76
|
+
const snapshot = await ctx.skills.snapshot({
|
|
69
77
|
scope,
|
|
70
78
|
cwd: exec?.agent?.session?.header?.cwd,
|
|
71
79
|
signal: exec?.signal,
|
|
72
80
|
})
|
|
73
|
-
|
|
81
|
+
exec?.signal?.throwIfAborted()
|
|
82
|
+
const matches = snapshot.skills.filter(isModelInvocable).filter((skill) => {
|
|
74
83
|
if (wanted.length === 0) return true
|
|
75
84
|
const haystack = tokens(`${skill.name} ${skill.description ?? ''} ${skill.whenToUse ?? ''}`).join(' ')
|
|
76
85
|
return wanted.every((token) => haystack.includes(token))
|
|
@@ -80,9 +89,12 @@ export function apply(ctx) {
|
|
|
80
89
|
const desc = (skill.description || '').split('\n')[0]
|
|
81
90
|
return `- ${skill.name}: ${desc}`
|
|
82
91
|
})
|
|
83
|
-
if (lines.length === 0) return { text:
|
|
92
|
+
if (lines.length === 0) return { text: snapshot.complete
|
|
93
|
+
? `No skills match "${args.query}". Use skill_search with other keywords.`
|
|
94
|
+
: 'Skill discovery is incomplete. Retry skill_search before concluding no skills match.' }
|
|
84
95
|
const extra = matches.length > MAX_RESULTS ? `\n…(${matches.length - MAX_RESULTS} more)` : ''
|
|
85
|
-
|
|
96
|
+
const incomplete = snapshot.complete ? '' : '\nSkill discovery is incomplete; these results may be partial.'
|
|
97
|
+
return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}${incomplete}\n\nLoad one with skill_load (exact name).` }
|
|
86
98
|
} catch (error) {
|
|
87
99
|
return { text: `skill_search unavailable: ${String((error && error.message) || error)}` }
|
|
88
100
|
}
|
|
@@ -103,16 +115,27 @@ export function apply(ctx) {
|
|
|
103
115
|
try {
|
|
104
116
|
const agent = exec?.agent
|
|
105
117
|
if (agent === undefined) return { text: 'skill_load requires an agent context.' }
|
|
106
|
-
const
|
|
118
|
+
const options = {
|
|
107
119
|
scope: agent,
|
|
108
120
|
cwd: agent.session.header.cwd,
|
|
109
121
|
signal: exec?.signal,
|
|
110
|
-
}
|
|
122
|
+
}
|
|
123
|
+
const snapshot = await ctx.skills.snapshot(options)
|
|
124
|
+
options.signal?.throwIfAborted()
|
|
125
|
+
const summary = snapshot.skills.find((entry) => entry.name === args.name)
|
|
126
|
+
if (summary === undefined) {
|
|
127
|
+
return { text: snapshot.complete
|
|
128
|
+
? `No skill named "${args.name}". Run skill_search to list available skills.`
|
|
129
|
+
: 'Skill discovery is incomplete. Retry skill_search before loading this skill.' }
|
|
130
|
+
}
|
|
131
|
+
if (!isModelInvocable(summary)) return { text: `Skill "${args.name}" is not available for model invocation.` }
|
|
132
|
+
const skill = await ctx.skills.get(args.name, options)
|
|
133
|
+
options.signal?.throwIfAborted()
|
|
111
134
|
if (skill === undefined) {
|
|
112
|
-
return { text: `
|
|
135
|
+
return { text: `Skill "${args.name}" is no longer available. Run skill_search again.` }
|
|
113
136
|
}
|
|
114
|
-
|
|
115
|
-
if (
|
|
137
|
+
if (!isModelInvocable(skill)) return { text: `Skill "${args.name}" is not available for model invocation.` }
|
|
138
|
+
if (skill.content.trim().length === 0) {
|
|
116
139
|
return { text: `Skill "${args.name}" has no loadable body.` }
|
|
117
140
|
}
|
|
118
141
|
// Queue the skill content as a non-waking next-step context message,
|
|
@@ -120,7 +143,7 @@ export function apply(ctx) {
|
|
|
120
143
|
agent.inject({
|
|
121
144
|
id: `skill-load-${args.name}-${Date.now()}`,
|
|
122
145
|
role: 'user',
|
|
123
|
-
content: [{ type: 'text', text:
|
|
146
|
+
content: [{ type: 'text', text: renderSkillContent(skill) }],
|
|
124
147
|
source: { kind: 'skill-invocation', name: args.name, form: 'instructions' },
|
|
125
148
|
})
|
|
126
149
|
return { text: `Skill "${args.name}" loaded; its instructions will be injected for the next request.` }
|
|
@@ -130,13 +153,3 @@ export function apply(ctx) {
|
|
|
130
153
|
},
|
|
131
154
|
})
|
|
132
155
|
}
|
|
133
|
-
|
|
134
|
-
/** Extract the model-facing body of a loaded skill definition. */
|
|
135
|
-
function extractSkillBody(skill) {
|
|
136
|
-
const content = skill?.content ?? skill?.instructions ?? skill?.body
|
|
137
|
-
if (typeof content === 'string') return content
|
|
138
|
-
if (Array.isArray(content)) {
|
|
139
|
-
return content.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join('\n')
|
|
140
|
-
}
|
|
141
|
-
return ''
|
|
142
|
-
}
|