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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instruction-hint — 通用内置指令文件提示引擎。
|
|
3
|
+
*
|
|
4
|
+
* 本模块同时是「可 import 的纯函数库」与「可被组合源挂载的 plugin」:
|
|
5
|
+
* - prompt-config 的 strategy=placeholder + fill=instruction-hint
|
|
6
|
+
* 直接使用本模块的探测与消息构造;
|
|
7
|
+
* - plugin 形态(见文件末尾 apply):**挂本行 = 开启**「晋升后把
|
|
8
|
+
* agent-instructions 全文替换为一次性 hint,后续全文消息丢弃」的转换。
|
|
9
|
+
*
|
|
10
|
+
* 只提示参考文件存在,不把文件正文塞进每轮上下文。文件探测失败时返回空结果,
|
|
11
|
+
* 不阻断会话;独立生成的 hint 使用随机 id,替换既有消息时保留其 id。
|
|
12
|
+
*
|
|
13
|
+
* PLUGIN CONFIG(挂载期校验;声明即校验,未声明 `enabled` = 关闭):
|
|
14
|
+
* - `enabled`: boolean,缺省 false(组合源须显式写 true)。
|
|
15
|
+
* - `promoteOn`: 'either'(缺省)| 'tool-call' | 'assistant-message'。
|
|
16
|
+
* - `includeSubagents`: boolean,缺省 false(子代理首次请求即视为已晋升)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { randomUUID } from 'node:crypto'
|
|
20
|
+
import { readFileSync } from 'node:fs'
|
|
21
|
+
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
22
|
+
import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
|
|
23
|
+
|
|
24
|
+
export const name = 'instruction-hint'
|
|
25
|
+
|
|
26
|
+
/** 无 inject:监听器只在使用时触碰 ctx 服务(同 tool-bootstrap / context-gate 纪律)。 */
|
|
27
|
+
export const inject = []
|
|
28
|
+
|
|
29
|
+
/** 项目目录链中按优先级探测的指令文件名。 */
|
|
30
|
+
export const PROJECT_INSTRUCTION_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
|
|
31
|
+
export const USER_GLOBAL_INSTRUCTION_CANDIDATE = 'AGENTS.md'
|
|
32
|
+
const REFERENCE_HINT_SUFFIX = "They are reference documents about the user's environment and workspace conventions, not task instructions. Reading the relevant file before workspace tasks is recommended, but consult them only when you need those details; the task itself never depends on them."
|
|
33
|
+
|
|
34
|
+
/** Join one path segment onto a directory (platform-agnostic string join). */
|
|
35
|
+
export function joinPath(dir, segment) {
|
|
36
|
+
if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
|
|
37
|
+
const sep = dir.includes('\\') ? '\\' : '/'
|
|
38
|
+
return dir + sep + segment
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parent of an absolute Windows or POSIX path. */
|
|
42
|
+
export function parentPath(path) {
|
|
43
|
+
const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
44
|
+
if (idx <= 0) return path
|
|
45
|
+
const parent = path.slice(0, idx)
|
|
46
|
+
return parent.length === 0 ? path : parent
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Find the project root: first ancestor containing any root marker. */
|
|
50
|
+
export async function findProjectRoot(fs, cwd, signal) {
|
|
51
|
+
let current = cwd
|
|
52
|
+
for (;;) {
|
|
53
|
+
for (const marker of ['.git', '.hg', '.svn']) {
|
|
54
|
+
try {
|
|
55
|
+
const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
|
|
56
|
+
const info = await fs.stat(target, signal)
|
|
57
|
+
if (info !== undefined) return current
|
|
58
|
+
} catch {
|
|
59
|
+
// Probe failure = marker absent; continue.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const parent = parentPath(current)
|
|
63
|
+
if (parent === current || parent.length === 0) return cwd
|
|
64
|
+
current = parent
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** List instruction files present in one directory. */
|
|
69
|
+
export async function presentInDir(fs, dir, candidates, signal) {
|
|
70
|
+
const found = []
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
|
|
74
|
+
const info = await fs.stat(target, signal)
|
|
75
|
+
if (info !== undefined && info.type === 'file') found.push(candidate)
|
|
76
|
+
} catch {
|
|
77
|
+
// Absent or unreadable — skip.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return found
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 探测 cwd 到项目根的完整目录链,以及 DSH_HOME 下的用户级 AGENTS.md。
|
|
85
|
+
* home 未传时使用当前宿主的 DSH_HOME/USERPROFILE 推导值。
|
|
86
|
+
*/
|
|
87
|
+
export async function collectInstructionFiles(fs, cwd, signal, home) {
|
|
88
|
+
const root = await findProjectRoot(fs, cwd, signal)
|
|
89
|
+
const projectFiles = []
|
|
90
|
+
let probed = cwd
|
|
91
|
+
for (;;) {
|
|
92
|
+
for (const candidate of await presentInDir(fs, probed, PROJECT_INSTRUCTION_CANDIDATES, signal)) {
|
|
93
|
+
projectFiles.push(probed === root ? candidate : joinPath(probed, candidate))
|
|
94
|
+
}
|
|
95
|
+
if (probed === root) break
|
|
96
|
+
const parent = parentPath(probed)
|
|
97
|
+
if (parent === probed || parent.length === 0) break
|
|
98
|
+
probed = parent
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const userGlobalFiles = []
|
|
102
|
+
const dshHome = home ?? process.env.DSH_HOME
|
|
103
|
+
?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
|
|
104
|
+
if (dshHome !== undefined) {
|
|
105
|
+
userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_INSTRUCTION_CANDIDATE], signal))
|
|
106
|
+
}
|
|
107
|
+
return { root, projectFiles, userGlobalFiles, userGlobalHome: dshHome }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 将探测结果格式化为建议式 hint;无文件时返回空字符串。
|
|
112
|
+
* scope 限定只报告某一来源:all(默认)/ global($DSH_HOME/AGENTS.md)/ project(cwd→项目根链)。
|
|
113
|
+
*/
|
|
114
|
+
export function buildInstructionHintText({ root, projectFiles = [], userGlobalFiles = [], userGlobalHome } = {}, scope = 'all') {
|
|
115
|
+
const sections = []
|
|
116
|
+
if (scope !== 'global' && projectFiles.length > 0) {
|
|
117
|
+
sections.push(`Reference documents exist: ${projectFiles.join(', ')} (project root: ${root}).`)
|
|
118
|
+
}
|
|
119
|
+
if (scope !== 'project' && userGlobalFiles.length > 0) {
|
|
120
|
+
const paths = typeof userGlobalHome === 'string' && userGlobalHome.length > 0
|
|
121
|
+
? userGlobalFiles.map((name) => joinPath(userGlobalHome, name))
|
|
122
|
+
: userGlobalFiles
|
|
123
|
+
sections.push(`A user reference document exists: ${paths.join(', ')}.`)
|
|
124
|
+
}
|
|
125
|
+
if (sections.length === 0) return ''
|
|
126
|
+
sections.push(REFERENCE_HINT_SUFFIX)
|
|
127
|
+
return sections.join(' ')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const HINT_SCOPES = new Set(['all', 'global', 'project'])
|
|
131
|
+
|
|
132
|
+
function hintScope(value) {
|
|
133
|
+
return typeof value === 'string' && HINT_SCOPES.has(value) ? value : 'all'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* prompt-config placeholder fill=instruction-hint 的 resolver。
|
|
138
|
+
* params.text(自定义提示)优先;params.file(文件卡绑定的单个指令文件)次之——
|
|
139
|
+
* 运行时读该文件正文并加 `Instructions from:` 头注入;最后按 params.scope 探测来源
|
|
140
|
+
* (all / global / project)只发「文件存在」提示。
|
|
141
|
+
* 文件缺失、不可读或空内容时返回 null,不注入任何消息。
|
|
142
|
+
*/
|
|
143
|
+
export function createInstructionHintResolver(config = {}) {
|
|
144
|
+
const customText = typeof config?.params?.text === 'string' && config.params.text.trim().length > 0
|
|
145
|
+
? config.params.text.trim()
|
|
146
|
+
: ''
|
|
147
|
+
const scope = hintScope(config?.params?.scope)
|
|
148
|
+
const file = typeof config?.params?.file === 'string' && config.params.file.trim().length > 0
|
|
149
|
+
? config.params.file.trim()
|
|
150
|
+
: ''
|
|
151
|
+
const fileLabel = typeof config?.params?.displayPath === 'string' && config.params.displayPath.trim().length > 0
|
|
152
|
+
? config.params.displayPath.trim()
|
|
153
|
+
: file
|
|
154
|
+
/** 文件即真相:每次注入都重读绑定文件,外部编辑立即生效。 */
|
|
155
|
+
const readBoundFile = () => {
|
|
156
|
+
try {
|
|
157
|
+
return readFileSync(file, 'utf8').trim()
|
|
158
|
+
} catch {
|
|
159
|
+
return ''
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return async ({ ctx, agent, session }) => {
|
|
164
|
+
const id = `instruction-hint-${session.id}-${randomUUID()}`
|
|
165
|
+
if (customText.length > 0) {
|
|
166
|
+
return { id, text: customText, source: { kind: 'instruction-hint', form: 'hint' } }
|
|
167
|
+
}
|
|
168
|
+
if (file.length > 0) {
|
|
169
|
+
const content = readBoundFile()
|
|
170
|
+
return content.length > 0
|
|
171
|
+
? { id, text: `Instructions from: ${fileLabel}\n\n${content}`, source: { kind: 'instruction-file', form: 'instructions' } }
|
|
172
|
+
: null
|
|
173
|
+
}
|
|
174
|
+
const fs = ctx.get('fs')
|
|
175
|
+
if (fs === undefined) return null
|
|
176
|
+
const cwd = session.header?.cwd ?? process.cwd()
|
|
177
|
+
const found = await collectInstructionFiles(fs, cwd, agent.signal)
|
|
178
|
+
const text = buildInstructionHintText(found, scope)
|
|
179
|
+
return text.length > 0
|
|
180
|
+
? { id, text, source: { kind: 'instruction-hint', form: 'hint' } }
|
|
181
|
+
: null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const INSTRUCTION_FROM_RE = /(?:^|\n) *(?:Additional |Updated )?Instructions from: ([^\n]+)/g
|
|
186
|
+
|
|
187
|
+
/** 从 agent-instructions 注入消息提取参考文件路径。 */
|
|
188
|
+
export function extractInstructionPaths(message) {
|
|
189
|
+
const paths = []
|
|
190
|
+
const blocks = Array.isArray(message?.content) ? message.content : []
|
|
191
|
+
for (const block of blocks) {
|
|
192
|
+
if (block?.type !== 'text' || typeof block.text !== 'string') continue
|
|
193
|
+
for (const match of block.text.matchAll(INSTRUCTION_FROM_RE)) {
|
|
194
|
+
const path = match[1].trim()
|
|
195
|
+
if (path !== '' && !paths.includes(path)) paths.push(path)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return paths
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 构造一次性非命令式 hint 消息;替换已有消息时保留其 id。 */
|
|
202
|
+
export function buildInstructionHint(original, paths, sourceName = 'instruction-hint') {
|
|
203
|
+
return {
|
|
204
|
+
id: typeof original?.id === 'string' && original.id !== '' ? original.id : `instruction-hint-${randomUUID()}`,
|
|
205
|
+
role: 'user',
|
|
206
|
+
content: [{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: '<system-reminder>\n'
|
|
209
|
+
+ `Reference documents exist: ${paths.join(', ')}. ${REFERENCE_HINT_SUFFIX}`
|
|
210
|
+
+ '\n</system-reminder>',
|
|
211
|
+
}],
|
|
212
|
+
source: { kind: 'instruction-hint', form: 'hint', plugin: sourceName },
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 将 agent-instructions 全文替换为一次性 hint,后续全文消息丢弃。 */
|
|
217
|
+
export function instructionHintMessages(messages, state, sourceName = 'instruction-hint') {
|
|
218
|
+
const kept = []
|
|
219
|
+
for (const message of messages) {
|
|
220
|
+
if (message?.source?.kind !== 'agent-instructions') {
|
|
221
|
+
kept.push(message)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (state.instructionHinted) continue
|
|
225
|
+
const paths = extractInstructionPaths(message)
|
|
226
|
+
if (paths.length === 0) {
|
|
227
|
+
kept.push(message)
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
state.instructionHinted = true
|
|
231
|
+
kept.push(buildInstructionHint(message, paths, sourceName))
|
|
232
|
+
}
|
|
233
|
+
return kept
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── plugin 形态 ─────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/** Every config key this plugin accepts — anything else is a typo. */
|
|
239
|
+
const ALLOWED_KEYS = new Set(['enabled', 'promoteOn', 'includeSubagents'])
|
|
240
|
+
|
|
241
|
+
/** The visible surface, not the append-only log, owns hint lifetime. */
|
|
242
|
+
function hasVisibleInstructionHint(session) {
|
|
243
|
+
const messages = session?.deriveMessages?.()
|
|
244
|
+
return Array.isArray(messages) && messages.some((message) => message?.source?.kind === 'instruction-hint')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 注册晋升后的指令正文转换。
|
|
249
|
+
*
|
|
250
|
+
* `prepend: true` + 组合首选行:waterfall after-next 变换按注册逆序生效,本转换因此
|
|
251
|
+
* 是最外层——后注册的注入无法绕过它。`promotion.status(agent)` 兼作冷扫入口(resume /
|
|
252
|
+
* reload 后由 durable 事件流重建相位)。声明即校验:非法配置在挂载期暴露,未声明
|
|
253
|
+
* `enabled` 时同样报错。
|
|
254
|
+
*/
|
|
255
|
+
export function apply(ctx, config) {
|
|
256
|
+
const source = validateConfig(name, config, ALLOWED_KEYS)
|
|
257
|
+
const promoteEvents = parsePromoteOn(name, source.promoteOn)
|
|
258
|
+
const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
|
|
259
|
+
// 开关语义:未声明 = 关闭(组合源为本模块显式写 enabled: true)。
|
|
260
|
+
const enabled = booleanOption(name, source.enabled, 'enabled', false)
|
|
261
|
+
if (!enabled) return
|
|
262
|
+
|
|
263
|
+
const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
|
|
264
|
+
const warnOnce = createWarnOnce(ctx, name)
|
|
265
|
+
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
266
|
+
|
|
267
|
+
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
268
|
+
// Downstream errors propagate untouched; only this filter's own logic is guarded.
|
|
269
|
+
const decision = await next()
|
|
270
|
+
if (decision.kind === 'reject') return decision
|
|
271
|
+
try {
|
|
272
|
+
if (!promotion.status(agent).promoted) return decision
|
|
273
|
+
if (!Array.isArray(decision.messages)) return decision
|
|
274
|
+
if (agent?.session === undefined) return decision
|
|
275
|
+
// 1 换 1 的转换不能按长度判断(长度相同仍可能已转换),
|
|
276
|
+
// instructionHintMessages 本身保留非目标消息,直接采用结果。
|
|
277
|
+
const hintState = { instructionHinted: hasVisibleInstructionHint(agent.session) }
|
|
278
|
+
return { ...decision, messages: instructionHintMessages(decision.messages, hintState, name) }
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// 转换失败不阻断会话:保留原消息。
|
|
281
|
+
warnOnce(`${name}: instruction hint conversion failed, keeping messages: ${String((error && error.message) || error)}`)
|
|
282
|
+
return decision
|
|
283
|
+
}
|
|
284
|
+
}, { prepend: true })
|
|
285
|
+
}
|
package/engine/interpolate.mjs
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
* - 键字符集:字母数字、下划线、点、中文、连字符(与 ST setvar/getvar 一致)。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { sessionEvents } from './shared.mjs'
|
|
17
|
+
import { createHash } from 'node:crypto'
|
|
18
|
+
|
|
16
19
|
/** 会话事件中最后一条指定类型消息的文本(事件倒序扫描;无则空串)。 */
|
|
17
20
|
function lastMessageOf(session, type) {
|
|
18
|
-
const events = session
|
|
19
|
-
if (!Array.isArray(events)) return ''
|
|
21
|
+
const events = sessionEvents(session)
|
|
20
22
|
for (let index = events.length - 1; index >= 0; index--) {
|
|
21
23
|
const event = events[index]
|
|
22
24
|
if (event?.type !== type) continue
|
|
@@ -31,22 +33,26 @@ function lastMessageOf(session, type) {
|
|
|
31
33
|
return ''
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
/**
|
|
35
|
-
function pickRandom(arg) {
|
|
36
|
-
const
|
|
36
|
+
/** ST 双冒号/逗号列表;有 seed 时稳定选择,否则每次采样。 */
|
|
37
|
+
function pickRandom(arg, seed) {
|
|
38
|
+
const input = String(arg ?? '')
|
|
39
|
+
const items = input.includes('::') ? input.split('::')
|
|
40
|
+
: input.split(/(?<!\\),/).map(item => item.trim().replaceAll('\\,', ','))
|
|
37
41
|
if (items.length === 0) return ''
|
|
38
|
-
|
|
42
|
+
const random = seed === undefined ? Math.random() : createHash('sha256').update(seed).digest().readUInt32BE() / 2 ** 32
|
|
43
|
+
return items[Math.floor(random * items.length)]
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
/** 骰子表达式({{roll::2d6+3}} / {{roll::
|
|
46
|
+
/** 骰子表达式({{roll::2d6+3}} / {{roll::6}};非法或不安全输入为空)。 */
|
|
42
47
|
function rollDice(arg) {
|
|
43
|
-
const
|
|
48
|
+
const raw = String(arg ?? '').replace(/\s+/g, '').toLowerCase()
|
|
49
|
+
const text = /^\d+$/.test(raw) ? `1d${raw}` : raw
|
|
44
50
|
const match = text.match(/^(\d*)d(\d+)([+-]\d+)?$/)
|
|
45
|
-
if (match === null) return
|
|
51
|
+
if (match === null) return ''
|
|
46
52
|
const count = match[1] === '' ? 1 : Number.parseInt(match[1], 10)
|
|
47
53
|
const sides = Number.parseInt(match[2], 10)
|
|
48
54
|
const modifier = match[3] === undefined ? 0 : Number.parseInt(match[3], 10)
|
|
49
|
-
if (!Number.isSafeInteger(count) || count <= 0 || count > 100 || !Number.isSafeInteger(sides) || sides <= 0) return
|
|
55
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > 100 || !Number.isSafeInteger(sides) || sides <= 0 || !Number.isSafeInteger(modifier) || !Number.isSafeInteger(count * sides + Math.abs(modifier))) return ''
|
|
50
56
|
let sum = 0
|
|
51
57
|
for (let index = 0; index < count; index++) sum += 1 + Math.floor(Math.random() * sides)
|
|
52
58
|
return String(sum + modifier)
|
|
@@ -64,7 +70,7 @@ function formatTime(now) {
|
|
|
64
70
|
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
/**
|
|
73
|
+
/** 本地 YYYY-MM-DD({{date}});UTC 日期由 isodate 提供。 */
|
|
68
74
|
function formatDate(now) {
|
|
69
75
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
70
76
|
}
|
|
@@ -89,28 +95,150 @@ const DYNAMIC_MACROS = {
|
|
|
89
95
|
pipe: () => '|',
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
/** 内置变量:路径类事实(无会话上下文时回退进程 cwd);两条插值通道同源。 */
|
|
99
|
+
function builtinVariables(session) {
|
|
100
|
+
return {
|
|
95
101
|
DSH_HOME: process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : ''),
|
|
96
102
|
WORKSPACE: process.env.DSH_WORKSPACE ?? session?.header?.cwd ?? process.cwd(),
|
|
97
103
|
CWD: session?.header?.cwd ?? process.cwd(),
|
|
98
104
|
}
|
|
99
|
-
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)(?:::(.*?))?\}\}/g, (whole, key, arg) => {
|
|
100
|
-
if (Object.prototype.hasOwnProperty.call(variables, key)) return String(variables[key])
|
|
101
|
-
const dynamic = DYNAMIC_MACROS[key.toLowerCase()]
|
|
102
|
-
if (dynamic !== undefined) return dynamic(arg, session)
|
|
103
|
-
return Object.prototype.hasOwnProperty.call(builtins, key) ? builtins[key] : whole
|
|
104
|
-
})
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
export function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
/** 模板变量插值:配置 variables 优先,ST 运行时宏次之,内置 {{DSH_HOME}} / {{WORKSPACE}} / {{CWD}} 兜底。 */
|
|
108
|
+
export function interpolateVariables(text, variables, session, keep, sourceId = '') {
|
|
109
|
+
const builtins = builtinVariables(session)
|
|
110
|
+
const active = new Set()
|
|
111
|
+
// 限制展开增加的字符数、递归深度与工作量;原始正文不截断,失败引用留给出口清洗。
|
|
112
|
+
let remainingChars = 1024 * 1024
|
|
113
|
+
let remainingExpansions = 4096
|
|
114
|
+
function render(input, depth = 0) {
|
|
115
|
+
return normalizeMacroSyntax(input).replace(REFERENCE_RE, (whole, key, arg, offset) => {
|
|
116
|
+
if (arg === undefined && keep?.has(key)) return `{{${key}}}`
|
|
117
|
+
if (active.has(key)) return whole
|
|
118
|
+
let value
|
|
119
|
+
if (Object.hasOwn(variables, key)) value = String(variables[key] ?? '')
|
|
120
|
+
else if (key.toLowerCase() === 'pick') value = pickRandom(arg, JSON.stringify([session?.id ?? '', sourceId, input, offset]))
|
|
121
|
+
else if (Object.hasOwn(DYNAMIC_MACROS, key.toLowerCase())) value = DYNAMIC_MACROS[key.toLowerCase()](arg, session)
|
|
122
|
+
else if (Object.hasOwn(builtins, key)) value = builtins[key]
|
|
123
|
+
else return whole
|
|
124
|
+
const growth = Math.max(0, value.length - whole.length)
|
|
125
|
+
const nested = value.includes('{{')
|
|
126
|
+
if (growth > remainingChars || (nested && (depth >= 32 || remainingExpansions <= 0))) return whole
|
|
127
|
+
remainingChars -= growth
|
|
128
|
+
if (!nested) return value
|
|
129
|
+
remainingExpansions--
|
|
130
|
+
active.add(key)
|
|
131
|
+
try {
|
|
132
|
+
return render(value, depth + 1)
|
|
133
|
+
} finally {
|
|
134
|
+
active.delete(key)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
return render(text)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 静态层插值:配置 variables 优先,ST 运行时宏取空串,内置路径变量按无会话语义解析。
|
|
143
|
+
* @param keep 需要交给官方变量通道按 assembly 求值的名字(运行时事实与已注册变量)——
|
|
144
|
+
* 命中即原样保留引用,不做静态替换。
|
|
145
|
+
*/
|
|
146
|
+
export function interpolateStatic(text, variables, keep) {
|
|
147
|
+
return interpolateVariables(text, variables, undefined, keep)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 需要按 assembly 求值的运行时事实(官方变量注册用)。
|
|
152
|
+
* random/pick/roll/chance 是「每次出现各算一次」的求值宏,newline/pipe 是字面宏,
|
|
153
|
+
* 都不在其中——它们继续由 interpolate* 内联处理。
|
|
154
|
+
*/
|
|
155
|
+
export const RUNTIME_FACTS = new Set([
|
|
156
|
+
'lastusermessage',
|
|
157
|
+
'lastcharmessage',
|
|
158
|
+
'charifnotgroup',
|
|
159
|
+
'time',
|
|
160
|
+
'date',
|
|
161
|
+
'weekday',
|
|
162
|
+
'isotime',
|
|
163
|
+
'isodate',
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 运行时事实求值(大小写不敏感)。非事实名返回 undefined。
|
|
168
|
+
* @param session 会话对象;缺省时按静态语义(运行时宏为空、时间取当前值)。
|
|
169
|
+
*/
|
|
170
|
+
export function runtimeFactValue(name, session) {
|
|
171
|
+
const key = String(name ?? '').toLowerCase()
|
|
172
|
+
if (!RUNTIME_FACTS.has(key)) return undefined
|
|
173
|
+
return DYNAMIC_MACROS[key](undefined, session)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 引用正则:键允许字母数字、下划线、点、中文与连字符(与 ST setvar/getvar 一致),
|
|
178
|
+
* `::` 之后是本项目宏参数。官方插值语法更窄,故这两条通道各用各的。
|
|
179
|
+
*/
|
|
180
|
+
const REFERENCE_RE = /\{\{\s*([A-Za-z0-9_.\u4e00-\u9fff-]+)\s*(?:::([^{}]*?))?\}\}/g
|
|
181
|
+
|
|
182
|
+
/** ST 宏形态 → 本项目语法(幂等)。 */
|
|
183
|
+
const MACRO_ALIASES = 'roll|random|pick|chance'
|
|
184
|
+
/** 空格形态:{{roll 1d6}}(ST 语料常见,本项目语法要求 ::)。 */
|
|
185
|
+
const MACRO_SPACE_RE = new RegExp(`\\{\\{\\s*(${MACRO_ALIASES})\\s+([^{}]*?)\\s*\\}\\}`, 'gi')
|
|
186
|
+
/** 单冒号形态:{{roll:1d6}}(`::` 已是本项目语法,用负向断言排除)。 */
|
|
187
|
+
const MACRO_SINGLE_COLON_RE = new RegExp(`\\{\\{\\s*(${MACRO_ALIASES})\\s*:(?!:)\\s*([^{}]*?)\\s*\\}\\}`, 'gi')
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* ST 宏写法归一:{{roll 1d6}} / {{roll:1d6}} → {{roll::1d6}}。
|
|
191
|
+
* 与 `src/host/sillytavern.ts` 的导入期归一同源(两处物理隔离,改动需同步)。
|
|
192
|
+
*/
|
|
193
|
+
export function normalizeMacroSyntax(text) {
|
|
194
|
+
return text
|
|
195
|
+
.replace(MACRO_SPACE_RE, (_whole, macro, arg) => `{{${String(macro).toLowerCase()}::${arg}}}`)
|
|
196
|
+
.replace(MACRO_SINGLE_COLON_RE, (_whole, macro, arg) => `{{${String(macro).toLowerCase()}::${arg}}}`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 官方插值通道(system-section / runtime-context)专用清洗。
|
|
201
|
+
*
|
|
202
|
+
* 官方 renderPrompt 对 section/context 文本做严格插值:畸形引用、`{{}}`、未注册名、
|
|
203
|
+
* 值为 undefined 都抛错并让整轮 assembly 失败,而 runtime-context 在 0.1.6 没有
|
|
204
|
+
* `interpolate:false`(PromptContext 无该字段)。因此交给官方之前必须保证文本里
|
|
205
|
+
* 不再有本项目解析后残留的引用:
|
|
206
|
+
* - `{{…}}` 成组引用(含畸形名字)→ 整段剥离(决策:未命中引用剥离);
|
|
207
|
+
* - 有 `{{` 却没成组、但后文存在 `}}` → 只移除 `{{`(官方判畸形),保留可见文本;
|
|
208
|
+
* - 只有 `{{` 且其后无 `}}` → 官方按字面处理,原样保留。
|
|
209
|
+
* @returns 清洗后文本与已剥离片段(供调用方一次性告警)。
|
|
210
|
+
*/
|
|
211
|
+
export function stripUnresolvedRefs(text, keep) {
|
|
212
|
+
const stripped = []
|
|
213
|
+
let out = ''
|
|
214
|
+
let index = 0
|
|
215
|
+
while (index < text.length) {
|
|
216
|
+
const open = text.indexOf('{{', index)
|
|
217
|
+
if (open < 0) {
|
|
218
|
+
out += text.slice(index)
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
out += text.slice(index, open)
|
|
222
|
+
const rest = text.slice(open)
|
|
223
|
+
const group = /^\{\{([^{}]*)\}\}/.exec(rest)
|
|
224
|
+
if (group !== null) {
|
|
225
|
+
// 白名单(已注册的官方变量名)原样保留,交给官方按 assembly 求值。
|
|
226
|
+
if (/^[a-z][a-z0-9_]*$/.test(group[1]) && keep?.has(group[1])) {
|
|
227
|
+
out += group[0]
|
|
228
|
+
index = open + group[0].length
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
stripped.push(group[0])
|
|
232
|
+
index = open + group[0].length
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
if (rest.indexOf('}}', 2) >= 0) {
|
|
236
|
+
stripped.push(rest.slice(0, 2))
|
|
237
|
+
index = open + 2
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
out += '{{'
|
|
241
|
+
index = open + 2
|
|
242
|
+
}
|
|
243
|
+
return { text: out, stripped }
|
|
116
244
|
}
|