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.
Files changed (169) hide show
  1. package/README.md +185 -46
  2. package/engine/THIRD_PARTY_LICENSES +25 -0
  3. package/engine/actions.mjs +931 -0
  4. package/engine/anchor-match.mjs +6 -3
  5. package/engine/character-tools.mjs +11 -0
  6. package/engine/classify-task.mjs +18 -0
  7. package/engine/compaction-epoch.mjs +41 -20
  8. package/engine/compositions/library/agent-instructions.yml +9 -0
  9. package/engine/compositions/library/command-goal.yml +2 -6
  10. package/engine/compositions/library/compaction.yml +2 -32
  11. package/engine/compositions/library/delegation-ptc.yml +58 -0
  12. package/engine/compositions/library/delegation.yml +5 -38
  13. package/engine/compositions/library/persistent-shell.yml +4 -13
  14. package/engine/compositions/library/planning.yml +8 -23
  15. package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
  16. package/engine/compositions/library/skill-filesystem.yml +2 -8
  17. package/engine/compositions/library/tool-ask-user.yml +2 -3
  18. package/engine/compositions/library/tool-bash.yml +4 -12
  19. package/engine/compositions/library/tool-cordis.yml +7 -0
  20. package/engine/compositions/library/tool-fs-search.yml +2 -11
  21. package/engine/compositions/library/tool-fs.yml +2 -5
  22. package/engine/compositions/library/tool-goal.yml +2 -6
  23. package/engine/compositions/library/tool-jobs.yml +2 -18
  24. package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
  25. package/engine/compositions/library/tool-plugin-manager.yml +8 -0
  26. package/engine/compositions/library/tool-present.yml +7 -0
  27. package/engine/compositions/library/tool-presentation.yml +9 -0
  28. package/engine/compositions/library/tool-pwsh.yml +2 -6
  29. package/engine/compositions/library/tool-skill.yml +7 -0
  30. package/engine/compositions/library/tool-todo.yml +2 -4
  31. package/engine/compositions/library/tool-web.yml +2 -1
  32. package/engine/compositions/source/local/character-tools.yml +5 -0
  33. package/engine/compositions/source/local/declared-triggers.yml +12 -0
  34. package/engine/compositions/source/local/filesystem-editor.yml +30 -0
  35. package/engine/compositions/source/local/instruction-hint.yml +15 -0
  36. package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
  37. package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
  38. package/engine/compositions/source/local/run-code-env.yml +4 -7
  39. package/engine/compositions/source/local/session-var-tools.yml +5 -0
  40. package/engine/compositions/source/local/skill-search.yml +3 -9
  41. package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
  42. package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
  43. package/engine/compositions/source/local/tool-config-engine.yml +8 -0
  44. package/engine/compositions/source/local/tool-git-bash.yml +15 -0
  45. package/engine/compositions/source/local/world-book-tools.yml +5 -0
  46. package/engine/condition.mjs +83 -0
  47. package/engine/declared-triggers.mjs +93 -0
  48. package/engine/executor.mjs +364 -185
  49. package/engine/fields.mjs +219 -0
  50. package/engine/fillers.mjs +12 -141
  51. package/engine/host-package.mjs +51 -0
  52. package/engine/instruction-hint.mjs +285 -0
  53. package/engine/interpolate.mjs +157 -29
  54. package/engine/layers.mjs +495 -75
  55. package/engine/predicates.mjs +753 -0
  56. package/engine/prompt-config-engine.mjs +63 -8
  57. package/engine/run-code-env.mjs +48 -31
  58. package/engine/schema.mjs +444 -103
  59. package/engine/sdk-strip.mjs +365 -0
  60. package/engine/session-var-tools.mjs +11 -0
  61. package/engine/session-vars.mjs +8 -6
  62. package/engine/shared.mjs +137 -0
  63. package/engine/skill-search.mjs +34 -21
  64. package/engine/st-macros.d.mts +22 -0
  65. package/engine/st-macros.mjs +208 -0
  66. package/engine/st-render.mjs +105 -0
  67. package/engine/st-world-book.d.mts +10 -0
  68. package/engine/st-world-book.mjs +259 -0
  69. package/engine/strategies.mjs +31 -20
  70. package/engine/subagent-tool-policy-core.mjs +400 -0
  71. package/engine/subagent-tool-policy.mjs +327 -0
  72. package/engine/tool-config-engine.mjs +207 -145
  73. package/engine/tool-definition.mjs +143 -0
  74. package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
  75. package/engine/trigger-editor-meta.mjs +48 -0
  76. package/engine/trigger-spec.mjs +257 -0
  77. package/engine/trigger.mjs +304 -0
  78. package/engine/world-book-tools.mjs +11 -0
  79. package/lib/client.js +16495 -5456
  80. package/lib/client.js.map +1 -1
  81. package/lib/index.d.mts +628 -382
  82. package/lib/index.mjs +5391 -2942
  83. package/lib/paths-CY5psBqE.d.mts +1233 -0
  84. package/lib/preset-package-BnJqrU4R.mjs +16792 -0
  85. package/lib/preset-transfer.d.mts +2 -0
  86. package/lib/preset-transfer.mjs +2 -0
  87. package/package.json +91 -49
  88. package/preset/pt-cordis/preset.yml +43 -0
  89. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
  90. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
  91. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
  92. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
  93. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
  94. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
  95. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
  96. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
  97. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
  98. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
  99. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
  100. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
  101. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
  102. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
  103. package/preset/pt-custom/preset.yml +15 -0
  104. package/preset/pt-minimal/preset.yml +27 -0
  105. package/preset/pt-ptc/preset.yml +45 -0
  106. package/preset/pt-standard/preset.yml +43 -0
  107. package/templates/14-first-turn-anchor.yml +4 -4
  108. package/templates/15-guide-auto.yml +3 -3
  109. package/templates/16-custom-fallback.yml +1 -0
  110. package/templates/18-placeholder-env-facts.yml +3 -3
  111. package/templates/20-system-section.yml +3 -2
  112. package/templates/65-turn-stop.yml +18 -0
  113. package/templates/66-subagent-start.yml +14 -0
  114. package/templates/75-subagent-end.yml +15 -0
  115. package/templates/tools/10-shell-command.yml +1 -1
  116. package/templates/tools/40-delegate-character.yml +1 -1
  117. package/templates/tools/50-delegate-worldbook.yml +1 -1
  118. package/templates/tools/60-delegate-session-var.yml +1 -1
  119. package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
  120. package/engine/anchor-turn.mjs +0 -68
  121. package/engine/code-presentation.mjs +0 -103
  122. package/engine/compositions/library/anchor-turn.yml +0 -10
  123. package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
  124. package/engine/compositions/library/code-presentation.yml +0 -8
  125. package/engine/compositions/library/context-gate.yml +0 -39
  126. package/engine/compositions/library/cot-drip.yml +0 -11
  127. package/engine/compositions/library/custom-bash.yml +0 -14
  128. package/engine/compositions/library/deliberation-gate.yml +0 -11
  129. package/engine/compositions/library/official-agent-instructions.yml +0 -17
  130. package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
  131. package/engine/compositions/library/official-tool-bash.yml +0 -6
  132. package/engine/compositions/library/official-tool-cordis.yml +0 -13
  133. package/engine/compositions/library/official-tool-presentation.yml +0 -7
  134. package/engine/compositions/library/official-tool-skill.yml +0 -14
  135. package/engine/compositions/library/persona.yml +0 -13
  136. package/engine/compositions/library/prompt-config-engine.yml +0 -16
  137. package/engine/compositions/library/run-code-env.yml +0 -15
  138. package/engine/compositions/library/skill-search.yml +0 -14
  139. package/engine/compositions/library/str-replace-editor.yml +0 -10
  140. package/engine/compositions/library/tool-bootstrap.yml +0 -14
  141. package/engine/compositions/library/tool-config-engine.yml +0 -9
  142. package/engine/compositions/library/tool-filter.yml +0 -8
  143. package/engine/compositions/source/local/code-presentation.yml +0 -8
  144. package/engine/compositions/source/local/context-gate.yml +0 -36
  145. package/engine/compositions/source/local/custom-bash.yml +0 -11
  146. package/engine/compositions/source/local/str-replace-editor.yml +0 -10
  147. package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
  148. package/engine/compositions/source/local/tool-filter.yml +0 -8
  149. package/engine/context-gate.mjs +0 -305
  150. package/engine/cot-drip.mjs +0 -137
  151. package/engine/deliberation-gate.mjs +0 -127
  152. package/engine/tool-bootstrap.mjs +0 -476
  153. package/engine/tool-filter.mjs +0 -80
  154. package/lib/preset-core.d.mts +0 -10
  155. package/lib/preset-core.mjs +0 -77
  156. package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
  157. package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
  158. package/preset/anchored/preset.yml +0 -296
  159. package/preset/creative/preset.yml +0 -67
  160. package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
  161. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
  162. package/preset/custom/preset.yml +0 -12
  163. package/preset/liangshen/preset.yml +0 -75
  164. package/preset/minimal/preset.yml +0 -45
  165. package/preset/ptc/preset.yml +0 -58
  166. package/preset/standard/preset.yml +0 -57
  167. package/skills/manifest.json +0 -7
  168. package/skills/sandboxmod/SKILL.md +0 -49
  169. 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
+ }
@@ -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
- vars = {}
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
- vars[String(key)] = String(value ?? '')
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
+ }
@@ -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(/[^a-z0-9_-]+/).filter(Boolean)
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 all = await ctx.skills.list({
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
- const matches = all.filter((skill) => {
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: `No skills match "${args.query}". Use skill_search with other keywords.` }
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
- return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}\n\nLoad one with skill_load (exact name).` }
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 skill = await ctx.skills.get(args.name, {
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: `No skill named "${args.name}". Run skill_search to list available skills.` }
135
+ return { text: `Skill "${args.name}" is no longer available. Run skill_search again.` }
113
136
  }
114
- const body = extractSkillBody(skill)
115
- if (body.length === 0) {
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: body }],
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
- }